aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.config/dotnet-tools.json2
-rw-r--r--.devcontainer/Dev - Server Ffmpeg/devcontainer.json28
-rw-r--r--.devcontainer/devcontainer.json8
-rw-r--r--.devcontainer/install-ffmpeg.sh (renamed from .devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh)4
-rw-r--r--.editorconfig338
-rw-r--r--.github/ISSUE_TEMPLATE/issue report.yml4
-rw-r--r--.github/workflows/ci-codeql-analysis.yml10
-rw-r--r--.github/workflows/ci-compat.yml14
-rw-r--r--.github/workflows/ci-openapi.yml20
-rw-r--r--.github/workflows/ci-tests.yml7
-rw-r--r--.github/workflows/commands.yml90
-rw-r--r--.github/workflows/issue-stale.yml2
-rw-r--r--.github/workflows/issue-template-check.yml2
-rw-r--r--.github/workflows/pull-request-conflict.yml2
-rw-r--r--.github/workflows/pull-request-stale.yaml2
-rw-r--r--.vscode/extensions.json11
-rw-r--r--.vscode/launch.json6
-rw-r--r--CONTRIBUTORS.md4
-rw-r--r--Directory.Build.props2
-rw-r--r--Directory.Packages.props93
-rw-r--r--Emby.Naming/Common/NamingOptions.cs8
-rw-r--r--Emby.Naming/Emby.Naming.csproj4
-rw-r--r--Emby.Naming/TV/SeriesResolver.cs2
-rw-r--r--Emby.Photos/Emby.Photos.csproj2
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs3
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs23
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs1
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs269
-rw-r--r--Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs30
-rw-r--r--Emby.Server.Implementations/Data/ItemTypeLookup.cs64
-rw-r--r--Emby.Server.Implementations/Data/ManagedConnection.cs62
-rw-r--r--Emby.Server.Implementations/Data/SqliteExtensions.cs12
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs5971
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs369
-rw-r--r--Emby.Server.Implementations/Data/SynchronousMode.cs30
-rw-r--r--Emby.Server.Implementations/Data/TempStoreMode.cs23
-rw-r--r--Emby.Server.Implementations/Devices/DeviceId.cs3
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs12
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj2
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs2
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs8
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs14
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketManager.cs4
-rw-r--r--Emby.Server.Implementations/IO/FileRefresher.cs4
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs14
-rw-r--r--Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs6
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs83
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs50
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs26
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs2
-rw-r--r--Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs18
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs140
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs30
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json19
-rw-r--r--Emby.Server.Implementations/Localization/Core/bg-BG.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/bn.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json76
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json52
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/eu.json70
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/ga.json143
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/hi.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/ht.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/ko.json28
-rw-r--r--Emby.Server.Implementations/Localization/Core/lb.json139
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json22
-rw-r--r--Emby.Server.Implementations/Localization/Core/lv.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/mk.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/mt.json114
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pa.json17
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/sr.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/uz.json89
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json6
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs4
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/br.csv8
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ca.csv2
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/es.csv2
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/gb.csv3
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ie.csv1
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/no.csv1
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/nz.csv1
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/us.csv34
-rw-r--r--Emby.Server.Implementations/MediaEncoder/EncodingManager.cs4
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs40
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistsFolder.cs2
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs53
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs16
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs11
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs4
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs6
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs2
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs10
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs8
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs98
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs2
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs9
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs4
-rw-r--r--Jellyfin.Api/Auth/CustomAuthenticationHandler.cs11
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs68
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs14
-rw-r--r--Jellyfin.Api/Controllers/CollectionController.cs6
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs25
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs6
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs12
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs48
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs7
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs152
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs32
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs5
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs38
-rw-r--r--Jellyfin.Api/Controllers/MediaSegmentsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs5
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs12
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs10
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs20
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs10
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs6
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs6
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs1
-rw-r--r--Jellyfin.Api/Controllers/StudiosController.cs8
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs2
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs8
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs60
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs16
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs4
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs46
-rw-r--r--Jellyfin.Api/Controllers/UserViewsController.cs4
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs2
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs21
-rw-r--r--Jellyfin.Api/Formatters/CssOutputFormatter.cs24
-rw-r--r--Jellyfin.Api/Formatters/XmlOutputFormatter.cs15
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs12
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs9
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj2
-rw-r--r--Jellyfin.Api/ModelBinders/CommaDelimitedCollectionModelBinder.cs (renamed from Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs)12
-rw-r--r--Jellyfin.Api/ModelBinders/PipeDelimitedCollectionModelBinder.cs (renamed from Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs)14
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs5
-rw-r--r--Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs14
-rw-r--r--Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs2
-rw-r--r--Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs2
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs2
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs2
-rw-r--r--Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs2
-rw-r--r--Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs4
-rw-r--r--Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs4
-rw-r--r--Jellyfin.Data/Entities/AncestorId.cs29
-rw-r--r--Jellyfin.Data/Entities/AttachmentStreamInfo.cs49
-rw-r--r--Jellyfin.Data/Entities/BaseItemEntity.cs186
-rw-r--r--Jellyfin.Data/Entities/BaseItemExtraType.cs18
-rw-r--r--Jellyfin.Data/Entities/BaseItemImageInfo.cs59
-rw-r--r--Jellyfin.Data/Entities/BaseItemMetadataField.cs24
-rw-r--r--Jellyfin.Data/Entities/BaseItemProvider.cs32
-rw-r--r--Jellyfin.Data/Entities/BaseItemTrailerType.cs24
-rw-r--r--Jellyfin.Data/Entities/Chapter.cs44
-rw-r--r--Jellyfin.Data/Entities/ImageInfoImageType.cs76
-rw-r--r--Jellyfin.Data/Entities/ItemValue.cs37
-rw-r--r--Jellyfin.Data/Entities/ItemValueMap.cs30
-rw-r--r--Jellyfin.Data/Entities/ItemValueType.cs38
-rw-r--r--Jellyfin.Data/Entities/Libraries/CollectionItem.cs4
-rw-r--r--Jellyfin.Data/Entities/Libraries/Series.cs2
-rw-r--r--Jellyfin.Data/Entities/MediaStreamInfo.cs103
-rw-r--r--Jellyfin.Data/Entities/MediaStreamTypeEntity.cs37
-rw-r--r--Jellyfin.Data/Entities/People.cs32
-rw-r--r--Jellyfin.Data/Entities/PeopleBaseItemMap.cs44
-rw-r--r--Jellyfin.Data/Entities/ProgramAudioEntity.cs37
-rw-r--r--Jellyfin.Data/Entities/TrickplayInfo.cs2
-rw-r--r--Jellyfin.Data/Entities/User.cs2
-rw-r--r--Jellyfin.Data/Entities/UserData.cs92
-rw-r--r--Jellyfin.Data/Enums/ItemSortBy.cs10
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj4
-rw-r--r--Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs2
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs2178
-rw-r--r--Jellyfin.Server.Implementations/Item/ChapterRepository.cs123
-rw-r--r--Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs73
-rw-r--r--Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs213
-rw-r--r--Jellyfin.Server.Implementations/Item/PeopleRepository.cs190
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj2
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbContext.cs98
-rw-r--r--Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs42
-rw-r--r--Jellyfin.Server.Implementations/Migrations/.gitattributes1
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.Designer.cs1607
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.cs639
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.Designer.cs1610
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.cs28
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs1610
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.cs54
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs1603
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.cs49
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.Designer.cs1600
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.cs702
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.Designer.cs1594
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.cs144
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs1595
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.cs37
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs1595
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20250204092455_MakeStartEndDateNullable.cs55
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20250214031148_ChannelIdGuid.Designer.cs1595
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20250214031148_ChannelIdGuid.cs22
-rw-r--r--Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs3
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs885
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs21
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs17
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs59
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs22
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs20
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs22
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs19
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs19
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs20
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs22
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs22
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs20
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs23
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs40
-rw-r--r--Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs16
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs2
-rw-r--r--Jellyfin.Server/Helpers/StartupHelpers.cs8
-rw-r--r--Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs36
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj2
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs4
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs4
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs1201
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs69
-rw-r--r--Jellyfin.Server/Program.cs8
-rw-r--r--Jellyfin.Server/Startup.cs1
-rw-r--r--Jellyfin.sln4
-rw-r--r--MediaBrowser.Common/Configuration/IConfigurationManager.cs2
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj4
-rw-r--r--MediaBrowser.Common/Net/NetworkConfiguration.cs1
-rw-r--r--MediaBrowser.Common/Net/NetworkUtils.cs3
-rw-r--r--MediaBrowser.Common/Plugins/BasePluginOfT.cs7
-rw-r--r--MediaBrowser.Common/RequiresSourceSerialisationAttribute.cs11
-rw-r--r--MediaBrowser.Controller/Channels/Channel.cs4
-rw-r--r--MediaBrowser.Controller/Chapters/IChapterManager.cs19
-rw-r--r--MediaBrowser.Controller/Chapters/IChapterRepository.cs49
-rw-r--r--MediaBrowser.Controller/Devices/IDeviceManager.cs4
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs25
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs1
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs9
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicGenre.cs3
-rw-r--r--MediaBrowser.Controller/Entities/AudioBook.cs1
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs65
-rw-r--r--MediaBrowser.Controller/Entities/Book.cs1
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs52
-rw-r--r--MediaBrowser.Controller/Entities/Genre.cs3
-rw-r--r--MediaBrowser.Controller/Entities/IHasMediaSources.cs4
-rw-r--r--MediaBrowser.Controller/Entities/IItemByName.cs2
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs5
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChild.cs7
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs19
-rw-r--r--MediaBrowser.Controller/Entities/PeopleHelper.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Person.cs3
-rw-r--r--MediaBrowser.Controller/Entities/PersonInfo.cs6
-rw-r--r--MediaBrowser.Controller/Entities/PhotoAlbum.cs1
-rw-r--r--MediaBrowser.Controller/Entities/Studio.cs3
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs4
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs4
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs4
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs4
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Year.cs3
-rw-r--r--MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs13
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs18
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs16
-rw-r--r--MediaBrowser.Controller/Library/IMusicManager.cs6
-rw-r--r--MediaBrowser.Controller/Library/IUserDataManager.cs6
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvChannel.cs16
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs1
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj4
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs147
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs4
-rw-r--r--MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs (renamed from MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs)16
-rw-r--r--MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs (renamed from MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs)0
-rw-r--r--MediaBrowser.Controller/Net/AuthorizationInfo.cs20
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs2
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketConnection.cs4
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs221
-rw-r--r--MediaBrowser.Controller/Persistence/IItemTypeLookup.cs22
-rw-r--r--MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.cs28
-rw-r--r--MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs31
-rw-r--r--MediaBrowser.Controller/Persistence/IPeopleRepository.cs33
-rw-r--r--MediaBrowser.Controller/Persistence/IUserDataRepository.cs55
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs3
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs14
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs3
-rw-r--r--MediaBrowser.Controller/Providers/MetadataResult.cs16
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs2
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs4
-rw-r--r--MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs2
-rw-r--r--MediaBrowser.Controller/Streaming/StreamState.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs2
-rw-r--r--MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj2
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs6
-rw-r--r--MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs6
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs26
-rw-r--r--MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj2
-rw-r--r--MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs37
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs4
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs16
-rw-r--r--MediaBrowser.Model/Dlna/ConditionProcessor.cs4
-rw-r--r--MediaBrowser.Model/Dlna/DirectPlayProfile.cs2
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs99
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs4
-rw-r--r--MediaBrowser.Model/Dlna/TranscodingProfile.cs29
-rw-r--r--MediaBrowser.Model/Drawing/ImageFormatExtensions.cs8
-rw-r--r--MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs4
-rw-r--r--MediaBrowser.Model/Dto/SessionInfoDto.cs2
-rw-r--r--MediaBrowser.Model/Entities/HardwareAccelerationType.cs2
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs4
-rw-r--r--MediaBrowser.Model/Entities/MetadataProvider.cs2
-rw-r--r--MediaBrowser.Model/Entities/ProviderIdsExtensions.cs6
-rw-r--r--MediaBrowser.Model/Extensions/ContainerHelper.cs15
-rw-r--r--MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs6
-rw-r--r--MediaBrowser.Model/Globalization/ILocalizationManager.cs2
-rw-r--r--MediaBrowser.Model/IO/IFileSystem.cs4
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj6
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs4
-rw-r--r--MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs2
-rw-r--r--MediaBrowser.Model/Plugins/PluginStatus.cs7
-rw-r--r--MediaBrowser.Model/Session/TranscodingInfo.cs2
-rw-r--r--MediaBrowser.Model/System/PublicSystemInfo.cs2
-rw-r--r--MediaBrowser.Model/Tasks/TaskTriggerInfo.cs22
-rw-r--r--MediaBrowser.Model/Tasks/TaskTriggerInfoType.cs28
-rw-r--r--MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs2
-rw-r--r--MediaBrowser.Providers/Chapters/ChapterManager.cs26
-rw-r--r--MediaBrowser.Providers/Lyric/LyricScheduledTask.cs2
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs1
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs25
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs91
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs43
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj5
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs48
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs9
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs23
-rw-r--r--MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs2
-rw-r--r--MediaBrowser.Providers/MediaInfo/ProbeProvider.cs17
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs6
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs2
-rw-r--r--MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Music/AlbumMetadataService.cs4
-rw-r--r--MediaBrowser.Providers/Music/ArtistMetadataService.cs3
-rw-r--r--MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs2
-rw-r--r--MediaBrowser.Providers/TV/SeasonMetadataService.cs6
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs45
-rw-r--r--MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj2
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs25
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs38
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs15
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs4
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs61
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs4
-rw-r--r--README.md4
-rw-r--r--SharedVersion.cs4
-rwxr-xr-xbump_version4
-rw-r--r--fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj2
-rwxr-xr-xfuzz/Emby.Server.Implementations.Fuzz/fuzz.sh2
-rw-r--r--fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj2
-rwxr-xr-xfuzz/Jellyfin.Api.Fuzz/fuzz.sh2
-rw-r--r--global.json2
-rw-r--r--jellyfin.code-workspace2
-rw-r--r--jellyfin.ruleset221
-rw-r--r--src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj2
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs4
-rw-r--r--src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs2
-rw-r--r--src/Jellyfin.Drawing/ImageProcessor.cs28
-rw-r--r--src/Jellyfin.Drawing/Jellyfin.Drawing.csproj2
-rw-r--r--src/Jellyfin.Extensions/Jellyfin.Extensions.csproj4
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedCollectionConverter.cs (renamed from src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs)8
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedCollectionConverterFactory.cs (renamed from src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs)11
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonDelimitedCollectionConverter.cs (renamed from src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs)54
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedCollectionConverter.cs (renamed from src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs)6
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedCollectionConverterFactory.cs (renamed from src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs)11
-rw-r--r--src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs2
-rw-r--r--src/Jellyfin.LiveTv/Guide/GuideManager.cs233
-rw-r--r--src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs2
-rw-r--r--src/Jellyfin.LiveTv/IO/EncodedRecorder.cs30
-rw-r--r--src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj2
-rw-r--r--src/Jellyfin.LiveTv/Listings/ListingsManager.cs7
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs31
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs2
-rw-r--r--src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs6
-rw-r--r--src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs3
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs4
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj2
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj2
-rw-r--r--src/Jellyfin.Networking/Jellyfin.Networking.csproj5
-rw-r--r--src/Jellyfin.Networking/Manager/NetworkManager.cs19
-rw-r--r--src/Jellyfin.Networking/PortForwardingHost.cs192
-rw-r--r--tests/Directory.Build.props3
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs3
-rw-r--r--tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedCollectionModelBinderTests.cs (renamed from tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs)18
-rw-r--r--tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedCollectionModelBinderTests.cs (renamed from tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs)18
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs1
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedCollectionTests.cs (renamed from tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs)77
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs13
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs4
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyCollectionModel.cs19
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs4
-rw-r--r--tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyListModel.cs22
-rw-r--r--tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs8
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj4
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Listings/ListingsManagerTests.cs50
-rw-r--r--tests/Jellyfin.LiveTv.Tests/SchedulesDirect/SchedulesDirectDeserializeTests.cs2
-rw-r--r--tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs1
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/TvParserHelpersTest.cs8
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs1
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkParseTests.cs2
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs6
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs2
-rw-r--r--tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs4
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs122
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest.json2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs2
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs3
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs6
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs2
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs6
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs15
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs8
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs16
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs6
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs2
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs20
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/Lilo & Stitch.nfo7
-rw-r--r--tests/jellyfin-tests.ruleset28
472 files changed, 26415 insertions, 9690 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index a3847dcdf..ea2675a3d 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "8.0.8",
+ "version": "9.0.2",
"commands": [
"dotnet-ef"
]
diff --git a/.devcontainer/Dev - Server Ffmpeg/devcontainer.json b/.devcontainer/Dev - Server Ffmpeg/devcontainer.json
deleted file mode 100644
index 0b848d9f3..000000000
--- a/.devcontainer/Dev - Server Ffmpeg/devcontainer.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "name": "Development Jellyfin Server - FFmpeg",
- "image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy",
- // restores nuget packages, installs the dotnet workloads and installs the dev https certificate
- "postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust; sudo bash \"./.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh\"",
- // reads the extensions list and installs them
- "postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
- "features": {
- "ghcr.io/devcontainers/features/dotnet:2": {
- "version": "none",
- "dotnetRuntimeVersions": "8.0",
- "aspNetCoreRuntimeVersions": "8.0"
- },
- "ghcr.io/devcontainers-contrib/features/apt-packages:1": {
- "preserve_apt_list": false,
- "packages": ["libfontconfig1"]
- },
- "ghcr.io/devcontainers/features/docker-in-docker:2": {
- "dockerDashComposeVersion": "v2"
- },
- "ghcr.io/devcontainers/features/github-cli:1": {},
- "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}
- },
- "hostRequirements": {
- "memory": "8gb",
- "cpus": 4
- }
-}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 063901c80..228d4a17c 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,15 +1,15 @@
{
"name": "Development Jellyfin Server",
- "image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy",
+ "image":"mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm",
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
- "postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust",
+ "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"",
// reads the extensions list and installs them
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
"features": {
"ghcr.io/devcontainers/features/dotnet:2": {
"version": "none",
- "dotnetRuntimeVersions": "8.0",
- "aspNetCoreRuntimeVersions": "8.0"
+ "dotnetRuntimeVersions": "9.0",
+ "aspNetCoreRuntimeVersions": "9.0"
},
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
"preserve_apt_list": false,
diff --git a/.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh b/.devcontainer/install-ffmpeg.sh
index c867ef538..1e58e6ef4 100644
--- a/.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh
+++ b/.devcontainer/install-ffmpeg.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-## configure the following for a manuall install of a specific version from the repo
+## configure the following for a manual install of a specific version from the repo
# wget https://repo.jellyfin.org/releases/server/ubuntu/versions/jellyfin-ffmpeg/6.0.1-1/jellyfin-ffmpeg6_6.0.1-1-jammy_amd64.deb -O ffmpeg.deb
@@ -29,4 +29,4 @@ Signed-By: /etc/apt/keyrings/jellyfin.gpg
EOF
sudo apt update -y
-sudo apt install jellyfin-ffmpeg6 -y
+sudo apt install jellyfin-ffmpeg7 -y
diff --git a/.editorconfig b/.editorconfig
index b84e563ef..ab5d3d9dd 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -192,3 +192,341 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false
# Wrapping preferences
csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true
+
+###############################
+# C# Analyzer Rules #
+###############################
+### ERROR #
+###########
+# error on SA1000: The keyword 'new' should be followed by a space
+dotnet_diagnostic.SA1000.severity = error
+
+# error on SA1001: Commas should not be preceded by whitespace
+dotnet_diagnostic.SA1001.severity = error
+
+# error on SA1106: Code should not contain empty statements
+dotnet_diagnostic.SA1106.severity = error
+
+# error on SA1107: Code should not contain multiple statements on one line
+dotnet_diagnostic.SA1107.severity = error
+
+# error on SA1028: Code should not contain trailing whitespace
+dotnet_diagnostic.SA1028.severity = error
+
+# error on SA1117: The parameters should all be placed on the same line or each parameter should be placed on its own line
+dotnet_diagnostic.SA1117.severity = error
+
+# error on SA1137: Elements should have the same indentation
+dotnet_diagnostic.SA1137.severity = error
+
+# error on SA1142: Refer to tuple fields by name
+dotnet_diagnostic.SA1142.severity = error
+
+# error on SA1210: Using directives should be ordered alphabetically by the namespaces
+dotnet_diagnostic.SA1210.severity = error
+
+# error on SA1316: Tuple element names should use correct casing
+dotnet_diagnostic.SA1316.severity = error
+
+# error on SA1414: Tuple types in signatures should have element names
+dotnet_diagnostic.SA1414.severity = error
+
+# disable warning SA1513: Closing brace should be followed by blank line
+dotnet_diagnostic.SA1513.severity = error
+
+# error on SA1518: File is required to end with a single newline character
+dotnet_diagnostic.SA1518.severity = error
+
+# error on SA1629: Documentation text should end with a period
+dotnet_diagnostic.SA1629.severity = error
+
+# error on CA1001: Types that own disposable fields should be disposable
+dotnet_diagnostic.CA1001.severity = error
+
+# error on CA1012: Abstract types should not have public constructors
+dotnet_diagnostic.CA1012.severity = error
+
+# error on CA1063: Implement IDisposable correctly
+dotnet_diagnostic.CA1063.severity = error
+
+# error on CA1305: Specify IFormatProvider
+dotnet_diagnostic.CA1305.severity = error
+
+# error on CA1307: Specify StringComparison for clarity
+dotnet_diagnostic.CA1307.severity = error
+
+# error on CA1309: Use ordinal StringComparison
+dotnet_diagnostic.CA1309.severity = error
+
+# error on CA1310: Specify StringComparison for correctness
+dotnet_diagnostic.CA1310.severity = error
+
+# error on CA1513: Use 'ObjectDisposedException.ThrowIf' instead of explicitly throwing a new exception instance
+dotnet_diagnostic.CA1513.severity = error
+
+# error on CA1725: Parameter names should match base declaration
+dotnet_diagnostic.CA1725.severity = error
+
+# error on CA1725: Call async methods when in an async method
+dotnet_diagnostic.CA1727.severity = error
+
+# error on CA1813: Avoid unsealed attributes
+dotnet_diagnostic.CA1813.severity = error
+
+# error on CA1834: Use 'StringBuilder.Append(char)' instead of 'StringBuilder.Append(string)' when the input is a constant unit string
+dotnet_diagnostic.CA1834.severity = error
+
+# error on CA1843: Do not use 'WaitAll' with a single task
+dotnet_diagnostic.CA1843.severity = error
+
+# error on CA1845: Use span-based 'string.Concat'
+dotnet_diagnostic.CA1845.severity = error
+
+# error on CA1849: Call async methods when in an async method
+dotnet_diagnostic.CA1849.severity = error
+
+# error on CA1851: Possible multiple enumerations of IEnumerable collection
+dotnet_diagnostic.CA1851.severity = error
+
+# error on CA1854: Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup
+dotnet_diagnostic.CA1854.severity = error
+
+# error on CA1860: Avoid using 'Enumerable.Any()' extension method
+dotnet_diagnostic.CA1860.severity = error
+
+# error on CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
+dotnet_diagnostic.CA1862.severity = error
+
+# error on CA1863: Use 'CompositeFormat'
+dotnet_diagnostic.CA1863.severity = error
+
+# error on CA1864: Prefer the 'IDictionary.TryAdd(TKey, TValue)' method
+dotnet_diagnostic.CA1864.severity = error
+
+# error on CA1865-CA1867: Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char
+dotnet_diagnostic.CA1865.severity = error
+dotnet_diagnostic.CA1866.severity = error
+dotnet_diagnostic.CA1867.severity = error
+
+# error on CA1868: Unnecessary call to 'Contains' for sets
+dotnet_diagnostic.CA1868.severity = error
+
+# error on CA1869: Cache and reuse 'JsonSerializerOptions' instances
+dotnet_diagnostic.CA1869.severity = error
+
+# error on CA1870: Use a cached 'SearchValues' instance
+dotnet_diagnostic.CA1870.severity = error
+
+# error on CA1871: Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull'
+dotnet_diagnostic.CA1871.severity = error
+
+# error on CA1872: Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString'
+dotnet_diagnostic.CA1872.severity = 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
+dotnet_diagnostic.CA2016.severity = error
+
+# error on CA2201: Exception type System.Exception is not sufficiently specific
+dotnet_diagnostic.CA2201.severity = error
+
+# error on CA2215: Dispose methods should call base class dispose
+dotnet_diagnostic.CA2215.severity = error
+
+# error on CA2249: Use 'string.Contains' instead of 'string.IndexOf' to improve readability
+dotnet_diagnostic.CA2249.severity = error
+
+# error on CA2254: Template should be a static expression
+dotnet_diagnostic.CA2254.severity = error
+
+################
+### SUGGESTION #
+################
+# disable warning CA1014: Mark assemblies with CLSCompliantAttribute
+dotnet_diagnostic.CA1014.severity = suggestion
+
+# disable warning CA1024: Use properties where appropriate
+dotnet_diagnostic.CA1024.severity = suggestion
+
+# disable warning CA1031: Do not catch general exception types
+dotnet_diagnostic.CA1031.severity = suggestion
+
+# disable warning CA1032: Implement standard exception constructors
+dotnet_diagnostic.CA1032.severity = suggestion
+
+# disable warning CA1040: Avoid empty interfaces
+dotnet_diagnostic.CA1040.severity = suggestion
+
+# disable warning CA1062: Validate arguments of public methods
+dotnet_diagnostic.CA1062.severity = suggestion
+
+# TODO: enable when false positives are fixed
+# disable warning CA1508: Avoid dead conditional code
+dotnet_diagnostic.CA1508.severity = suggestion
+
+# disable warning CA1515: Consider making public types internal
+dotnet_diagnostic.CA1515.severity = suggestion
+
+# disable warning CA1716: Identifiers should not match keywords
+dotnet_diagnostic.CA1716.severity = suggestion
+
+# disable warning CA1720: Identifiers should not contain type names
+dotnet_diagnostic.CA1720.severity = suggestion
+
+# disable warning CA1724: Type names should not match namespaces
+dotnet_diagnostic.CA1724.severity = suggestion
+
+# disable warning CA1805: Do not initialize unnecessarily
+dotnet_diagnostic.CA1805.severity = suggestion
+
+# disable warning CA1812: internal class that is apparently never instantiated.
+# If so, remove the code from the assembly.
+# If this class is intended to contain only static members, make it static
+dotnet_diagnostic.CA1812.severity = suggestion
+
+# disable warning CA1822: Member does not access instance data and can be marked as static
+dotnet_diagnostic.CA1822.severity = suggestion
+
+# CA1859: Use concrete types when possible for improved performance
+dotnet_diagnostic.CA1859.severity = suggestion
+
+# TODO: Enable
+# CA1861: Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array
+dotnet_diagnostic.CA1861.severity = suggestion
+
+# disable warning CA2000: Dispose objects before losing scope
+dotnet_diagnostic.CA2000.severity = suggestion
+
+# disable warning CA2253: Named placeholders should not be numeric values
+dotnet_diagnostic.CA2253.severity = suggestion
+
+# disable warning CA5394: Do not use insecure randomness
+dotnet_diagnostic.CA5394.severity = suggestion
+
+# error on CA3003: Review code for file path injection vulnerabilities
+dotnet_diagnostic.CA3003.severity = suggestion
+
+# error on CA3006: Review code for process command injection vulnerabilities
+dotnet_diagnostic.CA3006.severity = suggestion
+
+###############
+### DISABLED #
+###############
+# disable warning SA1009: Closing parenthesis should be followed by a space.
+dotnet_diagnostic.SA1009.severity = none
+
+# disable warning SA1011: Closing square bracket should be followed by a space.
+dotnet_diagnostic.SA1011.severity = none
+
+# disable warning SA1101: Prefix local calls with 'this.'
+dotnet_diagnostic.SA1101.severity = none
+
+# disable warning SA1108: Block statements should not contain embedded comments
+dotnet_diagnostic.SA1108.severity = none
+
+# disable warning SA1118: Parameter must not span multiple lines.
+dotnet_diagnostic.SA1118.severity = none
+
+# disable warning SA1128:: Put constructor initializers on their own line
+dotnet_diagnostic.SA1128.severity = none
+
+# disable warning SA1130: Use lambda syntax
+dotnet_diagnostic.SA1130.severity = none
+
+# disable warning SA1200: 'using' directive must appear within a namespace declaration
+dotnet_diagnostic.SA1200.severity = none
+
+# disable warning SA1202: 'public' members must come before 'private' members
+dotnet_diagnostic.SA1202.severity = none
+
+# disable warning SA1204: Static members must appear before non-static members
+dotnet_diagnostic.SA1204.severity = none
+
+# disable warning SA1309: Fields must not begin with an underscore
+dotnet_diagnostic.SA1309.severity = none
+
+# disable warning SA1311: Static readonly fields should begin with upper-case letter
+dotnet_diagnostic.SA1311.severity = none
+
+# disable warning SA1413: Use trailing comma in multi-line initializers
+dotnet_diagnostic.SA1413.severity = none
+
+# disable warning SA1512: Single-line comments must not be followed by blank line
+dotnet_diagnostic.SA1512.severity = none
+
+# disable warning SA1515: Single-line comment should be preceded by blank line
+dotnet_diagnostic.SA1515.severity = none
+
+# disable warning SA1600: Elements should be documented
+dotnet_diagnostic.SA1600.severity = none
+
+# disable warning SA1601: Partial elements should be documented
+dotnet_diagnostic.SA1601.severity = none
+
+# disable warning SA1602: Enumeration items should be documented
+dotnet_diagnostic.SA1602.severity = none
+
+# disable warning SA1633: The file header is missing or not located at the top of the file
+dotnet_diagnostic.SA1633.severity = none
+
+# disable warning CA1054: Change the type of parameter url from string to System.Uri
+dotnet_diagnostic.CA1054.severity = none
+
+# disable warning CA1055: URI return values should not be strings
+dotnet_diagnostic.CA1055.severity = none
+
+# disable warning CA1056: URI properties should not be strings
+dotnet_diagnostic.CA1056.severity = none
+
+# disable warning CA1303: Do not pass literals as localized parameters
+dotnet_diagnostic.CA1303.severity = none
+
+# disable warning CA1308: Normalize strings to uppercase
+dotnet_diagnostic.CA1308.severity = none
+
+# disable warning CA1848: Use the LoggerMessage delegates
+dotnet_diagnostic.CA1848.severity = none
+
+# disable warning CA2101: Specify marshaling for P/Invoke string arguments
+dotnet_diagnostic.CA2101.severity = none
+
+# disable warning CA2234: Pass System.Uri objects instead of strings
+dotnet_diagnostic.CA2234.severity = none
+
+# error on RS0030: Do not used banned APIs
+dotnet_diagnostic.RS0030.severity = error
+
+# disable warning IDISP001: Dispose created
+dotnet_diagnostic.IDISP001.severity = suggestion
+
+# TODO: Enable when false positives are fixed
+# disable warning IDISP003: Dispose previous before re-assigning
+dotnet_diagnostic.IDISP003.severity = suggestion
+
+# disable warning IDISP004: Don't ignore created IDisposable
+dotnet_diagnostic.IDISP004.severity = suggestion
+
+# disable warning IDISP007: Don't dispose injected
+dotnet_diagnostic.IDISP007.severity = suggestion
+
+# disable warning IDISP008: Don't assign member with injected and created disposables
+dotnet_diagnostic.IDISP008.severity = suggestion
+
+[tests/**.{cs,vb}]
+# disable warning SA0001: XML comment analysis is disabled due to project configuration
+dotnet_diagnostic.SA0001.severity = none
+
+# disable warning CA1707: Identifiers should not contain underscores
+dotnet_diagnostic.CA1707.severity = none
+
+# disable warning CA2007: Consider calling ConfigureAwait on the awaited task
+dotnet_diagnostic.CA2007.severity = none
+
+# disable warning CA2234: Pass system uri objects instead of strings
+dotnet_diagnostic.CA2234.severity = suggestion
+
+# disable warning xUnit1028: Test methods must have a supported return type.
+dotnet_diagnostic.xUnit1028.severity = none
+
+# CA1826: Do not use Enumerable methods on indexable collections
+dotnet_diagnostic.CA1826.severity = suggestion
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
index b52241208..4f58c5bc5 100644
--- a/.github/ISSUE_TEMPLATE/issue report.yml
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -14,7 +14,7 @@ body:
label: "This issue respects the following points:"
description: All conditions are **required**. Failure to comply with any of these conditions may cause your issue to be closed without comment.
options:
- - label: This is a **bug**, not a question or a configuration issue; Please visit our forum or chat rooms first to troubleshoot with volunteers, before creating a report. The links can be found [here](https://jellyfin.org/contact/).
+ - label: This is a **bug**, not a question or a configuration issue; Please visit our [forum or chat rooms](https://jellyfin.org/contact/) first to troubleshoot with volunteers, before creating a report.
required: true
- label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin/issues?q=is%3Aopen+is%3Aissue) _(I've searched it)_.
required: true
@@ -86,7 +86,7 @@ body:
label: Jellyfin Server version
description: What version of Jellyfin are you using?
options:
- - 10.9.11+
+ - 10.10.0+
- Master
- Unstable
- Older*
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index 1c55437a4..00f7e9e6d 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -22,16 +22,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup .NET
- uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
+ uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '9.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
+ uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
+ uses: github/codeql-action/autobuild@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
+ uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml
index c9d9f8449..07e61024e 100644
--- a/.github/workflows/ci-compat.yml
+++ b/.github/workflows/ci-compat.yml
@@ -16,12 +16,17 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
+ - name: Setup .NET
+ uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+ with:
+ dotnet-version: '9.0.x'
+
- name: Build
run: |
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
+ uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: abi-head
retention-days: 14
@@ -41,6 +46,11 @@ jobs:
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
+ - name: Setup .NET
+ uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+ with:
+ dotnet-version: '9.0.x'
+
- name: Checkout common ancestor
env:
HEAD_REF: ${{ github.head_ref }}
@@ -55,7 +65,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
+ uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: abi-base
retention-days: 14
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index 4633461ad..e82988200 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -21,18 +21,18 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
- uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
+ uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '9.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
+ uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: openapi-head
retention-days: 14
if-no-files-found: error
- path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json
+ path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
openapi-base:
name: OpenAPI - BASE
@@ -55,18 +55,18 @@ jobs:
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
- uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
+ uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '9.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
+ uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: openapi-base
retention-days: 14
if-no-files-found: error
- path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json
+ path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
openapi-diff:
permissions:
@@ -172,7 +172,7 @@ jobs:
strip_components: 1
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (unstable) into place
- uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0
+ uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
@@ -234,7 +234,7 @@ jobs:
strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place
- uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0
+ uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index 54fb762e6..ec78396db 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -9,19 +9,20 @@ on:
pull_request:
env:
- SDK_VERSION: "8.0.x"
+ SDK_VERSION: "9.0.x"
jobs:
run-tests:
strategy:
matrix:
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
+ fail-fast: false
runs-on: "${{ matrix.os }}"
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- - uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
+ - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
with:
dotnet-version: ${{ env.SDK_VERSION }}
@@ -34,7 +35,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
- uses: danielpalme/ReportGenerator-GitHub-Action@62f9e70ab348d56eee76d446b4db903a85ab0ea8 # v5.3.11
+ uses: danielpalme/ReportGenerator-GitHub-Action@f1927db1dbfc029b056583ee488832e939447fe6 # v5.4.4
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 26b98f973..1ab7ae029 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -34,94 +34,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
- check-backport:
- permissions:
- contents: read
-
- name: Check Backport
- if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
- runs-on: ubuntu-latest
- steps:
- - name: Notify as seen
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
- if: ${{ github.event.comment != null }}
- with:
- token: ${{ secrets.JF_BOT_TOKEN }}
- comment-id: ${{ github.event.comment.id }}
- reactions: eyes
-
- - name: Checkout the latest code
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- with:
- token: ${{ secrets.JF_BOT_TOKEN }}
- fetch-depth: 0
-
- - name: Notify as running
- id: comment_running
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
- if: ${{ github.event.comment != null }}
- with:
- token: ${{ secrets.JF_BOT_TOKEN }}
- issue-number: ${{ github.event.issue.number }}
- body: |
- Running backport tests...
-
- - name: Perform test backport
- id: run_tests
- run: |
- set +o errexit
- git config --global user.name "Jellyfin Bot"
- git config --global user.email "team@jellyfin.org"
- CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
- git checkout master
- git merge --no-ff ${CURRENT_BRANCH}
- MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
- git fetch --all
- CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
- stable_branch="Current stable release branch: ${CURRENT_STABLE}"
- echo ${stable_branch}
- echo ::set-output name=branch::${stable_branch}
- git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
- git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
- retcode=$?
- cat output.txt | grep -v 'hint:'
- output="$( grep -v 'hint:' output.txt )"
- output="${output//'%'/'%25'}"
- output="${output//$'\n'/'%0A'}"
- output="${output//$'\r'/'%0D'}"
- echo ::set-output name=output::$output
- exit ${retcode}
-
- - name: Notify with result success
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
- if: ${{ github.event.comment != null && success() }}
- with:
- token: ${{ secrets.JF_BOT_TOKEN }}
- comment-id: ${{ steps.comment_running.outputs.comment-id }}
- body: |
- ${{ steps.run_tests.outputs.branch }}
- Output from `git cherry-pick`:
-
- ---
-
- ${{ steps.run_tests.outputs.output }}
- reactions: hooray
-
- - name: Notify with result failure
- uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
- if: ${{ github.event.comment != null && failure() }}
- with:
- token: ${{ secrets.JF_BOT_TOKEN }}
- comment-id: ${{ steps.comment_running.outputs.comment-id }}
- body: |
- ${{ steps.run_tests.outputs.branch }}
- Output from `git cherry-pick`:
-
- ---
-
- ${{ steps.run_tests.outputs.output }}
- reactions: confused
-
rename:
name: Rename
if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER'
@@ -132,7 +44,7 @@ jobs:
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
+ uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: '3.12'
cache: 'pip'
diff --git a/.github/workflows/issue-stale.yml b/.github/workflows/issue-stale.yml
index 5a1ca9f7a..624ea564f 100644
--- a/.github/workflows/issue-stale.yml
+++ b/.github/workflows/issue-stale.yml
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
+ - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true
diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml
index b72e552af..3c5ba68f9 100644
--- a/.github/workflows/issue-template-check.yml
+++ b/.github/workflows/issue-template-check.yml
@@ -14,7 +14,7 @@ jobs:
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
+ uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: '3.12'
cache: 'pip'
diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml
index 5d342b7f8..411ebf829 100644
--- a/.github/workflows/pull-request-conflict.yml
+++ b/.github/workflows/pull-request-conflict.yml
@@ -15,7 +15,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
- uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
+ uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'
diff --git a/.github/workflows/pull-request-stale.yaml b/.github/workflows/pull-request-stale.yaml
index d01b3f4a1..7ce5b0fa6 100644
--- a/.github/workflows/pull-request-stale.yaml
+++ b/.github/workflows/pull-request-stale.yaml
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
+ - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 3be946e44..e4205ce0b 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,12 +1,13 @@
{
- "recommendations": [
+ "recommendations": [
"ms-dotnettools.csharp",
"editorconfig.editorconfig",
"github.vscode-github-actions",
"ms-dotnettools.vscode-dotnet-runtime",
- "ms-dotnettools.csdevkit"
- ],
- "unwantedRecommendations": [
+ "ms-dotnettools.csdevkit",
+ "alexcvzz.vscode-sqlite"
+ ],
+ "unwantedRecommendations": [
- ]
+ ]
}
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 7e50d4f0a..d97d8de84 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -6,7 +6,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
"args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
@@ -22,7 +22,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
@@ -34,7 +34,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
- "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
+ "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
"args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index a9deb1c4a..ae1a2fd71 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -192,6 +192,9 @@
- [jaina heartles](https://github.com/heartles)
- [oxixes](https://github.com/oxixes)
- [elfalem](https://github.com/elfalem)
+ - [Kenneth Cochran](https://github.com/kennethcochran)
+ - [benedikt257](https://github.com/benedikt257)
+ - [revam](https://github.com/revam)
# Emby Contributors
@@ -265,3 +268,4 @@
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
- [Robert Lützner](https://github.com/rluetzner)
- [Nathan McCrina](https://github.com/nfmccrina)
+ - [Martin Reuter](https://github.com/reuterma24)
diff --git a/Directory.Build.props b/Directory.Build.props
index 44a60ffb5..31ae8bfbe 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,11 +3,11 @@
<PropertyGroup>
<Nullable>enable</Nullable>
- <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <WarningsNotAsErrors>NU1902;NU1903</WarningsNotAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5aadeb254..999e4ba74 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,88 +4,87 @@
</PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies">
- <PackageVersion Include="AsyncKeyedLock" Version="7.0.2" />
+ <PackageVersion Include="AsyncKeyedLock" Version="7.1.4" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" />
- <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.3" />
- <PackageVersion Include="BlurHashSharp" Version="1.3.3" />
+ <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
+ <PackageVersion Include="BlurHashSharp" Version="1.3.4" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
- <PackageVersion Include="coverlet.collector" Version="6.0.2" />
+ <PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Diacritics" Version="3.3.29" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
- <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
- <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
+ <PackageVersion Include="FsCheck.Xunit" Version="3.1.0" />
+ <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.3" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
- <PackageVersion Include="libse" Version="4.0.8" />
+ <PackageVersion Include="libse" Version="4.0.10" />
<PackageVersion Include="LrcParser" Version="2024.0728.2" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.10" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
- <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.10" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.10" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
- <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
- <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.10" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.10" />
- <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
- <PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
- <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
- <PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
- <PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
- <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
- <PackageVersion Include="MimeTypes" Version="2.4.0" />
- <PackageVersion Include="Mono.Nat" Version="3.0.4" />
+ <PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.2" />
+ <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
+ <PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Moq" Version="4.18.4" />
- <PackageVersion Include="NEbml" Version="0.11.0" />
+ <PackageVersion Include="NEbml" Version="0.12.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
<PackageVersion Include="prometheus-net" Version="8.2.1" />
- <PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
+ <PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
- <PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.4" />
+ <PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
- <PackageVersion Include="SharpFuzz" Version="2.1.1" />
- <PackageVersion Include="SkiaSharp" Version="2.88.8" />
- <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.8" />
- <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.8" />
+ <PackageVersion Include="SharpFuzz" Version="2.2.0" />
+ <PackageVersion Include="SkiaSharp" Version="2.88.9" />
+ <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.9" />
+ <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
- <PackageVersion Include="Svg.Skia" Version="2.0.0.1" />
+ <PackageVersion Include="Svg.Skia" Version="2.0.0.4" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
- <PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
- <PackageVersion Include="System.Text.Json" Version="8.0.5" />
- <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
+ <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.2" />
+ <PackageVersion Include="System.Text.Json" Version="9.0.2" />
+ <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.2" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
- <PackageVersion Include="z440.atl.core" Version="6.6.0" />
+ <PackageVersion Include="z440.atl.core" Version="6.16.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
- <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
- <PackageVersion Include="xunit" Version="2.9.2" />
+ <PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
+ <PackageVersion Include="xunit" Version="2.9.3" />
</ItemGroup>
-</Project> \ No newline at end of file
+</Project>
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 333d237a2..48338daf4 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -467,6 +467,14 @@ namespace Emby.Naming.Common
{
IsNamed = true
},
+
+ // Anime style expression
+ // "[Group][Series Name][21][1080p][FLAC][HASH]"
+ // "[Group] Series Name [04][BDRIP]"
+ new EpisodeExpression(@"(?:\[(?:[^\]]+)\]\s*)?(?<seriesname>\[[^\]]+\]|[^[\]]+)\s*\[(?<epnumber>[0-9]+)\]")
+ {
+ IsNamed = true
+ },
};
VideoExtraRules = new[]
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 7eb131575..20b32f3a6 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -36,7 +36,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId>
- <VersionPrefix>10.10.0</VersionPrefix>
+ <VersionPrefix>10.11.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
diff --git a/Emby.Naming/TV/SeriesResolver.cs b/Emby.Naming/TV/SeriesResolver.cs
index d8fa41743..c955b8a0d 100644
--- a/Emby.Naming/TV/SeriesResolver.cs
+++ b/Emby.Naming/TV/SeriesResolver.cs
@@ -12,7 +12,7 @@ namespace Emby.Naming.TV
/// <summary>
/// Regex that matches strings of at least 2 characters separated by a dot or underscore.
/// Used for removing separators between words, i.e turns "The_show" into "The show" while
- /// preserving namings like "S.H.O.W".
+ /// preserving names like "S.H.O.W".
/// </summary>
[GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")]
private static partial Regex SeriesNameRegex();
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 55dbe393c..645a74aea 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -19,7 +19,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 9e98d5ce0..9bc3a0204 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
@@ -19,7 +20,7 @@ namespace Emby.Server.Implementations.AppBase
public abstract class BaseConfigurationManager : IConfigurationManager
{
private readonly ConcurrentDictionary<string, object> _configurations = new();
- private readonly object _configurationSyncLock = new();
+ private readonly Lock _configurationSyncLock = new();
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 5292003f0..29967c6df 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -40,6 +40,7 @@ using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Manager;
using Jellyfin.Networking.Udp;
using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Implementations.Item;
using Jellyfin.Server.Implementations.MediaSegments;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
@@ -83,7 +84,6 @@ using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
-using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Lyric;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.Tmdb;
@@ -268,6 +268,11 @@ namespace Emby.Server.Implementations
public string ExpandVirtualPath(string path)
{
+ if (path is null)
+ {
+ return null;
+ }
+
var appPaths = ApplicationPaths;
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
@@ -492,10 +497,14 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
- serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
- serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
+ serviceCollection.AddSingleton<IItemRepository, BaseItemRepository>();
+ serviceCollection.AddSingleton<IPeopleRepository, PeopleRepository>();
+ serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
+ serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
+ serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>();
+ serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>();
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
serviceCollection.AddSingleton<EncodingHelper>();
@@ -540,8 +549,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
- serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
-
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
serviceCollection.AddSingleton<IAuthService, AuthService>();
@@ -579,9 +586,6 @@ namespace Emby.Server.Implementations
}
}
- ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize();
- ((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize();
-
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
await localizationManager.LoadAll().ConfigureAwait(false);
@@ -607,7 +611,7 @@ namespace Emby.Server.Implementations
// Don't use an empty string password
password = string.IsNullOrWhiteSpace(password) ? null : password;
- var localCert = new X509Certificate2(path, password, X509KeyStorageFlags.UserKeySet);
+ var localCert = X509CertificateLoader.LoadPkcs12FromFile(path, password, X509KeyStorageFlags.UserKeySet);
if (!localCert.HasPrivateKey)
{
Logger.LogError("No private key included in SSL cert {CertificateLocation}.", path);
@@ -635,6 +639,7 @@ namespace Emby.Server.Implementations
BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.ItemRepository = Resolve<IItemRepository>();
+ BaseItem.ChapterRepository = Resolve<IChapterRepository>();
BaseItem.FileSystem = Resolve<IFileSystem>();
BaseItem.UserDataManager = Resolve<IUserDataManager>();
BaseItem.ChannelManager = Resolve<IChannelManager>();
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index 91791a1c8..a06f6e7fe 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -17,7 +17,6 @@ namespace Emby.Server.Implementations
{ DefaultRedirectKey, "web/" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
- { PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString },
{ SqliteCacheSizeKey, "20000" },
{ FfmpegSkipValidationKey, bool.FalseString },
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
deleted file mode 100644
index 8ed72c208..000000000
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ /dev/null
@@ -1,269 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using Jellyfin.Extensions;
-using Microsoft.Data.Sqlite;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Data
-{
- public abstract class BaseSqliteRepository : IDisposable
- {
- private bool _disposed = false;
- private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
- private SqliteConnection _writeConnection;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- protected BaseSqliteRepository(ILogger<BaseSqliteRepository> logger)
- {
- Logger = logger;
- }
-
- /// <summary>
- /// Gets or sets the path to the DB file.
- /// </summary>
- protected string DbFilePath { get; set; }
-
- /// <summary>
- /// Gets the logger.
- /// </summary>
- /// <value>The logger.</value>
- protected ILogger<BaseSqliteRepository> Logger { get; }
-
- /// <summary>
- /// Gets the cache size.
- /// </summary>
- /// <value>The cache size or null.</value>
- protected virtual int? CacheSize => null;
-
- /// <summary>
- /// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
- /// </summary>
- protected virtual string LockingMode => "NORMAL";
-
- /// <summary>
- /// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
- /// </summary>
- /// <value>The journal mode.</value>
- protected virtual string JournalMode => "WAL";
-
- /// <summary>
- /// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
- /// The default (-1) is overridden to prevent unconstrained WAL size, as reported by users.
- /// </summary>
- /// <value>The journal size limit.</value>
- protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
-
- /// <summary>
- /// Gets the page size.
- /// </summary>
- /// <value>The page size or null.</value>
- protected virtual int? PageSize => null;
-
- /// <summary>
- /// Gets the temp store mode.
- /// </summary>
- /// <value>The temp store mode.</value>
- /// <see cref="TempStoreMode"/>
- protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
-
- /// <summary>
- /// Gets the synchronous mode.
- /// </summary>
- /// <value>The synchronous mode or null.</value>
- /// <see cref="SynchronousMode"/>
- protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
-
- public virtual void Initialize()
- {
- // Configuration and pragmas can affect VACUUM so it needs to be last.
- using (var connection = GetConnection())
- {
- connection.Execute("VACUUM");
- }
- }
-
- protected ManagedConnection GetConnection(bool readOnly = false)
- {
- if (!readOnly)
- {
- _writeLock.Wait();
- if (_writeConnection is not null)
- {
- return new ManagedConnection(_writeConnection, _writeLock);
- }
-
- var writeConnection = new SqliteConnection($"Filename={DbFilePath};Pooling=False");
- writeConnection.Open();
-
- if (CacheSize.HasValue)
- {
- writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
- }
-
- if (!string.IsNullOrWhiteSpace(LockingMode))
- {
- writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
- }
-
- if (!string.IsNullOrWhiteSpace(JournalMode))
- {
- writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
- }
-
- if (JournalSizeLimit.HasValue)
- {
- writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
- }
-
- if (Synchronous.HasValue)
- {
- writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
- }
-
- if (PageSize.HasValue)
- {
- writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
- }
-
- writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
-
- return new ManagedConnection(_writeConnection = writeConnection, _writeLock);
- }
-
- var connection = new SqliteConnection($"Filename={DbFilePath};Mode=ReadOnly");
- connection.Open();
-
- if (CacheSize.HasValue)
- {
- connection.Execute("PRAGMA cache_size=" + CacheSize.Value);
- }
-
- if (!string.IsNullOrWhiteSpace(LockingMode))
- {
- connection.Execute("PRAGMA locking_mode=" + LockingMode);
- }
-
- if (!string.IsNullOrWhiteSpace(JournalMode))
- {
- connection.Execute("PRAGMA journal_mode=" + JournalMode);
- }
-
- if (JournalSizeLimit.HasValue)
- {
- connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
- }
-
- if (Synchronous.HasValue)
- {
- connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
- }
-
- if (PageSize.HasValue)
- {
- connection.Execute("PRAGMA page_size=" + PageSize.Value);
- }
-
- connection.Execute("PRAGMA temp_store=" + (int)TempStore);
-
- return new ManagedConnection(connection, null);
- }
-
- public SqliteCommand PrepareStatement(ManagedConnection connection, string sql)
- {
- var command = connection.CreateCommand();
- command.CommandText = sql;
- return command;
- }
-
- protected bool TableExists(ManagedConnection connection, string name)
- {
- using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
- foreach (var row in statement.ExecuteQuery())
- {
- if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
-
- return false;
- }
-
- protected List<string> GetColumnNames(ManagedConnection connection, string table)
- {
- var columnNames = new List<string>();
-
- foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
- {
- if (row.TryGetString(1, out var columnName))
- {
- columnNames.Add(columnName);
- }
- }
-
- return columnNames;
- }
-
- protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
- {
- if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
- {
- return;
- }
-
- connection.Execute("alter table " + table + " add column " + columnName + " " + type + " NULL");
- }
-
- protected void CheckDisposed()
- {
- ObjectDisposedException.ThrowIf(_disposed, this);
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
- {
- if (_disposed)
- {
- return;
- }
-
- if (dispose)
- {
- _writeLock.Wait();
- try
- {
- _writeConnection.Dispose();
- }
- finally
- {
- _writeLock.Release();
- }
-
- _writeLock.Dispose();
- }
-
- _writeConnection = null;
- _writeLock = null;
-
- _disposed = true;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
index 4516b89dc..7ea863d76 100644
--- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
+++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
@@ -1,10 +1,13 @@
#pragma warning disable CS1591
using System;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Server.Implementations;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Data
@@ -13,20 +16,24 @@ namespace Emby.Server.Implementations.Data
{
private readonly ILibraryManager _libraryManager;
private readonly ILogger<CleanDatabaseScheduledTask> _logger;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
- public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger<CleanDatabaseScheduledTask> logger)
+ public CleanDatabaseScheduledTask(
+ ILibraryManager libraryManager,
+ ILogger<CleanDatabaseScheduledTask> logger,
+ IDbContextFactory<JellyfinDbContext> dbProvider)
{
_libraryManager = libraryManager;
_logger = logger;
+ _dbProvider = dbProvider;
}
- public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+ public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
- CleanDeadItems(cancellationToken, progress);
- return Task.CompletedTask;
+ await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
}
- private void CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
+ private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
{
var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
@@ -34,7 +41,7 @@ namespace Emby.Server.Implementations.Data
});
var numComplete = 0;
- var numItems = itemIds.Count;
+ var numItems = itemIds.Count + 1;
_logger.LogDebug("Cleaning {0} items with dead parent links", numItems);
@@ -60,6 +67,17 @@ namespace Emby.Server.Implementations.Data
progress.Report(percent * 100);
}
+ var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (context.ConfigureAwait(false))
+ {
+ var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
+ await using (transaction.ConfigureAwait(false))
+ {
+ await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+ await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
+ }
+ }
+
progress.Report(100);
}
}
diff --git a/Emby.Server.Implementations/Data/ItemTypeLookup.cs b/Emby.Server.Implementations/Data/ItemTypeLookup.cs
new file mode 100644
index 000000000..82c0a8b6c
--- /dev/null
+++ b/Emby.Server.Implementations/Data/ItemTypeLookup.cs
@@ -0,0 +1,64 @@
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.Threading.Channels;
+using Emby.Server.Implementations.Playlists;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Playlists;
+
+namespace Emby.Server.Implementations.Data;
+
+/// <inheritdoc />
+public class ItemTypeLookup : IItemTypeLookup
+{
+ /// <inheritdoc />
+ public IReadOnlyList<string> MusicGenreTypes { get; } = [
+ typeof(Audio).FullName!,
+ typeof(MusicVideo).FullName!,
+ typeof(MusicAlbum).FullName!,
+ typeof(MusicArtist).FullName!,
+ ];
+
+ /// <inheritdoc />
+ public IReadOnlyDictionary<BaseItemKind, string> BaseItemKindNames { get; } = new Dictionary<BaseItemKind, string>()
+ {
+ { 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! }
+ }.ToFrozenDictionary();
+}
diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs
deleted file mode 100644
index 860950b30..000000000
--- a/Emby.Server.Implementations/Data/ManagedConnection.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using Microsoft.Data.Sqlite;
-
-namespace Emby.Server.Implementations.Data;
-
-public sealed class ManagedConnection : IDisposable
-{
- private readonly SemaphoreSlim? _writeLock;
-
- private SqliteConnection _db;
-
- private bool _disposed = false;
-
- public ManagedConnection(SqliteConnection db, SemaphoreSlim? writeLock)
- {
- _db = db;
- _writeLock = writeLock;
- }
-
- public SqliteTransaction BeginTransaction()
- => _db.BeginTransaction();
-
- public SqliteCommand CreateCommand()
- => _db.CreateCommand();
-
- public void Execute(string commandText)
- => _db.Execute(commandText);
-
- public SqliteCommand PrepareStatement(string sql)
- => _db.PrepareStatement(sql);
-
- public IEnumerable<SqliteDataReader> Query(string commandText)
- => _db.Query(commandText);
-
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- if (_writeLock is null)
- {
- // Read connections are managed with an internal pool
- _db.Dispose();
- }
- else
- {
- // Write lock is managed by BaseSqliteRepository
- // Don't dispose here
- _writeLock.Release();
- }
-
- _db = null!;
-
- _disposed = true;
- }
-}
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 25ef57d27..0efef4ded 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -127,8 +127,16 @@ namespace Emby.Server.Implementations.Data
return false;
}
- result = reader.GetGuid(index);
- return true;
+ try
+ {
+ result = reader.GetGuid(index);
+ return true;
+ }
+ catch
+ {
+ result = Guid.Empty;
+ return false;
+ }
}
public static bool TryGetString(this SqliteDataReader reader, int index, out string result)
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
deleted file mode 100644
index 3477194cf..000000000
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ /dev/null
@@ -1,5971 +0,0 @@
-#nullable disable
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Text;
-using System.Text.Json;
-using System.Threading;
-using Emby.Server.Implementations.Playlists;
-using Jellyfin.Data.Enums;
-using Jellyfin.Extensions;
-using Jellyfin.Extensions.Json;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Channels;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Extensions;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Querying;
-using Microsoft.Data.Sqlite;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Data
-{
- /// <summary>
- /// Class SQLiteItemRepository.
- /// </summary>
- public class SqliteItemRepository : BaseSqliteRepository, IItemRepository
- {
- private const string FromText = " from TypedBaseItems A";
- private const string ChaptersTableName = "Chapters2";
-
- private const string SaveItemCommandText =
- @"replace into TypedBaseItems
- (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,NormalizationGain,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
- values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@NormalizationGain,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
-
- private readonly IServerConfigurationManager _config;
- private readonly IServerApplicationHost _appHost;
- private readonly ILocalizationManager _localization;
- // TODO: Remove this dependency. GetImageCacheTag() is the only method used and it can be converted to a static helper method
- private readonly IImageProcessor _imageProcessor;
-
- private readonly TypeMapper _typeMapper;
- private readonly JsonSerializerOptions _jsonOptions;
-
- private readonly ItemFields[] _allItemFields = Enum.GetValues<ItemFields>();
-
- private static readonly string[] _retrieveItemColumns =
- {
- "type",
- "data",
- "StartDate",
- "EndDate",
- "ChannelId",
- "IsMovie",
- "IsSeries",
- "EpisodeTitle",
- "IsRepeat",
- "CommunityRating",
- "CustomRating",
- "IndexNumber",
- "IsLocked",
- "PreferredMetadataLanguage",
- "PreferredMetadataCountryCode",
- "Width",
- "Height",
- "DateLastRefreshed",
- "Name",
- "Path",
- "PremiereDate",
- "Overview",
- "ParentIndexNumber",
- "ProductionYear",
- "OfficialRating",
- "ForcedSortName",
- "RunTimeTicks",
- "Size",
- "DateCreated",
- "DateModified",
- "guid",
- "Genres",
- "ParentId",
- "Audio",
- "ExternalServiceId",
- "IsInMixedFolder",
- "DateLastSaved",
- "LockedFields",
- "Studios",
- "Tags",
- "TrailerTypes",
- "OriginalTitle",
- "PrimaryVersionId",
- "DateLastMediaAdded",
- "Album",
- "LUFS",
- "NormalizationGain",
- "CriticRating",
- "IsVirtualItem",
- "SeriesName",
- "SeasonName",
- "SeasonId",
- "SeriesId",
- "PresentationUniqueKey",
- "InheritedParentalRatingValue",
- "ExternalSeriesId",
- "Tagline",
- "ProviderIds",
- "Images",
- "ProductionLocations",
- "ExtraIds",
- "TotalBitrate",
- "ExtraType",
- "Artists",
- "AlbumArtists",
- "ExternalId",
- "SeriesPresentationUniqueKey",
- "ShowId",
- "OwnerId"
- };
-
- private static readonly string _retrieveItemColumnsSelectQuery = $"select {string.Join(',', _retrieveItemColumns)} from TypedBaseItems where guid = @guid";
-
- private static readonly string[] _mediaStreamSaveColumns =
- {
- "ItemId",
- "StreamIndex",
- "StreamType",
- "Codec",
- "Language",
- "ChannelLayout",
- "Profile",
- "AspectRatio",
- "Path",
- "IsInterlaced",
- "BitRate",
- "Channels",
- "SampleRate",
- "IsDefault",
- "IsForced",
- "IsExternal",
- "Height",
- "Width",
- "AverageFrameRate",
- "RealFrameRate",
- "Level",
- "PixelFormat",
- "BitDepth",
- "IsAnamorphic",
- "RefFrames",
- "CodecTag",
- "Comment",
- "NalLengthSize",
- "IsAvc",
- "Title",
- "TimeBase",
- "CodecTimeBase",
- "ColorPrimaries",
- "ColorSpace",
- "ColorTransfer",
- "DvVersionMajor",
- "DvVersionMinor",
- "DvProfile",
- "DvLevel",
- "RpuPresentFlag",
- "ElPresentFlag",
- "BlPresentFlag",
- "DvBlSignalCompatibilityId",
- "IsHearingImpaired",
- "Rotation"
- };
-
- private static readonly string _mediaStreamSaveColumnsInsertQuery =
- $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
-
- private static readonly string _mediaStreamSaveColumnsSelectQuery =
- $"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
-
- private static readonly string[] _mediaAttachmentSaveColumns =
- {
- "ItemId",
- "AttachmentIndex",
- "Codec",
- "CodecTag",
- "Comment",
- "Filename",
- "MIMEType"
- };
-
- private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
- $"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
-
- private static readonly string _mediaAttachmentInsertPrefix = BuildMediaAttachmentInsertPrefix();
-
- private static readonly BaseItemKind[] _programTypes = new[]
- {
- BaseItemKind.Program,
- BaseItemKind.TvChannel,
- BaseItemKind.LiveTvProgram,
- BaseItemKind.LiveTvChannel
- };
-
- private static readonly BaseItemKind[] _programExcludeParentTypes = new[]
- {
- BaseItemKind.Series,
- BaseItemKind.Season,
- BaseItemKind.MusicAlbum,
- BaseItemKind.MusicArtist,
- BaseItemKind.PhotoAlbum
- };
-
- private static readonly BaseItemKind[] _serviceTypes = new[]
- {
- BaseItemKind.TvChannel,
- BaseItemKind.LiveTvChannel
- };
-
- private static readonly BaseItemKind[] _startDateTypes = new[]
- {
- BaseItemKind.Program,
- BaseItemKind.LiveTvProgram
- };
-
- private static readonly BaseItemKind[] _seriesTypes = new[]
- {
- BaseItemKind.Book,
- BaseItemKind.AudioBook,
- BaseItemKind.Episode,
- BaseItemKind.Season
- };
-
- private static readonly BaseItemKind[] _artistExcludeParentTypes = new[]
- {
- BaseItemKind.Series,
- BaseItemKind.Season,
- BaseItemKind.PhotoAlbum
- };
-
- private static readonly BaseItemKind[] _artistsTypes = new[]
- {
- BaseItemKind.Audio,
- BaseItemKind.MusicAlbum,
- BaseItemKind.MusicVideo,
- BaseItemKind.AudioBook
- };
-
- private static readonly 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 }
- };
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
- /// </summary>
- /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{SqliteItemRepository}"/> interface.</param>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- /// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param>
- /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
- /// <exception cref="ArgumentNullException">config is null.</exception>
- public SqliteItemRepository(
- IServerConfigurationManager config,
- IServerApplicationHost appHost,
- ILogger<SqliteItemRepository> logger,
- ILocalizationManager localization,
- IImageProcessor imageProcessor,
- IConfiguration configuration)
- : base(logger)
- {
- _config = config;
- _appHost = appHost;
- _localization = localization;
- _imageProcessor = imageProcessor;
-
- _typeMapper = new TypeMapper();
- _jsonOptions = JsonDefaults.Options;
-
- DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
-
- CacheSize = configuration.GetSqliteCacheSize();
- }
-
- /// <inheritdoc />
- protected override int? CacheSize { get; }
-
- /// <inheritdoc />
- protected override TempStoreMode TempStore => TempStoreMode.Memory;
-
- /// <summary>
- /// Opens the connection to the database.
- /// </summary>
- public override void Initialize()
- {
- base.Initialize();
-
- const string CreateMediaStreamsTableCommand
- = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, Rotation INT NULL, PRIMARY KEY (ItemId, StreamIndex))";
-
- const string CreateMediaAttachmentsTableCommand
- = "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))";
-
- string[] queries =
- {
- "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)",
-
- "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))",
- "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)",
- "create index if not exists idx_AncestorIds5 on AncestorIds(AncestorIdText,ItemId)",
-
- "create table if not exists ItemValues (ItemId GUID NOT NULL, Type INT NOT NULL, Value TEXT NOT NULL, CleanValue TEXT NOT NULL)",
-
- "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)",
-
- "drop index if exists idxPeopleItemId",
- "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)",
- "create index if not exists idxPeopleName on People(Name)",
-
- "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))",
-
- CreateMediaStreamsTableCommand,
- CreateMediaAttachmentsTableCommand,
-
- "pragma shrink_memory"
- };
-
- string[] postQueries =
- {
- "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)",
- "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)",
-
- "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)",
- "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)",
- "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)",
-
- // covering index
- "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)",
-
- // series
- "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)",
-
- // series counts
- // seriesdateplayed sort order
- "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)",
-
- // live tv programs
- "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)",
-
- // covering index for getitemvalues
- "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)",
-
- // used by movie suggestions
- "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)",
- "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)",
-
- // latest items
- "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)",
- "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)",
-
- // resume
- "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)",
-
- // items by name
- "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)",
- "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)",
-
- // Used to update inherited tags
- "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)",
-
- "CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type)",
- "CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder)"
- };
-
- using (var connection = GetConnection())
- using (var transaction = connection.BeginTransaction())
- {
- connection.Execute(string.Join(';', queries));
-
- var existingColumnNames = GetColumnNames(connection, "AncestorIds");
- AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
-
- existingColumnNames = GetColumnNames(connection, "TypedBaseItems");
-
- AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "NormalizationGain", "Float", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames);
- AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames);
-
- existingColumnNames = GetColumnNames(connection, "ItemValues");
- AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames);
-
- existingColumnNames = GetColumnNames(connection, ChaptersTableName);
- AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames);
-
- existingColumnNames = GetColumnNames(connection, "MediaStreams");
- AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames);
-
- AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames);
-
- AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames);
- AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames);
-
- AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames);
-
- AddColumn(connection, "MediaStreams", "Rotation", "INT", existingColumnNames);
-
- connection.Execute(string.Join(';', postQueries));
-
- transaction.Commit();
- }
- }
-
- /// <inheritdoc />
- public void SaveImages(BaseItem item)
- {
- ArgumentNullException.ThrowIfNull(item);
-
- CheckDisposed();
-
- var images = SerializeImages(item.ImageInfos);
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- using var saveImagesStatement = PrepareStatement(connection, "Update TypedBaseItems set Images=@Images where guid=@Id");
- saveImagesStatement.TryBind("@Id", item.Id);
- saveImagesStatement.TryBind("@Images", images);
-
- saveImagesStatement.ExecuteNonQuery();
- transaction.Commit();
- }
-
- /// <summary>
- /// Saves the items.
- /// </summary>
- /// <param name="items">The items.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <exception cref="ArgumentNullException">
- /// <paramref name="items"/> or <paramref name="cancellationToken"/> is <c>null</c>.
- /// </exception>
- public void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull(items);
-
- cancellationToken.ThrowIfCancellationRequested();
-
- CheckDisposed();
-
- var itemsLen = items.Count;
- var tuples = new ValueTuple<BaseItem, List<Guid>, BaseItem, string, List<string>>[itemsLen];
- for (int i = 0; i < itemsLen; i++)
- {
- var item = items[i];
- var ancestorIds = item.SupportsAncestors ?
- item.GetAncestorIds().Distinct().ToList() :
- null;
-
- var topParent = item.GetTopParent();
-
- var userdataKey = item.GetUserDataKeys().FirstOrDefault();
- var inheritedTags = item.GetInheritedTags();
-
- tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags);
- }
-
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- SaveItemsInTransaction(connection, tuples);
- transaction.Commit();
- }
-
- private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
- {
- using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
- using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
- {
- var requiresReset = false;
- foreach (var tuple in tuples)
- {
- if (requiresReset)
- {
- saveItemStatement.Parameters.Clear();
- deleteAncestorsStatement.Parameters.Clear();
- }
-
- var item = tuple.Item;
- var topParent = tuple.TopParent;
- var userDataKey = tuple.UserDataKey;
-
- SaveItem(item, topParent, userDataKey, saveItemStatement);
-
- var inheritedTags = tuple.InheritedTags;
-
- if (item.SupportsAncestors)
- {
- UpdateAncestors(item.Id, tuple.AncestorIds, db, deleteAncestorsStatement);
- }
-
- UpdateItemValues(item.Id, GetItemValuesToSave(item, inheritedTags), db);
-
- requiresReset = true;
- }
- }
- }
-
- private string GetPathToSave(string path)
- {
- if (path is null)
- {
- return null;
- }
-
- return _appHost.ReverseVirtualPath(path);
- }
-
- private string RestorePath(string path)
- {
- return _appHost.ExpandVirtualPath(path);
- }
-
- private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, SqliteCommand saveItemStatement)
- {
- Type type = item.GetType();
-
- saveItemStatement.TryBind("@guid", item.Id);
- saveItemStatement.TryBind("@type", type.FullName);
-
- if (TypeRequiresDeserialization(type))
- {
- saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions), true);
- }
- else
- {
- saveItemStatement.TryBindNull("@data");
- }
-
- saveItemStatement.TryBind("@Path", GetPathToSave(item.Path));
-
- if (item is IHasStartDate hasStartDate)
- {
- saveItemStatement.TryBind("@StartDate", hasStartDate.StartDate);
- }
- else
- {
- saveItemStatement.TryBindNull("@StartDate");
- }
-
- if (item.EndDate.HasValue)
- {
- saveItemStatement.TryBind("@EndDate", item.EndDate.Value);
- }
- else
- {
- saveItemStatement.TryBindNull("@EndDate");
- }
-
- saveItemStatement.TryBind("@ChannelId", item.ChannelId.IsEmpty() ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture));
-
- if (item is IHasProgramAttributes hasProgramAttributes)
- {
- saveItemStatement.TryBind("@IsMovie", hasProgramAttributes.IsMovie);
- saveItemStatement.TryBind("@IsSeries", hasProgramAttributes.IsSeries);
- saveItemStatement.TryBind("@EpisodeTitle", hasProgramAttributes.EpisodeTitle);
- saveItemStatement.TryBind("@IsRepeat", hasProgramAttributes.IsRepeat);
- }
- else
- {
- saveItemStatement.TryBindNull("@IsMovie");
- saveItemStatement.TryBindNull("@IsSeries");
- saveItemStatement.TryBindNull("@EpisodeTitle");
- saveItemStatement.TryBindNull("@IsRepeat");
- }
-
- saveItemStatement.TryBind("@CommunityRating", item.CommunityRating);
- saveItemStatement.TryBind("@CustomRating", item.CustomRating);
- saveItemStatement.TryBind("@IndexNumber", item.IndexNumber);
- saveItemStatement.TryBind("@IsLocked", item.IsLocked);
- saveItemStatement.TryBind("@Name", item.Name);
- saveItemStatement.TryBind("@OfficialRating", item.OfficialRating);
- saveItemStatement.TryBind("@MediaType", item.MediaType.ToString());
- saveItemStatement.TryBind("@Overview", item.Overview);
- saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber);
- saveItemStatement.TryBind("@PremiereDate", item.PremiereDate);
- saveItemStatement.TryBind("@ProductionYear", item.ProductionYear);
-
- var parentId = item.ParentId;
- if (parentId.IsEmpty())
- {
- saveItemStatement.TryBindNull("@ParentId");
- }
- else
- {
- saveItemStatement.TryBind("@ParentId", parentId);
- }
-
- if (item.Genres.Length > 0)
- {
- saveItemStatement.TryBind("@Genres", string.Join('|', item.Genres));
- }
- else
- {
- saveItemStatement.TryBindNull("@Genres");
- }
-
- saveItemStatement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue);
-
- saveItemStatement.TryBind("@SortName", item.SortName);
-
- saveItemStatement.TryBind("@ForcedSortName", item.ForcedSortName);
-
- saveItemStatement.TryBind("@RunTimeTicks", item.RunTimeTicks);
- saveItemStatement.TryBind("@Size", item.Size);
-
- saveItemStatement.TryBind("@DateCreated", item.DateCreated);
- saveItemStatement.TryBind("@DateModified", item.DateModified);
-
- saveItemStatement.TryBind("@PreferredMetadataLanguage", item.PreferredMetadataLanguage);
- saveItemStatement.TryBind("@PreferredMetadataCountryCode", item.PreferredMetadataCountryCode);
-
- if (item.Width > 0)
- {
- saveItemStatement.TryBind("@Width", item.Width);
- }
- else
- {
- saveItemStatement.TryBindNull("@Width");
- }
-
- if (item.Height > 0)
- {
- saveItemStatement.TryBind("@Height", item.Height);
- }
- else
- {
- saveItemStatement.TryBindNull("@Height");
- }
-
- if (item.DateLastRefreshed != default(DateTime))
- {
- saveItemStatement.TryBind("@DateLastRefreshed", item.DateLastRefreshed);
- }
- else
- {
- saveItemStatement.TryBindNull("@DateLastRefreshed");
- }
-
- if (item.DateLastSaved != default(DateTime))
- {
- saveItemStatement.TryBind("@DateLastSaved", item.DateLastSaved);
- }
- else
- {
- saveItemStatement.TryBindNull("@DateLastSaved");
- }
-
- saveItemStatement.TryBind("@IsInMixedFolder", item.IsInMixedFolder);
-
- if (item.LockedFields.Length > 0)
- {
- saveItemStatement.TryBind("@LockedFields", string.Join('|', item.LockedFields));
- }
- else
- {
- saveItemStatement.TryBindNull("@LockedFields");
- }
-
- if (item.Studios.Length > 0)
- {
- saveItemStatement.TryBind("@Studios", string.Join('|', item.Studios));
- }
- else
- {
- saveItemStatement.TryBindNull("@Studios");
- }
-
- if (item.Audio.HasValue)
- {
- saveItemStatement.TryBind("@Audio", item.Audio.Value.ToString());
- }
- else
- {
- saveItemStatement.TryBindNull("@Audio");
- }
-
- if (item is LiveTvChannel liveTvChannel)
- {
- saveItemStatement.TryBind("@ExternalServiceId", liveTvChannel.ServiceName);
- }
- else
- {
- saveItemStatement.TryBindNull("@ExternalServiceId");
- }
-
- if (item.Tags.Length > 0)
- {
- saveItemStatement.TryBind("@Tags", string.Join('|', item.Tags));
- }
- else
- {
- saveItemStatement.TryBindNull("@Tags");
- }
-
- saveItemStatement.TryBind("@IsFolder", item.IsFolder);
-
- saveItemStatement.TryBind("@UnratedType", item.GetBlockUnratedType().ToString());
-
- if (topParent is null)
- {
- saveItemStatement.TryBindNull("@TopParentId");
- }
- else
- {
- saveItemStatement.TryBind("@TopParentId", topParent.Id.ToString("N", CultureInfo.InvariantCulture));
- }
-
- if (item is Trailer trailer && trailer.TrailerTypes.Length > 0)
- {
- saveItemStatement.TryBind("@TrailerTypes", string.Join('|', trailer.TrailerTypes));
- }
- else
- {
- saveItemStatement.TryBindNull("@TrailerTypes");
- }
-
- saveItemStatement.TryBind("@CriticRating", item.CriticRating);
-
- if (string.IsNullOrWhiteSpace(item.Name))
- {
- saveItemStatement.TryBindNull("@CleanName");
- }
- else
- {
- saveItemStatement.TryBind("@CleanName", GetCleanValue(item.Name));
- }
-
- saveItemStatement.TryBind("@PresentationUniqueKey", item.PresentationUniqueKey);
- saveItemStatement.TryBind("@OriginalTitle", item.OriginalTitle);
-
- if (item is Video video)
- {
- saveItemStatement.TryBind("@PrimaryVersionId", video.PrimaryVersionId);
- }
- else
- {
- saveItemStatement.TryBindNull("@PrimaryVersionId");
- }
-
- if (item is Folder folder && folder.DateLastMediaAdded.HasValue)
- {
- saveItemStatement.TryBind("@DateLastMediaAdded", folder.DateLastMediaAdded.Value);
- }
- else
- {
- saveItemStatement.TryBindNull("@DateLastMediaAdded");
- }
-
- saveItemStatement.TryBind("@Album", item.Album);
- saveItemStatement.TryBind("@LUFS", item.LUFS);
- saveItemStatement.TryBind("@NormalizationGain", item.NormalizationGain);
- saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
-
- if (item is IHasSeries hasSeriesName)
- {
- saveItemStatement.TryBind("@SeriesName", hasSeriesName.SeriesName);
- }
- else
- {
- saveItemStatement.TryBindNull("@SeriesName");
- }
-
- if (string.IsNullOrWhiteSpace(userDataKey))
- {
- saveItemStatement.TryBindNull("@UserDataKey");
- }
- else
- {
- saveItemStatement.TryBind("@UserDataKey", userDataKey);
- }
-
- if (item is Episode episode)
- {
- saveItemStatement.TryBind("@SeasonName", episode.SeasonName);
-
- var nullableSeasonId = episode.SeasonId.IsEmpty() ? (Guid?)null : episode.SeasonId;
-
- saveItemStatement.TryBind("@SeasonId", nullableSeasonId);
- }
- else
- {
- saveItemStatement.TryBindNull("@SeasonName");
- saveItemStatement.TryBindNull("@SeasonId");
- }
-
- if (item is IHasSeries hasSeries)
- {
- var nullableSeriesId = hasSeries.SeriesId.IsEmpty() ? (Guid?)null : hasSeries.SeriesId;
-
- saveItemStatement.TryBind("@SeriesId", nullableSeriesId);
- saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey);
- }
- else
- {
- saveItemStatement.TryBindNull("@SeriesId");
- saveItemStatement.TryBindNull("@SeriesPresentationUniqueKey");
- }
-
- saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId);
- saveItemStatement.TryBind("@Tagline", item.Tagline);
-
- saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item.ProviderIds));
- saveItemStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
-
- if (item.ProductionLocations.Length > 0)
- {
- saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations));
- }
- else
- {
- saveItemStatement.TryBindNull("@ProductionLocations");
- }
-
- if (item.ExtraIds.Length > 0)
- {
- saveItemStatement.TryBind("@ExtraIds", string.Join('|', item.ExtraIds));
- }
- else
- {
- saveItemStatement.TryBindNull("@ExtraIds");
- }
-
- saveItemStatement.TryBind("@TotalBitrate", item.TotalBitrate);
- if (item.ExtraType.HasValue)
- {
- saveItemStatement.TryBind("@ExtraType", item.ExtraType.Value.ToString());
- }
- else
- {
- saveItemStatement.TryBindNull("@ExtraType");
- }
-
- string artists = null;
- if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0)
- {
- artists = string.Join('|', hasArtists.Artists);
- }
-
- saveItemStatement.TryBind("@Artists", artists);
-
- string albumArtists = null;
- if (item is IHasAlbumArtist hasAlbumArtists
- && hasAlbumArtists.AlbumArtists.Count > 0)
- {
- albumArtists = string.Join('|', hasAlbumArtists.AlbumArtists);
- }
-
- saveItemStatement.TryBind("@AlbumArtists", albumArtists);
- saveItemStatement.TryBind("@ExternalId", item.ExternalId);
-
- if (item is LiveTvProgram program)
- {
- saveItemStatement.TryBind("@ShowId", program.ShowId);
- }
- else
- {
- saveItemStatement.TryBindNull("@ShowId");
- }
-
- Guid ownerId = item.OwnerId;
- if (ownerId.IsEmpty())
- {
- saveItemStatement.TryBindNull("@OwnerId");
- }
- else
- {
- saveItemStatement.TryBind("@OwnerId", ownerId);
- }
-
- saveItemStatement.ExecuteNonQuery();
- }
-
- internal static string SerializeProviderIds(Dictionary<string, string> providerIds)
- {
- StringBuilder str = new StringBuilder();
- foreach (var i in providerIds)
- {
- // Ideally we shouldn't need this IsNullOrWhiteSpace check,
- // but we're seeing some cases of bad data slip through
- if (string.IsNullOrWhiteSpace(i.Value))
- {
- continue;
- }
-
- str.Append(i.Key)
- .Append('=')
- .Append(i.Value)
- .Append('|');
- }
-
- if (str.Length == 0)
- {
- return null;
- }
-
- str.Length -= 1; // Remove last |
- return str.ToString();
- }
-
- internal static void DeserializeProviderIds(string value, IHasProviderIds item)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return;
- }
-
- foreach (var part in value.SpanSplit('|'))
- {
- var providerDelimiterIndex = part.IndexOf('=');
- // Don't let empty values through
- if (providerDelimiterIndex != -1 && part.Length != providerDelimiterIndex + 1)
- {
- item.SetProviderId(part[..providerDelimiterIndex].ToString(), part[(providerDelimiterIndex + 1)..].ToString());
- }
- }
- }
-
- internal string SerializeImages(ItemImageInfo[] images)
- {
- if (images.Length == 0)
- {
- return null;
- }
-
- StringBuilder str = new StringBuilder();
- foreach (var i in images)
- {
- if (string.IsNullOrWhiteSpace(i.Path))
- {
- continue;
- }
-
- AppendItemImageInfo(str, i);
- str.Append('|');
- }
-
- str.Length -= 1; // Remove last |
- return str.ToString();
- }
-
- internal ItemImageInfo[] DeserializeImages(string value)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return Array.Empty<ItemImageInfo>();
- }
-
- // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed
- var valueSpan = value.AsSpan();
- var count = valueSpan.Count('|') + 1;
-
- var position = 0;
- var result = new ItemImageInfo[count];
- foreach (var part in valueSpan.Split('|'))
- {
- var image = ItemImageInfoFromValueString(part);
-
- if (image is not null)
- {
- result[position++] = image;
- }
- }
-
- if (position == count)
- {
- return result;
- }
-
- if (position == 0)
- {
- return Array.Empty<ItemImageInfo>();
- }
-
- // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
- return result[..position];
- }
-
- private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
- {
- const char Delimiter = '*';
-
- var path = image.Path ?? string.Empty;
-
- bldr.Append(GetPathToSave(path))
- .Append(Delimiter)
- .Append(image.DateModified.Ticks)
- .Append(Delimiter)
- .Append(image.Type)
- .Append(Delimiter)
- .Append(image.Width)
- .Append(Delimiter)
- .Append(image.Height);
-
- var hash = image.BlurHash;
- if (!string.IsNullOrEmpty(hash))
- {
- bldr.Append(Delimiter)
- // Replace delimiters with other characters.
- // This can be removed when we migrate to a proper DB.
- .Append(hash.Replace(Delimiter, '/').Replace('|', '\\'));
- }
- }
-
- internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan<char> value)
- {
- const char Delimiter = '*';
-
- var nextSegment = value.IndexOf(Delimiter);
- if (nextSegment == -1)
- {
- return null;
- }
-
- ReadOnlySpan<char> path = value[..nextSegment];
- value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf(Delimiter);
- if (nextSegment == -1)
- {
- return null;
- }
-
- ReadOnlySpan<char> dateModified = value[..nextSegment];
- value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf(Delimiter);
- if (nextSegment == -1)
- {
- nextSegment = value.Length;
- }
-
- ReadOnlySpan<char> imageType = value[..nextSegment];
-
- var image = new ItemImageInfo
- {
- Path = RestorePath(path.ToString())
- };
-
- if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks)
- && ticks >= DateTime.MinValue.Ticks
- && ticks <= DateTime.MaxValue.Ticks)
- {
- image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
- }
- else
- {
- return null;
- }
-
- if (Enum.TryParse(imageType, true, out ImageType type))
- {
- image.Type = type;
- }
- else
- {
- return null;
- }
-
- // Optional parameters: width*height*blurhash
- if (nextSegment + 1 < value.Length - 1)
- {
- value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf(Delimiter);
- if (nextSegment == -1 || nextSegment == value.Length)
- {
- return image;
- }
-
- ReadOnlySpan<char> widthSpan = value[..nextSegment];
-
- value = value[(nextSegment + 1)..];
- nextSegment = value.IndexOf(Delimiter);
- if (nextSegment == -1)
- {
- nextSegment = value.Length;
- }
-
- ReadOnlySpan<char> heightSpan = value[..nextSegment];
-
- if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
- && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
- {
- image.Width = width;
- image.Height = height;
- }
-
- if (nextSegment < value.Length - 1)
- {
- value = value[(nextSegment + 1)..];
- var length = value.Length;
-
- Span<char> blurHashSpan = stackalloc char[length];
- for (int i = 0; i < length; i++)
- {
- var c = value[i];
- blurHashSpan[i] = c switch
- {
- '/' => Delimiter,
- '\\' => '|',
- _ => c
- };
- }
-
- image.BlurHash = new string(blurHashSpan);
- }
- }
-
- return image;
- }
-
- /// <summary>
- /// Internal retrieve from items or users table.
- /// </summary>
- /// <param name="id">The id.</param>
- /// <returns>BaseItem.</returns>
- /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
- /// <exception cref="ArgumentException"><paramr name="id"/> is <seealso cref="Guid.Empty"/>.</exception>
- public BaseItem RetrieveItem(Guid id)
- {
- if (id.IsEmpty())
- {
- throw new ArgumentException("Guid can't be empty", nameof(id));
- }
-
- CheckDisposed();
-
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
- {
- statement.TryBind("@guid", id);
-
- foreach (var row in statement.ExecuteQuery())
- {
- return GetItem(row, new InternalItemsQuery());
- }
- }
-
- return null;
- }
-
- private bool TypeRequiresDeserialization(Type type)
- {
- if (_config.Configuration.SkipDeserializationForBasicTypes)
- {
- if (type == typeof(Channel)
- || type == typeof(UserRootFolder))
- {
- return false;
- }
- }
-
- return type != typeof(Season)
- && type != typeof(MusicArtist)
- && type != typeof(Person)
- && type != typeof(MusicGenre)
- && type != typeof(Genre)
- && type != typeof(Studio)
- && type != typeof(PlaylistsFolder)
- && type != typeof(PhotoAlbum)
- && type != typeof(Year)
- && type != typeof(Book)
- && type != typeof(LiveTvProgram)
- && type != typeof(AudioBook)
- && type != typeof(MusicAlbum);
- }
-
- private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query)
- {
- return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query), false);
- }
-
- private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields, bool skipDeserialization)
- {
- var typeString = reader.GetString(0);
-
- var type = _typeMapper.GetType(typeString);
-
- if (type is null)
- {
- return null;
- }
-
- BaseItem item = null;
-
- if (TypeRequiresDeserialization(type) && !skipDeserialization)
- {
- try
- {
- item = JsonSerializer.Deserialize(reader.GetStream(1), type, _jsonOptions) as BaseItem;
- }
- catch (JsonException ex)
- {
- Logger.LogError(ex, "Error deserializing item with JSON: {Data}", reader.GetString(1));
- }
- }
-
- if (item is null)
- {
- try
- {
- item = Activator.CreateInstance(type) as BaseItem;
- }
- catch
- {
- }
- }
-
- if (item is null)
- {
- return null;
- }
-
- var index = 2;
-
- if (queryHasStartDate)
- {
- if (item is IHasStartDate hasStartDate && reader.TryReadDateTime(index, out var startDate))
- {
- hasStartDate.StartDate = startDate;
- }
-
- index++;
- }
-
- if (reader.TryReadDateTime(index++, out var endDate))
- {
- item.EndDate = endDate;
- }
-
- if (reader.TryGetGuid(index, out var guid))
- {
- item.ChannelId = guid;
- }
-
- index++;
-
- if (enableProgramAttributes)
- {
- if (item is IHasProgramAttributes hasProgramAttributes)
- {
- if (reader.TryGetBoolean(index++, out var isMovie))
- {
- hasProgramAttributes.IsMovie = isMovie;
- }
-
- if (reader.TryGetBoolean(index++, out var isSeries))
- {
- hasProgramAttributes.IsSeries = isSeries;
- }
-
- if (reader.TryGetString(index++, out var episodeTitle))
- {
- hasProgramAttributes.EpisodeTitle = episodeTitle;
- }
-
- if (reader.TryGetBoolean(index++, out var isRepeat))
- {
- hasProgramAttributes.IsRepeat = isRepeat;
- }
- }
- else
- {
- index += 4;
- }
- }
-
- if (reader.TryGetSingle(index++, out var communityRating))
- {
- item.CommunityRating = communityRating;
- }
-
- if (HasField(query, ItemFields.CustomRating))
- {
- if (reader.TryGetString(index++, out var customRating))
- {
- item.CustomRating = customRating;
- }
- }
-
- if (reader.TryGetInt32(index++, out var indexNumber))
- {
- item.IndexNumber = indexNumber;
- }
-
- if (HasField(query, ItemFields.Settings))
- {
- if (reader.TryGetBoolean(index++, out var isLocked))
- {
- item.IsLocked = isLocked;
- }
-
- if (reader.TryGetString(index++, out var preferredMetadataLanguage))
- {
- item.PreferredMetadataLanguage = preferredMetadataLanguage;
- }
-
- if (reader.TryGetString(index++, out var preferredMetadataCountryCode))
- {
- item.PreferredMetadataCountryCode = preferredMetadataCountryCode;
- }
- }
-
- if (HasField(query, ItemFields.Width))
- {
- if (reader.TryGetInt32(index++, out var width))
- {
- item.Width = width;
- }
- }
-
- if (HasField(query, ItemFields.Height))
- {
- if (reader.TryGetInt32(index++, out var height))
- {
- item.Height = height;
- }
- }
-
- if (HasField(query, ItemFields.DateLastRefreshed))
- {
- if (reader.TryReadDateTime(index++, out var dateLastRefreshed))
- {
- item.DateLastRefreshed = dateLastRefreshed;
- }
- }
-
- if (reader.TryGetString(index++, out var name))
- {
- item.Name = name;
- }
-
- if (reader.TryGetString(index++, out var restorePath))
- {
- item.Path = RestorePath(restorePath);
- }
-
- if (reader.TryReadDateTime(index++, out var premiereDate))
- {
- item.PremiereDate = premiereDate;
- }
-
- if (HasField(query, ItemFields.Overview))
- {
- if (reader.TryGetString(index++, out var overview))
- {
- item.Overview = overview;
- }
- }
-
- if (reader.TryGetInt32(index++, out var parentIndexNumber))
- {
- item.ParentIndexNumber = parentIndexNumber;
- }
-
- if (reader.TryGetInt32(index++, out var productionYear))
- {
- item.ProductionYear = productionYear;
- }
-
- if (reader.TryGetString(index++, out var officialRating))
- {
- item.OfficialRating = officialRating;
- }
-
- if (HasField(query, ItemFields.SortName))
- {
- if (reader.TryGetString(index++, out var forcedSortName))
- {
- item.ForcedSortName = forcedSortName;
- }
- }
-
- if (reader.TryGetInt64(index++, out var runTimeTicks))
- {
- item.RunTimeTicks = runTimeTicks;
- }
-
- if (reader.TryGetInt64(index++, out var size))
- {
- item.Size = size;
- }
-
- if (HasField(query, ItemFields.DateCreated))
- {
- if (reader.TryReadDateTime(index++, out var dateCreated))
- {
- item.DateCreated = dateCreated;
- }
- }
-
- if (reader.TryReadDateTime(index++, out var dateModified))
- {
- item.DateModified = dateModified;
- }
-
- item.Id = reader.GetGuid(index++);
-
- if (HasField(query, ItemFields.Genres))
- {
- if (reader.TryGetString(index++, out var genres))
- {
- item.Genres = genres.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- if (reader.TryGetGuid(index++, out var parentId))
- {
- item.ParentId = parentId;
- }
-
- if (reader.TryGetString(index++, out var audioString))
- {
- if (Enum.TryParse(audioString, true, out ProgramAudio audio))
- {
- item.Audio = audio;
- }
- }
-
- // TODO: Even if not needed by apps, the server needs it internally
- // But get this excluded from contexts where it is not needed
- if (hasServiceName)
- {
- if (item is LiveTvChannel liveTvChannel)
- {
- if (reader.TryGetString(index, out var serviceName))
- {
- liveTvChannel.ServiceName = serviceName;
- }
- }
-
- index++;
- }
-
- if (reader.TryGetBoolean(index++, out var isInMixedFolder))
- {
- item.IsInMixedFolder = isInMixedFolder;
- }
-
- if (HasField(query, ItemFields.DateLastSaved))
- {
- if (reader.TryReadDateTime(index++, out var dateLastSaved))
- {
- item.DateLastSaved = dateLastSaved;
- }
- }
-
- if (HasField(query, ItemFields.Settings))
- {
- if (reader.TryGetString(index++, out var lockedFields))
- {
- List<MetadataField> fields = null;
- foreach (var i in lockedFields.AsSpan().Split('|'))
- {
- if (Enum.TryParse(i, true, out MetadataField parsedValue))
- {
- (fields ??= new List<MetadataField>()).Add(parsedValue);
- }
- }
-
- item.LockedFields = fields?.ToArray() ?? Array.Empty<MetadataField>();
- }
- }
-
- if (HasField(query, ItemFields.Studios))
- {
- if (reader.TryGetString(index++, out var studios))
- {
- item.Studios = studios.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- if (HasField(query, ItemFields.Tags))
- {
- if (reader.TryGetString(index++, out var tags))
- {
- item.Tags = tags.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- if (hasTrailerTypes)
- {
- if (item is Trailer trailer)
- {
- if (reader.TryGetString(index, out var trailerTypes))
- {
- List<TrailerType> types = null;
- foreach (var i in trailerTypes.AsSpan().Split('|'))
- {
- if (Enum.TryParse(i, true, out TrailerType parsedValue))
- {
- (types ??= new List<TrailerType>()).Add(parsedValue);
- }
- }
-
- trailer.TrailerTypes = types?.ToArray() ?? Array.Empty<TrailerType>();
- }
- }
-
- index++;
- }
-
- if (HasField(query, ItemFields.OriginalTitle))
- {
- if (reader.TryGetString(index++, out var originalTitle))
- {
- item.OriginalTitle = originalTitle;
- }
- }
-
- if (item is Video video)
- {
- if (reader.TryGetString(index, out var primaryVersionId))
- {
- video.PrimaryVersionId = primaryVersionId;
- }
- }
-
- index++;
-
- if (HasField(query, ItemFields.DateLastMediaAdded))
- {
- if (item is Folder folder && reader.TryReadDateTime(index, out var dateLastMediaAdded))
- {
- folder.DateLastMediaAdded = dateLastMediaAdded;
- }
-
- index++;
- }
-
- if (reader.TryGetString(index++, out var album))
- {
- item.Album = album;
- }
-
- if (reader.TryGetSingle(index++, out var lUFS))
- {
- item.LUFS = lUFS;
- }
-
- if (reader.TryGetSingle(index++, out var normalizationGain))
- {
- item.NormalizationGain = normalizationGain;
- }
-
- if (reader.TryGetSingle(index++, out var criticRating))
- {
- item.CriticRating = criticRating;
- }
-
- if (reader.TryGetBoolean(index++, out var isVirtualItem))
- {
- item.IsVirtualItem = isVirtualItem;
- }
-
- if (item is IHasSeries hasSeriesName)
- {
- if (reader.TryGetString(index, out var seriesName))
- {
- hasSeriesName.SeriesName = seriesName;
- }
- }
-
- index++;
-
- if (hasEpisodeAttributes)
- {
- if (item is Episode episode)
- {
- if (reader.TryGetString(index, out var seasonName))
- {
- episode.SeasonName = seasonName;
- }
-
- index++;
- if (reader.TryGetGuid(index, out var seasonId))
- {
- episode.SeasonId = seasonId;
- }
- }
- else
- {
- index++;
- }
-
- index++;
- }
-
- var hasSeries = item as IHasSeries;
- if (hasSeriesFields)
- {
- if (hasSeries is not null)
- {
- if (reader.TryGetGuid(index, out var seriesId))
- {
- hasSeries.SeriesId = seriesId;
- }
- }
-
- index++;
- }
-
- if (HasField(query, ItemFields.PresentationUniqueKey))
- {
- if (reader.TryGetString(index++, out var presentationUniqueKey))
- {
- item.PresentationUniqueKey = presentationUniqueKey;
- }
- }
-
- if (HasField(query, ItemFields.InheritedParentalRatingValue))
- {
- if (reader.TryGetInt32(index++, out var parentalRating))
- {
- item.InheritedParentalRatingValue = parentalRating;
- }
- }
-
- if (HasField(query, ItemFields.ExternalSeriesId))
- {
- if (reader.TryGetString(index++, out var externalSeriesId))
- {
- item.ExternalSeriesId = externalSeriesId;
- }
- }
-
- if (HasField(query, ItemFields.Taglines))
- {
- if (reader.TryGetString(index++, out var tagLine))
- {
- item.Tagline = tagLine;
- }
- }
-
- if (item.ProviderIds.Count == 0 && reader.TryGetString(index, out var providerIds))
- {
- DeserializeProviderIds(providerIds, item);
- }
-
- index++;
-
- if (query.DtoOptions.EnableImages)
- {
- if (item.ImageInfos.Length == 0 && reader.TryGetString(index, out var imageInfos))
- {
- item.ImageInfos = DeserializeImages(imageInfos);
- }
-
- index++;
- }
-
- if (HasField(query, ItemFields.ProductionLocations))
- {
- if (reader.TryGetString(index++, out var productionLocations))
- {
- item.ProductionLocations = productionLocations.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
- }
-
- if (HasField(query, ItemFields.ExtraIds))
- {
- if (reader.TryGetString(index++, out var extraIds))
- {
- item.ExtraIds = SplitToGuids(extraIds);
- }
- }
-
- if (reader.TryGetInt32(index++, out var totalBitrate))
- {
- item.TotalBitrate = totalBitrate;
- }
-
- if (reader.TryGetString(index++, out var extraTypeString))
- {
- if (Enum.TryParse(extraTypeString, true, out ExtraType extraType))
- {
- item.ExtraType = extraType;
- }
- }
-
- if (hasArtistFields)
- {
- if (item is IHasArtist hasArtists && reader.TryGetString(index, out var artists))
- {
- hasArtists.Artists = artists.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
-
- index++;
-
- if (item is IHasAlbumArtist hasAlbumArtists && reader.TryGetString(index, out var albumArtists))
- {
- hasAlbumArtists.AlbumArtists = albumArtists.Split('|', StringSplitOptions.RemoveEmptyEntries);
- }
-
- index++;
- }
-
- if (reader.TryGetString(index++, out var externalId))
- {
- item.ExternalId = externalId;
- }
-
- if (HasField(query, ItemFields.SeriesPresentationUniqueKey))
- {
- if (hasSeries is not null)
- {
- if (reader.TryGetString(index, out var seriesPresentationUniqueKey))
- {
- hasSeries.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
- }
- }
-
- index++;
- }
-
- if (enableProgramAttributes)
- {
- if (item is LiveTvProgram program && reader.TryGetString(index, out var showId))
- {
- program.ShowId = showId;
- }
-
- index++;
- }
-
- if (reader.TryGetGuid(index, out var ownerId))
- {
- item.OwnerId = ownerId;
- }
-
- return item;
- }
-
- private static Guid[] SplitToGuids(string value)
- {
- var ids = value.Split('|');
-
- var result = new Guid[ids.Length];
-
- for (var i = 0; i < result.Length; i++)
- {
- result[i] = new Guid(ids[i]);
- }
-
- return result;
- }
-
- /// <inheritdoc />
- public List<ChapterInfo> GetChapters(BaseItem item)
- {
- CheckDisposed();
-
- var chapters = new List<ChapterInfo>();
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
- {
- statement.TryBind("@ItemId", item.Id);
-
- foreach (var row in statement.ExecuteQuery())
- {
- chapters.Add(GetChapter(row, item));
- }
- }
-
- return chapters;
- }
-
- /// <inheritdoc />
- public ChapterInfo GetChapter(BaseItem item, int index)
- {
- CheckDisposed();
-
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
- {
- statement.TryBind("@ItemId", item.Id);
- statement.TryBind("@ChapterIndex", index);
-
- foreach (var row in statement.ExecuteQuery())
- {
- return GetChapter(row, item);
- }
- }
-
- return null;
- }
-
- /// <summary>
- /// Gets the chapter.
- /// </summary>
- /// <param name="reader">The reader.</param>
- /// <param name="item">The item.</param>
- /// <returns>ChapterInfo.</returns>
- private ChapterInfo GetChapter(SqliteDataReader reader, BaseItem item)
- {
- var chapter = new ChapterInfo
- {
- StartPositionTicks = reader.GetInt64(0)
- };
-
- if (reader.TryGetString(1, out var chapterName))
- {
- chapter.Name = chapterName;
- }
-
- if (reader.TryGetString(2, out var imagePath))
- {
- chapter.ImagePath = imagePath;
- chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter);
- }
-
- if (reader.TryReadDateTime(3, out var imageDateModified))
- {
- chapter.ImageDateModified = imageDateModified;
- }
-
- return chapter;
- }
-
- /// <summary>
- /// Saves the chapters.
- /// </summary>
- /// <param name="id">The item id.</param>
- /// <param name="chapters">The chapters.</param>
- public void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters)
- {
- CheckDisposed();
-
- if (id.IsEmpty())
- {
- throw new ArgumentNullException(nameof(id));
- }
-
- ArgumentNullException.ThrowIfNull(chapters);
-
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- // First delete chapters
- using var command = connection.PrepareStatement($"delete from {ChaptersTableName} where ItemId=@ItemId");
- command.TryBind("@ItemId", id);
- command.ExecuteNonQuery();
-
- InsertChapters(id, chapters, connection);
- transaction.Commit();
- }
-
- private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, ManagedConnection db)
- {
- var startIndex = 0;
- var limit = 100;
- var chapterIndex = 0;
-
- const string StartInsertText = "insert into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath, ImageDateModified) values ";
- var insertText = new StringBuilder(StartInsertText, 256);
-
- while (startIndex < chapters.Count)
- {
- var endIndex = Math.Min(chapters.Count, startIndex + limit);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- insertText.AppendFormat(CultureInfo.InvariantCulture, "(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture));
- }
-
- insertText.Length -= 1; // Remove trailing comma
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", idBlob);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var chapter = chapters[i];
-
- statement.TryBind("@ChapterIndex" + index, chapterIndex);
- statement.TryBind("@StartPositionTicks" + index, chapter.StartPositionTicks);
- statement.TryBind("@Name" + index, chapter.Name);
- statement.TryBind("@ImagePath" + index, chapter.ImagePath);
- statement.TryBind("@ImageDateModified" + index, chapter.ImageDateModified);
-
- chapterIndex++;
- }
-
- statement.ExecuteNonQuery();
- }
-
- startIndex += limit;
- insertText.Length = StartInsertText.Length;
- }
- }
-
- private static bool EnableJoinUserData(InternalItemsQuery query)
- {
- if (query.User is null)
- {
- return false;
- }
-
- var sortingFields = new HashSet<ItemSortBy>(query.OrderBy.Select(i => i.OrderBy));
-
- return sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked)
- || sortingFields.Contains(ItemSortBy.IsPlayed)
- || sortingFields.Contains(ItemSortBy.IsUnplayed)
- || sortingFields.Contains(ItemSortBy.PlayCount)
- || sortingFields.Contains(ItemSortBy.DatePlayed)
- || sortingFields.Contains(ItemSortBy.SeriesDatePlayed)
- || query.IsFavoriteOrLiked.HasValue
- || query.IsFavorite.HasValue
- || query.IsResumable.HasValue
- || query.IsPlayed.HasValue
- || query.IsLiked.HasValue;
- }
-
- private bool HasField(InternalItemsQuery query, ItemFields name)
- {
- switch (name)
- {
- case ItemFields.Tags:
- return query.DtoOptions.ContainsField(name) || HasProgramAttributes(query);
- case ItemFields.CustomRating:
- case ItemFields.ProductionLocations:
- case ItemFields.Settings:
- case ItemFields.OriginalTitle:
- case ItemFields.Taglines:
- case ItemFields.SortName:
- case ItemFields.Studios:
- case ItemFields.ExtraIds:
- case ItemFields.DateCreated:
- case ItemFields.Overview:
- case ItemFields.Genres:
- case ItemFields.DateLastMediaAdded:
- case ItemFields.PresentationUniqueKey:
- case ItemFields.InheritedParentalRatingValue:
- case ItemFields.ExternalSeriesId:
- case ItemFields.SeriesPresentationUniqueKey:
- case ItemFields.DateLastRefreshed:
- case ItemFields.DateLastSaved:
- return query.DtoOptions.ContainsField(name);
- case ItemFields.ServiceName:
- return HasServiceName(query);
- default:
- return true;
- }
- }
-
- private bool HasProgramAttributes(InternalItemsQuery query)
- {
- if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value))
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Any(x => _programTypes.Contains(x));
- }
-
- private bool HasServiceName(InternalItemsQuery query)
- {
- if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value))
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Any(x => _serviceTypes.Contains(x));
- }
-
- private bool HasStartDate(InternalItemsQuery query)
- {
- if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value))
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Any(x => _startDateTypes.Contains(x));
- }
-
- private bool HasEpisodeAttributes(InternalItemsQuery query)
- {
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Contains(BaseItemKind.Episode);
- }
-
- private bool HasTrailerTypes(InternalItemsQuery query)
- {
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Contains(BaseItemKind.Trailer);
- }
-
- private bool HasArtistFields(InternalItemsQuery query)
- {
- if (query.ParentType is not null && _artistExcludeParentTypes.Contains(query.ParentType.Value))
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Any(x => _artistsTypes.Contains(x));
- }
-
- private bool HasSeriesFields(InternalItemsQuery query)
- {
- if (query.ParentType == BaseItemKind.PhotoAlbum)
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return query.IncludeItemTypes.Any(x => _seriesTypes.Contains(x));
- }
-
- private void SetFinalColumnsToSelect(InternalItemsQuery query, List<string> columns)
- {
- foreach (var field in _allItemFields)
- {
- if (!HasField(query, field))
- {
- switch (field)
- {
- case ItemFields.Settings:
- columns.Remove("IsLocked");
- columns.Remove("PreferredMetadataCountryCode");
- columns.Remove("PreferredMetadataLanguage");
- columns.Remove("LockedFields");
- break;
- case ItemFields.ServiceName:
- columns.Remove("ExternalServiceId");
- break;
- case ItemFields.SortName:
- columns.Remove("ForcedSortName");
- break;
- case ItemFields.Taglines:
- columns.Remove("Tagline");
- break;
- case ItemFields.Tags:
- columns.Remove("Tags");
- break;
- case ItemFields.IsHD:
- // do nothing
- break;
- default:
- columns.Remove(field.ToString());
- break;
- }
- }
- }
-
- if (!HasProgramAttributes(query))
- {
- columns.Remove("IsMovie");
- columns.Remove("IsSeries");
- columns.Remove("EpisodeTitle");
- columns.Remove("IsRepeat");
- columns.Remove("ShowId");
- }
-
- if (!HasEpisodeAttributes(query))
- {
- columns.Remove("SeasonName");
- columns.Remove("SeasonId");
- }
-
- if (!HasStartDate(query))
- {
- columns.Remove("StartDate");
- }
-
- if (!HasTrailerTypes(query))
- {
- columns.Remove("TrailerTypes");
- }
-
- if (!HasArtistFields(query))
- {
- columns.Remove("AlbumArtists");
- columns.Remove("Artists");
- }
-
- if (!HasSeriesFields(query))
- {
- columns.Remove("SeriesId");
- }
-
- if (!HasEpisodeAttributes(query))
- {
- columns.Remove("SeasonName");
- columns.Remove("SeasonId");
- }
-
- if (!query.DtoOptions.EnableImages)
- {
- columns.Remove("Images");
- }
-
- if (EnableJoinUserData(query))
- {
- columns.Add("UserDatas.UserId");
- columns.Add("UserDatas.lastPlayedDate");
- columns.Add("UserDatas.playbackPositionTicks");
- columns.Add("UserDatas.playcount");
- columns.Add("UserDatas.isFavorite");
- columns.Add("UserDatas.played");
- columns.Add("UserDatas.rating");
- }
-
- if (query.SimilarTo is not null)
- {
- var item = query.SimilarTo;
-
- var builder = new StringBuilder();
- builder.Append('(');
-
- if (item.InheritedParentalRatingValue == 0)
- {
- builder.Append("((InheritedParentalRatingValue=0) * 10)");
- }
- else
- {
- builder.Append(
- @"(SELECT CASE WHEN COALESCE(InheritedParentalRatingValue, 0)=0
- THEN 0
- ELSE 10.0 / (1.0 + ABS(InheritedParentalRatingValue - @InheritedParentalRatingValue))
- END)");
- }
-
- if (item.ProductionYear.HasValue)
- {
- builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 10 Then 10 Else 0 End )");
- builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )");
- }
-
- // genres, tags, studios, person, year?
- builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from ItemValues where ItemId=@SimilarItemId))");
- builder.Append("+ (Select count(1) * 10 from People where ItemId=Guid and Name in (select Name from People where ItemId=@SimilarItemId))");
-
- if (item is MusicArtist)
- {
- // Match albums where the artist is AlbumArtist against other albums.
- // It is assumed that similar albums => similar artists.
- builder.Append(
- @"+ (WITH artistValues AS (
- SELECT DISTINCT albumValues.CleanValue
- FROM ItemValues albumValues
- INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
- INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = @SimilarItemId
- ), similarArtist AS (
- SELECT albumValues.ItemId
- FROM ItemValues albumValues
- INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
- INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = A.Guid
- ) SELECT COUNT(DISTINCT(CleanValue)) * 10 FROM ItemValues WHERE ItemId IN (SELECT ItemId FROM similarArtist) AND CleanValue IN (SELECT CleanValue FROM artistValues))");
- }
-
- builder.Append(") as SimilarityScore");
-
- columns.Add(builder.ToString());
-
- query.ExcludeItemIds = [.. query.ExcludeItemIds, item.Id, .. item.ExtraIds];
- query.ExcludeProviderIds = item.ProviderIds;
- }
-
- if (!string.IsNullOrEmpty(query.SearchTerm))
- {
- var builder = new StringBuilder();
- builder.Append('(');
-
- builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
- builder.Append("+ ((CleanName = @SearchTermStartsWith COLLATE NOCASE or (OriginalTitle not null and OriginalTitle = @SearchTermStartsWith COLLATE NOCASE)) * 10)");
-
- if (query.SearchTerm.Length > 1)
- {
- builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)");
- }
-
- builder.Append(") as SearchScore");
-
- columns.Add(builder.ToString());
- }
- }
-
- private void BindSearchParams(InternalItemsQuery query, SqliteCommand statement)
- {
- var searchTerm = query.SearchTerm;
-
- if (string.IsNullOrEmpty(searchTerm))
- {
- return;
- }
-
- searchTerm = FixUnicodeChars(searchTerm);
- searchTerm = GetCleanValue(searchTerm);
-
- var commandText = statement.CommandText;
- if (commandText.Contains("@SearchTermStartsWith", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@SearchTermStartsWith", searchTerm + "%");
- }
-
- if (commandText.Contains("@SearchTermContains", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@SearchTermContains", "%" + searchTerm + "%");
- }
- }
-
- private void BindSimilarParams(InternalItemsQuery query, SqliteCommand statement)
- {
- var item = query.SimilarTo;
-
- if (item is null)
- {
- return;
- }
-
- var commandText = statement.CommandText;
-
- if (commandText.Contains("@ItemOfficialRating", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@ItemOfficialRating", item.OfficialRating);
- }
-
- if (commandText.Contains("@ItemProductionYear", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@ItemProductionYear", item.ProductionYear ?? 0);
- }
-
- if (commandText.Contains("@SimilarItemId", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@SimilarItemId", item.Id);
- }
-
- if (commandText.Contains("@InheritedParentalRatingValue", StringComparison.OrdinalIgnoreCase))
- {
- statement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue);
- }
- }
-
- private string GetJoinUserDataText(InternalItemsQuery query)
- {
- if (!EnableJoinUserData(query))
- {
- return string.Empty;
- }
-
- return " left join UserDatas on UserDataKey=UserDatas.Key And (UserId=@UserId)";
- }
-
- private string GetGroupBy(InternalItemsQuery query)
- {
- var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(query);
- if (enableGroupByPresentationUniqueKey && query.GroupBySeriesPresentationUniqueKey)
- {
- return " Group by PresentationUniqueKey, SeriesPresentationUniqueKey";
- }
-
- if (enableGroupByPresentationUniqueKey)
- {
- return " Group by PresentationUniqueKey";
- }
-
- if (query.GroupBySeriesPresentationUniqueKey)
- {
- return " Group by SeriesPresentationUniqueKey";
- }
-
- return string.Empty;
- }
-
- /// <inheritdoc />
- public int GetCount(InternalItemsQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- // Hack for right now since we currently don't support filtering out these duplicates within a query
- if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
- {
- query.Limit = query.Limit.Value + 4;
- }
-
- var columns = new List<string> { "count(distinct PresentationUniqueKey)" };
- SetFinalColumnsToSelect(query, columns);
- var commandTextBuilder = new StringBuilder("select ", 256)
- .AppendJoin(',', columns)
- .Append(FromText)
- .Append(GetJoinUserDataText(query));
-
- var whereClauses = GetWhereClauses(query, null);
- if (whereClauses.Count != 0)
- {
- commandTextBuilder.Append(" where ")
- .AppendJoin(" AND ", whereClauses);
- }
-
- var commandText = commandTextBuilder.ToString();
-
- using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- return statement.SelectScalarInt();
- }
- }
-
- /// <inheritdoc />
- public List<BaseItem> GetItemList(InternalItemsQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- // Hack for right now since we currently don't support filtering out these duplicates within a query
- if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
- {
- query.Limit = query.Limit.Value + 4;
- }
-
- var columns = _retrieveItemColumns.ToList();
- SetFinalColumnsToSelect(query, columns);
- var commandTextBuilder = new StringBuilder("select ", 1024)
- .AppendJoin(',', columns)
- .Append(FromText)
- .Append(GetJoinUserDataText(query));
-
- var whereClauses = GetWhereClauses(query, null);
-
- if (whereClauses.Count != 0)
- {
- commandTextBuilder.Append(" where ")
- .AppendJoin(" AND ", whereClauses);
- }
-
- commandTextBuilder.Append(GetGroupBy(query))
- .Append(GetOrderByText(query));
-
- if (query.Limit.HasValue || query.StartIndex.HasValue)
- {
- var offset = query.StartIndex ?? 0;
-
- if (query.Limit.HasValue || offset > 0)
- {
- commandTextBuilder.Append(" LIMIT ")
- .Append(query.Limit ?? int.MaxValue);
- }
-
- if (offset > 0)
- {
- commandTextBuilder.Append(" OFFSET ")
- .Append(offset);
- }
- }
-
- var commandText = commandTextBuilder.ToString();
- var items = new List<BaseItem>();
- using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- var hasEpisodeAttributes = HasEpisodeAttributes(query);
- var hasServiceName = HasServiceName(query);
- var hasProgramAttributes = HasProgramAttributes(query);
- var hasStartDate = HasStartDate(query);
- var hasTrailerTypes = HasTrailerTypes(query);
- var hasArtistFields = HasArtistFields(query);
- var hasSeriesFields = HasSeriesFields(query);
-
- foreach (var row in statement.ExecuteQuery())
- {
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, query.SkipDeserialization);
- if (item is not null)
- {
- items.Add(item);
- }
- }
- }
-
- // Hack for right now since we currently don't support filtering out these duplicates within a query
- if (query.EnableGroupByMetadataKey)
- {
- var limit = query.Limit ?? int.MaxValue;
- limit -= 4;
- var newList = new List<BaseItem>();
-
- foreach (var item in items)
- {
- AddItem(newList, item);
-
- if (newList.Count >= limit)
- {
- break;
- }
- }
-
- items = newList;
- }
-
- return items;
- }
-
- private string FixUnicodeChars(string buffer)
- {
- buffer = buffer.Replace('\u2013', '-'); // en dash
- buffer = buffer.Replace('\u2014', '-'); // em dash
- buffer = buffer.Replace('\u2015', '-'); // horizontal bar
- buffer = buffer.Replace('\u2017', '_'); // double low line
- buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
- buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
- buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
- buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
- buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
- buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
- buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
- buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
- buffer = buffer.Replace('\u2032', '\''); // prime
- buffer = buffer.Replace('\u2033', '\"'); // double prime
- buffer = buffer.Replace('\u0060', '\''); // grave accent
- return buffer.Replace('\u00B4', '\''); // acute accent
- }
-
- private void AddItem(List<BaseItem> items, BaseItem newItem)
- {
- for (var i = 0; i < items.Count; i++)
- {
- var item = items[i];
-
- foreach (var providerId in newItem.ProviderIds)
- {
- if (string.Equals(providerId.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.Ordinal))
- {
- continue;
- }
-
- if (string.Equals(item.GetProviderId(providerId.Key), providerId.Value, StringComparison.Ordinal))
- {
- if (newItem.SourceType == SourceType.Library)
- {
- items[i] = newItem;
- }
-
- return;
- }
- }
- }
-
- items.Add(newItem);
- }
-
- /// <inheritdoc />
- public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0))
- {
- var returnList = GetItemList(query);
- return new QueryResult<BaseItem>(
- query.StartIndex,
- returnList.Count,
- returnList);
- }
-
- // Hack for right now since we currently don't support filtering out these duplicates within a query
- if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
- {
- query.Limit = query.Limit.Value + 4;
- }
-
- var columns = _retrieveItemColumns.ToList();
- SetFinalColumnsToSelect(query, columns);
- var commandTextBuilder = new StringBuilder("select ", 512)
- .AppendJoin(',', columns)
- .Append(FromText)
- .Append(GetJoinUserDataText(query));
-
- var whereClauses = GetWhereClauses(query, null);
-
- var whereText = whereClauses.Count == 0 ?
- string.Empty :
- string.Join(" AND ", whereClauses);
-
- if (!string.IsNullOrEmpty(whereText))
- {
- commandTextBuilder.Append(" where ")
- .Append(whereText);
- }
-
- commandTextBuilder.Append(GetGroupBy(query))
- .Append(GetOrderByText(query));
-
- if (query.Limit.HasValue || query.StartIndex.HasValue)
- {
- var offset = query.StartIndex ?? 0;
-
- if (query.Limit.HasValue || offset > 0)
- {
- commandTextBuilder.Append(" LIMIT ")
- .Append(query.Limit ?? int.MaxValue);
- }
-
- if (offset > 0)
- {
- commandTextBuilder.Append(" OFFSET ")
- .Append(offset);
- }
- }
-
- var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
-
- var itemQuery = string.Empty;
- var totalRecordCountQuery = string.Empty;
- if (!isReturningZeroItems)
- {
- itemQuery = commandTextBuilder.ToString();
- }
-
- if (query.EnableTotalRecordCount)
- {
- commandTextBuilder.Clear();
-
- commandTextBuilder.Append(" select ");
-
- List<string> columnsToSelect;
- if (EnableGroupByPresentationUniqueKey(query))
- {
- columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
- }
- else if (query.GroupBySeriesPresentationUniqueKey)
- {
- columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" };
- }
- else
- {
- columnsToSelect = new List<string> { "count (guid)" };
- }
-
- SetFinalColumnsToSelect(query, columnsToSelect);
-
- commandTextBuilder.AppendJoin(',', columnsToSelect)
- .Append(FromText)
- .Append(GetJoinUserDataText(query));
- if (!string.IsNullOrEmpty(whereText))
- {
- commandTextBuilder.Append(" where ")
- .Append(whereText);
- }
-
- totalRecordCountQuery = commandTextBuilder.ToString();
- }
-
- var list = new List<BaseItem>();
- var result = new QueryResult<BaseItem>();
- using var connection = GetConnection(true);
- using var transaction = connection.BeginTransaction();
- if (!isReturningZeroItems)
- {
- using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery"))
- using (var statement = PrepareStatement(connection, itemQuery))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- var hasEpisodeAttributes = HasEpisodeAttributes(query);
- var hasServiceName = HasServiceName(query);
- var hasProgramAttributes = HasProgramAttributes(query);
- var hasStartDate = HasStartDate(query);
- var hasTrailerTypes = HasTrailerTypes(query);
- var hasArtistFields = HasArtistFields(query);
- var hasSeriesFields = HasSeriesFields(query);
-
- foreach (var row in statement.ExecuteQuery())
- {
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false);
- if (item is not null)
- {
- list.Add(item);
- }
- }
- }
- }
-
- if (query.EnableTotalRecordCount)
- {
- using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount"))
- using (var statement = PrepareStatement(connection, totalRecordCountQuery))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- result.TotalRecordCount = statement.SelectScalarInt();
- }
- }
-
- transaction.Commit();
-
- result.StartIndex = query.StartIndex ?? 0;
- result.Items = list;
- return result;
- }
-
- private string GetOrderByText(InternalItemsQuery query)
- {
- var orderBy = query.OrderBy;
- bool hasSimilar = query.SimilarTo is not null;
- bool hasSearch = !string.IsNullOrEmpty(query.SearchTerm);
-
- if (hasSimilar || hasSearch)
- {
- List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4);
- if (hasSearch)
- {
- prepend.Add((ItemSortBy.SearchScore, SortOrder.Descending));
- prepend.Add((ItemSortBy.SortName, SortOrder.Ascending));
- }
-
- if (hasSimilar)
- {
- prepend.Add((ItemSortBy.SimilarityScore, SortOrder.Descending));
- prepend.Add((ItemSortBy.Random, SortOrder.Ascending));
- }
-
- orderBy = query.OrderBy = [.. prepend, .. orderBy];
- }
- else if (orderBy.Count == 0)
- {
- return string.Empty;
- }
-
- return " ORDER BY " + string.Join(',', orderBy.Select(i =>
- {
- var sortBy = MapOrderByField(i.OrderBy, query);
- var sortOrder = i.SortOrder == SortOrder.Ascending ? "ASC" : "DESC";
- return sortBy + " " + sortOrder;
- }));
- }
-
- private string MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query)
- {
- return sortBy switch
- {
- ItemSortBy.AirTime => "SortName", // TODO
- ItemSortBy.Runtime => "RuntimeTicks",
- ItemSortBy.Random => "RANDOM()",
- ItemSortBy.DatePlayed when query.GroupBySeriesPresentationUniqueKey => "MAX(LastPlayedDate)",
- ItemSortBy.DatePlayed => "LastPlayedDate",
- ItemSortBy.PlayCount => "PlayCount",
- ItemSortBy.IsFavoriteOrLiked => "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )",
- ItemSortBy.IsFolder => "IsFolder",
- ItemSortBy.IsPlayed => "played",
- ItemSortBy.IsUnplayed => "played",
- ItemSortBy.DateLastContentAdded => "DateLastMediaAdded",
- ItemSortBy.Artist => "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)",
- ItemSortBy.AlbumArtist => "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)",
- ItemSortBy.OfficialRating => "InheritedParentalRatingValue",
- ItemSortBy.Studio => "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)",
- ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)",
- ItemSortBy.SeriesSortName => "SeriesName",
- ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
- ItemSortBy.Album => "Album",
- ItemSortBy.DateCreated => "DateCreated",
- ItemSortBy.PremiereDate => "PremiereDate",
- ItemSortBy.StartDate => "StartDate",
- ItemSortBy.Name => "Name",
- ItemSortBy.CommunityRating => "CommunityRating",
- ItemSortBy.ProductionYear => "ProductionYear",
- ItemSortBy.CriticRating => "CriticRating",
- ItemSortBy.VideoBitRate => "VideoBitRate",
- ItemSortBy.ParentIndexNumber => "ParentIndexNumber",
- ItemSortBy.IndexNumber => "IndexNumber",
- ItemSortBy.SimilarityScore => "SimilarityScore",
- ItemSortBy.SearchScore => "SearchScore",
- _ => "SortName"
- };
- }
-
- /// <inheritdoc />
- public List<Guid> GetItemIdsList(InternalItemsQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- var columns = new List<string> { "guid" };
- SetFinalColumnsToSelect(query, columns);
- var commandTextBuilder = new StringBuilder("select ", 256)
- .AppendJoin(',', columns)
- .Append(FromText)
- .Append(GetJoinUserDataText(query));
-
- var whereClauses = GetWhereClauses(query, null);
- if (whereClauses.Count != 0)
- {
- commandTextBuilder.Append(" where ")
- .AppendJoin(" AND ", whereClauses);
- }
-
- commandTextBuilder.Append(GetGroupBy(query))
- .Append(GetOrderByText(query));
-
- if (query.Limit.HasValue || query.StartIndex.HasValue)
- {
- var offset = query.StartIndex ?? 0;
-
- if (query.Limit.HasValue || offset > 0)
- {
- commandTextBuilder.Append(" LIMIT ")
- .Append(query.Limit ?? int.MaxValue);
- }
-
- if (offset > 0)
- {
- commandTextBuilder.Append(" OFFSET ")
- .Append(offset);
- }
- }
-
- var commandText = commandTextBuilder.ToString();
- var list = new List<Guid>();
- using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(row.GetGuid(0));
- }
- }
-
- return list;
- }
-
- private bool IsAlphaNumeric(string str)
- {
- if (string.IsNullOrWhiteSpace(str))
- {
- return false;
- }
-
- for (int i = 0; i < str.Length; i++)
- {
- if (!char.IsLetter(str[i]) && !char.IsNumber(str[i]))
- {
- return false;
- }
- }
-
- return true;
- }
-
- private bool IsValidPersonType(string value)
- {
- return IsAlphaNumeric(value);
- }
-
-#nullable enable
- private List<string> GetWhereClauses(InternalItemsQuery query, SqliteCommand? statement)
- {
- if (query.IsResumable ?? false)
- {
- query.IsVirtualItem = false;
- }
-
- var minWidth = query.MinWidth;
- var maxWidth = query.MaxWidth;
-
- if (query.IsHD.HasValue)
- {
- const int Threshold = 1200;
- if (query.IsHD.Value)
- {
- minWidth = Threshold;
- }
- else
- {
- maxWidth = Threshold - 1;
- }
- }
-
- if (query.Is4K.HasValue)
- {
- const int Threshold = 3800;
- if (query.Is4K.Value)
- {
- minWidth = Threshold;
- }
- else
- {
- maxWidth = Threshold - 1;
- }
- }
-
- var whereClauses = new List<string>();
-
- if (minWidth.HasValue)
- {
- whereClauses.Add("Width>=@MinWidth");
- statement?.TryBind("@MinWidth", minWidth);
- }
-
- if (query.MinHeight.HasValue)
- {
- whereClauses.Add("Height>=@MinHeight");
- statement?.TryBind("@MinHeight", query.MinHeight);
- }
-
- if (maxWidth.HasValue)
- {
- whereClauses.Add("Width<=@MaxWidth");
- statement?.TryBind("@MaxWidth", maxWidth);
- }
-
- if (query.MaxHeight.HasValue)
- {
- whereClauses.Add("Height<=@MaxHeight");
- statement?.TryBind("@MaxHeight", query.MaxHeight);
- }
-
- if (query.IsLocked.HasValue)
- {
- whereClauses.Add("IsLocked=@IsLocked");
- statement?.TryBind("@IsLocked", query.IsLocked);
- }
-
- var tags = query.Tags.ToList();
- var excludeTags = query.ExcludeTags.ToList();
-
- if (query.IsMovie == true)
- {
- if (query.IncludeItemTypes.Length == 0
- || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
- || query.IncludeItemTypes.Contains(BaseItemKind.Trailer))
- {
- whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)");
- }
- else
- {
- whereClauses.Add("IsMovie=@IsMovie");
- }
-
- statement?.TryBind("@IsMovie", true);
- }
- else if (query.IsMovie.HasValue)
- {
- whereClauses.Add("IsMovie=@IsMovie");
- statement?.TryBind("@IsMovie", query.IsMovie);
- }
-
- if (query.IsSeries.HasValue)
- {
- whereClauses.Add("IsSeries=@IsSeries");
- statement?.TryBind("@IsSeries", query.IsSeries);
- }
-
- if (query.IsSports.HasValue)
- {
- if (query.IsSports.Value)
- {
- tags.Add("Sports");
- }
- else
- {
- excludeTags.Add("Sports");
- }
- }
-
- if (query.IsNews.HasValue)
- {
- if (query.IsNews.Value)
- {
- tags.Add("News");
- }
- else
- {
- excludeTags.Add("News");
- }
- }
-
- if (query.IsKids.HasValue)
- {
- if (query.IsKids.Value)
- {
- tags.Add("Kids");
- }
- else
- {
- excludeTags.Add("Kids");
- }
- }
-
- if (query.SimilarTo is not null && query.MinSimilarityScore > 0)
- {
- whereClauses.Add("SimilarityScore > " + (query.MinSimilarityScore - 1).ToString(CultureInfo.InvariantCulture));
- }
-
- if (!string.IsNullOrEmpty(query.SearchTerm))
- {
- whereClauses.Add("SearchScore > 0");
- }
-
- if (query.IsFolder.HasValue)
- {
- whereClauses.Add("IsFolder=@IsFolder");
- statement?.TryBind("@IsFolder", query.IsFolder);
- }
-
- var includeTypes = query.IncludeItemTypes;
- // Only specify excluded types if no included types are specified
- if (query.IncludeItemTypes.Length == 0)
- {
- var excludeTypes = query.ExcludeItemTypes;
- if (excludeTypes.Length == 1)
- {
- 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 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)
- {
- 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 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)
- {
- whereClauses.Add("ChannelId=@ChannelId");
- statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture));
- }
- else if (query.ChannelIds.Count > 1)
- {
- var inClause = string.Join(',', query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
- whereClauses.Add($"ChannelId in ({inClause})");
- }
-
- if (!query.ParentId.IsEmpty())
- {
- whereClauses.Add("ParentId=@ParentId");
- statement?.TryBind("@ParentId", query.ParentId);
- }
-
- if (!string.IsNullOrWhiteSpace(query.Path))
- {
- whereClauses.Add("Path=@Path");
- statement?.TryBind("@Path", GetPathToSave(query.Path));
- }
-
- if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
- {
- whereClauses.Add("PresentationUniqueKey=@PresentationUniqueKey");
- statement?.TryBind("@PresentationUniqueKey", query.PresentationUniqueKey);
- }
-
- if (query.MinCommunityRating.HasValue)
- {
- whereClauses.Add("CommunityRating>=@MinCommunityRating");
- statement?.TryBind("@MinCommunityRating", query.MinCommunityRating.Value);
- }
-
- if (query.MinIndexNumber.HasValue)
- {
- whereClauses.Add("IndexNumber>=@MinIndexNumber");
- statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value);
- }
-
- if (query.MinParentAndIndexNumber.HasValue)
- {
- whereClauses.Add("((ParentIndexNumber=@MinParentAndIndexNumberParent and IndexNumber>=@MinParentAndIndexNumberIndex) or ParentIndexNumber>@MinParentAndIndexNumberParent)");
- statement?.TryBind("@MinParentAndIndexNumberParent", query.MinParentAndIndexNumber.Value.ParentIndexNumber);
- statement?.TryBind("@MinParentAndIndexNumberIndex", query.MinParentAndIndexNumber.Value.IndexNumber);
- }
-
- if (query.MinDateCreated.HasValue)
- {
- whereClauses.Add("DateCreated>=@MinDateCreated");
- statement?.TryBind("@MinDateCreated", query.MinDateCreated.Value);
- }
-
- if (query.MinDateLastSaved.HasValue)
- {
- whereClauses.Add("(DateLastSaved not null and DateLastSaved>=@MinDateLastSavedForUser)");
- statement?.TryBind("@MinDateLastSaved", query.MinDateLastSaved.Value);
- }
-
- if (query.MinDateLastSavedForUser.HasValue)
- {
- whereClauses.Add("(DateLastSaved not null and DateLastSaved>=@MinDateLastSavedForUser)");
- statement?.TryBind("@MinDateLastSavedForUser", query.MinDateLastSavedForUser.Value);
- }
-
- if (query.IndexNumber.HasValue)
- {
- whereClauses.Add("IndexNumber=@IndexNumber");
- statement?.TryBind("@IndexNumber", query.IndexNumber.Value);
- }
-
- if (query.ParentIndexNumber.HasValue)
- {
- whereClauses.Add("ParentIndexNumber=@ParentIndexNumber");
- statement?.TryBind("@ParentIndexNumber", query.ParentIndexNumber.Value);
- }
-
- if (query.ParentIndexNumberNotEquals.HasValue)
- {
- whereClauses.Add("(ParentIndexNumber<>@ParentIndexNumberNotEquals or ParentIndexNumber is null)");
- statement?.TryBind("@ParentIndexNumberNotEquals", query.ParentIndexNumberNotEquals.Value);
- }
-
- var minEndDate = query.MinEndDate;
- var maxEndDate = query.MaxEndDate;
-
- if (query.HasAired.HasValue)
- {
- if (query.HasAired.Value)
- {
- maxEndDate = DateTime.UtcNow;
- }
- else
- {
- minEndDate = DateTime.UtcNow;
- }
- }
-
- if (minEndDate.HasValue)
- {
- whereClauses.Add("EndDate>=@MinEndDate");
- statement?.TryBind("@MinEndDate", minEndDate.Value);
- }
-
- if (maxEndDate.HasValue)
- {
- whereClauses.Add("EndDate<=@MaxEndDate");
- statement?.TryBind("@MaxEndDate", maxEndDate.Value);
- }
-
- if (query.MinStartDate.HasValue)
- {
- whereClauses.Add("StartDate>=@MinStartDate");
- statement?.TryBind("@MinStartDate", query.MinStartDate.Value);
- }
-
- if (query.MaxStartDate.HasValue)
- {
- whereClauses.Add("StartDate<=@MaxStartDate");
- statement?.TryBind("@MaxStartDate", query.MaxStartDate.Value);
- }
-
- if (query.MinPremiereDate.HasValue)
- {
- whereClauses.Add("PremiereDate>=@MinPremiereDate");
- statement?.TryBind("@MinPremiereDate", query.MinPremiereDate.Value);
- }
-
- if (query.MaxPremiereDate.HasValue)
- {
- whereClauses.Add("PremiereDate<=@MaxPremiereDate");
- statement?.TryBind("@MaxPremiereDate", query.MaxPremiereDate.Value);
- }
-
- StringBuilder clauseBuilder = new StringBuilder();
- const string Or = " OR ";
-
- var trailerTypes = query.TrailerTypes;
- int trailerTypesLen = trailerTypes.Length;
- if (trailerTypesLen > 0)
- {
- clauseBuilder.Append('(');
-
- for (int i = 0; i < trailerTypesLen; i++)
- {
- var paramName = "@TrailerTypes" + i;
- clauseBuilder.Append("TrailerTypes like ")
- .Append(paramName)
- .Append(Or);
- statement?.TryBind(paramName, "%" + trailerTypes[i] + "%");
- }
-
- clauseBuilder.Length -= Or.Length;
- clauseBuilder.Append(')');
-
- whereClauses.Add(clauseBuilder.ToString());
-
- clauseBuilder.Length = 0;
- }
-
- if (query.IsAiring.HasValue)
- {
- if (query.IsAiring.Value)
- {
- whereClauses.Add("StartDate<=@MaxStartDate");
- statement?.TryBind("@MaxStartDate", DateTime.UtcNow);
-
- whereClauses.Add("EndDate>=@MinEndDate");
- statement?.TryBind("@MinEndDate", DateTime.UtcNow);
- }
- else
- {
- whereClauses.Add("(StartDate>@IsAiringDate OR EndDate < @IsAiringDate)");
- statement?.TryBind("@IsAiringDate", DateTime.UtcNow);
- }
- }
-
- int personIdsLen = query.PersonIds.Length;
- if (personIdsLen > 0)
- {
- // TODO: Should this query with CleanName ?
-
- clauseBuilder.Append('(');
-
- Span<byte> idBytes = stackalloc byte[16];
- for (int i = 0; i < personIdsLen; i++)
- {
- string paramName = "@PersonId" + i;
- clauseBuilder.Append("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=")
- .Append(paramName)
- .Append("))) OR ");
-
- statement?.TryBind(paramName, query.PersonIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- clauseBuilder.Append(')');
-
- whereClauses.Add(clauseBuilder.ToString());
-
- clauseBuilder.Length = 0;
- }
-
- if (!string.IsNullOrWhiteSpace(query.Person))
- {
- whereClauses.Add("Guid in (select ItemId from People where Name=@PersonName)");
- statement?.TryBind("@PersonName", query.Person);
- }
-
- if (!string.IsNullOrWhiteSpace(query.MinSortName))
- {
- whereClauses.Add("SortName>=@MinSortName");
- statement?.TryBind("@MinSortName", query.MinSortName);
- }
-
- if (!string.IsNullOrWhiteSpace(query.ExternalSeriesId))
- {
- whereClauses.Add("ExternalSeriesId=@ExternalSeriesId");
- statement?.TryBind("@ExternalSeriesId", query.ExternalSeriesId);
- }
-
- if (!string.IsNullOrWhiteSpace(query.ExternalId))
- {
- whereClauses.Add("ExternalId=@ExternalId");
- statement?.TryBind("@ExternalId", query.ExternalId);
- }
-
- if (!string.IsNullOrWhiteSpace(query.Name))
- {
- whereClauses.Add("CleanName=@Name");
- statement?.TryBind("@Name", GetCleanValue(query.Name));
- }
-
- // These are the same, for now
- var nameContains = query.NameContains;
- if (!string.IsNullOrWhiteSpace(nameContains))
- {
- whereClauses.Add("(CleanName like @NameContains or OriginalTitle like @NameContains)");
- if (statement is not null)
- {
- nameContains = FixUnicodeChars(nameContains);
- statement.TryBind("@NameContains", "%" + GetCleanValue(nameContains) + "%");
- }
- }
-
- if (!string.IsNullOrWhiteSpace(query.NameStartsWith))
- {
- whereClauses.Add("SortName like @NameStartsWith");
- statement?.TryBind("@NameStartsWith", query.NameStartsWith + "%");
- }
-
- if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater))
- {
- whereClauses.Add("SortName >= @NameStartsWithOrGreater");
- // lowercase this because SortName is stored as lowercase
- statement?.TryBind("@NameStartsWithOrGreater", query.NameStartsWithOrGreater.ToLowerInvariant());
- }
-
- if (!string.IsNullOrWhiteSpace(query.NameLessThan))
- {
- whereClauses.Add("SortName < @NameLessThan");
- // lowercase this because SortName is stored as lowercase
- statement?.TryBind("@NameLessThan", query.NameLessThan.ToLowerInvariant());
- }
-
- if (query.ImageTypes.Length > 0)
- {
- foreach (var requiredImage in query.ImageTypes)
- {
- whereClauses.Add("Images like '%" + requiredImage + "%'");
- }
- }
-
- if (query.IsLiked.HasValue)
- {
- if (query.IsLiked.Value)
- {
- whereClauses.Add("rating>=@UserRating");
- statement?.TryBind("@UserRating", UserItemData.MinLikeValue);
- }
- else
- {
- whereClauses.Add("(rating is null or rating<@UserRating)");
- statement?.TryBind("@UserRating", UserItemData.MinLikeValue);
- }
- }
-
- if (query.IsFavoriteOrLiked.HasValue)
- {
- if (query.IsFavoriteOrLiked.Value)
- {
- whereClauses.Add("IsFavorite=@IsFavoriteOrLiked");
- }
- else
- {
- whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavoriteOrLiked)");
- }
-
- statement?.TryBind("@IsFavoriteOrLiked", query.IsFavoriteOrLiked.Value);
- }
-
- if (query.IsFavorite.HasValue)
- {
- if (query.IsFavorite.Value)
- {
- whereClauses.Add("IsFavorite=@IsFavorite");
- }
- else
- {
- whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavorite)");
- }
-
- statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
- }
-
- if (EnableJoinUserData(query))
- {
- 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 && query.IncludeItemTypes[0] == BaseItemKind.Series)
- {
- if (query.IsPlayed.Value)
- {
- whereClauses.Add("PresentationUniqueKey not in (select S.SeriesPresentationUniqueKey from TypedBaseitems S left join UserDatas UD on S.UserDataKey=UD.Key And UD.UserId=@UserId where Coalesce(UD.Played, 0)=0 and S.IsFolder=0 and S.IsVirtualItem=0 and S.SeriesPresentationUniqueKey not null)");
- }
- else
- {
- whereClauses.Add("PresentationUniqueKey in (select S.SeriesPresentationUniqueKey from TypedBaseitems S left join UserDatas UD on S.UserDataKey=UD.Key And UD.UserId=@UserId where Coalesce(UD.Played, 0)=0 and S.IsFolder=0 and S.IsVirtualItem=0 and S.SeriesPresentationUniqueKey not null)");
- }
- }
- else
- {
- if (query.IsPlayed.Value)
- {
- whereClauses.Add("(played=@IsPlayed)");
- }
- else
- {
- whereClauses.Add("(played is null or played=@IsPlayed)");
- }
-
- statement?.TryBind("@IsPlayed", query.IsPlayed.Value);
- }
- }
- }
-
- if (query.IsResumable.HasValue)
- {
- if (query.IsResumable.Value)
- {
- whereClauses.Add("playbackPositionTicks > 0");
- }
- else
- {
- whereClauses.Add("(playbackPositionTicks is null or playbackPositionTicks = 0)");
- }
- }
-
- if (query.ArtistIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.ArtistIds.Length; i++)
- {
- clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds")
- .Append(i)
- .Append(") and Type<=1)) OR ");
- statement?.TryBind("@ArtistIds" + i, query.ArtistIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.AlbumArtistIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.AlbumArtistIds.Length; i++)
- {
- clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds")
- .Append(i)
- .Append(") and Type=1)) OR ");
- statement?.TryBind("@ArtistIds" + i, query.AlbumArtistIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.ContributingArtistIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.ContributingArtistIds.Length; i++)
- {
- clauseBuilder.Append("((select CleanName from TypedBaseItems where guid=@ArtistIds")
- .Append(i)
- .Append(") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=@ArtistIds")
- .Append(i)
- .Append(") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1)) OR ");
- statement?.TryBind("@ArtistIds" + i, query.ContributingArtistIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.AlbumIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.AlbumIds.Length; i++)
- {
- clauseBuilder.Append("Album in (select Name from typedbaseitems where guid=@AlbumIds")
- .Append(i)
- .Append(") OR ");
- statement?.TryBind("@AlbumIds" + i, query.AlbumIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.ExcludeArtistIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.ExcludeArtistIds.Length; i++)
- {
- clauseBuilder.Append("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ExcludeArtistId")
- .Append(i)
- .Append(") and Type<=1)) OR ");
- statement?.TryBind("@ExcludeArtistId" + i, query.ExcludeArtistIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.GenreIds.Count > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.GenreIds.Count; i++)
- {
- clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreId")
- .Append(i)
- .Append(") and Type=2)) OR ");
- statement?.TryBind("@GenreId" + i, query.GenreIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.Genres.Count > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.Genres.Count; i++)
- {
- clauseBuilder.Append("@Genre")
- .Append(i)
- .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=2) OR ");
- statement?.TryBind("@Genre" + i, GetCleanValue(query.Genres[i]));
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (tags.Count > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < tags.Count; i++)
- {
- clauseBuilder.Append("@Tag")
- .Append(i)
- .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR ");
- statement?.TryBind("@Tag" + i, GetCleanValue(tags[i]));
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (excludeTags.Count > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < excludeTags.Count; i++)
- {
- clauseBuilder.Append("@ExcludeTag")
- .Append(i)
- .Append(" not in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR ");
- statement?.TryBind("@ExcludeTag" + i, GetCleanValue(excludeTags[i]));
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.StudioIds.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.StudioIds.Length; i++)
- {
- clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@StudioId")
- .Append(i)
- .Append(") and Type=3)) OR ");
- statement?.TryBind("@StudioId" + i, query.StudioIds[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.OfficialRatings.Length > 0)
- {
- clauseBuilder.Append('(');
- for (var i = 0; i < query.OfficialRatings.Length; i++)
- {
- clauseBuilder.Append("OfficialRating=@OfficialRating").Append(i).Append(Or);
- statement?.TryBind("@OfficialRating" + i, query.OfficialRatings[i]);
- }
-
- clauseBuilder.Length -= Or.Length;
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- clauseBuilder.Append('(');
- if (query.HasParentalRating ?? false)
- {
- clauseBuilder.Append("InheritedParentalRatingValue not null");
- if (query.MinParentalRating.HasValue)
- {
- clauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating");
- statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
- }
-
- if (query.MaxParentalRating.HasValue)
- {
- clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
- statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
- }
- }
- else if (query.BlockUnratedItems.Length > 0)
- {
- const string ParamName = "@UnratedType";
- clauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in (");
-
- for (int i = 0; i < query.BlockUnratedItems.Length; i++)
- {
- clauseBuilder.Append(ParamName).Append(i).Append(',');
- statement?.TryBind(ParamName + i, query.BlockUnratedItems[i].ToString());
- }
-
- // Remove trailing comma
- clauseBuilder.Length--;
- clauseBuilder.Append("))");
-
- if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
- {
- clauseBuilder.Append(" OR (");
- }
-
- if (query.MinParentalRating.HasValue)
- {
- clauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating");
- statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
- }
-
- if (query.MaxParentalRating.HasValue)
- {
- if (query.MinParentalRating.HasValue)
- {
- clauseBuilder.Append(" AND ");
- }
-
- clauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating");
- statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
- }
-
- if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
- {
- clauseBuilder.Append(')');
- }
-
- if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue))
- {
- clauseBuilder.Append(" OR InheritedParentalRatingValue not null");
- }
- }
- else if (query.MinParentalRating.HasValue)
- {
- clauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating");
- statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
-
- if (query.MaxParentalRating.HasValue)
- {
- clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
- statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
- }
-
- clauseBuilder.Append(')');
- }
- else if (query.MaxParentalRating.HasValue)
- {
- clauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating");
- statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
- }
- else if (!query.HasParentalRating ?? false)
- {
- clauseBuilder.Append("InheritedParentalRatingValue is null");
- }
-
- if (clauseBuilder.Length > 1)
- {
- whereClauses.Add(clauseBuilder.Append(')').ToString());
- clauseBuilder.Length = 0;
- }
-
- if (query.HasOfficialRating.HasValue)
- {
- if (query.HasOfficialRating.Value)
- {
- whereClauses.Add("(OfficialRating not null AND OfficialRating<>'')");
- }
- else
- {
- whereClauses.Add("(OfficialRating is null OR OfficialRating='')");
- }
- }
-
- if (query.HasOverview.HasValue)
- {
- if (query.HasOverview.Value)
- {
- whereClauses.Add("(Overview not null AND Overview<>'')");
- }
- else
- {
- whereClauses.Add("(Overview is null OR Overview='')");
- }
- }
-
- if (query.HasOwnerId.HasValue)
- {
- if (query.HasOwnerId.Value)
- {
- whereClauses.Add("OwnerId not null");
- }
- else
- {
- whereClauses.Add("OwnerId is null");
- }
- }
-
- if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage))
- {
- whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Audio' and MediaStreams.Language=@HasNoAudioTrackWithLanguage limit 1) is null)");
- statement?.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage);
- }
-
- if (!string.IsNullOrWhiteSpace(query.HasNoInternalSubtitleTrackWithLanguage))
- {
- whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=0 and MediaStreams.Language=@HasNoInternalSubtitleTrackWithLanguage limit 1) is null)");
- statement?.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage);
- }
-
- if (!string.IsNullOrWhiteSpace(query.HasNoExternalSubtitleTrackWithLanguage))
- {
- whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=1 and MediaStreams.Language=@HasNoExternalSubtitleTrackWithLanguage limit 1) is null)");
- statement?.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage);
- }
-
- if (!string.IsNullOrWhiteSpace(query.HasNoSubtitleTrackWithLanguage))
- {
- whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.Language=@HasNoSubtitleTrackWithLanguage limit 1) is null)");
- statement?.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage);
- }
-
- if (query.HasSubtitles.HasValue)
- {
- if (query.HasSubtitles.Value)
- {
- whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) not null)");
- }
- else
- {
- whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) is null)");
- }
- }
-
- if (query.HasChapterImages.HasValue)
- {
- if (query.HasChapterImages.Value)
- {
- whereClauses.Add("((select imagepath from Chapters2 where Chapters2.ItemId=A.Guid and imagepath not null limit 1) not null)");
- }
- else
- {
- whereClauses.Add("((select imagepath from Chapters2 where Chapters2.ItemId=A.Guid and imagepath not null limit 1) is null)");
- }
- }
-
- if (query.HasDeadParentId.HasValue && query.HasDeadParentId.Value)
- {
- whereClauses.Add("ParentId NOT NULL AND ParentId NOT IN (select guid from TypedBaseItems)");
- }
-
- if (query.IsDeadArtist.HasValue && query.IsDeadArtist.Value)
- {
- whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type in (0,1))");
- }
-
- if (query.IsDeadStudio.HasValue && query.IsDeadStudio.Value)
- {
- whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type = 3)");
- }
-
- if (query.IsDeadPerson.HasValue && query.IsDeadPerson.Value)
- {
- whereClauses.Add("Name not in (Select Name From People)");
- }
-
- if (query.Years.Length == 1)
- {
- whereClauses.Add("ProductionYear=@Years");
- statement?.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture));
- }
- else if (query.Years.Length > 1)
- {
- var val = string.Join(',', query.Years);
- whereClauses.Add("ProductionYear in (" + val + ")");
- }
-
- var isVirtualItem = query.IsVirtualItem ?? query.IsMissing;
- if (isVirtualItem.HasValue)
- {
- whereClauses.Add("IsVirtualItem=@IsVirtualItem");
- statement?.TryBind("@IsVirtualItem", isVirtualItem.Value);
- }
-
- if (query.IsSpecialSeason.HasValue)
- {
- if (query.IsSpecialSeason.Value)
- {
- whereClauses.Add("IndexNumber = 0");
- }
- else
- {
- whereClauses.Add("IndexNumber <> 0");
- }
- }
-
- if (query.IsUnaired.HasValue)
- {
- if (query.IsUnaired.Value)
- {
- whereClauses.Add("PremiereDate >= DATETIME('now')");
- }
- else
- {
- whereClauses.Add("PremiereDate < DATETIME('now')");
- }
- }
-
- if (query.MediaTypes.Length == 1)
- {
- whereClauses.Add("MediaType=@MediaTypes");
- statement?.TryBind("@MediaTypes", query.MediaTypes[0].ToString());
- }
- else if (query.MediaTypes.Length > 1)
- {
- var val = string.Join(',', query.MediaTypes.Select(i => $"'{i}'"));
- whereClauses.Add("MediaType in (" + val + ")");
- }
-
- if (query.ItemIds.Length > 0)
- {
- var includeIds = new List<string>();
- var index = 0;
- foreach (var id in query.ItemIds)
- {
- includeIds.Add("Guid = @IncludeId" + index);
- statement?.TryBind("@IncludeId" + index, id);
- index++;
- }
-
- whereClauses.Add("(" + string.Join(" OR ", includeIds) + ")");
- }
-
- if (query.ExcludeItemIds.Length > 0)
- {
- var excludeIds = new List<string>();
- var index = 0;
- foreach (var id in query.ExcludeItemIds)
- {
- excludeIds.Add("Guid <> @ExcludeId" + index);
- statement?.TryBind("@ExcludeId" + index, id);
- index++;
- }
-
- whereClauses.Add(string.Join(" AND ", excludeIds));
- }
-
- if (query.ExcludeProviderIds is not null && query.ExcludeProviderIds.Count > 0)
- {
- var excludeIds = new List<string>();
-
- var index = 0;
- foreach (var pair in query.ExcludeProviderIds)
- {
- if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- var paramName = "@ExcludeProviderId" + index;
- excludeIds.Add("(ProviderIds is null or ProviderIds not like " + paramName + ")");
- statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
- index++;
-
- break;
- }
-
- if (excludeIds.Count > 0)
- {
- whereClauses.Add(string.Join(" AND ", excludeIds));
- }
- }
-
- if (query.HasAnyProviderId is not null && query.HasAnyProviderId.Count > 0)
- {
- var hasProviderIds = new List<string>();
-
- var index = 0;
- foreach (var pair in query.HasAnyProviderId)
- {
- if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- // TODO this seems to be an idea for a better schema where ProviderIds are their own table
- // but this is not implemented
- // hasProviderIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")");
-
- // TODO this is a really BAD way to do it since the pair:
- // Tmdb, 1234 matches Tmdb=1234 but also Tmdb=1234567
- // and maybe even NotTmdb=1234.
-
- // this is a placeholder for this specific pair to correlate it in the bigger query
- var paramName = "@HasAnyProviderId" + index;
-
- // this is a search for the placeholder
- hasProviderIds.Add("ProviderIds like " + paramName);
-
- // this replaces the placeholder with a value, here: %key=val%
- statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
- index++;
-
- break;
- }
-
- if (hasProviderIds.Count > 0)
- {
- whereClauses.Add("(" + string.Join(" OR ", hasProviderIds) + ")");
- }
- }
-
- if (query.HasImdbId.HasValue)
- {
- whereClauses.Add(GetProviderIdClause(query.HasImdbId.Value, "imdb"));
- }
-
- if (query.HasTmdbId.HasValue)
- {
- whereClauses.Add(GetProviderIdClause(query.HasTmdbId.Value, "tmdb"));
- }
-
- if (query.HasTvdbId.HasValue)
- {
- whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb"));
- }
-
- var queryTopParentIds = query.TopParentIds;
-
- if (queryTopParentIds.Length > 0)
- {
- var includedItemByNameTypes = GetItemByNameTypesInQuery(query);
- var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
-
- if (queryTopParentIds.Length == 1)
- {
- if (enableItemsByName && includedItemByNameTypes.Count == 1)
- {
- whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)");
- statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
- }
- else if (enableItemsByName && includedItemByNameTypes.Count > 1)
- {
- var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
- whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
- }
- else
- {
- whereClauses.Add("(TopParentId=@TopParentId)");
- }
-
- statement?.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture));
- }
- else if (queryTopParentIds.Length > 1)
- {
- var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
-
- if (enableItemsByName && includedItemByNameTypes.Count == 1)
- {
- whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))");
- statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
- }
- else if (enableItemsByName && includedItemByNameTypes.Count > 1)
- {
- var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
- whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
- }
- else
- {
- whereClauses.Add("TopParentId in (" + val + ")");
- }
- }
- }
-
- if (query.AncestorIds.Length == 1)
- {
- whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)");
- statement?.TryBind("@AncestorId", query.AncestorIds[0]);
- }
-
- if (query.AncestorIds.Length > 1)
- {
- var inClause = string.Join(',', query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
- whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
- }
-
- if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey))
- {
- var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey";
- whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
- statement?.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
- }
-
- if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey))
- {
- whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey");
- statement?.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey);
- }
-
- if (query.ExcludeInheritedTags.Length > 0)
- {
- var paramName = "@ExcludeInheritedTags";
- if (statement is null)
- {
- int index = 0;
- string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(_ => paramName + index++));
- whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
- }
- else
- {
- for (int index = 0; index < query.ExcludeInheritedTags.Length; index++)
- {
- statement.TryBind(paramName + index, GetCleanValue(query.ExcludeInheritedTags[index]));
- }
- }
- }
-
- if (query.IncludeInheritedTags.Length > 0)
- {
- var paramName = "@IncludeInheritedTags";
- if (statement is null)
- {
- int index = 0;
- string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++));
- // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client.
- // In addtion to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well.
- if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
- {
- whereClauses.Add($"""
- ((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null
- OR (select CleanValue from ItemValues where ItemId=ParentId and Type=6 and CleanValue in ({includedTags})) is not null)
- """);
- }
-
- // A playlist should be accessible to its owner regardless of allowed tags.
- else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
- {
- whereClauses.Add($"""
- ((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null
- OR data like @PlaylistOwnerUserId)
- """);
- }
- else
- {
- whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
- }
- }
- else
- {
- for (int index = 0; index < query.IncludeInheritedTags.Length; index++)
- {
- statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index]));
- }
-
- if (query.User is not null)
- {
- statement.TryBind("@PlaylistOwnerUserId", $"""%"OwnerUserId":"{query.User.Id.ToString("N")}"%""");
- }
- }
- }
-
- if (query.SeriesStatuses.Length > 0)
- {
- var statuses = new List<string>();
-
- foreach (var seriesStatus in query.SeriesStatuses)
- {
- statuses.Add("data like '%" + seriesStatus + "%'");
- }
-
- whereClauses.Add("(" + string.Join(" OR ", statuses) + ")");
- }
-
- if (query.BoxSetLibraryFolders.Length > 0)
- {
- var folderIdQueries = new List<string>();
-
- foreach (var folderId in query.BoxSetLibraryFolders)
- {
- folderIdQueries.Add("data like '%" + folderId.ToString("N", CultureInfo.InvariantCulture) + "%'");
- }
-
- whereClauses.Add("(" + string.Join(" OR ", folderIdQueries) + ")");
- }
-
- if (query.VideoTypes.Length > 0)
- {
- var videoTypes = new List<string>();
-
- foreach (var videoType in query.VideoTypes)
- {
- videoTypes.Add("data like '%\"VideoType\":\"" + videoType + "\"%'");
- }
-
- whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")");
- }
-
- if (query.Is3D.HasValue)
- {
- if (query.Is3D.Value)
- {
- whereClauses.Add("data like '%Video3DFormat%'");
- }
- else
- {
- whereClauses.Add("data not like '%Video3DFormat%'");
- }
- }
-
- if (query.IsPlaceHolder.HasValue)
- {
- if (query.IsPlaceHolder.Value)
- {
- whereClauses.Add("data like '%\"IsPlaceHolder\":true%'");
- }
- else
- {
- whereClauses.Add("(data is null or data not like '%\"IsPlaceHolder\":true%')");
- }
- }
-
- if (query.HasSpecialFeature.HasValue)
- {
- if (query.HasSpecialFeature.Value)
- {
- whereClauses.Add("ExtraIds not null");
- }
- else
- {
- whereClauses.Add("ExtraIds is null");
- }
- }
-
- if (query.HasTrailer.HasValue)
- {
- if (query.HasTrailer.Value)
- {
- whereClauses.Add("ExtraIds not null");
- }
- else
- {
- whereClauses.Add("ExtraIds is null");
- }
- }
-
- if (query.HasThemeSong.HasValue)
- {
- if (query.HasThemeSong.Value)
- {
- whereClauses.Add("ExtraIds not null");
- }
- else
- {
- whereClauses.Add("ExtraIds is null");
- }
- }
-
- if (query.HasThemeVideo.HasValue)
- {
- if (query.HasThemeVideo.Value)
- {
- whereClauses.Add("ExtraIds not null");
- }
- else
- {
- whereClauses.Add("ExtraIds is null");
- }
- }
-
- return whereClauses;
- }
-
- /// <summary>
- /// Formats a where clause for the specified provider.
- /// </summary>
- /// <param name="includeResults">Whether or not to include items with this provider's ids.</param>
- /// <param name="provider">Provider name.</param>
- /// <returns>Formatted SQL clause.</returns>
- private string GetProviderIdClause(bool includeResults, string provider)
- {
- return string.Format(
- CultureInfo.InvariantCulture,
- "ProviderIds {0} like '%{1}=%'",
- includeResults ? string.Empty : "not",
- provider);
- }
-
-#nullable disable
- private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
- {
- var list = new List<string>();
-
- if (IsTypeInQuery(BaseItemKind.Person, query))
- {
- list.Add(typeof(Person).FullName);
- }
-
- if (IsTypeInQuery(BaseItemKind.Genre, query))
- {
- list.Add(typeof(Genre).FullName);
- }
-
- if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
- {
- list.Add(typeof(MusicGenre).FullName);
- }
-
- if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
- {
- list.Add(typeof(MusicArtist).FullName);
- }
-
- if (IsTypeInQuery(BaseItemKind.Studio, query))
- {
- list.Add(typeof(Studio).FullName);
- }
-
- return list;
- }
-
- private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
- {
- if (query.ExcludeItemTypes.Contains(type))
- {
- return false;
- }
-
- return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
- }
-
- private string GetCleanValue(string value)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return value;
- }
-
- return value.RemoveDiacritics().ToLowerInvariant();
- }
-
- private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
- {
- if (!query.GroupByPresentationUniqueKey)
- {
- return false;
- }
-
- if (query.GroupBySeriesPresentationUniqueKey)
- {
- return false;
- }
-
- if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
- {
- return false;
- }
-
- if (query.User is null)
- {
- return false;
- }
-
- if (query.IncludeItemTypes.Length == 0)
- {
- return true;
- }
-
- return 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);
- }
-
- /// <inheritdoc />
- public void UpdateInheritedValues()
- {
- const string Statements = """
-delete from ItemValues where type = 6;
-insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4;
-insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
-FROM AncestorIds
-LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId)
-where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4;
-""";
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- connection.Execute(Statements);
- transaction.Commit();
- }
-
- /// <inheritdoc />
- public void DeleteItem(Guid id)
- {
- if (id.IsEmpty())
- {
- throw new ArgumentNullException(nameof(id));
- }
-
- CheckDisposed();
-
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- // Delete people
- ExecuteWithSingleParam(connection, "delete from People where ItemId=@Id", id);
-
- // Delete chapters
- ExecuteWithSingleParam(connection, "delete from " + ChaptersTableName + " where ItemId=@Id", id);
-
- // Delete media streams
- ExecuteWithSingleParam(connection, "delete from mediastreams where ItemId=@Id", id);
-
- // Delete ancestors
- ExecuteWithSingleParam(connection, "delete from AncestorIds where ItemId=@Id", id);
-
- // Delete item values
- ExecuteWithSingleParam(connection, "delete from ItemValues where ItemId=@Id", id);
-
- // Delete the item
- ExecuteWithSingleParam(connection, "delete from TypedBaseItems where guid=@Id", id);
-
- transaction.Commit();
- }
-
- private void ExecuteWithSingleParam(ManagedConnection db, string query, Guid value)
- {
- using (var statement = PrepareStatement(db, query))
- {
- statement.TryBind("@Id", value);
-
- statement.ExecuteNonQuery();
- }
- }
-
- /// <inheritdoc />
- public List<string> GetPeopleNames(InternalPeopleQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- var commandText = new StringBuilder("select Distinct p.Name from People p");
-
- var whereClauses = GetPeopleWhereClauses(query, null);
-
- if (whereClauses.Count != 0)
- {
- commandText.Append(" where ").AppendJoin(" AND ", whereClauses);
- }
-
- commandText.Append(" order by ListOrder");
-
- if (query.Limit > 0)
- {
- commandText.Append(" LIMIT ").Append(query.Limit);
- }
-
- var list = new List<string>();
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText.ToString()))
- {
- // Run this again to bind the params
- GetPeopleWhereClauses(query, statement);
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(row.GetString(0));
- }
- }
-
- return list;
- }
-
- /// <inheritdoc />
- public List<PersonInfo> GetPeople(InternalPeopleQuery query)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- CheckDisposed();
-
- StringBuilder commandText = new StringBuilder("select ItemId, Name, Role, PersonType, SortOrder from People p");
-
- var whereClauses = GetPeopleWhereClauses(query, null);
-
- if (whereClauses.Count != 0)
- {
- commandText.Append(" where ").AppendJoin(" AND ", whereClauses);
- }
-
- commandText.Append(" order by ListOrder");
-
- if (query.Limit > 0)
- {
- commandText.Append(" LIMIT ").Append(query.Limit);
- }
-
- var list = new List<PersonInfo>();
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText.ToString()))
- {
- // Run this again to bind the params
- GetPeopleWhereClauses(query, statement);
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(GetPerson(row));
- }
- }
-
- return list;
- }
-
- private List<string> GetPeopleWhereClauses(InternalPeopleQuery query, SqliteCommand statement)
- {
- var whereClauses = new List<string>();
-
- if (query.User is not null && query.IsFavorite.HasValue)
- {
- whereClauses.Add(@"p.Name IN (
-SELECT Name FROM TypedBaseItems WHERE UserDataKey IN (
-SELECT key FROM UserDatas WHERE isFavorite=@IsFavorite AND userId=@UserId)
-AND Type = @InternalPersonType)");
- statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
- statement?.TryBind("@InternalPersonType", typeof(Person).FullName);
- statement?.TryBind("@UserId", query.User.InternalId);
- }
-
- if (!query.ItemId.IsEmpty())
- {
- whereClauses.Add("ItemId=@ItemId");
- statement?.TryBind("@ItemId", query.ItemId);
- }
-
- if (!query.AppearsInItemId.IsEmpty())
- {
- whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
- statement?.TryBind("@AppearsInItemId", query.AppearsInItemId);
- }
-
- var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
-
- if (queryPersonTypes.Count == 1)
- {
- whereClauses.Add("PersonType=@PersonType");
- statement?.TryBind("@PersonType", queryPersonTypes[0]);
- }
- else if (queryPersonTypes.Count > 1)
- {
- var val = string.Join(',', queryPersonTypes.Select(i => "'" + i + "'"));
-
- whereClauses.Add("PersonType in (" + val + ")");
- }
-
- var queryExcludePersonTypes = query.ExcludePersonTypes.Where(IsValidPersonType).ToList();
-
- if (queryExcludePersonTypes.Count == 1)
- {
- whereClauses.Add("PersonType<>@PersonType");
- statement?.TryBind("@PersonType", queryExcludePersonTypes[0]);
- }
- else if (queryExcludePersonTypes.Count > 1)
- {
- var val = string.Join(',', queryExcludePersonTypes.Select(i => "'" + i + "'"));
-
- whereClauses.Add("PersonType not in (" + val + ")");
- }
-
- if (query.MaxListOrder.HasValue)
- {
- whereClauses.Add("ListOrder<=@MaxListOrder");
- statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value);
- }
-
- if (!string.IsNullOrWhiteSpace(query.NameContains))
- {
- whereClauses.Add("p.Name like @NameContains");
- statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
- }
-
- return whereClauses;
- }
-
- private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, ManagedConnection db, SqliteCommand deleteAncestorsStatement)
- {
- if (itemId.IsEmpty())
- {
- throw new ArgumentNullException(nameof(itemId));
- }
-
- ArgumentNullException.ThrowIfNull(ancestorIds);
-
- CheckDisposed();
-
- // First delete
- deleteAncestorsStatement.TryBind("@ItemId", itemId);
- deleteAncestorsStatement.ExecuteNonQuery();
-
- if (ancestorIds.Count == 0)
- {
- return;
- }
-
- var insertText = new StringBuilder("insert into AncestorIds (ItemId, AncestorId, AncestorIdText) values ");
-
- for (var i = 0; i < ancestorIds.Count; i++)
- {
- insertText.AppendFormat(
- CultureInfo.InvariantCulture,
- "(@ItemId, @AncestorId{0}, @AncestorIdText{0}),",
- i.ToString(CultureInfo.InvariantCulture));
- }
-
- // Remove trailing comma
- insertText.Length--;
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", itemId);
-
- for (var i = 0; i < ancestorIds.Count; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var ancestorId = ancestorIds[i];
-
- statement.TryBind("@AncestorId" + index, ancestorId);
- statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture));
- }
-
- statement.ExecuteNonQuery();
- }
- }
-
- /// <inheritdoc />
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName);
- }
-
- /// <inheritdoc />
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName);
- }
-
- /// <inheritdoc />
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName);
- }
-
- /// <inheritdoc />
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName);
- }
-
- /// <inheritdoc />
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName);
- }
-
- /// <inheritdoc />
- public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
- {
- return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName);
- }
-
- /// <inheritdoc />
- public List<string> GetStudioNames()
- {
- return GetItemValueNames(new[] { 3 }, Array.Empty<string>(), Array.Empty<string>());
- }
-
- /// <inheritdoc />
- public List<string> GetAllArtistNames()
- {
- return GetItemValueNames(new[] { 0, 1 }, Array.Empty<string>(), Array.Empty<string>());
- }
-
- /// <inheritdoc />
- public List<string> GetMusicGenreNames()
- {
- return GetItemValueNames(
- new[] { 2 },
- new string[]
- {
- typeof(Audio).FullName,
- typeof(MusicVideo).FullName,
- typeof(MusicAlbum).FullName,
- typeof(MusicArtist).FullName
- },
- Array.Empty<string>());
- }
-
- /// <inheritdoc />
- public List<string> GetGenreNames()
- {
- return GetItemValueNames(
- new[] { 2 },
- Array.Empty<string>(),
- new string[]
- {
- typeof(Audio).FullName,
- typeof(MusicVideo).FullName,
- typeof(MusicAlbum).FullName,
- typeof(MusicArtist).FullName
- });
- }
-
- private List<string> GetItemValueNames(int[] itemValueTypes, IReadOnlyList<string> withItemTypes, IReadOnlyList<string> excludeItemTypes)
- {
- CheckDisposed();
-
- var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128);
- if (itemValueTypes.Length == 1)
- {
- stringBuilder.Append('=')
- .Append(itemValueTypes[0]);
- }
- else
- {
- stringBuilder.Append(" in (")
- .AppendJoin(',', itemValueTypes)
- .Append(')');
- }
-
- if (withItemTypes.Count > 0)
- {
- stringBuilder.Append(" AND ItemId In (select guid from typedbaseitems where type in (")
- .AppendJoinInSingleQuotes(',', withItemTypes)
- .Append("))");
- }
-
- if (excludeItemTypes.Count > 0)
- {
- stringBuilder.Append(" AND ItemId not In (select guid from typedbaseitems where type in (")
- .AppendJoinInSingleQuotes(',', excludeItemTypes)
- .Append("))");
- }
-
- stringBuilder.Append(" Group By CleanValue");
- var commandText = stringBuilder.ToString();
-
- var list = new List<string>();
- using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText))
- {
- foreach (var row in statement.ExecuteQuery())
- {
- if (row.TryGetString(0, out var result))
- {
- list.Add(result);
- }
- }
- }
-
- return list;
- }
-
- private QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType)
- {
- ArgumentNullException.ThrowIfNull(query);
-
- if (!query.Limit.HasValue)
- {
- query.EnableTotalRecordCount = false;
- }
-
- CheckDisposed();
-
- var typeClause = itemValueTypes.Length == 1 ?
- ("Type=" + itemValueTypes[0]) :
- ("Type in (" + string.Join(',', itemValueTypes) + ")");
-
- InternalItemsQuery typeSubQuery = null;
-
- string itemCountColumns = null;
-
- var stringBuilder = new StringBuilder(1024);
- var typesToCount = query.IncludeItemTypes;
-
- if (typesToCount.Length > 0)
- {
- stringBuilder.Append("(select group_concat(type, '|') from TypedBaseItems B");
-
- typeSubQuery = new InternalItemsQuery(query.User)
- {
- ExcludeItemTypes = query.ExcludeItemTypes,
- IncludeItemTypes = query.IncludeItemTypes,
- MediaTypes = query.MediaTypes,
- AncestorIds = query.AncestorIds,
- ExcludeItemIds = query.ExcludeItemIds,
- ItemIds = query.ItemIds,
- TopParentIds = query.TopParentIds,
- ParentId = query.ParentId,
- IsPlayed = query.IsPlayed
- };
- var whereClauses = GetWhereClauses(typeSubQuery, null);
-
- stringBuilder.Append(" where ")
- .AppendJoin(" AND ", whereClauses)
- .Append(" AND ")
- .Append("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND ")
- .Append(typeClause)
- .Append(")) as itemTypes");
-
- itemCountColumns = stringBuilder.ToString();
- stringBuilder.Clear();
- }
-
- 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))
- {
- columns.Add(itemCountColumns);
- }
-
- // do this first before calling GetFinalColumnsToSelect, otherwise ExcludeItemIds will be set by SimilarTo
- var innerQuery = new InternalItemsQuery(query.User)
- {
- ExcludeItemTypes = query.ExcludeItemTypes,
- IncludeItemTypes = query.IncludeItemTypes,
- MediaTypes = query.MediaTypes,
- AncestorIds = query.AncestorIds,
- ItemIds = query.ItemIds,
- TopParentIds = query.TopParentIds,
- ParentId = query.ParentId,
- IsAiring = query.IsAiring,
- IsMovie = query.IsMovie,
- IsSports = query.IsSports,
- IsKids = query.IsKids,
- IsNews = query.IsNews,
- IsSeries = query.IsSeries
- };
-
- SetFinalColumnsToSelect(query, columns);
-
- var innerWhereClauses = GetWhereClauses(innerQuery, null);
-
- stringBuilder.Append(" where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where ")
- .Append(typeClause)
- .Append(" AND ItemId in (select guid from TypedBaseItems");
- if (innerWhereClauses.Count > 0)
- {
- stringBuilder.Append(" where ")
- .AppendJoin(" AND ", innerWhereClauses);
- }
-
- stringBuilder.Append("))");
-
- var outerQuery = new InternalItemsQuery(query.User)
- {
- IsPlayed = query.IsPlayed,
- IsFavorite = query.IsFavorite,
- IsFavoriteOrLiked = query.IsFavoriteOrLiked,
- IsLiked = query.IsLiked,
- IsLocked = query.IsLocked,
- NameLessThan = query.NameLessThan,
- NameStartsWith = query.NameStartsWith,
- NameStartsWithOrGreater = query.NameStartsWithOrGreater,
- Tags = query.Tags,
- OfficialRatings = query.OfficialRatings,
- StudioIds = query.StudioIds,
- GenreIds = query.GenreIds,
- Genres = query.Genres,
- Years = query.Years,
- NameContains = query.NameContains,
- SearchTerm = query.SearchTerm,
- SimilarTo = query.SimilarTo,
- ExcludeItemIds = query.ExcludeItemIds
- };
-
- var outerWhereClauses = GetWhereClauses(outerQuery, null);
- if (outerWhereClauses.Count != 0)
- {
- stringBuilder.Append(" AND ")
- .AppendJoin(" AND ", outerWhereClauses);
- }
-
- var whereText = stringBuilder.ToString();
- stringBuilder.Clear();
-
- stringBuilder.Append("select ")
- .AppendJoin(',', columns)
- .Append(FromText)
- .Append(GetJoinUserDataText(query))
- .Append(whereText)
- .Append(" group by PresentationUniqueKey");
-
- if (query.OrderBy.Count != 0
- || query.SimilarTo is not null
- || !string.IsNullOrEmpty(query.SearchTerm))
- {
- stringBuilder.Append(GetOrderByText(query));
- }
- else
- {
- stringBuilder.Append(" order by SortName");
- }
-
- if (query.Limit.HasValue || query.StartIndex.HasValue)
- {
- var offset = query.StartIndex ?? 0;
-
- if (query.Limit.HasValue || offset > 0)
- {
- stringBuilder.Append(" LIMIT ")
- .Append(query.Limit ?? int.MaxValue);
- }
-
- if (offset > 0)
- {
- stringBuilder.Append(" OFFSET ")
- .Append(offset);
- }
- }
-
- var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
-
- string commandText = string.Empty;
-
- if (!isReturningZeroItems)
- {
- commandText = stringBuilder.ToString();
- }
-
- string countText = string.Empty;
- if (query.EnableTotalRecordCount)
- {
- stringBuilder.Clear();
- var columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
- SetFinalColumnsToSelect(query, columnsToSelect);
- stringBuilder.Append("select ")
- .AppendJoin(',', columnsToSelect)
- .Append(FromText)
- .Append(GetJoinUserDataText(query))
- .Append(whereText);
-
- countText = stringBuilder.ToString();
- }
-
- var list = new List<(BaseItem, ItemCounts)>();
- var result = new QueryResult<(BaseItem, ItemCounts)>();
- using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
- using (var transaction = connection.BeginTransaction())
- {
- if (!isReturningZeroItems)
- {
- using (var statement = PrepareStatement(connection, commandText))
- {
- statement.TryBind("@SelectType", returnType);
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- if (typeSubQuery is not null)
- {
- GetWhereClauses(typeSubQuery, null);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
- GetWhereClauses(innerQuery, statement);
- GetWhereClauses(outerQuery, statement);
-
- var hasEpisodeAttributes = HasEpisodeAttributes(query);
- var hasProgramAttributes = HasProgramAttributes(query);
- var hasServiceName = HasServiceName(query);
- var hasStartDate = HasStartDate(query);
- var hasTrailerTypes = HasTrailerTypes(query);
- var hasArtistFields = HasArtistFields(query);
- var hasSeriesFields = HasSeriesFields(query);
-
- foreach (var row in statement.ExecuteQuery())
- {
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false);
- if (item is not null)
- {
- var countStartColumn = columns.Count - 1;
-
- list.Add((item, GetItemCounts(row, countStartColumn, typesToCount)));
- }
- }
- }
- }
-
- if (query.EnableTotalRecordCount)
- {
- using (var statement = PrepareStatement(connection, countText))
- {
- statement.TryBind("@SelectType", returnType);
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- if (typeSubQuery is not null)
- {
- GetWhereClauses(typeSubQuery, null);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
- GetWhereClauses(innerQuery, statement);
- GetWhereClauses(outerQuery, statement);
-
- result.TotalRecordCount = statement.SelectScalarInt();
- }
- }
-
- transaction.Commit();
- }
-
- if (result.TotalRecordCount == 0)
- {
- result.TotalRecordCount = list.Count;
- }
-
- result.StartIndex = query.StartIndex ?? 0;
- result.Items = list;
-
- return result;
- }
-
- private static ItemCounts GetItemCounts(SqliteDataReader reader, int countStartColumn, BaseItemKind[] typesToCount)
- {
- var counts = new ItemCounts();
-
- if (typesToCount.Length == 0)
- {
- return counts;
- }
-
- if (!reader.TryGetString(countStartColumn, out var typeString))
- {
- return counts;
- }
-
- foreach (var typeName in typeString.AsSpan().Split('|'))
- {
- if (typeName.Equals(typeof(Series).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.SeriesCount++;
- }
- else if (typeName.Equals(typeof(Episode).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.EpisodeCount++;
- }
- else if (typeName.Equals(typeof(Movie).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.MovieCount++;
- }
- else if (typeName.Equals(typeof(MusicAlbum).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.AlbumCount++;
- }
- else if (typeName.Equals(typeof(MusicArtist).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.ArtistCount++;
- }
- else if (typeName.Equals(typeof(Audio).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.SongCount++;
- }
- else if (typeName.Equals(typeof(Trailer).FullName, StringComparison.OrdinalIgnoreCase))
- {
- counts.TrailerCount++;
- }
-
- counts.ItemCount++;
- }
-
- return counts;
- }
-
- private List<(int MagicNumber, string Value)> GetItemValuesToSave(BaseItem item, List<string> inheritedTags)
- {
- var list = new List<(int, string)>();
-
- if (item is IHasArtist hasArtist)
- {
- list.AddRange(hasArtist.Artists.Select(i => (0, i)));
- }
-
- if (item is IHasAlbumArtist hasAlbumArtist)
- {
- list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i)));
- }
-
- list.AddRange(item.Genres.Select(i => (2, i)));
- list.AddRange(item.Studios.Select(i => (3, i)));
- list.AddRange(item.Tags.Select(i => (4, i)));
-
- // keywords was 5
-
- list.AddRange(inheritedTags.Select(i => (6, i)));
-
- // Remove all invalid values.
- list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
-
- return list;
- }
-
- private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, ManagedConnection db)
- {
- if (itemId.IsEmpty())
- {
- throw new ArgumentNullException(nameof(itemId));
- }
-
- ArgumentNullException.ThrowIfNull(values);
-
- CheckDisposed();
-
- // First delete
- using var command = db.PrepareStatement("delete from ItemValues where ItemId=@Id");
- command.TryBind("@Id", itemId);
- command.ExecuteNonQuery();
-
- InsertItemValues(itemId, values, db);
- }
-
- private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, ManagedConnection db)
- {
- const int Limit = 100;
- var startIndex = 0;
-
- const string StartInsertText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values ";
- var insertText = new StringBuilder(StartInsertText);
- while (startIndex < values.Count)
- {
- var endIndex = Math.Min(values.Count, startIndex + Limit);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- insertText.AppendFormat(
- CultureInfo.InvariantCulture,
- "(@ItemId, @Type{0}, @Value{0}, @CleanValue{0}),",
- i);
- }
-
- // Remove trailing comma
- insertText.Length--;
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", id);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var currentValueInfo = values[i];
-
- var itemValue = currentValueInfo.Value;
-
- statement.TryBind("@Type" + index, currentValueInfo.MagicNumber);
- statement.TryBind("@Value" + index, itemValue);
- statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue));
- }
-
- statement.ExecuteNonQuery();
- }
-
- startIndex += Limit;
- insertText.Length = StartInsertText.Length;
- }
- }
-
- /// <inheritdoc />
- public void UpdatePeople(Guid itemId, List<PersonInfo> people)
- {
- if (itemId.IsEmpty())
- {
- throw new ArgumentNullException(nameof(itemId));
- }
-
- CheckDisposed();
-
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- // Delete all existing people first
- using var command = connection.CreateCommand();
- command.CommandText = "delete from People where ItemId=@ItemId";
- command.TryBind("@ItemId", itemId);
- command.ExecuteNonQuery();
-
- if (people is not null)
- {
- InsertPeople(itemId, people, connection);
- }
-
- transaction.Commit();
- }
-
- private void InsertPeople(Guid id, List<PersonInfo> people, ManagedConnection db)
- {
- const int Limit = 100;
- var startIndex = 0;
- var listIndex = 0;
-
- const string StartInsertText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ";
- var insertText = new StringBuilder(StartInsertText);
- while (startIndex < people.Count)
- {
- var endIndex = Math.Min(people.Count, startIndex + Limit);
- for (var i = startIndex; i < endIndex; i++)
- {
- insertText.AppendFormat(
- CultureInfo.InvariantCulture,
- "(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0}),",
- i.ToString(CultureInfo.InvariantCulture));
- }
-
- // Remove trailing comma
- insertText.Length--;
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", id);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var person = people[i];
-
- statement.TryBind("@Name" + index, person.Name);
- statement.TryBind("@Role" + index, person.Role);
- statement.TryBind("@PersonType" + index, person.Type.ToString());
- statement.TryBind("@SortOrder" + index, person.SortOrder);
- statement.TryBind("@ListOrder" + index, listIndex);
-
- listIndex++;
- }
-
- statement.ExecuteNonQuery();
- }
-
- startIndex += Limit;
- insertText.Length = StartInsertText.Length;
- }
- }
-
- private PersonInfo GetPerson(SqliteDataReader reader)
- {
- var item = new PersonInfo
- {
- ItemId = reader.GetGuid(0),
- Name = reader.GetString(1)
- };
-
- if (reader.TryGetString(2, out var role))
- {
- item.Role = role;
- }
-
- if (reader.TryGetString(3, out var type)
- && Enum.TryParse(type, true, out PersonKind personKind))
- {
- item.Type = personKind;
- }
-
- if (reader.TryGetInt32(4, out var sortOrder))
- {
- item.SortOrder = sortOrder;
- }
-
- return item;
- }
-
- /// <inheritdoc />
- public List<MediaStream> GetMediaStreams(MediaStreamQuery query)
- {
- CheckDisposed();
-
- ArgumentNullException.ThrowIfNull(query);
-
- var cmdText = _mediaStreamSaveColumnsSelectQuery;
-
- if (query.Type.HasValue)
- {
- cmdText += " AND StreamType=@StreamType";
- }
-
- if (query.Index.HasValue)
- {
- cmdText += " AND StreamIndex=@StreamIndex";
- }
-
- cmdText += " order by StreamIndex ASC";
-
- using (var connection = GetConnection(true))
- {
- var list = new List<MediaStream>();
-
- using (var statement = PrepareStatement(connection, cmdText))
- {
- statement.TryBind("@ItemId", query.ItemId);
-
- if (query.Type.HasValue)
- {
- statement.TryBind("@StreamType", query.Type.Value.ToString());
- }
-
- if (query.Index.HasValue)
- {
- statement.TryBind("@StreamIndex", query.Index.Value);
- }
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(GetMediaStream(row));
- }
- }
-
- return list;
- }
- }
-
- /// <inheritdoc />
- public void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken)
- {
- CheckDisposed();
-
- if (id.IsEmpty())
- {
- throw new ArgumentNullException(nameof(id));
- }
-
- ArgumentNullException.ThrowIfNull(streams);
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using var connection = GetConnection();
- using var transaction = connection.BeginTransaction();
- // Delete existing mediastreams
- using var command = connection.PrepareStatement("delete from mediastreams where ItemId=@ItemId");
- command.TryBind("@ItemId", id);
- command.ExecuteNonQuery();
-
- InsertMediaStreams(id, streams, connection);
-
- transaction.Commit();
- }
-
- private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, ManagedConnection db)
- {
- const int Limit = 10;
- var startIndex = 0;
-
- var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
- while (startIndex < streams.Count)
- {
- var endIndex = Math.Min(streams.Count, startIndex + Limit);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- if (i != startIndex)
- {
- insertText.Append(',');
- }
-
- var index = i.ToString(CultureInfo.InvariantCulture);
- insertText.Append("(@ItemId, ");
-
- foreach (var column in _mediaStreamSaveColumns.Skip(1))
- {
- insertText.Append('@').Append(column).Append(index).Append(',');
- }
-
- insertText.Length -= 1; // Remove the last comma
-
- insertText.Append(')');
- }
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", id);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var stream = streams[i];
-
- statement.TryBind("@StreamIndex" + index, stream.Index);
- statement.TryBind("@StreamType" + index, stream.Type.ToString());
- statement.TryBind("@Codec" + index, stream.Codec);
- statement.TryBind("@Language" + index, stream.Language);
- statement.TryBind("@ChannelLayout" + index, stream.ChannelLayout);
- statement.TryBind("@Profile" + index, stream.Profile);
- statement.TryBind("@AspectRatio" + index, stream.AspectRatio);
- statement.TryBind("@Path" + index, GetPathToSave(stream.Path));
-
- statement.TryBind("@IsInterlaced" + index, stream.IsInterlaced);
- statement.TryBind("@BitRate" + index, stream.BitRate);
- statement.TryBind("@Channels" + index, stream.Channels);
- statement.TryBind("@SampleRate" + index, stream.SampleRate);
-
- statement.TryBind("@IsDefault" + index, stream.IsDefault);
- statement.TryBind("@IsForced" + index, stream.IsForced);
- statement.TryBind("@IsExternal" + index, stream.IsExternal);
-
- // Yes these are backwards due to a mistake
- statement.TryBind("@Width" + index, stream.Height);
- statement.TryBind("@Height" + index, stream.Width);
-
- statement.TryBind("@AverageFrameRate" + index, stream.AverageFrameRate);
- statement.TryBind("@RealFrameRate" + index, stream.RealFrameRate);
- statement.TryBind("@Level" + index, stream.Level);
-
- statement.TryBind("@PixelFormat" + index, stream.PixelFormat);
- statement.TryBind("@BitDepth" + index, stream.BitDepth);
- statement.TryBind("@IsAnamorphic" + index, stream.IsAnamorphic);
- statement.TryBind("@IsExternal" + index, stream.IsExternal);
- statement.TryBind("@RefFrames" + index, stream.RefFrames);
-
- statement.TryBind("@CodecTag" + index, stream.CodecTag);
- statement.TryBind("@Comment" + index, stream.Comment);
- statement.TryBind("@NalLengthSize" + index, stream.NalLengthSize);
- statement.TryBind("@IsAvc" + index, stream.IsAVC);
- statement.TryBind("@Title" + index, stream.Title);
-
- statement.TryBind("@TimeBase" + index, stream.TimeBase);
- statement.TryBind("@CodecTimeBase" + index, stream.CodecTimeBase);
-
- statement.TryBind("@ColorPrimaries" + index, stream.ColorPrimaries);
- statement.TryBind("@ColorSpace" + index, stream.ColorSpace);
- statement.TryBind("@ColorTransfer" + index, stream.ColorTransfer);
-
- statement.TryBind("@DvVersionMajor" + index, stream.DvVersionMajor);
- statement.TryBind("@DvVersionMinor" + index, stream.DvVersionMinor);
- statement.TryBind("@DvProfile" + index, stream.DvProfile);
- statement.TryBind("@DvLevel" + index, stream.DvLevel);
- statement.TryBind("@RpuPresentFlag" + index, stream.RpuPresentFlag);
- statement.TryBind("@ElPresentFlag" + index, stream.ElPresentFlag);
- statement.TryBind("@BlPresentFlag" + index, stream.BlPresentFlag);
- statement.TryBind("@DvBlSignalCompatibilityId" + index, stream.DvBlSignalCompatibilityId);
-
- statement.TryBind("@IsHearingImpaired" + index, stream.IsHearingImpaired);
-
- statement.TryBind("@Rotation" + index, stream.Rotation);
- }
-
- statement.ExecuteNonQuery();
- }
-
- startIndex += Limit;
- insertText.Length = _mediaStreamSaveColumnsInsertQuery.Length;
- }
- }
-
- /// <summary>
- /// Gets the media stream.
- /// </summary>
- /// <param name="reader">The reader.</param>
- /// <returns>MediaStream.</returns>
- private MediaStream GetMediaStream(SqliteDataReader reader)
- {
- var item = new MediaStream
- {
- Index = reader.GetInt32(1),
- Type = Enum.Parse<MediaStreamType>(reader.GetString(2), true)
- };
-
- if (reader.TryGetString(3, out var codec))
- {
- item.Codec = codec;
- }
-
- if (reader.TryGetString(4, out var language))
- {
- item.Language = language;
- }
-
- if (reader.TryGetString(5, out var channelLayout))
- {
- item.ChannelLayout = channelLayout;
- }
-
- if (reader.TryGetString(6, out var profile))
- {
- item.Profile = profile;
- }
-
- if (reader.TryGetString(7, out var aspectRatio))
- {
- item.AspectRatio = aspectRatio;
- }
-
- if (reader.TryGetString(8, out var path))
- {
- item.Path = RestorePath(path);
- }
-
- item.IsInterlaced = reader.GetBoolean(9);
-
- if (reader.TryGetInt32(10, out var bitrate))
- {
- item.BitRate = bitrate;
- }
-
- if (reader.TryGetInt32(11, out var channels))
- {
- item.Channels = channels;
- }
-
- if (reader.TryGetInt32(12, out var sampleRate))
- {
- item.SampleRate = sampleRate;
- }
-
- item.IsDefault = reader.GetBoolean(13);
- item.IsForced = reader.GetBoolean(14);
- item.IsExternal = reader.GetBoolean(15);
-
- if (reader.TryGetInt32(16, out var width))
- {
- item.Width = width;
- }
-
- if (reader.TryGetInt32(17, out var height))
- {
- item.Height = height;
- }
-
- if (reader.TryGetSingle(18, out var averageFrameRate))
- {
- item.AverageFrameRate = averageFrameRate;
- }
-
- if (reader.TryGetSingle(19, out var realFrameRate))
- {
- item.RealFrameRate = realFrameRate;
- }
-
- if (reader.TryGetSingle(20, out var level))
- {
- item.Level = level;
- }
-
- if (reader.TryGetString(21, out var pixelFormat))
- {
- item.PixelFormat = pixelFormat;
- }
-
- if (reader.TryGetInt32(22, out var bitDepth))
- {
- item.BitDepth = bitDepth;
- }
-
- if (reader.TryGetBoolean(23, out var isAnamorphic))
- {
- item.IsAnamorphic = isAnamorphic;
- }
-
- if (reader.TryGetInt32(24, out var refFrames))
- {
- item.RefFrames = refFrames;
- }
-
- if (reader.TryGetString(25, out var codecTag))
- {
- item.CodecTag = codecTag;
- }
-
- if (reader.TryGetString(26, out var comment))
- {
- item.Comment = comment;
- }
-
- if (reader.TryGetString(27, out var nalLengthSize))
- {
- item.NalLengthSize = nalLengthSize;
- }
-
- if (reader.TryGetBoolean(28, out var isAVC))
- {
- item.IsAVC = isAVC;
- }
-
- if (reader.TryGetString(29, out var title))
- {
- item.Title = title;
- }
-
- if (reader.TryGetString(30, out var timeBase))
- {
- item.TimeBase = timeBase;
- }
-
- if (reader.TryGetString(31, out var codecTimeBase))
- {
- item.CodecTimeBase = codecTimeBase;
- }
-
- if (reader.TryGetString(32, out var colorPrimaries))
- {
- item.ColorPrimaries = colorPrimaries;
- }
-
- if (reader.TryGetString(33, out var colorSpace))
- {
- item.ColorSpace = colorSpace;
- }
-
- if (reader.TryGetString(34, out var colorTransfer))
- {
- item.ColorTransfer = colorTransfer;
- }
-
- if (reader.TryGetInt32(35, out var dvVersionMajor))
- {
- item.DvVersionMajor = dvVersionMajor;
- }
-
- if (reader.TryGetInt32(36, out var dvVersionMinor))
- {
- item.DvVersionMinor = dvVersionMinor;
- }
-
- if (reader.TryGetInt32(37, out var dvProfile))
- {
- item.DvProfile = dvProfile;
- }
-
- if (reader.TryGetInt32(38, out var dvLevel))
- {
- item.DvLevel = dvLevel;
- }
-
- if (reader.TryGetInt32(39, out var rpuPresentFlag))
- {
- item.RpuPresentFlag = rpuPresentFlag;
- }
-
- if (reader.TryGetInt32(40, out var elPresentFlag))
- {
- item.ElPresentFlag = elPresentFlag;
- }
-
- if (reader.TryGetInt32(41, out var blPresentFlag))
- {
- item.BlPresentFlag = blPresentFlag;
- }
-
- if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
- {
- item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
- }
-
- item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
-
- if (reader.TryGetInt32(44, out var rotation))
- {
- item.Rotation = rotation;
- }
-
- if (item.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
- {
- item.LocalizedDefault = _localization.GetLocalizedString("Default");
- item.LocalizedExternal = _localization.GetLocalizedString("External");
-
- if (item.Type is MediaStreamType.Subtitle)
- {
- item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
- item.LocalizedForced = _localization.GetLocalizedString("Forced");
- item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
- }
- }
-
- return item;
- }
-
- /// <inheritdoc />
- public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
- {
- CheckDisposed();
-
- ArgumentNullException.ThrowIfNull(query);
-
- var cmdText = _mediaAttachmentSaveColumnsSelectQuery;
-
- if (query.Index.HasValue)
- {
- cmdText += " AND AttachmentIndex=@AttachmentIndex";
- }
-
- cmdText += " order by AttachmentIndex ASC";
-
- var list = new List<MediaAttachment>();
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, cmdText))
- {
- statement.TryBind("@ItemId", query.ItemId);
-
- if (query.Index.HasValue)
- {
- statement.TryBind("@AttachmentIndex", query.Index.Value);
- }
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(GetMediaAttachment(row));
- }
- }
-
- return list;
- }
-
- /// <inheritdoc />
- public void SaveMediaAttachments(
- Guid id,
- IReadOnlyList<MediaAttachment> attachments,
- CancellationToken cancellationToken)
- {
- CheckDisposed();
- if (id.IsEmpty())
- {
- throw new ArgumentException("Guid can't be empty.", nameof(id));
- }
-
- ArgumentNullException.ThrowIfNull(attachments);
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var connection = GetConnection())
- using (var transaction = connection.BeginTransaction())
- using (var command = connection.PrepareStatement("delete from mediaattachments where ItemId=@ItemId"))
- {
- command.TryBind("@ItemId", id);
- command.ExecuteNonQuery();
-
- InsertMediaAttachments(id, attachments, connection, cancellationToken);
-
- transaction.Commit();
- }
- }
-
- private void InsertMediaAttachments(
- Guid id,
- IReadOnlyList<MediaAttachment> attachments,
- ManagedConnection db,
- CancellationToken cancellationToken)
- {
- const int InsertAtOnce = 10;
-
- var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
- for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce)
- {
- var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- insertText.Append("(@ItemId, ");
-
- foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
- {
- insertText.Append('@')
- .Append(column)
- .Append(i)
- .Append(',');
- }
-
- insertText.Length -= 1;
-
- insertText.Append("),");
- }
-
- insertText.Length--;
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var statement = PrepareStatement(db, insertText.ToString()))
- {
- statement.TryBind("@ItemId", id);
-
- for (var i = startIndex; i < endIndex; i++)
- {
- var index = i.ToString(CultureInfo.InvariantCulture);
-
- var attachment = attachments[i];
-
- statement.TryBind("@AttachmentIndex" + index, attachment.Index);
- statement.TryBind("@Codec" + index, attachment.Codec);
- statement.TryBind("@CodecTag" + index, attachment.CodecTag);
- statement.TryBind("@Comment" + index, attachment.Comment);
- statement.TryBind("@Filename" + index, attachment.FileName);
- statement.TryBind("@MIMEType" + index, attachment.MimeType);
- }
-
- statement.ExecuteNonQuery();
- }
-
- insertText.Length = _mediaAttachmentInsertPrefix.Length;
- }
- }
-
- /// <summary>
- /// Gets the attachment.
- /// </summary>
- /// <param name="reader">The reader.</param>
- /// <returns>MediaAttachment.</returns>
- private MediaAttachment GetMediaAttachment(SqliteDataReader reader)
- {
- var item = new MediaAttachment
- {
- Index = reader.GetInt32(1)
- };
-
- if (reader.TryGetString(2, out var codec))
- {
- item.Codec = codec;
- }
-
- if (reader.TryGetString(3, out var codecTag))
- {
- item.CodecTag = codecTag;
- }
-
- if (reader.TryGetString(4, out var comment))
- {
- item.Comment = comment;
- }
-
- if (reader.TryGetString(5, out var fileName))
- {
- item.FileName = fileName;
- }
-
- if (reader.TryGetString(6, out var mimeType))
- {
- item.MimeType = mimeType;
- }
-
- return item;
- }
-
- private static string BuildMediaAttachmentInsertPrefix()
- {
- var queryPrefixText = new StringBuilder();
- queryPrefixText.Append("insert into mediaattachments (");
- foreach (var column in _mediaAttachmentSaveColumns)
- {
- queryPrefixText.Append(column)
- .Append(',');
- }
-
- queryPrefixText.Length -= 1;
- queryPrefixText.Append(") values ");
- return queryPrefixText.ToString();
- }
-
-#nullable enable
-
- private readonly struct QueryTimeLogger : IDisposable
- {
- private readonly ILogger _logger;
- private readonly string _commandText;
- private readonly string _methodName;
- private readonly long _startTimestamp;
-
- public QueryTimeLogger(ILogger logger, string commandText, [CallerMemberName] string methodName = "")
- {
- _logger = logger;
- _commandText = commandText;
- _methodName = methodName;
- _startTimestamp = logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : -1;
- }
-
- public void Dispose()
- {
- if (_startTimestamp == -1)
- {
- return;
- }
-
- var elapsedMs = Stopwatch.GetElapsedTime(_startTimestamp).TotalMilliseconds;
-
-#if DEBUG
- const int SlowThreshold = 100;
-#else
- const int SlowThreshold = 10;
-#endif
-
- if (elapsedMs >= SlowThreshold)
- {
- _logger.LogDebug(
- "{Method} query time (slow): {ElapsedMs}ms. Query: {Query}",
- _methodName,
- elapsedMs,
- _commandText);
- }
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
deleted file mode 100644
index bfdcc08f4..000000000
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ /dev/null
@@ -1,369 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
-using Microsoft.Data.Sqlite;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Data
-{
- public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
- {
- private readonly IUserManager _userManager;
-
- public SqliteUserDataRepository(
- ILogger<SqliteUserDataRepository> logger,
- IServerConfigurationManager config,
- IUserManager userManager)
- : base(logger)
- {
- _userManager = userManager;
-
- DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
- }
-
- /// <summary>
- /// Opens the connection to the database.
- /// </summary>
- public override void Initialize()
- {
- base.Initialize();
-
- using (var connection = GetConnection())
- {
- var userDatasTableExists = TableExists(connection, "UserDatas");
- var userDataTableExists = TableExists(connection, "userdata");
-
- var users = userDatasTableExists ? null : _userManager.Users;
- using var transaction = connection.BeginTransaction();
- connection.Execute(string.Join(
- ';',
- "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
- "drop index if exists idx_userdata",
- "drop index if exists idx_userdata1",
- "drop index if exists idx_userdata2",
- "drop index if exists userdataindex1",
- "drop index if exists userdataindex",
- "drop index if exists userdataindex3",
- "drop index if exists userdataindex4",
- "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
- "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
- "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
- "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)",
- "create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)"));
-
- if (!userDataTableExists)
- {
- transaction.Commit();
- return;
- }
-
- var existingColumnNames = GetColumnNames(connection, "userdata");
-
- AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
- AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
- AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
-
- if (userDatasTableExists)
- {
- return;
- }
-
- ImportUserIds(connection, users);
-
- connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
-
- transaction.Commit();
- }
- }
-
- private void ImportUserIds(ManagedConnection db, IEnumerable<User> users)
- {
- var userIdsWithUserData = GetAllUserIdsWithUserData(db);
-
- using (var statement = db.PrepareStatement("update userdata set InternalUserId=@InternalUserId where UserId=@UserId"))
- {
- foreach (var user in users)
- {
- if (!userIdsWithUserData.Contains(user.Id))
- {
- continue;
- }
-
- statement.TryBind("@UserId", user.Id);
- statement.TryBind("@InternalUserId", user.InternalId);
-
- statement.ExecuteNonQuery();
- }
- }
- }
-
- private List<Guid> GetAllUserIdsWithUserData(ManagedConnection db)
- {
- var list = new List<Guid>();
-
- using (var statement = PrepareStatement(db, "select DISTINCT UserId from UserData where UserId not null"))
- {
- foreach (var row in statement.ExecuteQuery())
- {
- try
- {
- list.Add(row.GetGuid(0));
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error while getting user");
- }
- }
- }
-
- return list;
- }
-
- /// <inheritdoc />
- public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull(userData);
-
- if (userId <= 0)
- {
- throw new ArgumentNullException(nameof(userId));
- }
-
- ArgumentException.ThrowIfNullOrEmpty(key);
-
- PersistUserData(userId, key, userData, cancellationToken);
- }
-
- /// <inheritdoc />
- public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull(userData);
-
- if (userId <= 0)
- {
- throw new ArgumentNullException(nameof(userId));
- }
-
- PersistAllUserData(userId, userData, cancellationToken);
- }
-
- /// <summary>
- /// Persists the user data.
- /// </summary>
- /// <param name="internalUserId">The user id.</param>
- /// <param name="key">The key.</param>
- /// <param name="userData">The user data.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var connection = GetConnection())
- using (var transaction = connection.BeginTransaction())
- {
- SaveUserData(connection, internalUserId, key, userData);
- transaction.Commit();
- }
- }
-
- private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData)
- {
- using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
- {
- statement.TryBind("@userId", internalUserId);
- statement.TryBind("@key", key);
-
- if (userData.Rating.HasValue)
- {
- statement.TryBind("@rating", userData.Rating.Value);
- }
- else
- {
- statement.TryBindNull("@rating");
- }
-
- statement.TryBind("@played", userData.Played);
- statement.TryBind("@playCount", userData.PlayCount);
- statement.TryBind("@isFavorite", userData.IsFavorite);
- statement.TryBind("@playbackPositionTicks", userData.PlaybackPositionTicks);
-
- if (userData.LastPlayedDate.HasValue)
- {
- statement.TryBind("@lastPlayedDate", userData.LastPlayedDate.Value.ToDateTimeParamValue());
- }
- else
- {
- statement.TryBindNull("@lastPlayedDate");
- }
-
- if (userData.AudioStreamIndex.HasValue)
- {
- statement.TryBind("@AudioStreamIndex", userData.AudioStreamIndex.Value);
- }
- else
- {
- statement.TryBindNull("@AudioStreamIndex");
- }
-
- if (userData.SubtitleStreamIndex.HasValue)
- {
- statement.TryBind("@SubtitleStreamIndex", userData.SubtitleStreamIndex.Value);
- }
- else
- {
- statement.TryBindNull("@SubtitleStreamIndex");
- }
-
- statement.ExecuteNonQuery();
- }
- }
-
- /// <summary>
- /// Persist all user data for the specified user.
- /// </summary>
- private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var connection = GetConnection())
- using (var transaction = connection.BeginTransaction())
- {
- foreach (var userItemData in userDataList)
- {
- SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
- }
-
- transaction.Commit();
- }
- }
-
- /// <summary>
- /// Gets the user data.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="key">The key.</param>
- /// <returns>Task{UserItemData}.</returns>
- /// <exception cref="ArgumentNullException">
- /// userId
- /// or
- /// key.
- /// </exception>
- public UserItemData GetUserData(long userId, string key)
- {
- if (userId <= 0)
- {
- throw new ArgumentNullException(nameof(userId));
- }
-
- ArgumentException.ThrowIfNullOrEmpty(key);
-
- using (var connection = GetConnection(true))
- {
- using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
- {
- statement.TryBind("@UserId", userId);
- statement.TryBind("@Key", key);
-
- foreach (var row in statement.ExecuteQuery())
- {
- return ReadRow(row);
- }
- }
-
- return null;
- }
- }
-
- public UserItemData GetUserData(long userId, List<string> keys)
- {
- ArgumentNullException.ThrowIfNull(keys);
-
- if (keys.Count == 0)
- {
- return null;
- }
-
- return GetUserData(userId, keys[0]);
- }
-
- /// <summary>
- /// Return all user-data associated with the given user.
- /// </summary>
- /// <param name="userId">The internal user id.</param>
- /// <returns>The list of user item data.</returns>
- public List<UserItemData> GetAllUserData(long userId)
- {
- if (userId <= 0)
- {
- throw new ArgumentNullException(nameof(userId));
- }
-
- var list = new List<UserItemData>();
-
- using (var connection = GetConnection())
- {
- using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
- {
- statement.TryBind("@UserId", userId);
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(ReadRow(row));
- }
- }
- }
-
- return list;
- }
-
- /// <summary>
- /// Read a row from the specified reader into the provided userData object.
- /// </summary>
- /// <param name="reader">The list of result set values.</param>
- /// <returns>The user item data.</returns>
- private UserItemData ReadRow(SqliteDataReader reader)
- {
- var userData = new UserItemData
- {
- Key = reader.GetString(0)
- };
-
- if (reader.TryGetDouble(2, out var rating))
- {
- userData.Rating = rating;
- }
-
- userData.Played = reader.GetBoolean(3);
- userData.PlayCount = reader.GetInt32(4);
- userData.IsFavorite = reader.GetBoolean(5);
- userData.PlaybackPositionTicks = reader.GetInt64(6);
-
- if (reader.TryReadDateTime(7, out var lastPlayedDate))
- {
- userData.LastPlayedDate = lastPlayedDate;
- }
-
- if (reader.TryGetInt32(8, out var audioStreamIndex))
- {
- userData.AudioStreamIndex = audioStreamIndex;
- }
-
- if (reader.TryGetInt32(9, out var subtitleStreamIndex))
- {
- userData.SubtitleStreamIndex = subtitleStreamIndex;
- }
-
- return userData;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Data/SynchronousMode.cs b/Emby.Server.Implementations/Data/SynchronousMode.cs
deleted file mode 100644
index cde524e2e..000000000
--- a/Emby.Server.Implementations/Data/SynchronousMode.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-namespace Emby.Server.Implementations.Data;
-
-/// <summary>
-/// The disk synchronization mode, controls how aggressively SQLite will write data
-/// all the way out to physical storage.
-/// </summary>
-public enum SynchronousMode
-{
- /// <summary>
- /// SQLite continues without syncing as soon as it has handed data off to the operating system.
- /// </summary>
- Off = 0,
-
- /// <summary>
- /// SQLite database engine will still sync at the most critical moments.
- /// </summary>
- Normal = 1,
-
- /// <summary>
- /// SQLite database engine will use the xSync method of the VFS
- /// to ensure that all content is safely written to the disk surface prior to continuing.
- /// </summary>
- Full = 2,
-
- /// <summary>
- /// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
- /// is synced after that journal is unlinked to commit a transaction in DELETE mode.
- /// </summary>
- Extra = 3
-}
diff --git a/Emby.Server.Implementations/Data/TempStoreMode.cs b/Emby.Server.Implementations/Data/TempStoreMode.cs
deleted file mode 100644
index d2427ce47..000000000
--- a/Emby.Server.Implementations/Data/TempStoreMode.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace Emby.Server.Implementations.Data;
-
-/// <summary>
-/// Storage mode used by temporary database files.
-/// </summary>
-public enum TempStoreMode
-{
- /// <summary>
- /// The compile-time C preprocessor macro SQLITE_TEMP_STORE
- /// is used to determine where temporary tables and indices are stored.
- /// </summary>
- Default = 0,
-
- /// <summary>
- /// Temporary tables and indices are stored in a file.
- /// </summary>
- File = 1,
-
- /// <summary>
- /// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
- /// </summary>
- Memory = 2
-}
diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs
index 2459178d8..0b3c3bbd4 100644
--- a/Emby.Server.Implementations/Devices/DeviceId.cs
+++ b/Emby.Server.Implementations/Devices/DeviceId.cs
@@ -4,6 +4,7 @@ using System;
using System.Globalization;
using System.IO;
using System.Text;
+using System.Threading;
using MediaBrowser.Common.Configuration;
using Microsoft.Extensions.Logging;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Devices
{
private readonly IApplicationPaths _appPaths;
private readonly ILogger<DeviceId> _logger;
- private readonly object _syncLock = new object();
+ private readonly Lock _syncLock = new();
private string? _id;
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 0c0ba7453..356d1e437 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -10,6 +10,7 @@ using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -51,6 +52,7 @@ namespace Emby.Server.Implementations.Dto
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
private readonly ITrickplayManager _trickplayManager;
+ private readonly IChapterRepository _chapterRepository;
public DtoService(
ILogger<DtoService> logger,
@@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
IApplicationHost appHost,
IMediaSourceManager mediaSourceManager,
Lazy<ILiveTvManager> livetvManagerFactory,
- ITrickplayManager trickplayManager)
+ ITrickplayManager trickplayManager,
+ IChapterRepository chapterRepository)
{
_logger = logger;
_libraryManager = libraryManager;
@@ -76,6 +79,7 @@ namespace Emby.Server.Implementations.Dto
_mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory;
_trickplayManager = trickplayManager;
+ _chapterRepository = chapterRepository;
}
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@@ -165,7 +169,7 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
- private static IList<BaseItem> GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
+ private static IReadOnlyList<BaseItem> GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
{
return byName.GetTaggedItems(
new InternalItemsQuery(user)
@@ -327,7 +331,7 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
- private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList<BaseItem> taggedItems)
+ private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList<BaseItem> taggedItems)
{
if (item is MusicArtist)
{
@@ -1060,7 +1064,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.Chapters))
{
- dto.Chapters = _itemRepo.GetChapters(item);
+ dto.Chapters = _chapterRepository.GetChapters(item.Id).ToList();
}
if (options.ContainsField(ItemFields.Trickplay))
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 34276355a..70dd5eb9a 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -37,7 +37,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index 4c668379c..fb0a55135 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -34,7 +34,7 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
private readonly IUserManager _userManager;
private readonly ILogger<LibraryChangedNotifier> _logger;
- private readonly object _libraryChangedSyncLock = new();
+ private readonly Lock _libraryChangedSyncLock = new();
private readonly List<Folder> _foldersAddedTo = new();
private readonly List<Folder> _foldersRemovedFrom = new();
private readonly List<BaseItem> _itemsAdded = new();
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index aef02ce6b..fc174b7c1 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IUserManager _userManager;
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new();
- private readonly object _syncLock = new();
+ private readonly Lock _syncLock = new();
private Timer? _updateTimer;
@@ -144,9 +144,15 @@ namespace Emby.Server.Implementations.EntryPoints
.Select(i =>
{
var dto = _userDataManager.GetUserDataDto(i, user);
+ if (dto is null)
+ {
+ return null!;
+ }
+
dto.ItemId = i.Id;
return dto;
})
+ .Where(e => e is not null)
.ToArray()
};
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index cb6f7e1d3..a720c86fb 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -82,17 +82,17 @@ namespace Emby.Server.Implementations.HttpServer
public WebSocketState State => _socket.State;
/// <inheritdoc />
- public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
+ public async Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
{
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
- return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
+ await _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
- public Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken)
+ public async Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken)
{
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
- return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
+ await _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -224,12 +224,12 @@ namespace Emby.Server.Implementations.HttpServer
return ret;
}
- private Task SendKeepAliveResponse()
+ private async Task SendKeepAliveResponse()
{
LastKeepAliveDate = DateTime.UtcNow;
- return SendAsync(
+ await SendAsync(
new OutboundKeepAliveMessage(),
- CancellationToken.None);
+ CancellationToken.None).ConfigureAwait(false);
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
index 774d3563c..cb5b3993b 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -84,7 +84,7 @@ namespace Emby.Server.Implementations.HttpServer
/// Processes the web socket message received.
/// </summary>
/// <param name="result">The result.</param>
- private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
+ private async Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
{
var tasks = new Task[_webSocketListeners.Length];
for (var i = 0; i < _webSocketListeners.Length; ++i)
@@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.HttpServer
tasks[i] = _webSocketListeners[i].ProcessMessageAsync(result);
}
- return Task.WhenAll(tasks);
+ await Task.WhenAll(tasks).ConfigureAwait(false);
}
}
}
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index e75cab64c..7378cf885 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -18,8 +18,8 @@ namespace Emby.Server.Implementations.IO
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
- private readonly List<string> _affectedPaths = new List<string>();
- private readonly object _timerLock = new object();
+ private readonly List<string> _affectedPaths = new();
+ private readonly Lock _timerLock = new();
private Timer? _timer;
private bool _disposed;
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 4b68f21d5..66b7839f7 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -276,6 +276,13 @@ namespace Emby.Server.Implementations.IO
{
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
}
+ catch (IOException ex)
+ {
+ // IOException generally means the file is not accessible due to filesystem issues
+ // Catch this exception and mark the file as not exist to ignore it
+ _logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName);
+ result.Exists = false;
+ }
}
}
@@ -561,7 +568,7 @@ namespace Emby.Server.Implementations.IO
{
var enumerationOptions = GetEnumerationOptions(recursive);
- // On linux and osx the search pattern is case sensitive
+ // On linux and macOS the search pattern is case-sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1)
{
@@ -590,6 +597,9 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
{
+ // Note: any of unhandled exceptions thrown by this method may cause the caller to believe the whole path is not accessible.
+ // But what causing the exception may be a single file under that path. This could lead to unexpected behavior.
+ // For example, the scanner will remove everything in that path due to unhandled errors.
var directoryInfo = new DirectoryInfo(path);
var enumerationOptions = GetEnumerationOptions(recursive);
@@ -618,7 +628,7 @@ namespace Emby.Server.Implementations.IO
{
var enumerationOptions = GetEnumerationOptions(recursive);
- // On linux and osx the search pattern is case sensitive
+ // On linux and macOS the search pattern is case-sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Length == 1)
{
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 82db7c46b..8b2869149 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -116,13 +117,12 @@ namespace Emby.Server.Implementations.Images
var mimeType = MimeTypes.GetMimeType(outputPath);
- if (string.Equals(mimeType, "application/octet-stream", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(mimeType, MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
{
- mimeType = "image/png";
+ mimeType = MediaTypeNames.Image.Png;
}
await ProviderManager.SaveImage(item, outputPath, mimeType, imageType, null, false, cancellationToken).ConfigureAwait(false);
- File.Delete(outputPath);
return ItemUpdateType.ImageUpdate;
}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 28f7ed659..c483f3c61 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -76,13 +76,14 @@ namespace Emby.Server.Implementations.Library
private readonly IItemRepository _itemRepository;
private readonly IImageProcessor _imageProcessor;
private readonly NamingOptions _namingOptions;
+ private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver;
/// <summary>
/// The _root folder sync lock.
/// </summary>
- private readonly object _rootFolderSyncLock = new object();
- private readonly object _userRootFolderSyncLock = new object();
+ private readonly Lock _rootFolderSyncLock = new();
+ private readonly Lock _userRootFolderSyncLock = new();
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
@@ -112,6 +113,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="imageProcessor">The image processor.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
+ /// <param name="peopleRepository">The People Repository.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILoggerFactory loggerFactory,
@@ -127,7 +129,8 @@ namespace Emby.Server.Implementations.Library
IItemRepository itemRepository,
IImageProcessor imageProcessor,
NamingOptions namingOptions,
- IDirectoryService directoryService)
+ IDirectoryService directoryService,
+ IPeopleRepository peopleRepository)
{
_appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>();
@@ -144,7 +147,7 @@ namespace Emby.Server.Implementations.Library
_imageProcessor = imageProcessor;
_cache = new ConcurrentDictionary<Guid, BaseItem>();
_namingOptions = namingOptions;
-
+ _peopleRepository = peopleRepository;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -751,14 +754,7 @@ namespace Emby.Server.Implementations.Library
if (folder.Id.IsEmpty())
{
- if (string.IsNullOrEmpty(folder.Path))
- {
- folder.Id = GetNewItemId(folder.GetType().Name, folder.GetType());
- }
- else
- {
- folder.Id = GetNewItemId(folder.Path, folder.GetType());
- }
+ folder.Id = GetNewItemId(folder.Path, folder.GetType());
}
var dbItem = GetItemById(folder.Id) as BasePluginFolder;
@@ -1053,9 +1049,17 @@ namespace Emby.Server.Implementations.Library
cancellationToken: cancellationToken).ConfigureAwait(false);
// Quickly scan CollectionFolders for changes
- foreach (var folder in GetUserRootFolder().Children.OfType<Folder>())
+ foreach (var child in GetUserRootFolder().Children.OfType<Folder>())
{
- await folder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ // If the user has somehow deleted the collection directory, remove the metadata from the database.
+ if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
+ {
+ _itemRepository.DeleteItem(collectionFolder.Id);
+ }
+ else
+ {
+ await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+ }
}
}
@@ -1274,7 +1278,7 @@ namespace Emby.Server.Implementations.Library
return ItemIsVisible(item, user) ? item : null;
}
- public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
+ public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
{
if (query.Recursive && !query.ParentId.IsEmpty())
{
@@ -1300,7 +1304,7 @@ namespace Emby.Server.Implementations.Library
return itemList;
}
- public List<BaseItem> GetItemList(InternalItemsQuery query)
+ public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
{
return GetItemList(query, true);
}
@@ -1324,7 +1328,7 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetCount(query);
}
- public List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
+ public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
{
SetTopParentIdsOrAncestors(query, parents);
@@ -1357,7 +1361,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository.GetItemList(query));
}
- public List<Guid> GetItemIds(InternalItemsQuery query)
+ public IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query)
{
if (query.User is not null)
{
@@ -1955,13 +1959,13 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{
+ _itemRepository.SaveItems(items, cancellationToken);
+
foreach (var item in items)
{
await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
}
- _itemRepository.SaveItems(items, cancellationToken);
-
if (ItemUpdated is not null)
{
foreach (var item in items)
@@ -2736,12 +2740,12 @@ namespace Emby.Server.Implementations.Library
return path;
}
- public List<PersonInfo> GetPeople(InternalPeopleQuery query)
+ public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
{
- return _itemRepository.GetPeople(query);
+ return _peopleRepository.GetPeople(query);
}
- public List<PersonInfo> GetPeople(BaseItem item)
+ public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
{
if (item.SupportsPeople)
{
@@ -2756,12 +2760,12 @@ namespace Emby.Server.Implementations.Library
}
}
- return new List<PersonInfo>();
+ return [];
}
- public List<Person> GetPeopleItems(InternalPeopleQuery query)
+ public IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query)
{
- return _itemRepository.GetPeopleNames(query)
+ return _peopleRepository.GetPeopleNames(query)
.Select(i =>
{
try
@@ -2779,9 +2783,9 @@ namespace Emby.Server.Implementations.Library
.ToList()!; // null values are filtered out
}
- public List<string> GetPeopleNames(InternalPeopleQuery query)
+ public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
{
- return _itemRepository.GetPeopleNames(query);
+ return _peopleRepository.GetPeopleNames(query);
}
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
@@ -2790,16 +2794,17 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken)
+ public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellationToken)
{
if (!item.SupportsPeople)
{
return;
}
- _itemRepository.UpdatePeople(item.Id, people);
if (people is not null)
{
+ people = people.Where(e => e is not null).ToArray();
+ _peopleRepository.UpdatePeople(item.Id, people);
await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
}
}
@@ -2914,14 +2919,13 @@ namespace Emby.Server.Implementations.Library
private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
{
- List<BaseItem>? personsToSave = null;
-
foreach (var person in people)
{
cancellationToken.ThrowIfCancellationRequested();
var itemUpdateType = ItemUpdateType.MetadataDownload;
var saveEntity = false;
+ var createEntity = false;
var personEntity = GetPerson(person.Name);
if (personEntity is null)
@@ -2938,6 +2942,7 @@ namespace Emby.Server.Implementations.Library
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
saveEntity = true;
+ createEntity = true;
}
foreach (var id in person.ProviderIds)
@@ -2965,15 +2970,15 @@ namespace Emby.Server.Implementations.Library
if (saveEntity)
{
- (personsToSave ??= new()).Add(personEntity);
+ if (createEntity)
+ {
+ CreateItems([personEntity], null, CancellationToken.None);
+ }
+
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
+ CreateItems([personEntity], null, CancellationToken.None);
}
}
-
- if (personsToSave is not null)
- {
- CreateItems(personsToSave, null, CancellationToken.None);
- }
}
private void StartScanInBackground()
@@ -3027,7 +3032,7 @@ namespace Emby.Server.Implementations.Library
{
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
- libraryOptions.PathInfos = [..libraryOptions.PathInfos, pathInfo];
+ libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo];
SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 90a01c052..5795c47cc 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -38,7 +39,7 @@ namespace Emby.Server.Implementations.Library
public class MediaSourceManager : IMediaSourceManager, IDisposable
{
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
- private const char LiveStreamIdDelimeter = '_';
+ private const char LiveStreamIdDelimiter = '_';
private readonly IServerApplicationHost _appHost;
private readonly IItemRepository _itemRepo;
@@ -51,7 +52,8 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
private readonly IDirectoryService _directoryService;
-
+ private readonly IMediaStreamRepository _mediaStreamRepository;
+ private readonly IMediaAttachmentRepository _mediaAttachmentRepository;
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@@ -69,7 +71,9 @@ namespace Emby.Server.Implementations.Library
IFileSystem fileSystem,
IUserDataManager userDataManager,
IMediaEncoder mediaEncoder,
- IDirectoryService directoryService)
+ IDirectoryService directoryService,
+ IMediaStreamRepository mediaStreamRepository,
+ IMediaAttachmentRepository mediaAttachmentRepository)
{
_appHost = appHost;
_itemRepo = itemRepo;
@@ -82,6 +86,8 @@ namespace Emby.Server.Implementations.Library
_localizationManager = localizationManager;
_appPaths = applicationPaths;
_directoryService = directoryService;
+ _mediaStreamRepository = mediaStreamRepository;
+ _mediaAttachmentRepository = mediaAttachmentRepository;
}
public void AddParts(IEnumerable<IMediaSourceProvider> providers)
@@ -89,9 +95,9 @@ namespace Emby.Server.Implementations.Library
_providers = providers.ToArray();
}
- public List<MediaStream> GetMediaStreams(MediaStreamQuery query)
+ public IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery query)
{
- var list = _itemRepo.GetMediaStreams(query);
+ var list = _mediaStreamRepository.GetMediaStreams(query);
foreach (var stream in list)
{
@@ -121,7 +127,7 @@ namespace Emby.Server.Implementations.Library
return false;
}
- public List<MediaStream> GetMediaStreams(Guid itemId)
+ public IReadOnlyList<MediaStream> GetMediaStreams(Guid itemId)
{
var list = GetMediaStreams(new MediaStreamQuery
{
@@ -131,7 +137,7 @@ namespace Emby.Server.Implementations.Library
return GetMediaStreamsForItem(list);
}
- private List<MediaStream> GetMediaStreamsForItem(List<MediaStream> streams)
+ private IReadOnlyList<MediaStream> GetMediaStreamsForItem(IReadOnlyList<MediaStream> streams)
{
foreach (var stream in streams)
{
@@ -145,13 +151,13 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
+ public IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
{
- return _itemRepo.GetMediaAttachments(query);
+ return _mediaAttachmentRepository.GetMediaAttachments(query);
}
/// <inheritdoc />
- public List<MediaAttachment> GetMediaAttachments(Guid itemId)
+ public IReadOnlyList<MediaAttachment> GetMediaAttachments(Guid itemId)
{
return GetMediaAttachments(new MediaAttachmentQuery
{
@@ -159,7 +165,7 @@ namespace Emby.Server.Implementations.Library
});
}
- public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
+ public async Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
{
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
@@ -212,7 +218,7 @@ namespace Emby.Server.Implementations.Library
list.Add(source);
}
- return SortMediaSources(list);
+ return SortMediaSources(list).ToArray();
}
/// <inheritdoc />>
@@ -307,7 +313,7 @@ namespace Emby.Server.Implementations.Library
private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource)
{
- var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimeter;
+ var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimiter;
if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
@@ -332,7 +338,7 @@ namespace Emby.Server.Implementations.Library
return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
}
- public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
+ public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
{
ArgumentNullException.ThrowIfNull(item);
@@ -453,7 +459,7 @@ namespace Emby.Server.Implementations.Library
}
}
- private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
+ private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
{
return sources.OrderBy(i =>
{
@@ -470,8 +476,7 @@ namespace Emby.Server.Implementations.Library
return stream?.Width ?? 0;
})
- .Where(i => i.Type != MediaSourceType.Placeholder)
- .ToList();
+ .Where(i => i.Type != MediaSourceType.Placeholder);
}
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
@@ -806,7 +811,7 @@ namespace Emby.Server.Implementations.Library
return result.Item1;
}
- public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
+ public async Task<IReadOnlyList<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
{
var stream = new MediaSourceInfo
{
@@ -829,10 +834,7 @@ namespace Emby.Server.Implementations.Library
await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
.AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
- return new List<MediaSourceInfo>
- {
- stream
- };
+ return [stream];
}
public async Task CloseLiveStream(string id)
@@ -864,11 +866,11 @@ namespace Emby.Server.Implementations.Library
{
ArgumentException.ThrowIfNullOrEmpty(key);
- var keys = key.Split(LiveStreamIdDelimeter, 2);
+ var keys = key.Split(LiveStreamIdDelimiter, 2);
var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
- var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
+ var splitIndex = key.IndexOf(LiveStreamIdDelimiter, StringComparison.Ordinal);
var keyId = key.Substring(splitIndex + 1);
return (provider, keyId);
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index a69a0f33f..71c69ec50 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
@@ -24,30 +25,23 @@ namespace Emby.Server.Implementations.Library
_libraryManager = libraryManager;
}
- public List<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
{
- var list = new List<BaseItem>
- {
- item
- };
-
- list.AddRange(GetInstantMixFromGenres(item.Genres, user, dtoOptions));
-
- return list;
+ return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
/// <inheritdoc />
- public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
{
var genres = item
.GetRecursiveChildren(user, new InternalItemsQuery(user)
@@ -63,12 +57,12 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenres(genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
{
var genreIds = genres.DistinctNames().Select(i =>
{
@@ -85,7 +79,7 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
}
- public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
{
return _libraryManager.GetItemList(new InternalItemsQuery(user)
{
@@ -97,7 +91,7 @@ namespace Emby.Server.Implementations.Library
});
}
- public List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
+ public IReadOnlyList<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
{
if (item is MusicGenre)
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index a03c1214d..14798dda6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (args.IsDirectory)
{
- // It's a boxset if the path is a directory with [playlist] in its name
+ // It's a playlist if the path is a directory with [playlist] in its name
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
if (string.IsNullOrEmpty(filename))
{
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 7f3f8615e..3ac1d0219 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -171,7 +171,7 @@ namespace Emby.Server.Implementations.Library
}
};
- List<BaseItem> mediaItems;
+ IReadOnlyList<BaseItem> mediaItems;
if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist)
{
diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
index 320685b1f..76e564d53 100644
--- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
@@ -43,14 +43,26 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
/// <inheritdoc />
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
- var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList();
- var backdrops = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList();
+ var posters = GetItemsWithImageType(ImageType.Primary)
+ .Select(x => x.GetImages(ImageType.Primary).FirstOrDefault()?.Path)
+ .Where(path => !string.IsNullOrEmpty(path))
+ .Select(path => path!)
+ .ToList();
+ var backdrops = GetItemsWithImageType(ImageType.Thumb)
+ .Select(x => x.GetImages(ImageType.Thumb).FirstOrDefault()?.Path)
+ .Where(path => !string.IsNullOrEmpty(path))
+ .Select(path => path!)
+ .ToList();
if (backdrops.Count == 0)
{
// Thumb images fit better because they include the title in the image but are not provided with TMDb.
// Using backdrops as a fallback to generate an image at all
_logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen");
- backdrops = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList();
+ backdrops = GetItemsWithImageType(ImageType.Backdrop)
+ .Select(x => x.GetImages(ImageType.Backdrop).FirstOrDefault()?.Path)
+ .Where(path => !string.IsNullOrEmpty(path))
+ .Select(path => path!)
+ .ToList();
}
_imageEncoder.CreateSplashscreen(posters, backdrops);
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index 62d22b23f..a41ef888b 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -1,17 +1,21 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Globalization;
+using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;
+using Jellyfin.Extensions;
+using Jellyfin.Server.Implementations;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
+using Microsoft.EntityFrameworkCore;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
using Book = MediaBrowser.Controller.Entities.Book;
@@ -26,22 +30,18 @@ namespace Emby.Server.Implementations.Library
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
private readonly IServerConfigurationManager _config;
- private readonly IUserManager _userManager;
- private readonly IUserDataRepository _repository;
+ private readonly IDbContextFactory<JellyfinDbContext> _repository;
/// <summary>
/// Initializes a new instance of the <see cref="UserDataManager"/> class.
/// </summary>
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="repository">Instance of the <see cref="IUserDataRepository"/> interface.</param>
+ /// <param name="repository">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
public UserDataManager(
IServerConfigurationManager config,
- IUserManager userManager,
- IUserDataRepository repository)
+ IDbContextFactory<JellyfinDbContext> repository)
{
_config = config;
- _userManager = userManager;
_repository = repository;
}
@@ -59,13 +59,27 @@ namespace Emby.Server.Implementations.Library
var keys = item.GetUserDataKeys();
- var userId = user.InternalId;
+ using var dbContext = _repository.CreateDbContext();
+ using var transaction = dbContext.Database.BeginTransaction();
foreach (var key in keys)
{
- _repository.SaveUserData(userId, key, userData, cancellationToken);
+ userData.Key = key;
+ var userDataEntry = Map(userData, user.Id, item.Id);
+ if (dbContext.UserData.Any(f => f.ItemId == userDataEntry.ItemId && f.UserId == userDataEntry.UserId && f.CustomDataKey == userDataEntry.CustomDataKey))
+ {
+ dbContext.UserData.Attach(userDataEntry).State = EntityState.Modified;
+ }
+ else
+ {
+ dbContext.UserData.Add(userDataEntry);
+ }
}
+ dbContext.SaveChanges();
+ transaction.Commit();
+
+ var userId = user.InternalId;
var cacheKey = GetCacheKey(userId, item.Id);
_userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
@@ -84,10 +98,9 @@ namespace Emby.Server.Implementations.Library
{
ArgumentNullException.ThrowIfNull(user);
ArgumentNullException.ThrowIfNull(item);
- ArgumentNullException.ThrowIfNull(reason);
ArgumentNullException.ThrowIfNull(userDataDto);
- var userData = GetUserData(user, item);
+ var userData = GetUserData(user, item) ?? throw new InvalidOperationException("UserData should not be null.");
if (userDataDto.PlaybackPositionTicks.HasValue)
{
@@ -127,33 +140,91 @@ namespace Emby.Server.Implementations.Library
SaveUserData(user, item, userData, reason, CancellationToken.None);
}
- private UserItemData GetUserData(User user, Guid itemId, List<string> keys)
+ private UserData Map(UserItemData dto, Guid userId, Guid itemId)
{
- var userId = user.InternalId;
-
- var cacheKey = GetCacheKey(userId, itemId);
+ return new UserData()
+ {
+ ItemId = itemId,
+ CustomDataKey = dto.Key,
+ Item = null,
+ User = null,
+ AudioStreamIndex = dto.AudioStreamIndex,
+ IsFavorite = dto.IsFavorite,
+ LastPlayedDate = dto.LastPlayedDate,
+ Likes = dto.Likes,
+ PlaybackPositionTicks = dto.PlaybackPositionTicks,
+ PlayCount = dto.PlayCount,
+ Played = dto.Played,
+ Rating = dto.Rating,
+ UserId = userId,
+ SubtitleStreamIndex = dto.SubtitleStreamIndex,
+ };
+ }
- return _userData.GetOrAdd(cacheKey, _ => GetUserDataInternal(userId, keys));
+ private UserItemData Map(UserData dto)
+ {
+ return new UserItemData()
+ {
+ Key = dto.CustomDataKey!,
+ AudioStreamIndex = dto.AudioStreamIndex,
+ IsFavorite = dto.IsFavorite,
+ LastPlayedDate = dto.LastPlayedDate,
+ Likes = dto.Likes,
+ PlaybackPositionTicks = dto.PlaybackPositionTicks,
+ PlayCount = dto.PlayCount,
+ Played = dto.Played,
+ Rating = dto.Rating,
+ SubtitleStreamIndex = dto.SubtitleStreamIndex,
+ };
}
- private UserItemData GetUserDataInternal(long internalUserId, List<string> keys)
+ private UserItemData? GetUserData(User user, Guid itemId, List<string> keys)
{
- var userData = _repository.GetUserData(internalUserId, keys);
+ var cacheKey = GetCacheKey(user.InternalId, itemId);
- if (userData is not null)
+ if (_userData.TryGetValue(cacheKey, out var data))
{
- return userData;
+ return data;
}
- if (keys.Count > 0)
+ data = GetUserDataInternal(user.Id, itemId, keys);
+
+ if (data is null)
{
- return new UserItemData
+ return new UserItemData()
{
- Key = keys[0]
+ Key = keys[0],
};
}
- throw new UnreachableException();
+ return _userData.GetOrAdd(cacheKey, data);
+ }
+
+ private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
+ {
+ if (keys.Count == 0)
+ {
+ return null;
+ }
+
+ using var context = _repository.CreateDbContext();
+ var userData = context.UserData.AsNoTracking().Where(e => e.ItemId == itemId && keys.Contains(e.CustomDataKey) && e.UserId.Equals(userId)).ToArray();
+
+ if (userData.Length > 0)
+ {
+ var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N"));
+ if (directDataReference is not null)
+ {
+ return Map(directDataReference);
+ }
+
+ return Map(userData.First());
+ }
+
+ return new UserItemData
+ {
+ Key = keys.Last()!
+ };
}
/// <summary>
@@ -166,20 +237,25 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
- public UserItemData GetUserData(User user, BaseItem item)
+ public UserItemData? GetUserData(User user, BaseItem item)
{
return GetUserData(user, item.Id, item.GetUserDataKeys());
}
/// <inheritdoc />
- public UserItemDataDto GetUserDataDto(BaseItem item, User user)
+ public UserItemDataDto? GetUserDataDto(BaseItem item, User user)
=> GetUserDataDto(item, null, user, new DtoOptions());
/// <inheritdoc />
- public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
+ public UserItemDataDto? GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
{
var userData = GetUserData(user, item);
- var dto = GetUserItemDataDto(userData);
+ if (userData is null)
+ {
+ return null;
+ }
+
+ var dto = GetUserItemDataDto(userData, item.Id);
item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
return dto;
@@ -189,9 +265,10 @@ namespace Emby.Server.Implementations.Library
/// Converts a UserItemData to a DTOUserItemData.
/// </summary>
/// <param name="data">The data.</param>
+ /// <param name="itemId">The reference key to an Item.</param>
/// <returns>DtoUserItemData.</returns>
/// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
- private UserItemDataDto GetUserItemDataDto(UserItemData data)
+ private UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
{
ArgumentNullException.ThrowIfNull(data);
@@ -204,6 +281,7 @@ namespace Emby.Server.Implementations.Library
Rating = data.Rating,
Played = data.Played,
LastPlayedDate = data.LastPlayedDate,
+ ItemId = itemId,
Key = data.Key
};
}
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index e9cf47d46..d42a0e7d2 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -308,39 +308,40 @@ namespace Emby.Server.Implementations.Library
}
}
- var mediaTypes = new List<MediaType>();
+ MediaType[] mediaTypes = [];
if (includeItemTypes.Length == 0)
{
+ HashSet<MediaType> tmpMediaTypes = [];
foreach (var parent in parents.OfType<ICollectionFolder>())
{
switch (parent.CollectionType)
{
case CollectionType.books:
- mediaTypes.Add(MediaType.Book);
- mediaTypes.Add(MediaType.Audio);
+ tmpMediaTypes.Add(MediaType.Book);
+ tmpMediaTypes.Add(MediaType.Audio);
break;
case CollectionType.music:
- mediaTypes.Add(MediaType.Audio);
+ tmpMediaTypes.Add(MediaType.Audio);
break;
case CollectionType.photos:
- mediaTypes.Add(MediaType.Photo);
- mediaTypes.Add(MediaType.Video);
+ tmpMediaTypes.Add(MediaType.Photo);
+ tmpMediaTypes.Add(MediaType.Video);
break;
case CollectionType.homevideos:
- mediaTypes.Add(MediaType.Photo);
- mediaTypes.Add(MediaType.Video);
+ tmpMediaTypes.Add(MediaType.Photo);
+ tmpMediaTypes.Add(MediaType.Video);
break;
default:
- mediaTypes.Add(MediaType.Video);
+ tmpMediaTypes.Add(MediaType.Video);
break;
}
}
- mediaTypes = mediaTypes.Distinct().ToList();
+ mediaTypes = tmpMediaTypes.ToArray();
}
- var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0
+ var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Length == 0
? new[]
{
BaseItemKind.Person,
@@ -366,14 +367,9 @@ namespace Emby.Server.Implementations.Library
Limit = limit * 5,
IsPlayed = isPlayed,
DtoOptions = options,
- MediaTypes = mediaTypes.ToArray()
+ MediaTypes = mediaTypes
};
- if (parents.Count == 0)
- {
- return _libraryManager.GetItemList(query, false);
- }
-
return _libraryManager.GetItemList(query, parents);
}
}
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index bd45b0b96..2d29eb5bf 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -16,7 +16,7 @@
"Folders": "المجلدات",
"Genres": "التصنيفات",
"HeaderAlbumArtists": "فناني الألبوم",
- "HeaderContinueWatching": "استئناف المشاهدة",
+ "HeaderContinueWatching": "إستئناف المشاهدة",
"HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون",
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
@@ -31,7 +31,7 @@
"ItemRemovedWithName": "أُزيل {0} من المكتبة",
"LabelIpAddressValue": "عنوان الآي بي: {0}",
"LabelRunningTimeValue": "مدة التشغيل: {0}",
- "Latest": "أحدث",
+ "Latest": "الأحدث",
"MessageApplicationUpdated": "حُدث خادم Jellyfin",
"MessageApplicationUpdatedTo": "حُدث خادم Jellyfin إلى {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "حُدثت إعدادات الخادم في قسم {0}",
@@ -52,7 +52,7 @@
"NotificationOptionInstallationFailed": "فشل في التثبيت",
"NotificationOptionNewLibraryContent": "أُضيف محتوى جديدا",
"NotificationOptionPluginError": "فشل في الملحق",
- "NotificationOptionPluginInstalled": "ثُبتت المكونات الإضافية",
+ "NotificationOptionPluginInstalled": "ثُبتت الملحق",
"NotificationOptionPluginUninstalled": "تمت إزالة الملحق",
"NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق",
"NotificationOptionServerRestartRequired": "يجب إعادة تشغيل الخادم",
@@ -90,10 +90,10 @@
"UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1} على {2}",
"UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}",
"ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط",
- "ValueSpecialEpisodeName": "حلقه خاصه - {0}",
+ "ValueSpecialEpisodeName": "حلقة خاصه - {0}",
"VersionNumber": "الإصدار {0}",
"TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.",
- "TaskCleanCache": "احذف ما بمجلد الملفات المؤقتة",
+ "TaskCleanCache": "حذف الملفات المؤقتة",
"TasksChannelsCategory": "قنوات الإنترنت",
"TasksLibraryCategory": "مكتبة",
"TasksMaintenanceCategory": "صيانة",
@@ -129,7 +129,12 @@
"TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة.",
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
- "TaskAudioNormalization": "تطبيع الصوت",
+ "TaskAudioNormalization": "تسوية الصوت",
"TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت.",
- "TaskDownloadMissingLyrics": "تنزيل عبارات القصيدة"
+ "TaskDownloadMissingLyrics": "تنزيل عبارات القصيدة",
+ "TaskDownloadMissingLyricsDescription": "كلمات",
+ "TaskExtractMediaSegments": "فحص مقاطع الوسائط",
+ "TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
+ "TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
+ "TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة."
}
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 4f95b4880..72f575753 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -121,7 +121,7 @@
"TaskCleanActivityLog": "Изчисти дневника с активност",
"TaskOptimizeDatabaseDescription": "Прави базата данни по-компактна и освобождава място. Пускането на тази задача след сканиране на библиотеката или правене на други промени, свързани с модификации на базата данни, може да подобри производителността.",
"TaskOptimizeDatabase": "Оптимизирай базата данни",
- "TaskKeyframeExtractorDescription": "Извличат се ключови кадри от видеофайловете ,за да се създаде по точен ХЛС списък . Задачата може да отнеме много време.",
+ "TaskKeyframeExtractorDescription": "Извличат се ключови кадри от видеофайловете ,за да се създаде по точен HLS списък . Задачата може да отнеме много време.",
"TaskKeyframeExtractor": "Извличане на ключови кадри",
"External": "Външен",
"HearingImpaired": "Увреден слух",
@@ -129,8 +129,12 @@
"TaskRefreshTrickplayImagesDescription": "Създава прегледи на Trickplay за видеа в активирани библиотеки.",
"TaskDownloadMissingLyrics": "Свали липсващи текстове",
"TaskDownloadMissingLyricsDescription": "Свали текстове за песни",
- "TaskCleanCollectionsAndPlaylists": "Изчисти колекциите и плейлистовете",
+ "TaskCleanCollectionsAndPlaylists": "Изчисти колекциите и плейлистите",
"TaskCleanCollectionsAndPlaylistsDescription": "Премахни несъществуващи файлове в колекциите и плейлистите.",
"TaskAudioNormalization": "Нормализиране на звука",
- "TaskAudioNormalizationDescription": "Сканирай файловете за нормализация на звука."
+ "TaskAudioNormalizationDescription": "Сканирай файловете за нормализация на звука.",
+ "TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.",
+ "TaskMoveTrickplayImages": "Мигриране на Локацията за Trickplay изображения",
+ "TaskMoveTrickplayImagesDescription": "Премества съществуващите trickplay изображения спрямо настройките на библиотеката.",
+ "TaskExtractMediaSegments": "Сканиране за сегменти"
}
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index 4724bba3b..268a141ff 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -125,5 +125,11 @@
"TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক",
"TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।",
"TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি করুন",
- "TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।"
+ "TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।",
+ "TaskDownloadMissingLyricsDescription": "গানের লিরিক্স ডাউনলোড করে",
+ "TaskCleanCollectionsAndPlaylists": "সংগ্রহ এবং প্লেলিস্ট পরিষ্কার করুন",
+ "TaskCleanCollectionsAndPlaylistsDescription": "সংগ্রহ এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
+ "TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
+ "TaskExtractMediaSegmentsDescription": "MediaSegment সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।",
+ "TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index 6b3b78fa1..6cce0e019 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -16,7 +16,7 @@
"Folders": "Carpetes",
"Genres": "Gèneres",
"HeaderAlbumArtists": "Artistes de l'àlbum",
- "HeaderContinueWatching": "Continuar veient",
+ "HeaderContinueWatching": "Continua veient",
"HeaderFavoriteAlbums": "Àlbums preferits",
"HeaderFavoriteArtists": "Artistes preferits",
"HeaderFavoriteEpisodes": "Episodis preferits",
@@ -24,13 +24,13 @@
"HeaderFavoriteSongs": "Cançons preferides",
"HeaderLiveTV": "TV en directe",
"HeaderNextUp": "A continuació",
- "HeaderRecordingGroups": "Grups d'enregistrament",
+ "HeaderRecordingGroups": "Grups Musicals",
"HomeVideos": "Vídeos domèstics",
- "Inherit": "Hereta",
- "ItemAddedWithName": "{0} ha sigut afegit a la biblioteca",
- "ItemRemovedWithName": "{0} ha sigut eliminat de la biblioteca",
+ "Inherit": "Heretat",
+ "ItemAddedWithName": "{0} s'ha afegit a la biblioteca",
+ "ItemRemovedWithName": "{0} s'ha eliminat de la biblioteca",
"LabelIpAddressValue": "Adreça IP: {0}",
- "LabelRunningTimeValue": "Temps en funcionament: {0}",
+ "LabelRunningTimeValue": "Temps en marxa: {0}",
"Latest": "Darrers",
"MessageApplicationUpdated": "El servidor de Jellyfin ha estat actualitzat",
"MessageApplicationUpdatedTo": "El servidor de Jellyfin ha estat actualitzat a {0}",
@@ -44,8 +44,8 @@
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada desconeguda",
"NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.",
- "NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicació disponible",
- "NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicació instal·lada",
+ "NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicatiu disponible",
+ "NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicatiu instal·lada",
"NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
"NotificationOptionAudioPlaybackStopped": "Reproducció d'àudio aturada",
"NotificationOptionCameraImageUploaded": "Imatge de càmera pujada",
@@ -54,8 +54,8 @@
"NotificationOptionPluginError": "Un complement ha fallat",
"NotificationOptionPluginInstalled": "Complement instal·lat",
"NotificationOptionPluginUninstalled": "Complement desinstal·lat",
- "NotificationOptionPluginUpdateInstalled": "Actualització de complement instal·lada",
- "NotificationOptionServerRestartRequired": "Reinici del servidor requerit",
+ "NotificationOptionPluginUpdateInstalled": "Actualització del complement instal·lada",
+ "NotificationOptionServerRestartRequired": "El servidor s'ha de reiniciar",
"NotificationOptionTaskFailed": "Tasca programada fallida",
"NotificationOptionUserLockedOut": "Usuari expulsat",
"NotificationOptionVideoPlayback": "Reproducció de vídeo iniciada",
@@ -64,15 +64,15 @@
"Playlists": "Llistes de reproducció",
"Plugin": "Complement",
"PluginInstalledWithName": "{0} ha estat instal·lat",
- "PluginUninstalledWithName": "{0} ha estat desinstal·lat",
- "PluginUpdatedWithName": "{0} ha estat actualitzat",
+ "PluginUninstalledWithName": "S'ha instalat {0}",
+ "PluginUpdatedWithName": "S'ha actualitzat {0}",
"ProviderValue": "Proveïdor: {0}",
"ScheduledTaskFailedWithName": "{0} ha fallat",
- "ScheduledTaskStartedWithName": "{0} s'ha iniciat",
- "ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat",
+ "ScheduledTaskStartedWithName": "S'ha iniciat {0}",
+ "ServerNameNeedsToBeRestarted": "S'ha de reiniciar {0}",
"Shows": "Sèries",
"Songs": "Cançons",
- "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho altre cop aviat.",
+ "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu de nou en una estona.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
"Sync": "Sincronitzar",
@@ -80,41 +80,41 @@
"TvShows": "Sèries de TV",
"User": "Usuari",
"UserCreatedWithName": "S'ha creat l'usuari {0}",
- "UserDeletedWithName": "L'usuari {0} ha estat eliminat",
+ "UserDeletedWithName": "S'ha eliminat l'usuari {0}",
"UserDownloadingItemWithValues": "{0} està descarregant {1}",
- "UserLockedOutWithName": "L'usuari {0} ha sigut expulsat",
+ "UserLockedOutWithName": "S'ha expulsat a l'usuari {0}",
"UserOfflineFromDevice": "{0} s'ha desconnectat de {1}",
"UserOnlineFromDevice": "{0} està connectat des de {1}",
- "UserPasswordChangedWithName": "La contrasenya ha estat canviada per a l'usuari {0}",
+ "UserPasswordChangedWithName": "S'ha canviat la contrasenya per a l'usuari {0}",
"UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}",
- "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1}",
- "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}",
- "ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva biblioteca",
+ "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1} a {2}",
+ "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1} a {2}",
+ "ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la teva biblioteca",
"ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versió {0}",
"TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
"TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin",
- "TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'internet.",
+ "TaskRefreshChannelsDescription": "Actualitza la informació dels canals per internet.",
"TaskRefreshChannels": "Actualitza els canals",
"TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
"TaskCleanTranscode": "Neteja les transcodificacions",
- "TaskUpdatePluginsDescription": "Actualitza els connectors que estan configurats per a actualitzar-se automàticament.",
- "TaskUpdatePlugins": "Actualitza els connectors",
- "TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva mediateca.",
+ "TaskUpdatePluginsDescription": "Actualitza els complements que estan configurats per a actualitzar-se automàticament.",
+ "TaskUpdatePlugins": "Actualitza els complements",
+ "TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva biblioteca de mitjans.",
"TaskRefreshPeople": "Actualitza les persones",
"TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja els registres",
- "TaskRefreshLibraryDescription": "Escaneja la mediateca buscant fitxers nous i refresca les metadades.",
+ "TaskRefreshLibraryDescription": "Escaneja la biblioteca de mitjans buscant fitxers nous i refresca les metadades.",
"TaskRefreshLibrary": "Escaneja la biblioteca de mitjans",
"TaskRefreshChapterImagesDescription": "Crea les miniatures dels vídeos que tinguin capítols.",
"TaskRefreshChapterImages": "Extreure les imatges dels capítols",
- "TaskCleanCacheDescription": "Elimina els arxius temporals que ja no són necessaris per al servidor.",
- "TaskCleanCache": "Elimina arxius temporals",
- "TasksChannelsCategory": "Canals d'internet",
- "TasksApplicationCategory": "Aplicació",
+ "TaskCleanCacheDescription": "Elimina la memòria cau no necessària per al servidor.",
+ "TaskCleanCache": "Elimina la memòria cau",
+ "TasksChannelsCategory": "Canals per internet",
+ "TasksApplicationCategory": "Aplicatiu",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manteniment",
- "TaskCleanActivityLogDescription": "Eliminat entrades del registre d'activitats mes antigues que l'antiguitat configurada.",
+ "TaskCleanActivityLogDescription": "Eliminades les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
"TaskCleanActivityLog": "Buidar el registre d'activitat",
"Undefined": "Indefinit",
"Forced": "Forçat",
@@ -128,9 +128,13 @@
"TaskRefreshTrickplayImages": "Generar miniatures de línia de temps",
"TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades.",
"TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
- "TaskCleanCollectionsAndPlaylists": "Neteja col·leccions i llistes de reproducció",
- "TaskAudioNormalization": "Normalització d'Àudio",
- "TaskAudioNormalizationDescription": "Escaneja arxius per dades de normalització d'àudio.",
- "TaskDownloadMissingLyricsDescription": "Baixar lletres de les cançons",
- "TaskDownloadMissingLyrics": "Baixar lletres que falten"
+ "TaskCleanCollectionsAndPlaylists": "Neteja les col·leccions i llistes de reproducció",
+ "TaskAudioNormalization": "Estabilització d'Àudio",
+ "TaskAudioNormalizationDescription": "Escaneja arxius per dades d'estabilització d'àudio.",
+ "TaskDownloadMissingLyricsDescription": "Baixar les lletres de les cançons",
+ "TaskDownloadMissingLyrics": "Baixar les lletres que falten",
+ "TaskExtractMediaSegments": "Escaneig de segments multimèdia",
+ "TaskExtractMediaSegmentsDescription": "Extreu o obté segments multimèdia usant els connectors MediaSegment activats.",
+ "TaskMoveTrickplayImages": "Migra la ubicació de la imatge de Trickplay",
+ "TaskMoveTrickplayImagesDescription": "Mou els fitxers trickplay existents segons la configuració de la biblioteca."
}
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index 121a0eba8..d43d4097f 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -1,5 +1,5 @@
{
- "Albums": "Album",
+ "Albums": "Albummer",
"AppDeviceValues": "App: {0}, Enhed: {1}",
"Application": "Applikation",
"Artists": "Kunstnere",
@@ -72,7 +72,7 @@
"ServerNameNeedsToBeRestarted": "{0} skal genstartes",
"Shows": "Serier",
"Songs": "Sange",
- "StartupEmbyServerIsLoading": "Jellyfin Server er i gang med at starte. Forsøg igen om et øjeblik.",
+ "StartupEmbyServerIsLoading": "Jellyfin er i gang med at starte. Prøv igen om et øjeblik.",
"SubtitleDownloadFailureForItem": "Fejlet i download af undertekster for {0}",
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}",
"Sync": "Synkroniser",
@@ -93,13 +93,13 @@
"ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
- "TaskDownloadMissingSubtitles": "Hentede medie mangler undertekster",
+ "TaskDownloadMissingSubtitles": "Hent manglende undertekster",
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er konfigurerede til at blive opdateret automatisk.",
- "TaskUpdatePlugins": "Opdater Plugins",
+ "TaskUpdatePlugins": "Opdater plugins",
"TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
- "TaskCleanLogs": "Ryd Log-mappe",
+ "TaskCleanLogs": "Ryd log-mappe",
"TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
- "TaskRefreshLibrary": "Scan Mediebibliotek",
+ "TaskRefreshLibrary": "Scan mediebibliotek",
"TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
"TaskCleanCache": "Ryd cache-mappe",
"TasksChannelsCategory": "Internetkanaler",
@@ -108,33 +108,33 @@
"TasksMaintenanceCategory": "Vedligeholdelse",
"TaskRefreshChapterImages": "Udtræk kapitelbilleder",
"TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
- "TaskRefreshChannelsDescription": "Opdaterer information for internetkanal.",
- "TaskRefreshChannels": "Opdater Kanaler",
- "TaskCleanTranscodeDescription": "Fjerner transcode-filer, som er mere end 1 dag gammel.",
- "TaskCleanTranscode": "Tøm Transcode-mappen",
- "TaskRefreshPeople": "Opdater Personer",
+ "TaskRefreshChannelsDescription": "Opdaterer information for internetkanaler.",
+ "TaskRefreshChannels": "Opdater kanaler",
+ "TaskCleanTranscodeDescription": "Fjerner omkodningsfiler, som er mere end 1 dag gamle.",
+ "TaskCleanTranscode": "Tøm omkodningsmappen",
+ "TaskRefreshPeople": "Opdater personer",
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
- "TaskCleanActivityLog": "Ryd Aktivitetslog",
+ "TaskCleanActivityLog": "Ryd aktivitetslog",
"Undefined": "Udefineret",
"Forced": "Tvunget",
"Default": "Standard",
- "TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.",
- "TaskOptimizeDatabase": "Optimér database",
- "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.",
- "TaskKeyframeExtractor": "Udtræk af nøglebillede",
+ "TaskOptimizeDatabaseDescription": "Komprimerer databasen for at frigøre plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen.",
+ "TaskOptimizeDatabase": "Optimer database",
+ "TaskKeyframeExtractorDescription": "Udtrækker rammer fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.",
+ "TaskKeyframeExtractor": "Udtræk nøglerammer",
"External": "Ekstern",
"HearingImpaired": "Hørehæmmet",
- "TaskRefreshTrickplayImages": "Generér Trickplay Billeder",
- "TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker.",
+ "TaskRefreshTrickplayImages": "Generer trickplay-billeder",
+ "TaskRefreshTrickplayImagesDescription": "Laver trickplay-billeder for videoer i aktiverede biblioteker.",
"TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
"TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra samlinger og afspilningslister der ikke eksisterer længere.",
- "TaskAudioNormalizationDescription": "Skanner filer for data vedrørende audio-normalisering.",
- "TaskAudioNormalization": "Audio-normalisering",
- "TaskDownloadMissingLyricsDescription": "Hentede sange mangler sangtekster",
- "TaskDownloadMissingLyrics": "Hentede medie mangler sangtekster",
- "TaskExtractMediaSegments": "Scan mediesegment",
- "TaskMoveTrickplayImages": "Migrer billedelokation for Trickplay",
- "TaskMoveTrickplayImagesDescription": "Flyt eksisterende trickplay-filer jævnfør biblioteksindstillilnger.",
- "TaskExtractMediaSegmentsDescription": "Ekstraherer eller henter mediesegmenter fra plugins som understøtter MediaSegment."
+ "TaskAudioNormalizationDescription": "Skanner filer for data vedrørende lydnormalisering.",
+ "TaskAudioNormalization": "Lydnormalisering",
+ "TaskDownloadMissingLyricsDescription": "Søger på internettet efter manglende sangtekster baseret på metadata-konfigurationen",
+ "TaskDownloadMissingLyrics": "Hent manglende sangtekster",
+ "TaskExtractMediaSegments": "Scan for mediesegmenter",
+ "TaskMoveTrickplayImages": "Migrer billedelokationer for trickplay-billeder",
+ "TaskMoveTrickplayImagesDescription": "Flyt eksisterende trickplay-billeder jævnfør biblioteksindstillinger.",
+ "TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment."
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 51c9e87d5..c38af5bf4 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -5,7 +5,7 @@
"Artists": "Interpreten",
"AuthenticationSucceededWithUserName": "{0} erfolgreich authentifiziert",
"Books": "Bücher",
- "CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
+ "CameraImageUploadedFrom": "Ein neues Kamerabild wurde von {0} hochgeladen",
"Channels": "Kanäle",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Sammlungen",
@@ -18,7 +18,7 @@
"HeaderAlbumArtists": "Album-Interpreten",
"HeaderContinueWatching": "Weiterschauen",
"HeaderFavoriteAlbums": "Lieblingsalben",
- "HeaderFavoriteArtists": "Lieblings-Interpreten",
+ "HeaderFavoriteArtists": "Lieblingsinterpreten",
"HeaderFavoriteEpisodes": "Lieblingsepisoden",
"HeaderFavoriteShows": "Lieblingsserien",
"HeaderFavoriteSongs": "Lieblingslieder",
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 056a2e475..55f266032 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -130,5 +130,11 @@
"TaskAudioNormalization": "Ομοιομορφία ήχου",
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
"TaskCleanCollectionsAndPlaylists": "Καθαρισμός συλλογών και λιστών αναπαραγωγής",
- "TaskCleanCollectionsAndPlaylistsDescription": "Αφαιρούνται στοιχεία από τις συλλογές και τις λίστες αναπαραγωγής που δεν υπάρχουν πλέον."
+ "TaskCleanCollectionsAndPlaylistsDescription": "Αφαιρούνται στοιχεία από τις συλλογές και τις λίστες αναπαραγωγής που δεν υπάρχουν πλέον.",
+ "TaskMoveTrickplayImages": "Αλλαγή τοποθεσίας εικόνων Trickplay",
+ "TaskDownloadMissingLyrics": "Λήψη στίχων που λείπουν",
+ "TaskMoveTrickplayImagesDescription": "Μετακινεί τα υπάρχοντα αρχεία trickplay σύμφωνα με τις ρυθμίσεις της βιβλιοθήκης.",
+ "TaskDownloadMissingLyricsDescription": "Κατεβάζει στίχους για τραγούδια",
+ "TaskExtractMediaSegments": "Σάρωση τμημάτων πολυμέσων",
+ "TaskExtractMediaSegmentsDescription": "Εξάγει ή βρίσκει τμήματα πολυμέσων από επεκτάσεις που χρησιμοποιούν το MediaSegment."
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index f2f657b04..cf31960f9 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -15,7 +15,7 @@
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
- "HeaderAlbumArtists": "Artistas de álbum",
+ "HeaderAlbumArtists": "Artistas del álbum",
"HeaderContinueWatching": "Seguir viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index b458ed423..2534f37c1 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -131,5 +131,9 @@
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
"TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
"TaskDownloadMissingLyrics": "Descargar letra faltante",
- "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones"
+ "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones",
+ "TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de complementos habilitados para MediaSegment.",
+ "TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.",
+ "TaskExtractMediaSegments": "Escaneo de segmentos de medios",
+ "TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay"
}
diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json
index 114c76c54..4df4b90d3 100644
--- a/Emby.Server.Implementations/Localization/Core/eu.json
+++ b/Emby.Server.Implementations/Localization/Core/eu.json
@@ -19,25 +19,25 @@
"Artists": "Artistak",
"Albums": "Albumak",
"TaskOptimizeDatabase": "Datu basea optimizatu",
- "TaskDownloadMissingSubtitlesDescription": "Metadataren konfigurazioan oinarrituta falta diren azpitituluak bilatzen ditu interneten.",
+ "TaskDownloadMissingSubtitlesDescription": "Falta diren azpitituluak bilatzen ditu interneten metadatuen konfigurazioaren arabera.",
"TaskDownloadMissingSubtitles": "Falta diren azpitituluak deskargatu",
"TaskRefreshChannelsDescription": "Internet kanalen informazioa eguneratu.",
"TaskRefreshChannels": "Kanalak eguneratu",
- "TaskCleanTranscodeDescription": "Egun bat baino zaharragoak diren transcode fitxategiak ezabatzen ditu.",
- "TaskCleanTranscode": "Transcode direktorioa garbitu",
- "TaskUpdatePluginsDescription": "Automatikoki eguneratzeko konfiguratutako pluginen eguneraketak deskargatu eta instalatzen ditu.",
+ "TaskCleanTranscodeDescription": "Egun bat baino zaharragoak diren transkodifikazio fitxategiak ezabatzen ditu.",
+ "TaskCleanTranscode": "Transkodifikazio direktorioa garbitu",
+ "TaskUpdatePluginsDescription": "Automatikoki deskargatu eta instalatu eguneraketak konfiguratutako pluginetarako.",
"TaskUpdatePlugins": "Pluginak eguneratu",
- "TaskRefreshPeopleDescription": "Zure liburutegiko aktore eta zuzendarien metadata eguneratzen du.",
+ "TaskRefreshPeopleDescription": "Zure liburutegiko aktore eta zuzendarien metadatuak eguneratzen ditu.",
"TaskRefreshPeople": "Jendea eguneratu",
"TaskCleanLogsDescription": "{0} egun baino zaharragoak diren log fitxategiak ezabatzen ditu.",
"TaskCleanLogs": "Log direktorioa garbitu",
- "TaskRefreshLibraryDescription": "Zure multimedia liburutegia eskaneatzen du fitxategi berriak eta metadatak eguneratzeko.",
- "TaskRefreshLibrary": "Multimedia Liburutegia eskaneatu",
+ "TaskRefreshLibraryDescription": "Zure multimedia liburutegia eskaneatzen du fitxategi berriak eta metadatuak eguneratzeko.",
+ "TaskRefreshLibrary": "Multimedia liburutegia eskaneatu",
"TaskRefreshChapterImagesDescription": "Kapituluak dituzten bideoen miniaturak sortzen ditu.",
"TaskRefreshChapterImages": "Kapituluen irudiak erauzi",
"TaskCleanCacheDescription": "Sistemak behar ez dituen cache fitxategiak ezabatzen ditu.",
- "TaskCleanCache": "Cache Directorioa garbitu",
- "TaskCleanActivityLogDescription": "Konfiguratuta data baino zaharragoak diren log-ak ezabatu.",
+ "TaskCleanCache": "Cache direktorioa garbitu",
+ "TaskCleanActivityLogDescription": "Konfiguratutako baino zaharragoak diren jarduera-log sarrerak ezabatzen ditu.",
"TaskCleanActivityLog": "Erabilera Log-a garbitu",
"TasksChannelsCategory": "Internet Kanalak",
"TasksApplicationCategory": "Aplikazioa",
@@ -45,22 +45,22 @@
"TasksMaintenanceCategory": "Mantenua",
"VersionNumber": "Bertsioa {0}",
"ValueHasBeenAddedToLibrary": "{0} zure multimedia liburutegian gehitu da",
- "UserStoppedPlayingItemWithValues": "{0}-ek {1} ikusteaz bukatu du {2}-(a)n",
- "UserStartedPlayingItemWithValues": "{0} {1} ikusten ari da {2}-(a)n",
- "UserPolicyUpdatedWithName": "{0} Erabiltzailearen politikak aldatu dira",
- "UserPasswordChangedWithName": "{0} Erabiltzailearen pasahitza aldatu da",
- "UserOnlineFromDevice": "{0} online dago {1}-tik",
- "UserOfflineFromDevice": "{0} {1}-tik deskonektatu da",
- "UserLockedOutWithName": "{0} Erabiltzailea blokeatu da",
- "UserDownloadingItemWithValues": "{1} {0}-tik deskargatzen",
+ "UserStoppedPlayingItemWithValues": "{0} {1} ikusten bukatu du {2}-(e)n",
+ "UserStartedPlayingItemWithValues": "{0} {1} ikusten ari da {2}-(e)n",
+ "UserPolicyUpdatedWithName": "{0} erabiltzailearen politikak aldatu dira",
+ "UserPasswordChangedWithName": "{0} erabiltzailearen pasahitza aldatu da",
+ "UserOnlineFromDevice": "{0} online dago {1}-(e)tik",
+ "UserOfflineFromDevice": "{0} {1}-(e)tik deskonektatu da",
+ "UserLockedOutWithName": "{0} erabiltzailea blokeatu da",
+ "UserDownloadingItemWithValues": "{0} {1} deskargatzen ari da",
"UserDeletedWithName": "{0} Erabiltzailea ezabatu da",
"UserCreatedWithName": "{0} Erabiltzailea sortu da",
"User": "Erabiltzailea",
"Undefined": "Ezezaguna",
- "TvShows": "TB showak",
+ "TvShows": "TB serieak",
"System": "Sistema",
- "SubtitleDownloadFailureFromForItem": "{1}-en azpitutuluak {0} deskargatzean huts egin du",
- "StartupEmbyServerIsLoading": "Jellyfin zerbitzaria kargatzen. Saiatu berriro beranduxeago.",
+ "SubtitleDownloadFailureFromForItem": "{1}-en azpitutuluak {0}-tik deskargatzeak huts egin du",
+ "StartupEmbyServerIsLoading": "Jellyfin zerbitzaria kargatzen. Saiatu berriro beranduago.",
"ServerNameNeedsToBeRestarted": "{0} berrabiarazi behar da",
"ScheduledTaskStartedWithName": "{0} hasi da",
"ScheduledTaskFailedWithName": "{0} huts egin du",
@@ -89,26 +89,26 @@
"NameSeasonNumber": "{0} Denboraldia",
"NameInstallFailed": "{0} instalazioak huts egin du",
"Music": "Musika",
- "MixedContent": "Denetariko edukia",
+ "MixedContent": "Eduki mistoa",
"MessageServerConfigurationUpdated": "Zerbitzariaren konfigurazioa eguneratu da",
- "MessageNamedServerConfigurationUpdatedWithValue": "Zerbitzariaren konfigurazio {0} atala eguneratu da",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Zerbitzariaren {0} konfigurazio atala eguneratu da",
"MessageApplicationUpdatedTo": "Jellyfin zerbitzaria {0}-ra eguneratu da",
"MessageApplicationUpdated": "Jellyfin zerbitzaria eguneratu da",
"Latest": "Azkena",
- "LabelRunningTimeValue": "Denbora martxan: {0}",
+ "LabelRunningTimeValue": "Iraupena: {0}",
"LabelIpAddressValue": "IP helbidea: {0}",
- "ItemRemovedWithName": "{0} liburutegitik ezabatu da",
+ "ItemRemovedWithName": "{0} liburutegitik kendu da",
"ItemAddedWithName": "{0} liburutegira gehitu da",
"HomeVideos": "Etxeko bideoak",
- "HeaderNextUp": "Nobedadeak",
+ "HeaderNextUp": "Hurrengoa",
"HeaderLiveTV": "Zuzeneko TB",
"HeaderFavoriteSongs": "Gogoko abestiak",
- "HeaderFavoriteShows": "Gogoko showak",
+ "HeaderFavoriteShows": "Gogoko serieak",
"HeaderFavoriteEpisodes": "Gogoko atalak",
"HeaderFavoriteArtists": "Gogoko artistak",
"HeaderFavoriteAlbums": "Gogoko albumak",
"Forced": "Behartuta",
- "FailedLoginAttemptWithUserName": "Login egiten akatsa, saiatu hemen {0}",
+ "FailedLoginAttemptWithUserName": "{0}-tik saioa hasteak huts egin du",
"External": "Kanpokoa",
"DeviceOnlineWithName": "{0} konektatu da",
"DeviceOfflineWithName": "{0} deskonektatu da",
@@ -117,13 +117,23 @@
"AuthenticationSucceededWithUserName": "{0} ongi autentifikatu da",
"Application": "Aplikazioa",
"AppDeviceValues": "App: {0}, Gailua: {1}",
- "HearingImpaired": "Entzunaldia aldatua",
+ "HearingImpaired": "Entzumen urritasuna",
"ProviderValue": "Hornitzailea: {0}",
"TaskKeyframeExtractorDescription": "Bideo fitxategietako fotograma gakoak ateratzen ditu HLS erreprodukzio-zerrenda zehatzagoak sortzeko. Zeregin honek denbora asko iraun dezake.",
"HeaderRecordingGroups": "Grabaketa taldeak",
"Inherit": "Oinordetu",
"TaskOptimizeDatabaseDescription": "Datu-basea trinkotu eta bertatik espazioa askatzen du. Liburutegia eskaneatu ondoren edo datu-basean aldaketak egin ondoren ataza hau exekutatzeak errendimendua hobetu lezake.",
"TaskKeyframeExtractor": "Fotograma gakoen erauzgailua",
- "TaskRefreshTrickplayImages": "\"Trickplay Irudiak Sortu",
- "TaskRefreshTrickplayImagesDescription": "Bideoentzako trickplay aurrebistak sortzen ditu gaitutako liburutegietan."
+ "TaskRefreshTrickplayImages": "Trickplay irudiak sortu",
+ "TaskRefreshTrickplayImagesDescription": "Bideoentzako trickplay aurrebistak sortzen ditu gaitutako liburutegietan.",
+ "TaskAudioNormalization": "Audio normalizazioa",
+ "TaskDownloadMissingLyrics": "Deskargatu falta diren letrak",
+ "TaskDownloadMissingLyricsDescription": "Deskargatu abestientzako letrak",
+ "TaskExtractMediaSegments": "Multimedia segmentuen eskaneoa",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Jada existitzen ez diren bildumak eta erreprodukzio-zerrendak kentzen ditu.",
+ "TaskCleanCollectionsAndPlaylists": "Garbitu bildumak eta erreprodukzio-zerrendak",
+ "TaskExtractMediaSegmentsDescription": "Media segmentuak atera edo lortzen ditu MediaSegment gaituta duten pluginetik.",
+ "TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua",
+ "TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.",
+ "TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index 8a88cf28e..c9f580cd5 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -130,5 +130,10 @@
"TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat",
"TaskAudioNormalization": "Äänenvoimakkuuden normalisointi",
"TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja.",
- "TaskDownloadMissingLyrics": "Lataa puuttuva lyriikka"
+ "TaskDownloadMissingLyrics": "Lataa puuttuva lyriikka",
+ "TaskExtractMediaSegments": "Mediasegmentin skannaus",
+ "TaskDownloadMissingLyricsDescription": "Ladataan sanoituksia",
+ "TaskExtractMediaSegmentsDescription": "Poimii tai hankkii mediasegmenttejä MediaSegment-yhteensopivista laajennuksista.",
+ "TaskMoveTrickplayImages": "Siirrä Trickplay-kuvien sijainti",
+ "TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 68ab4b617..a10912f01 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -135,5 +135,6 @@
"TaskDownloadMissingLyricsDescription": "Téléchargement des paroles des chansons",
"TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.",
"TaskDownloadMissingLyrics": "Télécharger les paroles des chansons manquantes",
- "TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay"
+ "TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
+ "TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ga.json b/Emby.Server.Implementations/Localization/Core/ga.json
index b511ed6ba..b8e787c20 100644
--- a/Emby.Server.Implementations/Localization/Core/ga.json
+++ b/Emby.Server.Implementations/Localization/Core/ga.json
@@ -1,16 +1,139 @@
{
"Albums": "Albaim",
- "Artists": "Ealaíontóir",
- "AuthenticationSucceededWithUserName": "{0} fíordheimhnithe",
- "Books": "leabhair",
- "CameraImageUploadedFrom": "Tá íomhá ceamara nua uaslódáilte ó {0}",
+ "Artists": "Ealaíontóirí",
+ "AuthenticationSucceededWithUserName": "D'éirigh le fíordheimhniú {0}",
+ "Books": "Leabhair",
+ "CameraImageUploadedFrom": "Uaslódáladh íomhá ceamara nua ó {0}",
"Channels": "Cainéil",
"ChapterNameValue": "Caibidil {0}",
"Collections": "Bailiúcháin",
- "Default": "Mainneachtain",
- "DeviceOfflineWithName": "scoireadh {0}",
- "DeviceOnlineWithName": "{0} ceangailte",
- "External": "Forimeallach",
- "FailedLoginAttemptWithUserName": "Iarracht ar theip ar fhíordheimhniú ó {0}",
- "Favorites": "Ceanáin"
+ "Default": "Réamhshocrú",
+ "DeviceOfflineWithName": "Tá {0} dícheangailte",
+ "DeviceOnlineWithName": "Tá {0} nasctha",
+ "External": "Seachtrach",
+ "FailedLoginAttemptWithUserName": "Theip ar iarracht logáil isteach ó {0}",
+ "Favorites": "Ceanáin",
+ "TaskExtractMediaSegments": "Scanadh Deighleog na Meán",
+ "TaskMoveTrickplayImages": "Imirce Suíomh Íomhá Trickplay",
+ "TaskDownloadMissingLyrics": "Íosluchtaigh liricí ar iarraidh",
+ "TaskKeyframeExtractor": "Keyframe Eastarraingteoir",
+ "TaskAudioNormalization": "Normalú Fuaime",
+ "TaskAudioNormalizationDescription": "Scanann comhaid le haghaidh sonraí normalaithe fuaime.",
+ "TaskRefreshLibraryDescription": "Déanann sé do leabharlann meán a scanadh le haghaidh comhaid nua agus athnuachana meiteashonraí.",
+ "TaskCleanLogs": "Eolaire Logchomhad Glan",
+ "TaskCleanLogsDescription": "Scriostar comhaid loga atá níos mó ná {0} lá d'aois.",
+ "TaskRefreshPeopleDescription": "Nuashonraítear meiteashonraí d’aisteoirí agus stiúrthóirí i do leabharlann meán.",
+ "TaskRefreshTrickplayImages": "Gin Íomhánna Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Cruthaíonn sé réamhamhairc trickplay le haghaidh físeáin i leabharlanna cumasaithe.",
+ "TaskRefreshChannels": "Cainéil Athnuaigh",
+ "TaskRefreshChannelsDescription": "Athnuachan eolas faoi chainéil idirlín.",
+ "TaskOptimizeDatabase": "Bunachar sonraí a bharrfheabhsú",
+ "TaskKeyframeExtractorDescription": "Baintear eochairfhrámaí as comhaid físe chun seinmliostaí HLS níos cruinne a chruthú. Féadfaidh an tasc seo a bheith ar siúl ar feadh i bhfad.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Baintear míreanna as bailiúcháin agus seinmliostaí nach ann dóibh a thuilleadh.",
+ "TaskDownloadMissingLyricsDescription": "Íosluchtaigh liricí do na hamhráin",
+ "TaskUpdatePluginsDescription": "Íoslódálann agus suiteálann nuashonruithe do bhreiseáin atá cumraithe le nuashonrú go huathoibríoch.",
+ "TaskDownloadMissingSubtitlesDescription": "Déanann sé cuardach ar an idirlíon le haghaidh fotheidil atá ar iarraidh bunaithe ar chumraíocht meiteashonraí.",
+ "TaskExtractMediaSegmentsDescription": "Sliocht nó faigheann codanna meán ó bhreiseáin chumasaithe MediaSegment.",
+ "TaskCleanCollectionsAndPlaylists": "Glan suas bailiúcháin agus seinmliostaí",
+ "TaskOptimizeDatabaseDescription": "Comhdhlúthaíonn bunachar sonraí agus gearrtar spás saor in aisce. Má ritheann tú an tasc seo tar éis scanadh a dhéanamh ar an leabharlann nó athruithe eile a dhéanamh a thugann le tuiscint gur cheart go bhfeabhsófaí an fheidhmíocht.",
+ "TaskMoveTrickplayImagesDescription": "Bogtar comhaid trickplay atá ann cheana de réir socruithe na leabharlainne.",
+ "AppDeviceValues": "Aip: {0}, Gléas: {1}",
+ "Application": "Feidhmchlár",
+ "Folders": "Fillteáin",
+ "Forced": "Éigean",
+ "Genres": "Seánraí",
+ "HeaderAlbumArtists": "Ealaíontóirí albam",
+ "HeaderContinueWatching": "Leanúint ar aghaidh ag Breathnú",
+ "HeaderFavoriteAlbums": "Albam is fearr leat",
+ "HeaderFavoriteArtists": "Ealaíontóirí is Fearr",
+ "HeaderFavoriteEpisodes": "Eipeasóid is fearr leat",
+ "HeaderFavoriteShows": "Seónna is Fearr",
+ "HeaderFavoriteSongs": "Amhráin is fearr leat",
+ "HeaderLiveTV": "Teilifís beo",
+ "HeaderNextUp": "Ar Aghaidh Suas",
+ "HeaderRecordingGroups": "Grúpaí Taifeadta",
+ "HearingImpaired": "Lag éisteachta",
+ "HomeVideos": "Físeáin Baile",
+ "Inherit": "Oidhreacht",
+ "ItemAddedWithName": "Cuireadh {0} leis an leabharlann",
+ "ItemRemovedWithName": "Baineadh {0} den leabharlann",
+ "LabelIpAddressValue": "Seoladh IP: {0}",
+ "LabelRunningTimeValue": "Am rite: {0}",
+ "Latest": "Is déanaí",
+ "MessageApplicationUpdated": "Tá Freastalaí Jellyfin nuashonraithe",
+ "MessageApplicationUpdatedTo": "Nuashonraíodh Freastalaí Jellyfin go {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Nuashonraíodh an chuid cumraíochta freastalaí {0}",
+ "MessageServerConfigurationUpdated": "Nuashonraíodh cumraíocht an fhreastalaí",
+ "MixedContent": "Ábhar measctha",
+ "Movies": "Scannáin",
+ "Music": "Ceol",
+ "MusicVideos": "Físeáin Ceoil",
+ "NameInstallFailed": "Theip ar shuiteáil {0}",
+ "NameSeasonNumber": "Séasúr {0}",
+ "NameSeasonUnknown": "Séasúr Anaithnid",
+ "NewVersionIsAvailable": "Tá leagan nua de Jellyfin Server ar fáil le híoslódáil.",
+ "NotificationOptionApplicationUpdateAvailable": "Nuashonrú feidhmchláir ar fáil",
+ "NotificationOptionApplicationUpdateInstalled": "Nuashonrú feidhmchláir suiteáilte",
+ "NotificationOptionAudioPlayback": "Cuireadh tús le hathsheinm fuaime",
+ "NotificationOptionAudioPlaybackStopped": "Cuireadh deireadh le hathsheinm fuaime",
+ "NotificationOptionCameraImageUploaded": "Íosluchtaigh grianghraf ceamara",
+ "NotificationOptionInstallationFailed": "Teip suiteála",
+ "NotificationOptionNewLibraryContent": "Ábhar nua curtha leis",
+ "NotificationOptionPluginError": "Teip breiseán",
+ "NotificationOptionPluginInstalled": "Breiseán suiteáilte",
+ "NotificationOptionPluginUninstalled": "Breiseán díshuiteáilte",
+ "NotificationOptionPluginUpdateInstalled": "Nuashonrú breiseán suiteáilte",
+ "NotificationOptionServerRestartRequired": "Teastaíonn atosú an fhreastalaí",
+ "NotificationOptionTaskFailed": "Teip tasc sceidealta",
+ "NotificationOptionUserLockedOut": "Úsáideoir glasáilte amach",
+ "NotificationOptionVideoPlayback": "Cuireadh tús le hathsheinm físe",
+ "NotificationOptionVideoPlaybackStopped": "Cuireadh deireadh le hathsheinm físe",
+ "Photos": "Grianghraif",
+ "Playlists": "Seinmliostaí",
+ "Plugin": "Breiseán",
+ "PluginInstalledWithName": "Suiteáladh {0}",
+ "PluginUninstalledWithName": "Díshuiteáladh {0}",
+ "PluginUpdatedWithName": "Nuashonraíodh {0}",
+ "ProviderValue": "Soláthraí: {0}",
+ "ScheduledTaskFailedWithName": "Theip ar {0}",
+ "ScheduledTaskStartedWithName": "Thosaigh {0}",
+ "ServerNameNeedsToBeRestarted": "Ní mór {0} a atosú",
+ "Shows": "Seónna",
+ "Songs": "Amhráin",
+ "StartupEmbyServerIsLoading": "Tá freastalaí Jellyfin á luchtú. Bain triail eile as gan mhoill.",
+ "SubtitleDownloadFailureFromForItem": "Theip ar fhotheidil a íoslódáil ó {0} le haghaidh {1}",
+ "Sync": "Sioncrónaigh",
+ "System": "Córas",
+ "TvShows": "Seónna Teilifíse",
+ "Undefined": "Neamhshainithe",
+ "User": "Úsáideoir",
+ "UserCreatedWithName": "Cruthaíodh úsáideoir {0}",
+ "UserDeletedWithName": "Scriosadh úsáideoir {0}",
+ "UserDownloadingItemWithValues": "Tá {0} á íoslódáil {1}",
+ "UserLockedOutWithName": "Tá úsáideoir {0} glasáilte amach",
+ "UserOfflineFromDevice": "Tá {0} dícheangailte ó {1}",
+ "UserOnlineFromDevice": "Tá {0} ar líne ó {1}",
+ "UserPasswordChangedWithName": "Athraíodh pasfhocal don úsáideoir {0}",
+ "UserPolicyUpdatedWithName": "Nuashonraíodh polasaí úsáideora le haghaidh {0}",
+ "UserStartedPlayingItemWithValues": "Tá {0} ag seinnt {1} ar {2}",
+ "UserStoppedPlayingItemWithValues": "Chríochnaigh {0} ag imirt {1} ar {2}",
+ "ValueHasBeenAddedToLibrary": "Cuireadh {0} le do leabharlann meán",
+ "ValueSpecialEpisodeName": "Speisialta - {0}",
+ "VersionNumber": "Leagan {0}",
+ "TasksMaintenanceCategory": "Cothabháil",
+ "TasksLibraryCategory": "Leabharlann",
+ "TasksApplicationCategory": "Feidhmchlár",
+ "TasksChannelsCategory": "Cainéil Idirlín",
+ "TaskCleanActivityLog": "Loga Gníomhaíochta Glan",
+ "TaskCleanActivityLogDescription": "Scrios iontrálacha loga gníomhaíochta atá níos sine ná an aois chumraithe.",
+ "TaskCleanCache": "Eolaire Taisce Glan",
+ "TaskCleanCacheDescription": "Scriostar comhaid taisce nach bhfuil ag teastáil ón gcóras a thuilleadh.",
+ "TaskRefreshChapterImages": "Sliocht Íomhánna Caibidil",
+ "TaskRefreshChapterImagesDescription": "Cruthaíonn mionsamhlacha le haghaidh físeáin a bhfuil caibidlí acu.",
+ "TaskRefreshLibrary": "Scan Leabharlann na Meán",
+ "TaskRefreshPeople": "Daoine Athnuaigh",
+ "TaskUpdatePlugins": "Nuashonraigh Breiseáin",
+ "TaskCleanTranscodeDescription": "Scriostar comhaid traschódaithe níos mó ná lá amháin d'aois.",
+ "TaskCleanTranscode": "Eolaire Transcode Glan",
+ "TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh"
}
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index af57b1693..34d5cf050 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -16,7 +16,7 @@
"Folders": "תיקיות",
"Genres": "ז׳אנרים",
"HeaderAlbumArtists": "אמני האלבום",
- "HeaderContinueWatching": "להמשיך לצפות",
+ "HeaderContinueWatching": "המשך צפייה",
"HeaderFavoriteAlbums": "אלבומים מועדפים",
"HeaderFavoriteArtists": "אמנים מועדפים",
"HeaderFavoriteEpisodes": "פרקים מועדפים",
@@ -32,8 +32,8 @@
"LabelIpAddressValue": "Ip כתובת: {0}",
"LabelRunningTimeValue": "משך צפייה: {0}",
"Latest": "אחרון",
- "MessageApplicationUpdated": "שרת הJellyfin עודכן",
- "MessageApplicationUpdatedTo": "שרת ה־Jellyfin עודכן לגרסה {0}",
+ "MessageApplicationUpdated": "שרת ג'ליפין עודכן",
+ "MessageApplicationUpdatedTo": "שרת ג'ליפין עודכן לגרסה {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן",
"MessageServerConfigurationUpdated": "תצורת השרת עודכנה",
"MixedContent": "תוכן מעורב",
@@ -43,7 +43,7 @@
"NameInstallFailed": "התקנת {0} נכשלה",
"NameSeasonNumber": "עונה {0}",
"NameSeasonUnknown": "עונה לא ידועה",
- "NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
+ "NewVersionIsAvailable": "גרסה חדשה של שרת ג'ליפין זמינה להורדה.",
"NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
"NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
"NotificationOptionAudioPlayback": "ניגון שמע החל",
@@ -72,7 +72,7 @@
"ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
"Shows": "סדרות",
"Songs": "שירים",
- "StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. נא לנסות שנית בהקדם.",
+ "StartupEmbyServerIsLoading": "שרת ג'ליפין טוען. נא לנסות שוב בקרוב.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
"Sync": "סנכרון",
@@ -133,8 +133,8 @@
"TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה",
"TaskDownloadMissingLyrics": "הורדת מילים חסרות",
"TaskDownloadMissingLyricsDescription": "הורדת מילים לשירים",
- "TaskMoveTrickplayImages": "מעביר את מיקום תמונות Trickplay",
+ "TaskMoveTrickplayImages": "העברת מיקום התמונות",
"TaskExtractMediaSegments": "סריקת מדיה",
"TaskExtractMediaSegmentsDescription": "מחלץ חלקי מדיה מתוספים המאפשרים זאת.",
- "TaskMoveTrickplayImagesDescription": "מזיז קבצי trickplay קיימים בהתאם להגדרות הספרייה."
+ "TaskMoveTrickplayImagesDescription": "הזזת קבצי טריקפליי קיימים בהתאם להגדרות הספרייה."
}
diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
index 380c08e0d..813b18ad4 100644
--- a/Emby.Server.Implementations/Localization/Core/hi.json
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -99,7 +99,7 @@
"ValueHasBeenAddedToLibrary": "{0} आपके माध्यम ग्रन्थालय में उपजात हो गया हैं",
"TasksLibraryCategory": "संग्रहालय",
"TaskOptimizeDatabase": "जानकारी प्रवृद्धि",
- "TaskDownloadMissingSubtitles": "असमेत अनुलेख को अवाहरति करें",
+ "TaskDownloadMissingSubtitles": "लापता अनुलेख डाउनलोड करें",
"TaskRefreshLibrary": "माध्यम संग्राहत को छाने",
"TaskCleanActivityLog": "क्रियाकलाप लॉग साफ करें",
"TasksChannelsCategory": "इंटरनेट प्रणाली",
@@ -127,5 +127,7 @@
"TaskRefreshTrickplayImages": "ट्रिकप्लै चित्रों को सृजन करे",
"TaskRefreshTrickplayImagesDescription": "नियत संग्रहों में चलचित्रों का ट्रीकप्लै दर्शनों को सृजन करे.",
"TaskAudioNormalization": "श्रव्य सामान्यीकरण",
- "TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें"
+ "TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें",
+ "TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ",
+ "TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ht.json b/Emby.Server.Implementations/Localization/Core/ht.json
new file mode 100644
index 000000000..4fcba99e9
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/ht.json
@@ -0,0 +1,3 @@
+{
+ "Books": "liv"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 6b0cfb359..297b3abce 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -134,5 +134,7 @@
"TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni",
"TaskDownloadMissingLyrics": "Scarica testi mancanti",
"TaskMoveTrickplayImages": "Sposta le immagini Trickplay",
- "TaskMoveTrickplayImagesDescription": "Sposta le immagini Trickplay esistenti secondo la configurazione della libreria."
+ "TaskMoveTrickplayImagesDescription": "Sposta le immagini Trickplay esistenti secondo la configurazione della libreria.",
+ "TaskExtractMediaSegmentsDescription": "Estrae o ottiene segmenti multimediali dai plugin abilitati MediaSegment.",
+ "TaskExtractMediaSegments": "Scansiona Segmento Media"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index 10f4aee25..14a576592 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -134,5 +134,6 @@
"TaskExtractMediaSegments": "メディアセグメントを読み取る",
"TaskMoveTrickplayImages": "Trickplayの画像を移動",
"TaskMoveTrickplayImagesDescription": "ライブラリ設定によりTrickplayのファイルを移動。",
- "TaskDownloadMissingLyrics": "記録されていない歌詞をダウンロード"
+ "TaskDownloadMissingLyrics": "失われた歌詞をダウンロード",
+ "TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index a739cba35..efc9f61dd 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -3,7 +3,7 @@
"AppDeviceValues": "앱: {0}, 장치: {1}",
"Application": "애플리케이션",
"Artists": "아티스트",
- "AuthenticationSucceededWithUserName": "{0}이(가) 성공적으로 인증됨",
+ "AuthenticationSucceededWithUserName": "{0} 사용자가 성공적으로 인증됨",
"Books": "도서",
"CameraImageUploadedFrom": "{0}에서 새로운 카메라 이미지가 업로드됨",
"Channels": "채널",
@@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} 실패",
"ScheduledTaskStartedWithName": "{0} 시작",
"ServerNameNeedsToBeRestarted": "{0}를 재시작해야합니다",
- "Shows": "쇼",
+ "Shows": "시리즈",
"Songs": "노래",
"StartupEmbyServerIsLoading": "Jellyfin 서버를 불러오고 있습니다. 잠시 후에 다시 시도하십시오.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
@@ -81,14 +81,14 @@
"User": "사용자",
"UserCreatedWithName": "사용자 {0} 생성됨",
"UserDeletedWithName": "사용자 {0} 삭제됨",
- "UserDownloadingItemWithValues": "{0}이(가) {1}을 다운로드 중입니다",
- "UserLockedOutWithName": "유저 {0} 은(는) 잠금처리 되었습니다",
- "UserOfflineFromDevice": "{1}에서 {0}의 연결이 끊킴",
- "UserOnlineFromDevice": "{0}이 {1}으로 접속",
- "UserPasswordChangedWithName": "사용자 {0}의 비밀번호가 변경되었습니다",
- "UserPolicyUpdatedWithName": "{0}의 사용자 정책이 업데이트되었습니다",
- "UserStartedPlayingItemWithValues": "{2}에서 {0}이 {1} 재생 중",
- "UserStoppedPlayingItemWithValues": "{2}에서 {0}이 {1} 재생을 마침",
+ "UserDownloadingItemWithValues": "{0} 사용자가 {1} 다운로드 중",
+ "UserLockedOutWithName": "{0} 사용자 잠김",
+ "UserOfflineFromDevice": "{0} 사용자의 {1}에서 연결이 끊김",
+ "UserOnlineFromDevice": "{0} 사용자가 {1}에서 접속함",
+ "UserPasswordChangedWithName": "{0} 사용자 비밀번호 변경됨",
+ "UserPolicyUpdatedWithName": "{0} 사용자 정책 업데이트됨",
+ "UserStartedPlayingItemWithValues": "{0} 사용자의 {2}에서 {1} 재생 중",
+ "UserStoppedPlayingItemWithValues": "{0} 사용자의 {2}에서 {1} 재생을 마침",
"ValueHasBeenAddedToLibrary": "{0}가 미디어 라이브러리에 추가되었습니다",
"ValueSpecialEpisodeName": "스페셜 - {0}",
"VersionNumber": "버전 {0}",
@@ -130,5 +130,11 @@
"TaskAudioNormalizationDescription": "오디오의 볼륨 수준을 일정하게 조정하기 위해 파일을 스캔합니다.",
"TaskRefreshTrickplayImages": "비디오 탐색용 미리보기 썸네일 생성",
"TaskRefreshTrickplayImagesDescription": "활성화된 라이브러리에서 비디오의 트릭플레이 미리보기를 생성합니다.",
- "TaskCleanCollectionsAndPlaylistsDescription": "더 이상 존재하지 않는 컬렉션 및 재생 목록에서 항목을 제거합니다."
+ "TaskCleanCollectionsAndPlaylistsDescription": "더 이상 존재하지 않는 컬렉션 및 재생 목록에서 항목을 제거합니다.",
+ "TaskExtractMediaSegments": "미디어 세그먼트 스캔",
+ "TaskExtractMediaSegmentsDescription": "MediaSegment를 지원하는 플러그인에서 미디어 세그먼트를 추출하거나 가져옵니다.",
+ "TaskMoveTrickplayImages": "트릭플레이 이미지 위치 마이그레이션",
+ "TaskMoveTrickplayImagesDescription": "추출된 트릭플레이 이미지를 라이브러리 설정에 따라 이동합니다.",
+ "TaskDownloadMissingLyrics": "누락된 가사 다운로드",
+ "TaskDownloadMissingLyricsDescription": "가사 다운로드"
}
diff --git a/Emby.Server.Implementations/Localization/Core/lb.json b/Emby.Server.Implementations/Localization/Core/lb.json
new file mode 100644
index 000000000..176f2ba2b
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/lb.json
@@ -0,0 +1,139 @@
+{
+ "Albums": "Alben",
+ "Application": "Applikatioun",
+ "Artists": "Kënschtler",
+ "Books": "Bicher",
+ "Channels": "Kanäl",
+ "Collections": "Kollektiounen",
+ "Default": "Standard",
+ "ChapterNameValue": "Kapitel {0}",
+ "DeviceOnlineWithName": "{0} ass Online",
+ "DeviceOfflineWithName": "{0} ass Offline",
+ "External": "Extern",
+ "Favorites": "Favoritten",
+ "Folders": "Dossieren",
+ "Forced": "Forcéiert",
+ "HeaderAlbumArtists": "Album Kënschtler",
+ "HeaderFavoriteAlbums": "Léifsten Alben",
+ "HeaderFavoriteArtists": "Léifsten Kënschtler",
+ "HeaderFavoriteEpisodes": "Léifsten Episoden",
+ "HeaderFavoriteShows": "Léifsten Shows",
+ "HeaderFavoriteSongs": "Léifsten Lidder",
+ "Genres": "Generen",
+ "HeaderContinueWatching": "Weider kucken",
+ "Inherit": "Iwwerhuelen",
+ "HeaderNextUp": "Als Nächst",
+ "HeaderRecordingGroups": "Opname Gruppen",
+ "HearingImpaired": "Daaf",
+ "HomeVideos": "Amateur Videoen",
+ "ItemRemovedWithName": "Element ewech geholl: {0}",
+ "LabelIpAddressValue": "IP Adress: {0}",
+ "LabelRunningTimeValue": "Lafzäit: {0}",
+ "Latest": "Dat Aktuellst",
+ "MessageApplicationUpdatedTo": "Jellyfin Server aktualiséiert op {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Server Konfiguratiounssektioun {0} aktualiséiert",
+ "MessageServerConfigurationUpdated": "Server Konfiguratioun aktualiséiert",
+ "Movies": "Filmer",
+ "Music": "Musek",
+ "NameInstallFailed": "{0} Installatioun net gelongen",
+ "NameSeasonNumber": "Staffel {0}",
+ "NameSeasonUnknown": "Staffel Onbekannt",
+ "MusicVideos": "Museksvideoen",
+ "NotificationOptionApplicationUpdateAvailable": "Applikatiouns Update verfügbar",
+ "NotificationOptionApplicationUpdateInstalled": "Applikatiouns Update nët Installéiert",
+ "NotificationOptionAudioPlayback": "Audio ofspillen gestart",
+ "NotificationOptionAudioPlaybackStopped": "Audio ofspillen gestoppt",
+ "NotificationOptionCameraImageUploaded": "Kamera Bild eropgelueden",
+ "NotificationOptionInstallationFailed": "Installatioun net gelongen",
+ "NotificationOptionNewLibraryContent": "Neien Bibliothéik Inhalt",
+ "NotificationOptionPluginError": "Plugin Feeler",
+ "NotificationOptionPluginInstalled": "Plugin installéiert",
+ "NotificationOptionPluginUninstalled": "Plugin desinstalléiert",
+ "NotificationOptionPluginUpdateInstalled": "Plugin Update installéiert",
+ "Photos": "Fotoen",
+ "NotificationOptionTaskFailed": "Aufgab net gelongen",
+ "NotificationOptionUserLockedOut": "Benotzer Gesperrt",
+ "NotificationOptionVideoPlaybackStopped": "Video ofspillen gestoppt",
+ "NotificationOptionVideoPlayback": "Video ofspillen gestartet",
+ "Plugin": "Plugin",
+ "PluginUninstalledWithName": "{0} desinstalléiert",
+ "PluginUpdatedWithName": "{0} aktualiséiert",
+ "ProviderValue": "Provider: {0}",
+ "ScheduledTaskFailedWithName": "Aufgab: {0} net gelongen",
+ "Playlists": "Playlëschten",
+ "Shows": "Shows",
+ "Songs": "Lidder",
+ "ServerNameNeedsToBeRestarted": "{0} muss nei gestart ginn",
+ "StartupEmbyServerIsLoading": "Jellyfin Server luedt. Probéier méi spéit nach eng Kéier.",
+ "Sync": "Synchroniséieren",
+ "System": "System",
+ "User": "Benotzer",
+ "TvShows": "TV Shows",
+ "Undefined": "Net definéiert",
+ "UserCreatedWithName": "Benotzer {0} erstellt",
+ "UserDownloadingItemWithValues": "{0} luet {1} erof",
+ "UserOfflineFromDevice": "{0} Benotzer Offline um Gerät {1}",
+ "UserLockedOutWithName": "Benotzer {0} gesperrt",
+ "UserOnlineFromDevice": "{0} Benotzer Online um Gerät {1}",
+ "UserPasswordChangedWithName": "Benotzer Passwuert geännert fir {0}",
+ "UserPolicyUpdatedWithName": "Benotzer Politik aktualiséiert fir: {0}",
+ "UserStartedPlayingItemWithValues": "{0} spillt {1} op {2} oof",
+ "ValueHasBeenAddedToLibrary": "{0} der Bibliothéik bäigefüügt",
+ "VersionNumber": "Versioun {0}",
+ "TasksMaintenanceCategory": "Ënnerhalt",
+ "TasksLibraryCategory": "Bibliothéik",
+ "ValueSpecialEpisodeName": "Spezial-Episodenumm",
+ "TasksChannelsCategory": "Internet Kanäl",
+ "TaskCleanActivityLog": "Aktivitéits Log botzen",
+ "TaskCleanActivityLogDescription": "Läscht Aktivitéitslogs méi al wéi konfiguréiert.",
+ "TaskCleanCache": "Aufgab Cache Botzen",
+ "TaskRefreshChapterImages": "Kapitel Biller erstellen",
+ "TaskRefreshChapterImagesDescription": "Erstellt Miniaturbiller fir Videoen, déi Kapitelen hunn.",
+ "TaskAudioNormalization": "Audio Normaliséierung",
+ "TaskRefreshLibrary": "Bibliothéik aktualiséieren",
+ "TaskRefreshLibraryDescription": "Scannt deng Mediebibliothéik no neien Dateien a frëscht d’Metadata op.",
+ "TaskCleanLogs": "Log Dateien botzen",
+ "TaskRefreshPeople": "Persounen aktualiséieren",
+ "TaskRefreshPeopleDescription": "Aktualiséiert Metadata fir Schauspiller a Regisseuren an denger Mediebibliothéik.",
+ "TaskRefreshTrickplayImagesDescription": "Erstellt Trickplay-Viraussiichten fir Videoen an aktivéierte Bibliothéiken.",
+ "TaskCleanTranscode": "Transkodéieren botzen",
+ "TaskCleanTranscodeDescription": "Läscht Transkodéierungsdateien, déi méi al wéi een Dag sinn.",
+ "TaskRefreshChannels": "Kanäl aktualiséieren",
+ "TaskDownloadMissingLyrics": "Fehlend Liddertexter eroflueden",
+ "TaskDownloadMissingLyricsDescription": "Lued Liddertexter fir Lidder erof",
+ "TaskDownloadMissingSubtitles": "Fehlend Ënnertitelen eroflueden",
+ "TaskOptimizeDatabase": "Datebank optiméieren",
+ "TaskKeyframeExtractor": "Schlësselbild Extrakter",
+ "TaskCleanCollectionsAndPlaylists": "Sammlungen a Playlisten botzen",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Ewechhuele vun Elementer aus Sammlungen a Playlisten, déi net méi existéieren.",
+ "TaskExtractMediaSegments": "Mediesegment-Scan",
+ "NewVersionIsAvailable": "Nei Versioun fir Jellyfin Server ass verfügbar.",
+ "CameraImageUploadedFrom": "En neit Kamera Bild gouf vu {0} eropgelueden",
+ "PluginInstalledWithName": "{0} installéiert",
+ "TaskMoveTrickplayImagesDescription": "Verschëfft existent Trickplay-Dateien no de Bibliothéik-Astellungen.",
+ "AppDeviceValues": "App: {0}, Geräter: {1}",
+ "FailedLoginAttemptWithUserName": "Net Gelongen Umeldung {0}",
+ "HeaderLiveTV": "LiveTV",
+ "ItemAddedWithName": "Element derbäi gesat: {0}",
+ "NotificationOptionServerRestartRequired": "Server Restart Erfuerderlech",
+ "ScheduledTaskStartedWithName": "Aufgab: {0} gestart",
+ "AuthenticationSucceededWithUserName": "{0} Authentifikatioun gelongen",
+ "MixedContent": "Gemëschten Inhalt",
+ "MessageApplicationUpdated": "Jellyfin Server Aktualiséiert",
+ "SubtitleDownloadFailureFromForItem": "Ënnertitel Download Feeler vun {0} fir {1}",
+ "TaskCleanLogsDescription": "Läscht Log-Dateien, déi méi al wéi {0} Deeg sinn.",
+ "TaskUpdatePlugins": "Plugins aktualiséieren",
+ "UserDeletedWithName": "Benotzer {0} geläscht",
+ "TasksApplicationCategory": "Applikatioun",
+ "TaskCleanCacheDescription": "Läscht Cache-Dateien, déi net méi vum System gebraucht ginn.",
+ "UserStoppedPlayingItemWithValues": "{0} ass mat {1} op {2} fäerdeg",
+ "TaskAudioNormalizationDescription": "Scannt Dateien no Donnéeën fir d’Audio-Normaliséierung.",
+ "TaskRefreshTrickplayImages": "Trickplay-Biller generéieren",
+ "TaskDownloadMissingSubtitlesDescription": "Sicht am Internet no fehlenden Ënnertitelen op Basis vun der Metadata-Konfiguratioun.",
+ "TaskMoveTrickplayImages": "Trickplay-Biller-Plaz migréieren",
+ "TaskUpdatePluginsDescription": "Lued Aktualiséierungen erof a installéiert se fir Plugins, déi fir automatesch Updates konfiguréiert sinn.",
+ "TaskKeyframeExtractorDescription": "Extrahéiert Schlësselbiller aus Videodateien, fir méi präzis HLS-Playlisten ze erstellen. Dës Aufgab kann eng längere Zäit daueren.",
+ "TaskRefreshChannelsDescription": "Aktualiséiert Informatiounen iwwer Internetkanäl.",
+ "TaskExtractMediaSegmentsDescription": "Extrahéiert oder kritt Mediesegmenter aus Plugins, déi MediaSegment ënnerstëtzen.",
+ "TaskOptimizeDatabaseDescription": "Kompriméiert d’Datebank a schneit de fräie Speicherplatz zou. Dës Aufgab no engem Bibliothéik-Scan oder anere Ännerungen, déi Datebankmodifikatioune mat sech bréngen, auszeféieren, kann d’Performance verbesseren."
+}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index 95f738bd5..46fc49f5e 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -94,14 +94,14 @@
"VersionNumber": "Version {0}",
"TaskUpdatePluginsDescription": "Atsisiųsti ir įdiegti atnaujinimus priedams kuriem yra nustatytas automatiškas atnaujinimas.",
"TaskUpdatePlugins": "Atnaujinti Priedus",
- "TaskDownloadMissingSubtitlesDescription": "Ieško internete trūkstamų subtitrų remiantis metaduomenų konfigūracija.",
+ "TaskDownloadMissingSubtitlesDescription": "Ieško trūkstamų subtitrų internete remiantis metaduomenų konfigūracija.",
"TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.",
"TaskCleanTranscode": "Išvalyti Perkodavimo Direktorija",
"TaskRefreshLibraryDescription": "Ieškoti naujų failų jūsų mediatekoje ir atnaujina metaduomenis.",
"TaskRefreshLibrary": "Skenuoti Mediateka",
"TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus",
- "TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informacija.",
- "TaskRefreshChannels": "Atnaujinti Kanalus",
+ "TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informaciją.",
+ "TaskRefreshChannels": "Atnaujinti kanalus",
"TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų mediatekoje.",
"TaskRefreshPeople": "Atnaujinti Žmones",
"TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.",
@@ -119,22 +119,22 @@
"Forced": "Priverstas",
"Default": "Numatytas",
"TaskCleanActivityLogDescription": "Ištrina veiklos žuranlo įrašus, kurie yra senesni nei nustatytas amžius.",
- "TaskOptimizeDatabase": "Optimizuoti duomenų bazės",
+ "TaskOptimizeDatabase": "Optimizuoti duomenų bazę",
"TaskKeyframeExtractorDescription": "Iš vaizdo įrašo paruošia reikšminius kadrus, kad būtų sukuriamas tikslenis HLS grojaraštis. Šios užduoties vykdymas gali ilgai užtrukti.",
- "TaskKeyframeExtractor": "Pagrindinių kadrų ištraukėjas",
- "TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazė, gali pagerinti greitaveiką.",
+ "TaskKeyframeExtractor": "Pagrindinių kadrų išgavėjas",
+ "TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazę, gali pagerinti greitaveiką.",
"External": "Išorinis",
"HearingImpaired": "Su klausos sutrikimais",
"TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
"TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
- "TaskCleanCollectionsAndPlaylists": "Sutvarko duomenis jūsų kolekcijose ir grojaraščiuose",
- "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina nebeegzistuojančius elementus iš kolekcijų ir grojaraščių.",
+ "TaskCleanCollectionsAndPlaylists": "Išvalo duomenis kolekcijose ir grojaraščiuose",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš kolekcijų ir grojaraščių.",
"TaskAudioNormalization": "Garso Normalizavimas",
"TaskAudioNormalizationDescription": "Skenuoti garso normalizavimo informacijos failuose.",
- "TaskExtractMediaSegments": "Medijos Segmentų Nuskaitymas",
+ "TaskExtractMediaSegments": "Medijos segmentų nuskaitymas",
"TaskDownloadMissingLyrics": "Parsisiųsti trūkstamus dainų tekstus",
"TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų papildinių.",
- "TaskMoveTrickplayImages": "Migruoti Trickplay Vaizdų Vietą",
- "TaskMoveTrickplayImagesDescription": "Perkelia egzisuojančius trickplay failus pagal bibliotekos nustatymus.",
+ "TaskMoveTrickplayImages": "Pakeisti Trickplay vaizdų vietą",
+ "TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius trickplay failus pagal bibliotekos nustatymus.",
"TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius"
}
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index 62277fd94..77340a57a 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -123,11 +123,17 @@
"External": "Ārējais",
"HearingImpaired": "Ar dzirdes traucējumiem",
"TaskKeyframeExtractor": "Atslēgkadru ekstraktors",
- "TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs.",
+ "TaskKeyframeExtractorDescription": "Izvelk atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs.",
"TaskRefreshTrickplayImages": "Ģenerēt partīšanas attēlus",
"TaskRefreshTrickplayImagesDescription": "Izveido priekšskatījumus videoklipu pārtīšanai iespējotajās bibliotēkās.",
"TaskAudioNormalization": "Audio normalizācija",
"TaskCleanCollectionsAndPlaylistsDescription": "Noņem vairs neeksistējošus vienumus no kolekcijām un atskaņošanas sarakstiem.",
"TaskAudioNormalizationDescription": "Skanē failus priekš audio normālizācijas informācijas.",
- "TaskCleanCollectionsAndPlaylists": "Notīrīt kolekcijas un atskaņošanas sarakstus"
+ "TaskCleanCollectionsAndPlaylists": "Notīrīt kolekcijas un atskaņošanas sarakstus",
+ "TaskExtractMediaSegments": "Multivides segmenta skenēšana",
+ "TaskExtractMediaSegmentsDescription": "Izvelk vai iegūst multivides segmentus no MediaSegment iespējotiem spraudņiem.",
+ "TaskMoveTrickplayImages": "Trickplay attēlu pārvietošana",
+ "TaskMoveTrickplayImagesDescription": "Pārvieto esošos trickplay failus atbilstoši bibliotēkas iestatījumiem.",
+ "TaskDownloadMissingLyrics": "Lejupielādēt trūkstošos vārdus",
+ "TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām"
}
diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json
index e149f8adf..6da31227d 100644
--- a/Emby.Server.Implementations/Localization/Core/mk.json
+++ b/Emby.Server.Implementations/Localization/Core/mk.json
@@ -131,5 +131,6 @@
"TaskRefreshTrickplayImages": "Генерирај слики за прегледување (Trickplay)",
"TaskAudioNormalization": "Нормализација на звукот",
"TaskRefreshTrickplayImagesDescription": "Креира трикплеј прегледи за видеа во овозможените библиотеки.",
- "TaskCleanCollectionsAndPlaylistsDescription": "Отстранува ставки од колекциите и плејлистите што веќе не постојат."
+ "TaskCleanCollectionsAndPlaylistsDescription": "Отстранува ставки од колекциите и плејлистите што веќе не постојат.",
+ "TaskExtractMediaSegments": "Скенирање на сегменти на содржина"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index ebd3f7560..c64bcda04 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -11,7 +11,7 @@
"Collections": "Koleksi",
"DeviceOfflineWithName": "{0} telah diputuskan sambungan",
"DeviceOnlineWithName": "{0} telah disambung",
- "FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}",
+ "FailedLoginAttemptWithUserName": "Percubaan log masuk daripada {0} gagal",
"Favorites": "Kegemaran",
"Folders": "Fail-fail",
"Genres": "Genre-genre",
@@ -126,5 +126,15 @@
"TaskKeyframeExtractor": "Ekstrak bingkai kunci",
"TaskKeyframeExtractorDescription": "Ekstrak bingkai kunci dari fail video untuk membina HLS playlist yang lebih tepat. Tugas ini mungkin perlukan masa yang panjang.",
"TaskRefreshTrickplayImagesDescription": "Jana gambar prebiu Trickplay untuk video dalam perpustakaan.",
- "TaskRefreshTrickplayImages": "Jana gambar Trickplay"
+ "TaskRefreshTrickplayImages": "Jana gambar Trickplay",
+ "TaskExtractMediaSegments": "Imbasan Segmen Media",
+ "TaskExtractMediaSegmentsDescription": "Mengekstrak atau mendapatkan segmen media daripada pemalam yang didayakan MediaSegment.",
+ "TaskMoveTrickplayImagesDescription": "Mengalihkan fail trickplay sedia ada mengikut tetapan pustakan digital.",
+ "TaskDownloadMissingLyrics": "Muat turun lirik yang hilang",
+ "TaskDownloadMissingLyricsDescription": "Memuat turun lirik-lirik untuk lagu-lagu",
+ "TaskMoveTrickplayImages": "Alih Lokasi Imej Trickplay",
+ "TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan senarai audio video",
+ "TaskAudioNormalization": "Normalisasi Audio",
+ "TaskAudioNormalizationDescription": "Mengimbas fail-fail untuk data normalisasi audio.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Mengalih keluar item daripada koleksi dan senarai audio video yang tidak wujud lagi."
}
diff --git a/Emby.Server.Implementations/Localization/Core/mt.json b/Emby.Server.Implementations/Localization/Core/mt.json
index c9e11165d..f7501ab40 100644
--- a/Emby.Server.Implementations/Localization/Core/mt.json
+++ b/Emby.Server.Implementations/Localization/Core/mt.json
@@ -1,86 +1,86 @@
{
"Albums": "Albums",
- "AppDeviceValues": "App: {0}, Apparat: {1}",
+ "AppDeviceValues": "Applikazzjoni: {0}, Device: {1}",
"Application": "Applikazzjoni",
"Artists": "Artisti",
"AuthenticationSucceededWithUserName": "{1} awtentikat b'suċċess",
"Books": "Kotba",
- "CameraImageUploadedFrom": "Ttellgħet immaġni ġdida tal-kamera minn {1}",
- "Channels": "Kanali",
+ "CameraImageUploadedFrom": "Ttella' ritratt ġdid tal-kamera minn {1}",
+ "Channels": "Stazzjonijiet",
"ChapterNameValue": "Kapitlu {0}",
"Collections": "Kollezzjonijiet",
- "DeviceOfflineWithName": "{0} inqatgħa",
- "DeviceOnlineWithName": "{0} qabad",
+ "DeviceOfflineWithName": "{0} tneħħa",
+ "DeviceOnlineWithName": "{0} tqabbad",
"External": "Estern",
- "FailedLoginAttemptWithUserName": "Tentattiv t'aċċess fallut minn {0}",
+ "FailedLoginAttemptWithUserName": "Attentat fallut ta' login minn {0}",
"Favorites": "Favoriti",
"Forced": "Sfurzat",
"Genres": "Ġeneri",
"HeaderAlbumArtists": "Artisti tal-album",
- "HeaderContinueWatching": "Kompli Segwi",
+ "HeaderContinueWatching": "Kompli Ara",
"HeaderFavoriteAlbums": "Albums Favoriti",
"HeaderFavoriteArtists": "Artisti Favoriti",
"HeaderFavoriteEpisodes": "Episodji Favoriti",
"HeaderFavoriteShows": "Programmi Favoriti",
"HeaderFavoriteSongs": "Kanzunetti Favoriti",
"HeaderNextUp": "Li Jmiss",
- "SubtitleDownloadFailureFromForItem": "Is-sottotitli naqsu milli jitniżżlu minn {0} għal {1}",
- "UserPasswordChangedWithName": "Il-password inbidel għall-utent {0}",
+ "SubtitleDownloadFailureFromForItem": "Is-sottotitli ma setgħux jitniżżlu minn {0} għal {1}",
+ "UserPasswordChangedWithName": "Il-password għall-utent {0} inbidlet",
"TaskUpdatePluginsDescription": "Iniżżel u jinstalla aġġornamenti għal plugins li huma kkonfigurati biex jaġġornaw awtomatikament.",
- "TaskDownloadMissingSubtitlesDescription": "Ifittex fuq l-internet għal sottotitli neqsin abbażi tal-konfigurazzjoni tal-metadata.",
- "TaskOptimizeDatabaseDescription": "Jikkompatti d-database u jaqta' l-ispazju ħieles. It-tħaddim ta' dan il-kompitu wara li tiskennja l-librerija jew tagħmel bidliet oħra li jimplikaw modifiki fid-database jistgħu jtejbu l-prestazzjoni.",
+ "TaskDownloadMissingSubtitlesDescription": "Ifittex fuq l-internet għal sottotitli neqsin skont il-konfigurazzjoni tal-metadata.",
+ "TaskOptimizeDatabaseDescription": "Jikkompatta d-database u jaqta' l-ispazju ħieles. It-tħaddim ta' dan it-task wara li tiskennja l-librerija jew tagħmel bidliet oħra li jimplikaw modifiki fid-database jistgħu jtejbu l-mod kif jaħdem.",
"Default": "Standard",
"Folders": "Folders",
"HeaderLiveTV": "TV Dirett",
- "HeaderRecordingGroups": "Gruppi ta' Reġistrazzjoni",
+ "HeaderRecordingGroups": "Gruppi ta' Rikordjar",
"HearingImpaired": "Nuqqas ta' Smigħ",
- "HomeVideos": "Vidjows Personali",
+ "HomeVideos": "Filmati Personali",
"Inherit": "Jiret",
- "ItemAddedWithName": "{0} ġie miżjud mal-librerija",
+ "ItemAddedWithName": "{0} żdied fil-librerija",
"ItemRemovedWithName": "{0} tneħħa mil-librerija",
- "LabelIpAddressValue": "Indirizz IP: {0}",
+ "LabelIpAddressValue": "Indirizz tal-IP: {0}",
"Latest": "Tal-Aħħar",
- "MessageApplicationUpdated": "Jellyfin Server ġie aġġornat",
- "MessageApplicationUpdatedTo": "JellyFin Server ġie aġġornat għal {0}",
+ "MessageApplicationUpdated": "Il-Jellyfin Server ġie aġġornat",
+ "MessageApplicationUpdatedTo": "Il-JellyFin Server ġie aġġornat għal {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Is-sezzjoni {0} tal-konfigurazzjoni tas-server ġiet aġġornata",
"MessageServerConfigurationUpdated": "Il-konfigurazzjoni tas-server ġiet aġġornata",
"MixedContent": "Kontenut imħallat",
"Movies": "Films",
"Music": "Mużika",
- "MusicVideos": "Vidjows tal-Mużika",
+ "MusicVideos": "Music Videos",
"NameInstallFailed": "L-installazzjoni ta' {0} falliet",
"NameSeasonNumber": "Staġun {0}",
"NameSeasonUnknown": "Staġun Mhux Magħruf",
- "NewVersionIsAvailable": "Verżjoni ġdida ta' Jellyfin Server hija disponibbli biex titniżżel.",
- "NotificationOptionApplicationUpdateAvailable": "Aġġornament tal-applikazzjoni disponibbli",
- "NotificationOptionCameraImageUploaded": "Immaġini tal-kamera mtella'",
+ "NewVersionIsAvailable": "Verżjoni ġdida tal-Jellyfin Server hija disponibbli biex titniżżel.",
+ "NotificationOptionApplicationUpdateAvailable": "Hemm aġġornament tal-applikazzjoni",
+ "NotificationOptionCameraImageUploaded": "Ritratt tal-kamera mtella'",
"LabelRunningTimeValue": "Tul: {0}",
"NotificationOptionApplicationUpdateInstalled": "Aġġornament tal-applikazzjoni ġie installat",
- "NotificationOptionAudioPlayback": "Il-playback tal-awdjo beda",
+ "NotificationOptionAudioPlayback": "Beda l-playback tal-awdjo",
"NotificationOptionAudioPlaybackStopped": "Il-playback tal-awdjo twaqqaf",
- "NotificationOptionInstallationFailed": "Installazzjoni falliet",
- "NotificationOptionNewLibraryContent": "Kontenut ġdid miżjud",
- "NotificationOptionPluginError": "Ħsara fil-plugin",
+ "NotificationOptionInstallationFailed": "L-Installazzjoni falliet",
+ "NotificationOptionNewLibraryContent": "Kontenut ġdid żdied",
+ "NotificationOptionPluginError": "Falliment fil-plugin",
"NotificationOptionPluginInstalled": "Plugin installat",
"NotificationOptionPluginUninstalled": "Plugin tneħħa",
- "NotificationOptionServerRestartRequired": "Meħtieġ l-istartjar mill-ġdid tas-server",
- "NotificationOptionTaskFailed": "Falliment tal-kompitu skedat",
+ "NotificationOptionServerRestartRequired": "Hemm bżonn li tagħmel restart tas-server",
+ "NotificationOptionTaskFailed": "Falliment tat-task skedat",
"NotificationOptionUserLockedOut": "Utent imsakkar",
"Photos": "Ritratti",
"Playlists": "Playlists",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} ġie installat",
- "PluginUninstalledWithName": "{0} ġie mneħħi",
+ "PluginUninstalledWithName": "{0} tneħħa",
"PluginUpdatedWithName": "{0} ġie aġġornat",
"ProviderValue": "Fornitur: {0}",
"ScheduledTaskFailedWithName": "{0} falla",
"ScheduledTaskStartedWithName": "{0} beda",
- "ServerNameNeedsToBeRestarted": "{0} jeħtieġ li jerġa' jinbeda",
+ "ServerNameNeedsToBeRestarted": "{0} jeħtieġ restart",
"Songs": "Kanzunetti",
- "StartupEmbyServerIsLoading": "Jellyfin Server qed jixgħel. Jekk jogħġbok erġa' pprova dalwaqt.",
+ "StartupEmbyServerIsLoading": "Jellyfin Server qed jillowdja. Jekk jogħġbok erġa' pprova ftit tal-ħin oħra.",
"Sync": "Sinkronizza",
"System": "Sistema",
- "Undefined": "Mhux Definit",
+ "Undefined": "Bla Definizzjoni",
"User": "Utent",
"UserCreatedWithName": "L-utent {0} inħoloq",
"UserDeletedWithName": "L-utent {0} tħassar",
@@ -89,45 +89,51 @@
"UserOfflineFromDevice": "{0} skonnettja minn {1}",
"UserOnlineFromDevice": "{0} huwa online minn {1}",
"NotificationOptionPluginUpdateInstalled": "Aġġornament ta' plugin ġie installat",
- "NotificationOptionVideoPlayback": "Il-playback tal-vidjow beda",
- "NotificationOptionVideoPlaybackStopped": "Il-playback tal-vidjow waqaf",
- "Shows": "Programmi",
- "TvShows": "Programmi tat-TV",
- "UserPolicyUpdatedWithName": "Il-policy tal-utent ġiet aġġornata għal {0}",
- "UserStartedPlayingItemWithValues": "{0} qed iħaddem {1} fuq {2}",
- "UserStoppedPlayingItemWithValues": "{0} waqaf iħaddem {1} fuq {2}",
+ "NotificationOptionVideoPlayback": "Il-playback tal-filmat beda",
+ "NotificationOptionVideoPlaybackStopped": "Il-playback tal-filmat twaqqaf",
+ "Shows": "Serje",
+ "TvShows": "Serje Televiżivi",
+ "UserPolicyUpdatedWithName": "Il-politka tal-utent ġiet aġġornata għal {0}",
+ "UserStartedPlayingItemWithValues": "{0} qed jara {1} fuq {2}",
+ "UserStoppedPlayingItemWithValues": "{0} waqaf jara {1} fuq {2}",
"ValueHasBeenAddedToLibrary": "{0} ġie miżjud mal-librerija tal-midja tiegħek",
"ValueSpecialEpisodeName": "Speċjali - {0}",
"VersionNumber": "Verżjoni {0}",
"TasksMaintenanceCategory": "Manutenzjoni",
"TasksLibraryCategory": "Librerija",
"TasksApplicationCategory": "Applikazzjoni",
- "TasksChannelsCategory": "Kanali tal-Internet",
+ "TasksChannelsCategory": "Stazzjonijiet tal-Internet",
"TaskCleanActivityLog": "Naddaf il-Logg tal-Attività",
- "TaskCleanActivityLogDescription": "Iħassar l-entrati tar-reġistru tal-attività eqdem mill-età kkonfigurata.",
+ "TaskCleanActivityLogDescription": "Iħassar id-daħliet tar-reġistru tal-attività eqdem mill-età li kienet kkonfigurata.",
"TaskCleanCache": "Naddaf id-Direttorju tal-Cache",
"TaskCleanCacheDescription": "Iħassar il-fajls tal-cache li m'għadhomx meħtieġa mis-sistema.",
- "TaskRefreshChapterImages": "Oħroġ l-Immaġini tal-Kapitolu",
+ "TaskRefreshChapterImages": "Oħroġ ir-Ritratti tal-Kapitlu",
"TaskRefreshChapterImagesDescription": "Joħloq thumbnails għal vidjows li għandhom kapitli.",
- "TaskAudioNormalization": "Normalizzazzjoni Awdjo",
- "TaskAudioNormalizationDescription": "Skennja fajls għal data ta' normalizzazzjoni awdjo.",
+ "TaskAudioNormalization": "Normalizzazzjoni tal-Awdjo",
+ "TaskAudioNormalizationDescription": "Skennja fajls għal data fuq in-normalizzazzjoni tal-awdjo.",
"TaskRefreshLibrary": "Skennja l-Librerija tal-Midja",
"TaskRefreshLibraryDescription": "Jiskennja l-librerija tal-midja tiegħek għal fajls ġodda u jġedded il-metadejta.",
"TaskCleanLogs": "Naddaf id-Direttorju tal-Logg",
"TaskCleanLogsDescription": "Iħassar fajls tal-logg eqdem minn {0} ijiem.",
- "TaskRefreshPeople": "Aġġorna Persuni",
- "TaskRefreshPeopleDescription": "Jaġġorna l-metadejta għall-atturi u d-diretturi fil-librerija tal-midja tiegħek.",
+ "TaskRefreshPeople": "Aġġorna l-Persuni",
+ "TaskRefreshPeopleDescription": "Jaġġorna l-metadata għall-atturi u d-diretturi fil-librerija tal-midja tiegħek.",
"TaskRefreshTrickplayImages": "Iġġenera Stampi Trickplay",
- "TaskRefreshTrickplayImagesDescription": "Joħloq previews trickplay għal vidjows fil-libreriji attivati.",
- "TaskUpdatePlugins": "Aġġorna il-Plugins",
- "TaskCleanTranscode": "Naddaf id-Direttorju tat-Transcode",
- "TaskCleanTranscodeDescription": "Iħassar fajls transcode eqdem minn ġurnata.",
- "TaskRefreshChannels": "Aġġorna l-Kanali",
- "TaskRefreshChannelsDescription": "Aġġorna l-informazzjoni tal-kanali tal-internet.",
+ "TaskRefreshTrickplayImagesDescription": "Joħloq previews trickplay għal videos fil-libreriji li għalihom hi attivata.",
+ "TaskUpdatePlugins": "Aġġorna l-Plugins",
+ "TaskCleanTranscode": "Naddaf id-Direttorju tat-Transcoding",
+ "TaskCleanTranscodeDescription": "Iħassar fajls tat-transcoding li huma eqdem minn ġurnata.",
+ "TaskRefreshChannels": "Aġġorna l-Istazzjonijiet",
+ "TaskRefreshChannelsDescription": "Aġġorna l-informazzjoni tal-istazzjonijiet tal-internet.",
"TaskDownloadMissingSubtitles": "Niżżel is-sottotitli nieqsa",
- "TaskOptimizeDatabase": "Ottimizza d-database",
+ "TaskOptimizeDatabase": "Ottimiżża d-database",
"TaskKeyframeExtractor": "Estrattur ta' Keyframes",
- "TaskKeyframeExtractorDescription": "Jiġbed il-keyframes mill-fajls tal-vidjow biex joħloq playlists HLS aktar preċiżi. Dan il-kompitu jista' jdum għal żmien twil.",
+ "TaskKeyframeExtractorDescription": "Jiġbed il-keyframes mill-fajls tal-videos biex jagħmel playlists HLS aktar preċiżi. Dan it-task jista' jdum żmien twil biex ilesti.",
"TaskCleanCollectionsAndPlaylists": "Naddaf il-kollezzjonijiet u l-playlists",
- "TaskCleanCollectionsAndPlaylistsDescription": "Ineħħi oġġetti minn kollezzjonijiet u playlists li m'għadhomx jeżistu."
+ "TaskCleanCollectionsAndPlaylistsDescription": "Ineħħi oġġetti minn kollezzjonijiet u playlists li m'għadhomx jeżistu.",
+ "TaskDownloadMissingLyrics": "Niżżel il-lirika nieqsa",
+ "TaskDownloadMissingLyricsDescription": "Iniżżel il-lirika għal-kanzunetti",
+ "TaskExtractMediaSegments": "Scan tas-Sezzjoni tal-Midja",
+ "TaskExtractMediaSegmentsDescription": "Jestratta jew iġib sezzjonijiet tal-midja minn plugins attivati tal-MediaSegment.",
+ "TaskMoveTrickplayImages": "Mexxi l-post tat-Trickplay Image",
+ "TaskMoveTrickplayImagesDescription": "Tmexxi l-files tat-trickplay li jeżistu skont kif inhi kkonfigurata l-librerija."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 2327a73a9..8828eadcb 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -11,7 +11,7 @@
"Collections": "Collecties",
"DeviceOfflineWithName": "Verbinding met {0} is verbroken",
"DeviceOnlineWithName": "{0} is verbonden",
- "FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging vanaf {0}",
+ "FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
"Favorites": "Favorieten",
"Folders": "Mappen",
"Genres": "Genres",
@@ -117,7 +117,7 @@
"TaskCleanActivityLogDescription": "Verwijdert activiteitenlogs ouder dan de ingestelde leeftijd.",
"TaskCleanActivityLog": "Activiteitenlogboek legen",
"Undefined": "Niet gedefinieerd",
- "Forced": "Geforceerd",
+ "Forced": "Gedwongen",
"Default": "Standaard",
"TaskOptimizeDatabaseDescription": "Comprimeert de database en trimt vrije ruimte. Het uitvoeren van deze taak kan de prestaties verbeteren, na het scannen van de bibliotheek of andere aanpassingen die invloed hebben op de database.",
"TaskOptimizeDatabase": "Database optimaliseren",
diff --git a/Emby.Server.Implementations/Localization/Core/pa.json b/Emby.Server.Implementations/Localization/Core/pa.json
index a25099ee0..6062d9700 100644
--- a/Emby.Server.Implementations/Localization/Core/pa.json
+++ b/Emby.Server.Implementations/Localization/Core/pa.json
@@ -120,5 +120,20 @@
"Albums": "ਐਲਬਮਾਂ",
"TaskOptimizeDatabase": "ਡਾਟਾਬੇਸ ਅਨੁਕੂਲ ਬਣਾਓ",
"External": "ਬਾਹਰੀ",
- "HearingImpaired": "ਸੁਨਣ ਵਿਚ ਕਮਜ਼ੋਰ"
+ "HearingImpaired": "ਸੁਨਣ ਵਿਚ ਕਮਜ਼ੋਰ",
+ "TaskAudioNormalizationDescription": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ ਡਾਟਾ ਲਈ ਫਾਇਲਾਂ ਖੋਜੋ।",
+ "TaskRefreshTrickplayImages": "ਟ੍ਰਿਕਪਲੇ ਤਸਵੀਰਾਂ ਤਿਆਰ ਕਰੋ",
+ "TaskExtractMediaSegments": "ਮੀਡੀਆ ਸੈਗਮੈਂਟ ਸਕੈਨ",
+ "TaskMoveTrickplayImagesDescription": "ਟ੍ਰਿਕਪਲੇ ਤਸਵੀਰਾਂ ਦੀ ਜਗਾ ਨੂੰ ਲਾਇਬ੍ਰੇਰੀ ਸੈਟਿੰਗਜ਼ ਅਨੁਸਾਰ ਬਦਲੋ।",
+ "TaskOptimizeDatabaseDescription": "ਡੇਟਾਬੇਸ ਨੂੰ ਸੰਗ੍ਰਹਿਤ ਕਰਦਾ ਹੈ ਅਤੇ ਖਾਲੀ ਜਗ੍ਹਾ ਘਟਾਉਂਦਾ ਹੈ। ਲਾਇਬ੍ਰੇਰੀ ਸਕੈਨ ਕਰਨ ਜਾਂ ਡੇਟਾਬੇਸ ਵਿੱਚ ਸੋਧਾਂ ਕਰਨ ਤੋਂ ਬਾਅਦ ਇਸ ਕੰਮ ਨੂੰ ਚਲਾਉਣਾ ਪ੍ਰਦਰਸ਼ਨ ਵਿੱਚ ਸੁਧਾਰ ਕਰ ਸਕਦਾ ਹੈ।",
+ "TaskExtractMediaSegmentsDescription": "ਮੀਡੀਆ ਸੈਗਮੈਂਟ ਨੂੰ ਮੀਡੀਆਸੈਗਮੈਂਟ ਯੋਗ ਪਲੱਗਇਨਾਂ ਤੋਂ ਨਿਕਾਲਦਾ ਜਾਂ ਪ੍ਰਾਪਤ ਕਰਦਾ ਹੈ।",
+ "TaskMoveTrickplayImages": "ਟ੍ਰਿਕਪਲੇ ਤਸਵੀਰਾਂ ਦੀ ਜਗਾ ਬਦਲੋ",
+ "TaskDownloadMissingLyrics": "ਅਧੂਰੇ ਬੋਲ ਡਾਊਨਲੋਡ ਕਰੋ",
+ "TaskDownloadMissingLyricsDescription": "ਗੀਤਾਂ ਲਈ ਡਾਊਨਲੋਡ ਕਿਤੇ ਬੋਲ",
+ "TaskKeyframeExtractor": "ਕੀ-ਫ੍ਰੇਮ ਐਕਸਟ੍ਰੈਕਟਰ",
+ "TaskCleanCollectionsAndPlaylistsDescription": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਵਿੱਚੋਂ ਉਹ ਆਈਟਮ ਹਟਾਉਂਦਾ ਹੈ ਜੋ ਹੁਣ ਮੌਜੂਦ ਨਹੀਂ ਹਨ।",
+ "TaskCleanCollectionsAndPlaylists": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਸਾਫ ਕਰੋ",
+ "TaskAudioNormalization": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ",
+ "TaskRefreshTrickplayImagesDescription": "ਚਲ ਰਹੀ ਲਾਇਬ੍ਰੇਰੀਆਂ ਵਿੱਚ ਵੀਡੀਓਜ਼ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ।",
+ "TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 2812832ca..0bf0491be 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -13,7 +13,7 @@
"HeaderContinueWatching": "Continuar a ver",
"HeaderAlbumArtists": "Artistas do Álbum",
"Genres": "Géneros",
- "Folders": "Diretórios",
+ "Folders": "Pastas",
"Favorites": "Favoritos",
"Channels": "Canais",
"UserDownloadingItemWithValues": "{0} está sendo baixado {1}",
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index bf59e1583..a873c157e 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -77,7 +77,7 @@
"HeaderAlbumArtists": "Artiști album",
"Genres": "Genuri",
"Folders": "Dosare",
- "Favorites": "Favorite",
+ "Favorites": "Preferate",
"FailedLoginAttemptWithUserName": "Încercare de conectare eșuată pentru {0}",
"DeviceOnlineWithName": "{0} este conectat",
"DeviceOfflineWithName": "{0} s-a deconectat",
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 01b8bfbe2..856ccb1ed 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -132,5 +132,9 @@
"TaskAudioNormalization": "Нормализация звука",
"TaskAudioNormalizationDescription": "Сканирует файлы на наличие данных о нормализации звука.",
"TaskDownloadMissingLyrics": "Загрузить недостающий текст",
- "TaskDownloadMissingLyricsDescription": "Загружает текст песен"
+ "TaskDownloadMissingLyricsDescription": "Загружает текст песен",
+ "TaskMoveTrickplayImages": "Перенесение местоположения изображений Trickplay",
+ "TaskExtractMediaSegments": "Сканирование медиасегментов",
+ "TaskExtractMediaSegmentsDescription": "Извлекает или получает медиасегменты из плагинов MediaSegment.",
+ "TaskMoveTrickplayImagesDescription": "Перемещает существующие файлы trickplay в соответствии с настройками медиатеки."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index 19be1a23e..b17e7ae55 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -126,5 +126,15 @@
"TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa.",
"HearingImpaired": "Oslabljen sluh",
"TaskRefreshTrickplayImages": "Ustvari Trickplay slike",
- "TaskRefreshTrickplayImagesDescription": "Ustvari trickplay predoglede za posnetke v omogočenih knjižnicah."
+ "TaskRefreshTrickplayImagesDescription": "Ustvari trickplay predoglede za posnetke v omogočenih knjižnicah.",
+ "TaskExtractMediaSegmentsDescription": "Ekstrahira ali pridobi medijske segmente iz vtičnikov, ki podpirajo MediaSegment.",
+ "TaskMoveTrickplayImagesDescription": "Premakne obstoječe datoteke trickplay v skladu z nastavitvami knjižnice.",
+ "TaskExtractMediaSegments": "Skeniranje segmentov v medijih",
+ "TaskMoveTrickplayImages": "Preseli lokacijo Trickplay slik",
+ "TaskDownloadMissingLyrics": "Prenesi manjkajoča besedila pesmi",
+ "TaskDownloadMissingLyricsDescription": "Prenesi besedila za pesmi",
+ "TaskCleanCollectionsAndPlaylists": "Počisti zbirke in sezname predvajanja",
+ "TaskAudioNormalization": "Normalizacija zvoka",
+ "TaskAudioNormalizationDescription": "Pregled datotek za podatke o normalizaciji zvoka.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Odstrani elemente iz zbirk in seznamov predvajanja, ki ne obstajajo več."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index 9739358df..af40b5e5a 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -78,7 +78,7 @@
"Genres": "Жанрови",
"Folders": "Фасцикле",
"Favorites": "Омиљено",
- "FailedLoginAttemptWithUserName": "Неуспела пријава са {0}",
+ "FailedLoginAttemptWithUserName": "Неуспели покушај пријавe са {0}",
"DeviceOnlineWithName": "{0} је повезан",
"DeviceOfflineWithName": "{0} је прекинуо везу",
"Collections": "Колекције",
@@ -121,7 +121,10 @@
"TaskOptimizeDatabase": "Оптимизуј банку података",
"TaskOptimizeDatabaseDescription": "Сажима базу података и скраћује слободан простор. Покретање овог задатка након скенирања библиотеке или других промена које подразумевају измене базе података које могу побољшати перформансе.",
"External": "Спољно",
- "TaskKeyframeExtractorDescription": "Екстрактује кљулне сличице из видео датотека да би креирао више преицзну HLS плеј-листу. Овај задатак може да потраје дуже време.",
+ "TaskKeyframeExtractorDescription": "Екстрактује кључне сличице из видео датотека да би креирао више прецизнију HLS плејлисту. Овај задатак може да потраје дуже време.",
"TaskKeyframeExtractor": "Екстрактор кључних сличица",
- "HearingImpaired": "ослабљен слух"
+ "HearingImpaired": "ослабљен слух",
+ "TaskAudioNormalization": "Нормализација звука",
+ "TaskCleanCollectionsAndPlaylists": "Очистите колекције и плејлисте",
+ "TaskAudioNormalizationDescription": "Скенира датотеке за податке о нормализацији звука."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index 5cf54522b..60810b45d 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -82,13 +82,13 @@
"UserCreatedWithName": "Användaren {0} har skapats",
"UserDeletedWithName": "Användaren {0} har tagits bort",
"UserDownloadingItemWithValues": "{0} laddar ner {1}",
- "UserLockedOutWithName": "Användare {0} har låsts ute",
- "UserOfflineFromDevice": "{0} har avbrutit anslutningen från {1}",
+ "UserLockedOutWithName": "Användare {0} har utelåsts",
+ "UserOfflineFromDevice": "{0} har kopplat ned från {1}",
"UserOnlineFromDevice": "{0} är uppkopplad från {1}",
"UserPasswordChangedWithName": "Lösenordet för {0} har ändrats",
"UserPolicyUpdatedWithName": "Användarpolicyn har uppdaterats för {0}",
- "UserStartedPlayingItemWithValues": "{0} spelar upp {1} på {2}",
- "UserStoppedPlayingItemWithValues": "{0} har avslutat uppspelningen av {1} på {2}",
+ "UserStartedPlayingItemWithValues": "{0} spelar {1} på {2}",
+ "UserStoppedPlayingItemWithValues": "{0} har stoppat uppspelningen av {1} på {2}",
"ValueHasBeenAddedToLibrary": "{0} har lagts till i ditt mediebibliotek",
"ValueSpecialEpisodeName": "Specialavsnitt - {0}",
"VersionNumber": "Version {0}",
@@ -98,8 +98,8 @@
"TaskRefreshChannels": "Uppdatera kanaler",
"TaskCleanTranscodeDescription": "Raderar omkodningsfiler äldre än en dag.",
"TaskCleanTranscode": "Rensa omkodningskatalog",
- "TaskUpdatePluginsDescription": "Laddar ned och installerar uppdateringar till tilläggsprogram som är konfigurerade att uppdateras automatiskt.",
- "TaskUpdatePlugins": "Uppdatera tilläggsprogram",
+ "TaskUpdatePluginsDescription": "Laddar ned och installerar uppdateringar till tillägg som är konfigurerade att uppdateras automatiskt.",
+ "TaskUpdatePlugins": "Uppdatera tillägg",
"TaskRefreshPeopleDescription": "Uppdaterar metadata för skådespelare och regissörer i ditt mediabibliotek.",
"TaskCleanLogsDescription": "Raderar loggfiler som är mer än {0} dagar gamla.",
"TaskCleanLogs": "Rensa loggkatalog",
diff --git a/Emby.Server.Implementations/Localization/Core/uz.json b/Emby.Server.Implementations/Localization/Core/uz.json
index a1b3035f3..150fb7126 100644
--- a/Emby.Server.Implementations/Localization/Core/uz.json
+++ b/Emby.Server.Implementations/Localization/Core/uz.json
@@ -23,5 +23,92 @@
"HeaderLiveTV": "Jonli TV",
"HeaderNextUp": "Keyingisi",
"ItemAddedWithName": "{0} kutbxonaga qo'shildi",
- "LabelIpAddressValue": "IP manzil: {0}"
+ "LabelIpAddressValue": "IP manzil: {0}",
+ "SubtitleDownloadFailureFromForItem": "{0} dan {1} uchun taglavhalarni yuklab boʻlmadi",
+ "UserPasswordChangedWithName": "Foydalanuvchi {0} paroli oʻzgartirildi",
+ "ValueHasBeenAddedToLibrary": "{0} kutubxonaga qoʻshildi",
+ "TaskCleanActivityLogDescription": "Belgilangan yoshdan kattaroq faoliyat jurnali yozuvlarini oʻchiradi.",
+ "TaskAudioNormalization": "Ovozni normallashtirish",
+ "TaskRefreshLibraryDescription": "Media kutubxonasi yangi fayllar uchun skanerlanmoqda va metama'lumotlar yangilanmoqda.",
+ "Default": "Joriy",
+ "HeaderFavoriteAlbums": "Tanlangan albomlar",
+ "HeaderFavoriteArtists": "Tanlangan artistlar",
+ "HeaderFavoriteEpisodes": "Tanlangan epizodlar",
+ "HeaderFavoriteShows": "Tanlangan shoular",
+ "HeaderFavoriteSongs": "Tanlangan qo'shiqlar",
+ "HeaderRecordingGroups": "Yozuvlar guruhi",
+ "HomeVideos": "Uy videolari",
+ "NotificationOptionVideoPlaybackStopped": "Video ijrosi toʻxtatildi",
+ "TvShows": "TV seriallar",
+ "Undefined": "Belgilanmagan",
+ "User": "Foydalanuvchi",
+ "UserCreatedWithName": "{0} foydalanuvchi yaratildi",
+ "TaskCleanCacheDescription": "Tizimga kerak bo'lmagan kesh fayllari o'chiriladi.",
+ "TaskAudioNormalizationDescription": "Ovozni normallashtirish ma'lumotlari uchun fayllarni skanerlaydi.",
+ "PluginInstalledWithName": "{0} - o'rnatildi",
+ "PluginUninstalledWithName": "{0} - o'chirildi",
+ "HearingImpaired": "Yaxshi eshitmaydiganlar uchun",
+ "Inherit": "Meroslangan",
+ "NotificationOptionApplicationUpdateAvailable": "Ilova yangilanishi mavjud",
+ "NotificationOptionApplicationUpdateInstalled": "Ilova yangilanishi oʻrnatildi",
+ "LabelRunningTimeValue": "Davomiyligi",
+ "NotificationOptionAudioPlayback": "Audio tinglash boshlandi",
+ "NotificationOptionAudioPlaybackStopped": "Audio tinglash to'xtatildi",
+ "NotificationOptionCameraImageUploaded": "Kamera tasvirlari yuklandi",
+ "NotificationOptionInstallationFailed": "O'rnatishda hatolik",
+ "NotificationOptionNewLibraryContent": "Yangi tarkib qo'shildi",
+ "NotificationOptionPluginError": "Plagin ishdan chiqdi",
+ "NotificationOptionPluginInstalled": "Plagin o'rnatildi",
+ "NotificationOptionPluginUninstalled": "Plagin o'chirildi",
+ "NotificationOptionPluginUpdateInstalled": "Plagin uchun yangilanish o'rnatildi",
+ "NotificationOptionServerRestartRequired": "Server-ni qayta yuklash lozim",
+ "NotificationOptionTaskFailed": "Rejalashtirilgan vazifa bajarilmadi",
+ "NotificationOptionUserLockedOut": "Foydalanuvchi bloklangan",
+ "NotificationOptionVideoPlayback": "Video ijrosi boshlandi",
+ "Photos": "Surat",
+ "Latest": "So'ngi",
+ "MessageApplicationUpdated": "Jellyfin Server yangilandi",
+ "MessageApplicationUpdatedTo": "Jellyfin Server {0} gacha yangilandi",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguratsiyasi ({0}-boʻlim) yangilandi",
+ "MessageServerConfigurationUpdated": "Server konfiguratsiyasi yangilandi",
+ "MixedContent": "Aralashgan tarkib",
+ "Movies": "Kinolar",
+ "Music": "Qo'shiqlar",
+ "MusicVideos": "Musiqali videolar",
+ "NameInstallFailed": "Omadsiz ornatish {0}",
+ "NameSeasonNumber": "{0} Fasl",
+ "NameSeasonUnknown": "Fasl aniqlanmagan",
+ "Playlists": "Pleylistlar",
+ "NewVersionIsAvailable": "Yuklab olish uchun Jellyfin Server ning yangi versiyasi mavjud",
+ "Plugin": "Plagin",
+ "TaskCleanLogs": "Jurnallar katalogini tozalash",
+ "PluginUpdatedWithName": "{0} - yangilandi",
+ "ProviderValue": "Yetkazib beruvchi: {0}",
+ "ScheduledTaskFailedWithName": "{0} - omadsiz",
+ "ScheduledTaskStartedWithName": "{0} - ishga tushirildi",
+ "ServerNameNeedsToBeRestarted": "Qayta yuklash kerak {0}",
+ "Shows": "Teleko'rsatuv",
+ "Songs": "Kompozitsiyalar",
+ "StartupEmbyServerIsLoading": "Jellyfin Server yuklanmoqda. Tez orada qayta urinib koʻring.",
+ "Sync": "Sinxronizatsiya",
+ "System": "Tizim",
+ "UserDeletedWithName": "{0} foydalanuvchisi oʻchirib tashlandi",
+ "UserDownloadingItemWithValues": "{0} yuklanmoqda {1}",
+ "UserLockedOutWithName": "{0} foydalanuvchisi bloklandi",
+ "UserOfflineFromDevice": "{0} {1}dan uzildi",
+ "UserOnlineFromDevice": "{0} {1} dan ulandi",
+ "UserPolicyUpdatedWithName": "{0} foydalanuvchisining siyosatlari yangilandi",
+ "UserStartedPlayingItemWithValues": "{0} - {2} da \"{1}\" ijrosi",
+ "UserStoppedPlayingItemWithValues": "{0} - ijro etish to‘xtatildi {1} {2}",
+ "ValueSpecialEpisodeName": "Maxsus qism – {0}",
+ "VersionNumber": "Versiya {0}",
+ "TasksMaintenanceCategory": "Xizmat ko'rsatish",
+ "TasksLibraryCategory": "Media kutubxona",
+ "TasksApplicationCategory": "Ilova",
+ "TasksChannelsCategory": "Internet kanallari",
+ "TaskCleanActivityLog": "Faoliyat jurnalini tozalash",
+ "TaskCleanCache": "Kesh katalogini tozalash",
+ "TaskRefreshChapterImages": "Sahnadan tasvirini chiqarish",
+ "TaskRefreshChapterImagesDescription": "Sahnalarni o'z ichiga olgan videolar uchun eskizlarni yaratadi.",
+ "TaskRefreshLibrary": "Media kutubxonangizni skanerlash"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index 3ab9774c2..286efb7e9 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -126,5 +126,15 @@
"External": "外部",
"HearingImpaired": "聽力障礙",
"TaskRefreshTrickplayImages": "建立 Trickplay 圖像",
- "TaskRefreshTrickplayImagesDescription": "為已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。"
+ "TaskRefreshTrickplayImagesDescription": "為已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。",
+ "TaskExtractMediaSegments": "掃描媒體段落",
+ "TaskExtractMediaSegmentsDescription": "從MediaSegment中被允許的插件獲取媒體段落。",
+ "TaskDownloadMissingLyrics": "下載欠缺歌詞",
+ "TaskDownloadMissingLyricsDescription": "下載歌詞",
+ "TaskCleanCollectionsAndPlaylists": "整理媒體與播放清單",
+ "TaskAudioNormalization": "音訊同等化",
+ "TaskAudioNormalizationDescription": "掃描檔案裏的音訊同等化資料。",
+ "TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。",
+ "TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。",
+ "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 81d5b83d6..a4ee68fc4 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "已從 {0} 成功上傳一張相片",
"Channels": "頻道",
"ChapterNameValue": "章節 {0}",
- "Collections": "系列",
+ "Collections": "系列作",
"DeviceOfflineWithName": "{0} 已中斷連接",
"DeviceOnlineWithName": "{0} 已連接",
"FailedLoginAttemptWithUserName": "來自使用者 {0} 的登入失敗嘗試",
@@ -126,8 +126,8 @@
"HearingImpaired": "聽力障礙",
"TaskRefreshTrickplayImages": "生成快轉縮圖",
"TaskRefreshTrickplayImagesDescription": "為啟用快轉縮圖的媒體庫生成快轉縮圖。",
- "TaskCleanCollectionsAndPlaylists": "清理系列和播放清單",
- "TaskCleanCollectionsAndPlaylistsDescription": "清理系列和播放清單中已不存在的項目。",
+ "TaskCleanCollectionsAndPlaylists": "清理系列作和播放清單",
+ "TaskCleanCollectionsAndPlaylistsDescription": "清理系列作品與播放清單中已不存在的項目。",
"TaskAudioNormalization": "音量標準化",
"TaskAudioNormalizationDescription": "掃描文件以找出音量標準化資料。",
"TaskDownloadMissingLyrics": "下載缺少的歌詞",
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index ac453a5b0..c939a5e09 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -231,13 +231,13 @@ namespace Emby.Server.Implementations.Localization
ratings.Add(new ParentalRating("21", 21));
}
- // A lot of countries don't excplicitly have a seperate rating for adult content
+ // A lot of countries don't explicitly have a separate rating for adult content
if (ratings.All(x => x.Value != 1000))
{
ratings.Add(new ParentalRating("XXX", 1000));
}
- // A lot of countries don't excplicitly have a seperate rating for banned content
+ // A lot of countries don't explicitly have a separate rating for banned content
if (ratings.All(x => x.Value != 1001))
{
ratings.Add(new ParentalRating("Banned", 1001));
diff --git a/Emby.Server.Implementations/Localization/Ratings/br.csv b/Emby.Server.Implementations/Localization/Ratings/br.csv
index 5ec1eb262..f6053c88c 100644
--- a/Emby.Server.Implementations/Localization/Ratings/br.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/br.csv
@@ -1,8 +1,14 @@
Livre,0
L,0
-ER,9
+AL,0
+ER,10
10,10
+A10,10
12,12
+A12,12
14,14
+A14,14
16,16
+A16,16
18,18
+A18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.csv b/Emby.Server.Implementations/Localization/Ratings/ca.csv
index 336ee2806..41dbda134 100644
--- a/Emby.Server.Implementations/Localization/Ratings/ca.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/ca.csv
@@ -6,8 +6,6 @@ TV-Y7,7
TV-Y7-FV,7
PG,9
TV-PG,9
-PG-13,13
-13+,13
TV-14,14
14A,14
16+,16
diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv
index 619e948d8..ee5866090 100644
--- a/Emby.Server.Implementations/Localization/Ratings/es.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/es.csv
@@ -1,7 +1,7 @@
A,0
A/fig,0
A/i,0
-A/fig/i,0
+A/i/fig,0
APTA,0
ERI,0
TP,0
diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.csv b/Emby.Server.Implementations/Localization/Ratings/gb.csv
index 75b1c2058..858b9a32d 100644
--- a/Emby.Server.Implementations/Localization/Ratings/gb.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/gb.csv
@@ -6,10 +6,11 @@ U,0
6+,6
7+,7
PG,8
-9+,9
+9,9
12,12
12+,12
12A,12
+12PG,12
Teen,13
13+,13
14+,14
diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.csv b/Emby.Server.Implementations/Localization/Ratings/ie.csv
index 6ef2e5012..d3c634fc9 100644
--- a/Emby.Server.Implementations/Localization/Ratings/ie.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/ie.csv
@@ -4,6 +4,7 @@ PG,12
12A,12
12PG,12
15,15
+15PG,15
15A,15
16,16
18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/no.csv b/Emby.Server.Implementations/Localization/Ratings/no.csv
index c8f8e93db..6856a2dbb 100644
--- a/Emby.Server.Implementations/Localization/Ratings/no.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/no.csv
@@ -6,4 +6,5 @@ A,0
12,12
15,15
18,18
+C,18
Not approved,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.csv b/Emby.Server.Implementations/Localization/Ratings/nz.csv
index f617f0c39..633da78fe 100644
--- a/Emby.Server.Implementations/Localization/Ratings/nz.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/nz.csv
@@ -10,6 +10,7 @@ R16,16
RP16,16
GA,18
R18,18
+RP18,18
MA,1000
R,1001
Objectionable,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/us.csv b/Emby.Server.Implementations/Localization/Ratings/us.csv
index d103ddf42..9aa5c00eb 100644
--- a/Emby.Server.Implementations/Localization/Ratings/us.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/us.csv
@@ -5,23 +5,23 @@ TV-Y,0
TV-Y7,7
TV-Y7-FV,7
PG,10
+TV-PG,10
+TV-PG-D,10
+TV-PG-L,10
+TV-PG-S,10
+TV-PG-V,10
+TV-PG-DL,10
+TV-PG-DS,10
+TV-PG-DV,10
+TV-PG-LS,10
+TV-PG-LV,10
+TV-PG-SV,10
+TV-PG-DLS,10
+TV-PG-DLV,10
+TV-PG-DSV,10
+TV-PG-LSV,10
+TV-PG-DLSV,10
PG-13,13
-TV-PG,13
-TV-PG-D,13
-TV-PG-L,13
-TV-PG-S,13
-TV-PG-V,13
-TV-PG-DL,13
-TV-PG-DS,13
-TV-PG-DV,13
-TV-PG-LS,13
-TV-PG-LV,13
-TV-PG-SV,13
-TV-PG-DLS,13
-TV-PG-DLV,13
-TV-PG-DSV,13
-TV-PG-LSV,13
-TV-PG-DLSV,13
TV-14,14
TV-14-D,14
TV-14-L,14
@@ -48,3 +48,5 @@ TV-MA-LS,17
TV-MA-LV,17
TV-MA-SV,17
TV-MA-LSV,17
+TV-X,18
+TV-AO,18
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index eb55e32c5..ea7896861 100644
--- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.MediaEncoder
private readonly IFileSystem _fileSystem;
private readonly ILogger<EncodingManager> _logger;
private readonly IMediaEncoder _encoder;
- private readonly IChapterManager _chapterManager;
+ private readonly IChapterRepository _chapterManager;
private readonly ILibraryManager _libraryManager;
/// <summary>
@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.MediaEncoder
ILogger<EncodingManager> logger,
IFileSystem fileSystem,
IMediaEncoder encoder,
- IChapterManager chapterManager,
+ IChapterRepository chapterManager,
ILibraryManager libraryManager)
{
_logger = logger;
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 47ff22c0b..daeb7fed8 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -216,14 +216,11 @@ namespace Emby.Server.Implementations.Playlists
var newItems = GetPlaylistItems(newItemIds, user, options)
.Where(i => i.SupportsAddingToPlaylist);
- // Filter out duplicate items, if necessary
- if (!_appConfig.DoPlaylistsAllowDuplicates())
- {
- var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet();
- newItems = newItems
- .Where(i => !existingIds.Contains(i.Id))
- .Distinct();
- }
+ // Filter out duplicate items
+ var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet();
+ newItems = newItems
+ .Where(i => !existingIds.Contains(i.Id))
+ .Distinct();
// Create a list of the new linked children to add to the playlist
var childrenToAdd = newItems
@@ -269,7 +266,7 @@ namespace Emby.Server.Implementations.Playlists
var idList = entryIds.ToList();
- var removals = children.Where(i => idList.Contains(i.Item1.Id));
+ var removals = children.Where(i => idList.Contains(i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture)));
playlist.LinkedChildren = children.Except(removals)
.Select(i => i.Item1)
@@ -286,26 +283,39 @@ namespace Emby.Server.Implementations.Playlists
RefreshPriority.High);
}
- public async Task MoveItemAsync(string playlistId, string entryId, int newIndex)
+ public async Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId)
{
if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
{
throw new ArgumentException("No Playlist exists with the supplied Id");
}
+ var user = _userManager.GetUserById(callingUserId);
var children = playlist.GetManageableItems().ToList();
+ var accessibleChildren = children.Where(c => c.Item2.IsVisible(user)).ToArray();
- var oldIndex = children.FindIndex(i => string.Equals(entryId, i.Item1.Id, StringComparison.OrdinalIgnoreCase));
+ var oldIndexAll = children.FindIndex(i => string.Equals(entryId, i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
+ var oldIndexAccessible = accessibleChildren.FindIndex(i => string.Equals(entryId, i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
- if (oldIndex == newIndex)
+ if (oldIndexAccessible == newIndex)
{
return;
}
- var item = playlist.LinkedChildren[oldIndex];
+ var newPriorItemIndex = newIndex > oldIndexAccessible ? newIndex : newIndex - 1 < 0 ? 0 : newIndex - 1;
+ var newPriorItemId = accessibleChildren[newPriorItemIndex].Item1.ItemId;
+ var newPriorItemIndexOnAllChildren = children.FindIndex(c => c.Item1.ItemId.Equals(newPriorItemId));
+ var adjustedNewIndex = newPriorItemIndexOnAllChildren + 1;
- var newList = playlist.LinkedChildren.ToList();
+ var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
+ if (item is null)
+ {
+ _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", item.ItemId, playlistId);
+ return;
+ }
+
+ var newList = playlist.LinkedChildren.ToList();
newList.Remove(item);
if (newIndex >= newList.Count)
@@ -314,7 +324,7 @@ namespace Emby.Server.Implementations.Playlists
}
else
{
- newList.Insert(newIndex, item);
+ newList.Insert(adjustedNewIndex, item);
}
playlist.LinkedChildren = [.. newList];
diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
index f65d609c7..db3aeaaf3 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
@@ -5,12 +5,14 @@ using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Playlists
{
+ [RequiresSourceSerialisation]
public class PlaylistsFolder : BasePluginFolder
{
public PlaylistsFolder()
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index db82a2900..8eeca3667 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -119,7 +119,7 @@ namespace Emby.Server.Implementations.Plugins
// Now load the assemblies..
foreach (var plugin in _plugins)
{
- UpdatePluginSuperceedStatus(plugin);
+ UpdatePluginSupersededStatus(plugin);
if (plugin.IsEnabledAndSupported == false)
{
@@ -214,7 +214,7 @@ namespace Emby.Server.Implementations.Plugins
continue;
}
- UpdatePluginSuperceedStatus(plugin);
+ UpdatePluginSupersededStatus(plugin);
if (!plugin.IsEnabledAndSupported)
{
continue;
@@ -624,9 +624,9 @@ namespace Emby.Server.Implementations.Plugins
}
}
- private void UpdatePluginSuperceedStatus(LocalPlugin plugin)
+ private void UpdatePluginSupersededStatus(LocalPlugin plugin)
{
- if (plugin.Manifest.Status != PluginStatus.Superceded)
+ if (plugin.Manifest.Status != PluginStatus.Superseded)
{
return;
}
@@ -785,30 +785,27 @@ namespace Emby.Server.Implementations.Plugins
var cleaned = false;
var path = entry.Path;
- if (_config.RemoveOldPlugins)
+ // Attempt a cleanup of old folders.
+ try
{
- // Attempt a cleanup of old folders.
- try
- {
- _logger.LogDebug("Deleting {Path}", path);
- Directory.Delete(path, true);
- cleaned = true;
- }
+ _logger.LogDebug("Deleting {Path}", path);
+ Directory.Delete(path, true);
+ cleaned = true;
+ }
#pragma warning disable CA1031 // Do not catch general exception types
- catch (Exception e)
+ catch (Exception e)
#pragma warning restore CA1031 // Do not catch general exception types
- {
- _logger.LogWarning(e, "Unable to delete {Path}", path);
- }
+ {
+ _logger.LogWarning(e, "Unable to delete {Path}", path);
+ }
- if (cleaned)
- {
- versions.RemoveAt(x);
- }
- else
- {
- ChangePluginState(entry, PluginStatus.Deleted);
- }
+ if (cleaned)
+ {
+ versions.RemoveAt(x);
+ }
+ else
+ {
+ ChangePluginState(entry, PluginStatus.Deleted);
}
}
@@ -835,7 +832,7 @@ namespace Emby.Server.Implementations.Plugins
/// <exception cref="ArgumentNullException">If the <see cref="LocalPlugin"/> is null.</exception>
private bool TryGetPluginDlls(LocalPlugin plugin, out IReadOnlyList<string> whitelistedDlls)
{
- ArgumentNullException.ThrowIfNull(nameof(plugin));
+ ArgumentNullException.ThrowIfNull(plugin);
IReadOnlyList<string> pluginDlls = Directory.GetFiles(plugin.Path, "*.dll", SearchOption.AllDirectories);
@@ -879,7 +876,7 @@ namespace Emby.Server.Implementations.Plugins
}
/// <summary>
- /// Changes the status of the other versions of the plugin to "Superceded".
+ /// Changes the status of the other versions of the plugin to "Superseded".
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> that's master.</param>
private void ProcessAlternative(LocalPlugin plugin)
@@ -899,11 +896,11 @@ namespace Emby.Server.Implementations.Plugins
return;
}
- if (plugin.Manifest.Status == PluginStatus.Active && !ChangePluginState(previousVersion, PluginStatus.Superceded))
+ if (plugin.Manifest.Status == PluginStatus.Active && !ChangePluginState(previousVersion, PluginStatus.Superseded))
{
_logger.LogError("Unable to enable version {Version} of {Name}", previousVersion.Version, previousVersion.Name);
}
- else if (plugin.Manifest.Status == PluginStatus.Superceded && !ChangePluginState(previousVersion, PluginStatus.Active))
+ else if (plugin.Manifest.Status == PluginStatus.Superseded && !ChangePluginState(previousVersion, PluginStatus.Active))
{
_logger.LogError("Unable to supercede version {Version} of {Name}", previousVersion.Version, previousVersion.Name);
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index 9b342cfbe..985f0a8f8 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
private readonly IApplicationPaths _applicationPaths;
private readonly ILogger _logger;
private readonly ITaskManager _taskManager;
- private readonly object _lastExecutionResultSyncLock = new();
+ private readonly Lock _lastExecutionResultSyncLock = new();
private bool _readFromFile;
private TaskResult _lastExecutionResult;
private Task _currentTask;
@@ -471,7 +471,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
new()
{
IntervalTicks = TimeSpan.FromDays(1).Ticks,
- Type = TaskTriggerInfo.TriggerInterval
+ Type = TaskTriggerInfoType.IntervalTrigger
}
];
}
@@ -543,7 +543,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
DisposeTriggers();
- var wassRunning = State == TaskState.Running;
+ var wasRunning = State == TaskState.Running;
var startTime = CurrentExecutionStartTime;
var token = CurrentCancellationTokenSource;
@@ -596,7 +596,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
}
- if (wassRunning)
+ if (wasRunning)
{
OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null);
}
@@ -616,7 +616,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
MaxRuntimeTicks = info.MaxRuntimeTicks
};
- if (info.Type.Equals(nameof(DailyTrigger), StringComparison.OrdinalIgnoreCase))
+ if (info.Type == TaskTriggerInfoType.DailyTrigger)
{
if (!info.TimeOfDayTicks.HasValue)
{
@@ -626,7 +626,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options);
}
- if (info.Type.Equals(nameof(WeeklyTrigger), StringComparison.OrdinalIgnoreCase))
+ if (info.Type == TaskTriggerInfoType.WeeklyTrigger)
{
if (!info.TimeOfDayTicks.HasValue)
{
@@ -641,7 +641,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options);
}
- if (info.Type.Equals(nameof(IntervalTrigger), StringComparison.OrdinalIgnoreCase))
+ if (info.Type == TaskTriggerInfoType.IntervalTrigger)
{
if (!info.IntervalTicks.HasValue)
{
@@ -651,7 +651,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options);
}
- if (info.Type.Equals(nameof(StartupTrigger), StringComparison.OrdinalIgnoreCase))
+ if (info.Type == TaskTriggerInfoType.StartupTrigger)
{
return new StartupTrigger(options);
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
index eb6afe05d..031d14776 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
@@ -156,7 +156,7 @@ public partial class AudioNormalizationTask : IScheduledTask
[
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval,
+ Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromHours(24).Ticks
}
];
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index cb3f5b836..563e90fbe 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -32,6 +33,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
private readonly IEncodingManager _encodingManager;
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
+ private readonly IChapterRepository _chapterRepository;
/// <summary>
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
@@ -43,6 +45,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="chapterRepository">Instance of the <see cref="IChapterRepository"/> interface.</param>
public ChapterImagesTask(
ILogger<ChapterImagesTask> logger,
ILibraryManager libraryManager,
@@ -50,7 +53,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
IApplicationPaths appPaths,
IEncodingManager encodingManager,
IFileSystem fileSystem,
- ILocalizationManager localization)
+ ILocalizationManager localization,
+ IChapterRepository chapterRepository)
{
_logger = logger;
_libraryManager = libraryManager;
@@ -59,6 +63,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
_encodingManager = encodingManager;
_fileSystem = fileSystem;
_localization = localization;
+ _chapterRepository = chapterRepository;
}
/// <inheritdoc />
@@ -80,7 +85,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
[
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerDaily,
+ Type = TaskTriggerInfoType.DailyTrigger,
TimeOfDayTicks = TimeSpan.FromHours(2).Ticks,
MaxRuntimeTicks = TimeSpan.FromHours(4).Ticks
}
@@ -141,7 +146,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
try
{
- var chapters = _itemRepo.GetChapters(video);
+ var chapters = _chapterRepository.GetChapters(video.Id);
var success = await _encodingManager.RefreshChapterImages(video, directoryService, chapters, extract, true, cancellationToken).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
index 25e7ebe79..316e4a8f0 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
@@ -135,6 +135,6 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
/// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
- return [new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup }];
+ return [new TaskTriggerInfo() { Type = TaskTriggerInfoType.StartupTrigger }];
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
index 0325cb9af..ff295d9b7 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
@@ -73,7 +73,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
return
[
// Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
+ new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks }
];
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
index 9babe8cf9..a091c2bd9 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
return
[
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
+ new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks }
];
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
index 315c245cc..d0896cc81 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
@@ -69,11 +69,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
[
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerStartup
+ Type = TaskTriggerInfoType.StartupTrigger
},
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval,
+ Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromHours(24).Ticks
}
];
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs
index d6fad7526..de1e60d30 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs
@@ -111,7 +111,7 @@ public class MediaSegmentExtractionTask : IScheduledTask
{
yield return new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval,
+ Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromHours(12).Ticks
};
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
index 3e4925f74..7d4e2377d 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
return
[
// Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
+ new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks }
];
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
index c63bad474..2907f18b5 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
@@ -58,7 +58,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval,
+ Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromDays(7).Ticks
}
};
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
index ad72a4c87..b74f4d1b2 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
@@ -60,10 +60,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
// At startup
- yield return new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerStartup };
+ yield return new TaskTriggerInfo { Type = TaskTriggerInfoType.StartupTrigger };
// Every so often
- yield return new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks };
+ yield return new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks };
}
/// <inheritdoc />
@@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
}
catch (OperationCanceledException)
{
- // InstallPackage has it's own inner cancellation token, so only throw this if it's ours
+ // InstallPackage has its own inner cancellation token, so only throw this if it's ours
if (cancellationToken.IsCancellationRequested)
{
throw;
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
index a59f0f366..172448dde 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
@@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
yield return new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval,
+ Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromHours(12).Ticks
};
}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 6a8ad2bdc..030da6f73 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1303,7 +1303,7 @@ namespace Emby.Server.Implementations.Session
if (item is null)
{
- _logger.LogError("A non-existent item Id {0} was passed into TranslateItemForPlayback", id);
+ _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForPlayback", id);
return Array.Empty<BaseItem>();
}
@@ -1356,7 +1356,7 @@ namespace Emby.Server.Implementations.Session
if (item is null)
{
- _logger.LogError("A non-existent item Id {0} was passed into TranslateItemForInstantMix", id);
+ _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForInstantMix", id);
return new List<BaseItem>();
}
@@ -1938,7 +1938,11 @@ namespace Emby.Server.Implementations.Session
// Don't report acceleration type for non-admin users.
result = result.Select(r =>
{
- r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
+ if (r.TranscodingInfo is not null)
+ {
+ r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
+ }
+
return r;
});
}
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index aba51de8f..d4606abd2 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Lock used for accessing the WebSockets watchlist.
/// </summary>
- private readonly object _webSocketsLock = new object();
+ private readonly Lock _webSocketsLock = new();
private readonly ISessionManager _sessionManager;
private readonly ILogger<SessionWebSocketListener> _logger;
@@ -276,11 +276,11 @@ namespace Emby.Server.Implementations.Session
/// </summary>
/// <param name="webSocket">The WebSocket.</param>
/// <returns>Task.</returns>
- private Task SendForceKeepAlive(IWebSocketConnection webSocket)
+ private async Task SendForceKeepAlive(IWebSocketConnection webSocket)
{
- return webSocket.SendAsync(
+ await webSocket.SendAsync(
new ForceKeepAliveMessage(WebSocketLostTimeout),
- CancellationToken.None);
+ CancellationToken.None).ConfigureAwait(false);
}
}
}
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index cf8e0fb00..c45a4a60f 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.Session
private readonly SessionInfo _session;
private readonly List<IWebSocketConnection> _sockets;
+ private readonly ReaderWriterLockSlim _socketsLock;
private bool _disposed = false;
public WebSocketController(
@@ -31,10 +32,26 @@ namespace Emby.Server.Implementations.Session
_logger = logger;
_session = session;
_sessionManager = sessionManager;
- _sockets = new List<IWebSocketConnection>();
+ _sockets = new();
+ _socketsLock = new();
}
- private bool HasOpenSockets => GetActiveSockets().Any();
+ private bool HasOpenSockets
+ {
+ get
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ try
+ {
+ _socketsLock.EnterReadLock();
+ return _sockets.Any(i => i.State == WebSocketState.Open);
+ }
+ finally
+ {
+ _socketsLock.ExitReadLock();
+ }
+ }
+ }
/// <inheritdoc />
public bool SupportsMediaControl => HasOpenSockets;
@@ -42,23 +59,38 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public bool IsSessionActive => HasOpenSockets;
- private IEnumerable<IWebSocketConnection> GetActiveSockets()
- => _sockets.Where(i => i.State == WebSocketState.Open);
-
public void AddWebSocket(IWebSocketConnection connection)
{
_logger.LogDebug("Adding websocket to session {Session}", _session.Id);
- _sockets.Add(connection);
-
- connection.Closed += OnConnectionClosed;
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ try
+ {
+ _socketsLock.EnterWriteLock();
+ _sockets.Add(connection);
+ connection.Closed += OnConnectionClosed;
+ }
+ finally
+ {
+ _socketsLock.ExitWriteLock();
+ }
}
private async void OnConnectionClosed(object? sender, EventArgs e)
{
var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
- _sockets.Remove(connection);
- connection.Closed -= OnConnectionClosed;
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ try
+ {
+ _socketsLock.EnterWriteLock();
+ _sockets.Remove(connection);
+ connection.Closed -= OnConnectionClosed;
+ }
+ finally
+ {
+ _socketsLock.ExitWriteLock();
+ }
+
await _sessionManager.CloseIfNeededAsync(_session).ConfigureAwait(false);
}
@@ -69,7 +101,17 @@ namespace Emby.Server.Implementations.Session
T data,
CancellationToken cancellationToken)
{
- var socket = GetActiveSockets().MaxBy(i => i.LastActivityDate);
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ IWebSocketConnection? socket;
+ try
+ {
+ _socketsLock.EnterReadLock();
+ socket = _sockets.Where(i => i.State == WebSocketState.Open).MaxBy(i => i.LastActivityDate);
+ }
+ finally
+ {
+ _socketsLock.ExitReadLock();
+ }
if (socket is null)
{
@@ -94,12 +136,23 @@ namespace Emby.Server.Implementations.Session
return;
}
- foreach (var socket in _sockets)
+ try
+ {
+ _socketsLock.EnterWriteLock();
+ foreach (var socket in _sockets)
+ {
+ socket.Closed -= OnConnectionClosed;
+ socket.Dispose();
+ }
+
+ _sockets.Clear();
+ }
+ finally
{
- socket.Closed -= OnConnectionClosed;
- socket.Dispose();
+ _socketsLock.ExitWriteLock();
}
+ _socketsLock.Dispose();
_disposed = true;
}
@@ -110,12 +163,23 @@ namespace Emby.Server.Implementations.Session
return;
}
- foreach (var socket in _sockets)
+ try
+ {
+ _socketsLock.EnterWriteLock();
+ foreach (var socket in _sockets)
+ {
+ socket.Closed -= OnConnectionClosed;
+ await socket.DisposeAsync().ConfigureAwait(false);
+ }
+
+ _sockets.Clear();
+ }
+ finally
{
- socket.Closed -= OnConnectionClosed;
- await socket.DisposeAsync().ConfigureAwait(false);
+ _socketsLock.ExitWriteLock();
}
+ _socketsLock.Dispose();
_disposed = true;
}
}
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
index 00c655634..fdfff8f3b 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.SyncPlay
/// <remarks>
/// This lock has priority on locks made on <see cref="Group"/>.
/// </remarks>
- private readonly object _groupsLock = new object();
+ private readonly Lock _groupsLock = new();
private bool _disposed = false;
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index d11b03a2e..f8ce473da 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -117,7 +117,7 @@ namespace Emby.Server.Implementations.TV
.ToList();
// Avoid implicitly captured closure
- var episodes = GetNextUpEpisodes(request, user, items, options);
+ var episodes = GetNextUpEpisodes(request, user, items.Distinct().ToArray(), options);
return GetResult(episodes, request);
}
@@ -262,7 +262,7 @@ namespace Emby.Server.Implementations.TV
{
var userData = _userDataManager.GetUserData(user, nextEpisode);
- if (userData.PlaybackPositionTicks > 0)
+ if (userData?.PlaybackPositionTicks > 0)
{
return null;
}
@@ -275,6 +275,11 @@ namespace Emby.Server.Implementations.TV
{
var userData = _userDataManager.GetUserData(user, lastWatchedEpisode);
+ if (userData is null)
+ {
+ return (DateTime.MinValue, GetEpisode);
+ }
+
var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
return (lastWatchedDate, GetEpisode);
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index ce3d6cab8..678475b31 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Updates
/// </summary>
/// <value>The application host.</value>
private readonly IServerApplicationHost _applicationHost;
- private readonly object _currentInstallationsLock = new object();
+ private readonly Lock _currentInstallationsLock = new();
/// <summary>
/// The current installations.
@@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.Updates
await _pluginManager.PopulateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
}
- // Remove versions with a target ABI greater then the current application version.
+ // Remove versions with a target ABI greater than the current application version.
if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
{
package.Versions.RemoveAt(i);
diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
index 2853e69b0..1286c92c7 100644
--- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
+++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
@@ -50,7 +50,8 @@ namespace Jellyfin.Api.Auth
}
var role = UserRoles.User;
- if (authorizationInfo.IsApiKey || authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
+ if (authorizationInfo.IsApiKey
+ || (authorizationInfo.User?.HasPermission(PermissionKind.IsAdministrator) ?? false))
{
role = UserRoles.Administrator;
}
@@ -60,10 +61,10 @@ namespace Jellyfin.Api.Auth
new Claim(ClaimTypes.Name, authorizationInfo.User?.Username ?? string.Empty),
new Claim(ClaimTypes.Role, role),
new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
- new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
- new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
- new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
- new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
+ new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId ?? string.Empty),
+ new Claim(InternalClaimTypes.Device, authorizationInfo.Device ?? string.Empty),
+ new Claim(InternalClaimTypes.Client, authorizationInfo.Client ?? string.Empty),
+ new Claim(InternalClaimTypes.Version, authorizationInfo.Version ?? string.Empty),
new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
new Claim(InternalClaimTypes.IsApiKey, authorizationInfo.IsApiKey.ToString(CultureInfo.InvariantCulture))
};
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index 8b931f162..10556da65 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -91,31 +91,31 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
@@ -295,31 +295,31 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index a47c60473..272b4034e 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -238,7 +238,7 @@ public class AudioController : BaseJellyfinApiController
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index f83c71b57..2f55e88ec 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -121,10 +121,10 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -197,9 +197,9 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] channelIds)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index 2d9f1ed69..c37f37633 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -50,7 +50,7 @@ public class CollectionController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
[FromQuery] string? name,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] ids,
[FromQuery] Guid? parentId,
[FromQuery] bool isLocked = false)
{
@@ -86,7 +86,7 @@ public class CollectionController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddToCollection(
[FromRoute, Required] Guid collectionId,
- [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ [FromQuery, Required, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids)
{
await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
return NoContent();
@@ -103,7 +103,7 @@ public class CollectionController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RemoveFromCollection(
[FromRoute, Required] Guid collectionId,
- [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ [FromQuery, Required, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids)
{
await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
return NoContent();
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 54e0527c9..ca8ab0ef7 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -459,7 +459,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] bool enableAdaptiveBitrateStreaming = true,
+ [FromQuery] bool enableAdaptiveBitrateStreaming = false,
[FromQuery] bool enableTrickplay = true,
[FromQuery] bool enableAudioVbrEncoding = true,
[FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false)
@@ -634,7 +634,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] bool enableAdaptiveBitrateStreaming = true,
+ [FromQuery] bool enableAdaptiveBitrateStreaming = false,
[FromQuery] bool enableAudioVbrEncoding = true)
{
var streamingRequest = new HlsAudioRequestDto
@@ -1778,7 +1778,7 @@ public class DynamicHlsController : BaseJellyfinApiController
}
else if (state.AudioStream?.CodecTag is not null && state.AudioStream.CodecTag.Equals("ac-4", StringComparison.Ordinal))
{
- // ac-4 audio tends to hava a super weird sample rate that will fail most encoders
+ // ac-4 audio tends to have a super weird sample rate that will fail most encoders
// force resample it to 48KHz
args += " -ar 48000";
}
@@ -1819,16 +1819,13 @@ public class DynamicHlsController : BaseJellyfinApiController
if (isActualOutputVideoCodecHevc || isActualOutputVideoCodecAv1)
{
var requestedRange = state.GetRequestedRangeTypes(state.ActualOutputVideoCodec);
- var requestHasDOVI = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
- var requestHasDOVIWithHDR10 = requestedRange.Contains(VideoRangeType.DOVIWithHDR10.ToString(), StringComparison.OrdinalIgnoreCase);
- var requestHasDOVIWithHLG = requestedRange.Contains(VideoRangeType.DOVIWithHLG.ToString(), StringComparison.OrdinalIgnoreCase);
- var requestHasDOVIWithSDR = requestedRange.Contains(VideoRangeType.DOVIWithSDR.ToString(), StringComparison.OrdinalIgnoreCase);
+ // Clients reporting Dolby Vision capabilities with fallbacks may only support the fallback layer.
+ // Only enable Dolby Vision remuxing if the client explicitly declares support for profiles without fallbacks.
+ var clientSupportsDoVi = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
+ var videoIsDoVi = state.VideoStream.VideoRangeType is VideoRangeType.DOVI or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG or VideoRangeType.DOVIWithSDR;
if (EncodingHelper.IsCopyCodec(codec)
- && ((state.VideoStream.VideoRangeType == VideoRangeType.DOVI && requestHasDOVI)
- || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10 && requestHasDOVIWithHDR10)
- || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG && requestHasDOVIWithHLG)
- || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithSDR && requestHasDOVIWithSDR)))
+ && (videoIsDoVi && clientSupportsDoVi))
{
if (isActualOutputVideoCodecHevc)
{
@@ -2059,16 +2056,16 @@ public class DynamicHlsController : BaseJellyfinApiController
}
}
- private Task DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
+ private async Task DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
{
var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem);
if (file is null)
{
- return Task.CompletedTask;
+ return;
}
- return DeleteFile(file.FullName, retryCount);
+ await DeleteFile(file.FullName, retryCount).ConfigureAwait(false);
}
private async Task DeleteFile(string path, int retryCount)
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 4abca3271..3f9aa93a6 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -50,8 +50,8 @@ public class FilterController : BaseJellyfinApiController
public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
[FromQuery] Guid? userId,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -137,7 +137,7 @@ public class FilterController : BaseJellyfinApiController
public ActionResult<QueryFilters> GetQueryFilters(
[FromQuery] Guid? userId,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool? isAiring,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSports,
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index 54d48aec2..f0d17decb 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -76,18 +76,18 @@ public class GenresController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool? isFavorite,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index dcbacf1d7..e326b925b 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
+using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -71,11 +73,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -115,11 +117,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -159,11 +161,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -201,11 +203,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromRoute, Required] string name,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -239,11 +241,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -283,11 +285,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -328,11 +330,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
return GetInstantMixFromArtists(
id,
@@ -366,11 +368,11 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -389,23 +391,19 @@ public class InstantMixController : BaseJellyfinApiController
return GetResult(items, user, limit, dtoOptions);
}
- private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
+ private QueryResult<BaseItemDto> GetResult(IReadOnlyList<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
{
- var list = items;
+ var totalCount = items.Count;
- var totalCount = list.Count;
-
- if (limit.HasValue && limit < list.Count)
+ if (limit.HasValue && limit < items.Count)
{
- list = list.GetRange(0, limit.Value);
+ items = items.Take(limit.Value).ToArray();
}
- var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
-
var result = new QueryResult<BaseItemDto>(
0,
totalCount,
- returnList);
+ _dtoService.GetBaseItemDtos(items, dtoOptions, user));
return result;
}
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index d7a8c37c4..7effe61e4 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -50,6 +50,7 @@ public class ItemRefreshController : BaseJellyfinApiController
/// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
/// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
/// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
+ /// <param name="regenerateTrickplay">(Optional) Determines if trickplay images should be replaced. Only applicable if mode is FullRefresh.</param>
/// <response code="204">Item metadata refresh queued.</response>
/// <response code="404">Item to refresh not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
@@ -62,7 +63,8 @@ public class ItemRefreshController : BaseJellyfinApiController
[FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
[FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
[FromQuery] bool replaceAllMetadata = false,
- [FromQuery] bool replaceAllImages = false)
+ [FromQuery] bool replaceAllImages = false,
+ [FromQuery] bool regenerateTrickplay = false)
{
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
@@ -81,7 +83,8 @@ public class ItemRefreshController : BaseJellyfinApiController
|| replaceAllImages
|| replaceAllMetadata,
IsAutomated = false,
- RemoveOldMetadata = replaceAllMetadata
+ RemoveOldMetadata = replaceAllMetadata,
+ RegenerateTrickplay = regenerateTrickplay
};
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 4001a6add..d49e0753e 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -457,7 +457,7 @@ public class ItemUpdateController : BaseJellyfinApiController
return null;
}
- return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true);
+ return Enum.Parse<SeriesStatus>(item.Status, true);
}
private DateTime NormalizeDateTime(DateTime val)
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 828bd5174..ed2f49b86 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -171,8 +171,8 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] locationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating,
@@ -190,42 +190,42 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool? isNews,
[FromQuery] bool? isKids,
[FromQuery] bool? isSports,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool? recursive,
[FromQuery] string? searchTerm,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] imageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? isPlayed,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] artists,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] artistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] contributingArtistIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] albums,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder,
@@ -236,12 +236,12 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] bool? is3D,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
@@ -638,8 +638,8 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] locationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating,
@@ -657,42 +657,42 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool? isNews,
[FromQuery] bool? isKids,
[FromQuery] bool? isSports,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool? recursive,
[FromQuery] string? searchTerm,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] imageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? isPlayed,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] artists,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] artistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] contributingArtistIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] albums,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder,
@@ -703,12 +703,12 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] bool? is3D,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
=> GetItems(
@@ -827,13 +827,13 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true,
[FromQuery] bool excludeActiveSessions = false)
@@ -929,13 +929,13 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true,
[FromQuery] bool excludeActiveSessions = false)
@@ -967,7 +967,7 @@ public class ItemsController : BaseJellyfinApiController
[HttpGet("UserItems/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<UserItemDataDto> GetItemUserData(
+ public ActionResult<UserItemDataDto?> GetItemUserData(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
@@ -1005,7 +1005,7 @@ public class ItemsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
- public ActionResult<UserItemDataDto> GetItemUserDataLegacy(
+ public ActionResult<UserItemDataDto?> GetItemUserDataLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> GetItemUserData(userId, itemId);
@@ -1022,7 +1022,7 @@ public class ItemsController : BaseJellyfinApiController
[HttpPost("UserItems/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<UserItemDataDto> UpdateItemUserData(
+ public ActionResult<UserItemDataDto?> UpdateItemUserData(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,
[FromBody, Required] UpdateUserItemDataDto userDataDto)
@@ -1064,7 +1064,7 @@ public class ItemsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
- public ActionResult<UserItemDataDto> UpdateItemUserDataLegacy(
+ public ActionResult<UserItemDataDto?> UpdateItemUserDataLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromBody, Required] UpdateUserItemDataDto userDataDto)
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index afc93c3a8..7c6160fc4 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -144,8 +144,8 @@ public class LibraryController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[]? sortBy = null,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[]? sortOrder = null)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -218,8 +218,8 @@ public class LibraryController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[]? sortBy = null,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[]? sortOrder = null)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -290,8 +290,8 @@ public class LibraryController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[]? sortBy = null,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[]? sortOrder = null)
{
var themeSongs = GetThemeSongs(
itemId,
@@ -400,7 +400,7 @@ public class LibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids)
{
var isApiKey = User.GetIsApiKey();
var userId = User.GetUserId();
@@ -722,10 +722,10 @@ public class LibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
[FromRoute, Required] Guid itemId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -780,11 +780,9 @@ public class LibraryController : BaseJellyfinApiController
Genres = item.Genres,
Limit = limit,
IncludeItemTypes = includeItemTypes.ToArray(),
- SimilarTo = item,
DtoOptions = dtoOptions,
EnableTotalRecordCount = !isMovie ?? true,
EnableGroupByMetadataKey = isMovie ?? false,
- MinSimilarityScore = 2 // A remnant from album/artist scoring
};
// ExcludeArtistIds
@@ -793,7 +791,7 @@ public class LibraryController : BaseJellyfinApiController
query.ExcludeArtistIds = excludeArtistIds;
}
- List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
+ var itemsResult = _libraryManager.GetItemList(query);
var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
@@ -867,6 +865,16 @@ public class LibraryController : BaseJellyfinApiController
.DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
+ result.MediaSegmentProviders = plugins
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MediaSegmentProvider))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = true
+ })
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
var typeOptions = new List<LibraryTypeOptionsDto>();
foreach (var type in types)
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 93c2393f3..2a885662b 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -77,7 +77,7 @@ public class LibraryStructureController : BaseJellyfinApiController
public async Task<ActionResult> AddVirtualFolder(
[FromQuery] string name,
[FromQuery] CollectionTypeOptions? collectionType,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] paths,
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
[FromQuery] bool refreshLibrary = false)
{
@@ -99,6 +99,7 @@ public class LibraryStructureController : BaseJellyfinApiController
/// <param name="name">The name of the folder.</param>
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <response code="204">Folder removed.</response>
+ /// <response code="404">Folder not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
@@ -106,7 +107,9 @@ public class LibraryStructureController : BaseJellyfinApiController
[FromQuery] string name,
[FromQuery] bool refreshLibrary = false)
{
+ // TODO: refactor! this relies on an FileNotFound exception to return NotFound when attempting to remove a library that does not exist.
await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
+
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 0ae8baa67..a3b4c8700 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -159,10 +159,10 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool? isDisliked,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery] SortOrder? sortOrder,
[FromQuery] bool enableFavoriteSorting = false,
[FromQuery] bool addCurrentProgram = true)
@@ -283,8 +283,8 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] string? seriesTimerId,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSeries,
@@ -371,8 +371,8 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] string? seriesTimerId,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true)
{
@@ -566,7 +566,7 @@ public class LiveTvController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.LiveTvAccess)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] channelIds,
[FromQuery] Guid? userId,
[FromQuery] DateTime? minStartDate,
[FromQuery] bool? hasAired,
@@ -581,17 +581,17 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool? isSports,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] string? seriesTimerId,
[FromQuery] Guid? librarySeriesId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool enableTotalRecordCount = true)
{
userId = RequestHelpers.GetUserId(User, userId);
@@ -730,9 +730,9 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool? isSports,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true)
{
@@ -962,9 +962,9 @@ public class LiveTvController : BaseJellyfinApiController
}
/// <summary>
- /// Get guid info.
+ /// Get guide info.
/// </summary>
- /// <response code="200">Guid info returned.</response>
+ /// <response code="200">Guide info returned.</response>
/// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
[HttpGet("GuideInfo")]
[Authorize(Policy = Policies.LiveTvAccess)]
diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
index 3dc5167a2..2d1d4e2c8 100644
--- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs
+++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
@@ -55,7 +55,7 @@ public class MediaSegmentsController : BaseJellyfinApiController
return NotFound();
}
- var items = await _mediaSegmentManager.GetSegmentsAsync(item.Id, includeSegmentTypes).ConfigureAwait(false);
+ var items = await _mediaSegmentManager.GetSegmentsAsync(item, includeSegmentTypes).ConfigureAwait(false);
return Ok(new QueryResult<MediaSegmentDto>(items.ToArray()));
}
}
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 471bcd096..cbbaaddbf 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -65,7 +65,7 @@ public class MoviesController : BaseJellyfinApiController
public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
[FromQuery] Guid? userId,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] int categoryLimit = 5,
[FromQuery] int itemLimit = 8)
{
@@ -120,7 +120,7 @@ public class MoviesController : BaseJellyfinApiController
DtoOptions = dtoOptions
});
- var mostRecentMovies = recentlyPlayedMovies.GetRange(0, Math.Min(recentlyPlayedMovies.Count, 6));
+ var mostRecentMovies = recentlyPlayedMovies.Take(Math.Min(recentlyPlayedMovies.Count, 6)).ToList();
// Get recently played directors
var recentDirectors = GetDirectors(mostRecentMovies)
.ToList();
@@ -276,7 +276,6 @@ public class MoviesController : BaseJellyfinApiController
Limit = itemLimit,
IncludeItemTypes = itemTypes.ToArray(),
IsMovie = true,
- SimilarTo = item,
EnableGroupByMetadataKey = true,
DtoOptions = dtoOptions
});
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 5411baa3e..e8bc8f265 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -76,18 +76,18 @@ public class MusicGenresController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool? isFavorite,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index 6ca308601..b0c493fbe 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -67,14 +67,14 @@ public class PersonsController : BaseJellyfinApiController
public ActionResult<QueryResult<BaseItemDto>> GetPersons(
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] excludePersonTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
[FromQuery] Guid? appearsInItemId,
[FromQuery] Guid? userId,
[FromQuery] bool? enableImages = true)
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index e6f23b136..ec5fdab38 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
+using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@@ -75,7 +76,7 @@ public class PlaylistsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
[FromQuery, ParameterObsolete] string? name,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids,
[FromQuery, ParameterObsolete] Guid? userId,
[FromQuery, ParameterObsolete] MediaType? mediaType,
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
@@ -369,7 +370,7 @@ public class PlaylistsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> AddItemToPlaylist(
[FromRoute, Required] Guid playlistId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
[FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
@@ -426,7 +427,7 @@ public class PlaylistsController : BaseJellyfinApiController
return Forbid();
}
- await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false);
+ await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex, callingUserId).ConfigureAwait(false);
return NoContent();
}
@@ -445,7 +446,7 @@ public class PlaylistsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> RemoveItemFromPlaylist(
[FromRoute, Required] string playlistId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] entryIds)
{
var callingUserId = User.GetUserId();
@@ -492,11 +493,11 @@ public class PlaylistsController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
{
var callingUserId = userId ?? User.GetUserId();
var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
@@ -514,7 +515,8 @@ public class PlaylistsController : BaseJellyfinApiController
return Forbid();
}
- var items = playlist.GetManageableItems().ToArray();
+ var user = _userManager.GetUserById(callingUserId);
+ var items = playlist.GetManageableItems().Where(i => i.Item2.IsVisible(user)).ToArray();
var count = items.Length;
if (startIndex.HasValue)
{
@@ -529,11 +531,11 @@ public class PlaylistsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var user = _userManager.GetUserById(callingUserId);
+
var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
for (int index = 0; index < dtos.Count; index++)
{
- dtos[index].PlaylistItemId = items[index].Item1.Id;
+ dtos[index].PlaylistItemId = items[index].Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture);
}
var result = new QueryResult<BaseItemDto>(
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 88aa0178f..794c6500c 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -72,7 +72,7 @@ public class PlaystateController : BaseJellyfinApiController
[HttpPost("UserPlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem(
+ public async Task<ActionResult<UserItemDataDto?>> MarkPlayedItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
@@ -121,7 +121,7 @@ public class PlaystateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
- public Task<ActionResult<UserItemDataDto>> MarkPlayedItemLegacy(
+ public Task<ActionResult<UserItemDataDto?>> MarkPlayedItemLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
@@ -138,7 +138,7 @@ public class PlaystateController : BaseJellyfinApiController
[HttpDelete("UserPlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem(
+ public async Task<ActionResult<UserItemDataDto?>> MarkUnplayedItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
@@ -185,7 +185,7 @@ public class PlaystateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
- public Task<ActionResult<UserItemDataDto>> MarkUnplayedItemLegacy(
+ public Task<ActionResult<UserItemDataDto?>> MarkUnplayedItemLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> MarkUnplayedItem(userId, itemId);
@@ -502,7 +502,7 @@ public class PlaystateController : BaseJellyfinApiController
/// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
/// <param name="datePlayed">The date played.</param>
/// <returns>Task.</returns>
- private UserItemDataDto UpdatePlayedStatus(User user, BaseItem item, bool wasPlayed, DateTime? datePlayed)
+ private UserItemDataDto? UpdatePlayedStatus(User user, BaseItem item, bool wasPlayed, DateTime? datePlayed)
{
if (wasPlayed)
{
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 8bae6fb9b..ecf2335ba 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -84,9 +84,9 @@ public class SearchController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] Guid? userId,
[FromQuery, Required] string searchTerm,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
[FromQuery] Guid? parentId,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSeries,
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 2f9e9f091..9886d03de 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -122,7 +122,7 @@ public class SessionController : BaseJellyfinApiController
public async Task<ActionResult> Play(
[FromRoute, Required] string sessionId,
[FromQuery, Required] PlayCommand playCommand,
- [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
+ [FromQuery, Required, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] itemIds,
[FromQuery] long? startPositionTicks,
[FromQuery] string? mediaSourceId,
[FromQuery] int? audioStreamIndex,
@@ -347,8 +347,8 @@ public class SessionController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> PostCapabilities(
[FromQuery] string? id,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] playableMediaTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] playableMediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsPersistentIdentifier = true)
{
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index 41b0858d1..a6bc84311 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -93,7 +93,6 @@ public class StartupController : BaseJellyfinApiController
{
NetworkConfiguration settings = _config.GetNetworkConfiguration();
settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
- settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
_config.SaveConfiguration(NetworkConfigurationStore.StoreKey, settings);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
index 708fc7436..43c5384dc 100644
--- a/Jellyfin.Api/Controllers/StudiosController.cs
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -73,13 +73,13 @@ public class StudiosController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool? isFavorite,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 9da1dce93..e5df873f5 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -395,7 +395,7 @@ public class SubtitleController : BaseJellyfinApiController
var url = string.Format(
CultureInfo.InvariantCulture,
- "stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
+ "stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&ApiKey={2}",
positionTicks.ToString(CultureInfo.InvariantCulture),
endPositionTicks.ToString(CultureInfo.InvariantCulture),
accessToken);
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index ad625cc6e..9b56d0849 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -59,8 +59,8 @@ public class SuggestionsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
[FromQuery] Guid? userId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaType,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] type,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool enableTotalRecordCount = false)
@@ -115,8 +115,8 @@ public class SuggestionsController : BaseJellyfinApiController
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<QueryResult<BaseItemDto>> GetSuggestionsLegacy(
[FromRoute, Required] Guid userId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaType,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] type,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool enableTotalRecordCount = false)
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index d7d0cc454..7ee4396bb 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -130,8 +130,8 @@ public class TrailersController : BaseJellyfinApiController
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] locationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating,
@@ -149,41 +149,41 @@ public class TrailersController : BaseJellyfinApiController
[FromQuery] bool? isNews,
[FromQuery] bool? isKids,
[FromQuery] bool? isSports,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool? recursive,
[FromQuery] string? searchTerm,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] imageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? isPlayed,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] studios,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] artists,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] albums,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] artists,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] artistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] contributingArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] albums,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder,
@@ -194,12 +194,12 @@ public class TrailersController : BaseJellyfinApiController
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] bool? is3D,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 914ccd7f9..df46c2dac 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -77,12 +77,12 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] Guid? seriesId,
[FromQuery] Guid? parentId,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] DateTime? nextUpDateCutoff,
[FromQuery] bool enableTotalRecordCount = true,
@@ -143,11 +143,11 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] Guid? parentId,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData)
{
userId = RequestHelpers.GetUserId(User, userId);
@@ -208,7 +208,7 @@ public class TvShowsController : BaseJellyfinApiController
public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
[FromRoute, Required] Guid seriesId,
[FromQuery] Guid? userId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] int? season,
[FromQuery] Guid? seasonId,
[FromQuery] bool? isMissing,
@@ -218,7 +218,7 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] ItemSortBy? sortBy)
{
@@ -332,13 +332,13 @@ public class TvShowsController : BaseJellyfinApiController
public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
[FromRoute, Required] Guid seriesId,
[FromQuery] Guid? userId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] bool? isSpecialSeason,
[FromQuery] bool? isMissing,
[FromQuery] Guid? adjacentTo,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData)
{
userId = RequestHelpers.GetUserId(User, userId);
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 41c4886d4..a5b5fde62 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -98,7 +98,7 @@ public class UniversalAudioController : BaseJellyfinApiController
[ProducesAudioFile]
public async Task<ActionResult> GetUniversalAudioStream(
[FromRoute, Required] Guid itemId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] container,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] Guid? userId,
@@ -222,7 +222,7 @@ public class UniversalAudioController : BaseJellyfinApiController
TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
Context = EncodingContext.Static,
StreamOptions = new Dictionary<string, string>(),
- EnableAdaptiveBitrateStreaming = true,
+ EnableAdaptiveBitrateStreaming = false,
EnableAudioVbrEncoding = enableAudioVbrEncoding
};
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index e7bf71727..6cc2b4244 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -305,7 +305,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("UserItems/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> DeleteUserItemRating(
+ public ActionResult<UserItemDataDto?> DeleteUserItemRating(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
@@ -338,7 +338,7 @@ public class UserLibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
- public ActionResult<UserItemDataDto> DeleteUserItemRatingLegacy(
+ public ActionResult<UserItemDataDto?> DeleteUserItemRatingLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> DeleteUserItemRating(userId, itemId);
@@ -353,7 +353,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("UserItems/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> UpdateUserItemRating(
+ public ActionResult<UserItemDataDto?> UpdateUserItemRating(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,
[FromQuery] bool? likes)
@@ -388,7 +388,7 @@ public class UserLibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
- public ActionResult<UserItemDataDto> UpdateUserItemRatingLegacy(
+ public ActionResult<UserItemDataDto?> UpdateUserItemRatingLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery] bool? likes)
@@ -523,12 +523,12 @@ public class UserLibraryController : BaseJellyfinApiController
public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
[FromQuery] Guid? userId,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int limit = 20,
[FromQuery] bool groupItems = true)
@@ -608,12 +608,12 @@ public class UserLibraryController : BaseJellyfinApiController
public ActionResult<IEnumerable<BaseItemDto>> GetLatestMediaLegacy(
[FromRoute, Required] Guid userId,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int limit = 20,
[FromQuery] bool groupItems = true)
@@ -634,10 +634,10 @@ public class UserLibraryController : BaseJellyfinApiController
{
if (item is Person)
{
- var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
- var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
+ var hasMetadata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
+ var performFullRefresh = !hasMetadata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
- if (!hasMetdata)
+ if (!hasMetadata)
{
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
@@ -662,12 +662,15 @@ public class UserLibraryController : BaseJellyfinApiController
// Get the user data for this item
var data = _userDataRepository.GetUserData(user, item);
- // Set favorite status
- data.IsFavorite = isFavorite;
+ if (data is not null)
+ {
+ // Set favorite status
+ data.IsFavorite = isFavorite;
- _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+ _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+ }
- return _userDataRepository.GetUserDataDto(item, user);
+ return _userDataRepository.GetUserDataDto(item, user)!;
}
/// <summary>
@@ -676,14 +679,17 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="user">The user.</param>
/// <param name="item">The item.</param>
/// <param name="likes">if set to <c>true</c> [likes].</param>
- private UserItemDataDto UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)
+ private UserItemDataDto? UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)
{
// Get the user data for this item
var data = _userDataRepository.GetUserData(user, item);
- data.Likes = likes;
+ if (data is not null)
+ {
+ data.Likes = likes;
- _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+ _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+ }
return _userDataRepository.GetUserDataDto(item, user);
}
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index e24f78a88..64b2dffb3 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -66,7 +66,7 @@ public class UserViewsController : BaseJellyfinApiController
public QueryResult<BaseItemDto> GetUserViews(
[FromQuery] Guid? userId,
[FromQuery] bool? includeExternalContent,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] CollectionType?[] presetViews,
[FromQuery] bool includeHidden = false)
{
userId = RequestHelpers.GetUserId(User, userId);
@@ -110,7 +110,7 @@ public class UserViewsController : BaseJellyfinApiController
public QueryResult<BaseItemDto> GetUserViewsLegacy(
[FromRoute, Required] Guid userId,
[FromQuery] bool? includeExternalContent,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] CollectionType?[] presetViews,
[FromQuery] bool includeHidden = false)
=> GetUserViews(userId, includeExternalContent, presetViews, includeHidden);
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 8348fd937..6f18c1603 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -184,7 +184,7 @@ public class VideosController : BaseJellyfinApiController
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids)
{
var userId = User.GetUserId();
var items = ids
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index e4aa0ea42..2b32ae728 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Extensions;
@@ -71,16 +72,16 @@ public class YearsController : BaseJellyfinApiController
public ActionResult<QueryResult<BaseItemDto>> GetYears(
[FromQuery] int? startIndex,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
[FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] bool recursive = true,
[FromQuery] bool? enableImages = true)
@@ -105,18 +106,18 @@ public class YearsController : BaseJellyfinApiController
bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
- IList<BaseItem> items;
+ IReadOnlyList<BaseItem> items;
if (parentItem.IsFolder)
{
var folder = (Folder)parentItem;
if (userId.IsNullOrEmpty())
{
- items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList();
+ items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToArray();
}
else
{
- items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList();
+ items = recursive ? folder.GetRecursiveChildren(user, query) : folder.GetChildren(user, true).Where(Filter).ToArray();
}
}
else
diff --git a/Jellyfin.Api/Formatters/CssOutputFormatter.cs b/Jellyfin.Api/Formatters/CssOutputFormatter.cs
index 495f771e1..9ad1c863e 100644
--- a/Jellyfin.Api/Formatters/CssOutputFormatter.cs
+++ b/Jellyfin.Api/Formatters/CssOutputFormatter.cs
@@ -1,6 +1,4 @@
-using System.Text;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
+using System.Net.Mime;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace Jellyfin.Api.Formatters;
@@ -8,28 +6,14 @@ namespace Jellyfin.Api.Formatters;
/// <summary>
/// Css output formatter.
/// </summary>
-public class CssOutputFormatter : TextOutputFormatter
+public sealed class CssOutputFormatter : StringOutputFormatter
{
/// <summary>
/// Initializes a new instance of the <see cref="CssOutputFormatter"/> class.
/// </summary>
public CssOutputFormatter()
{
- SupportedMediaTypes.Add("text/css");
-
- SupportedEncodings.Add(Encoding.UTF8);
- SupportedEncodings.Add(Encoding.Unicode);
- }
-
- /// <summary>
- /// Write context object to stream.
- /// </summary>
- /// <param name="context">Writer context.</param>
- /// <param name="selectedEncoding">Unused. Writer encoding.</param>
- /// <returns>Write stream task.</returns>
- public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
- {
- var stringResponse = context.Object?.ToString();
- return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
+ SupportedMediaTypes.Clear();
+ SupportedMediaTypes.Add(MediaTypeNames.Text.Css);
}
}
diff --git a/Jellyfin.Api/Formatters/XmlOutputFormatter.cs b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs
index 1c9feedcb..8dbb91d0a 100644
--- a/Jellyfin.Api/Formatters/XmlOutputFormatter.cs
+++ b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs
@@ -1,7 +1,4 @@
using System.Net.Mime;
-using System.Text;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace Jellyfin.Api.Formatters;
@@ -9,7 +6,7 @@ namespace Jellyfin.Api.Formatters;
/// <summary>
/// Xml output formatter.
/// </summary>
-public class XmlOutputFormatter : TextOutputFormatter
+public sealed class XmlOutputFormatter : StringOutputFormatter
{
/// <summary>
/// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class.
@@ -18,15 +15,5 @@ public class XmlOutputFormatter : TextOutputFormatter
{
SupportedMediaTypes.Clear();
SupportedMediaTypes.Add(MediaTypeNames.Text.Xml);
-
- SupportedEncodings.Add(Encoding.UTF8);
- SupportedEncodings.Add(Encoding.Unicode);
- }
-
- /// <inheritdoc />
- public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
- {
- var stringResponse = context.Object?.ToString();
- return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
}
}
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index 0e620e72a..648716030 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -267,7 +267,7 @@ public class DynamicHlsHelper
if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIP()))
{
- var requestedVideoBitrate = state.VideoRequest is null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
+ var requestedVideoBitrate = state.VideoRequest?.VideoBitRate ?? 0;
// By default, vary by just 200k
var variation = GetBitrateVariation(totalBitrate);
@@ -526,9 +526,7 @@ public class DynamicHlsHelper
return false;
}
- // Having problems in android
- return false;
- // return state.VideoRequest.VideoBitRate.HasValue;
+ return state.VideoRequest?.VideoBitRate.HasValue ?? false;
}
private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrincipal user)
@@ -550,7 +548,7 @@ public class DynamicHlsHelper
var url = string.Format(
CultureInfo.InvariantCulture,
- "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
+ "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&ApiKey={3}",
state.Request.MediaSourceId,
stream.Index.ToString(CultureInfo.InvariantCulture),
30.ToString(CultureInfo.InvariantCulture),
@@ -587,7 +585,7 @@ public class DynamicHlsHelper
var url = string.Format(
CultureInfo.InvariantCulture,
- "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&api_key={2}",
+ "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&ApiKey={2}",
width.ToString(CultureInfo.InvariantCulture),
state.Request.MediaSourceId,
user.GetToken());
@@ -616,7 +614,7 @@ public class DynamicHlsHelper
&& state.VideoStream is not null
&& state.VideoStream.Level.HasValue)
{
- levelString = state.VideoStream.Level.Value.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
+ levelString = state.VideoStream.Level.Value.ToString(CultureInfo.InvariantCulture);
}
else
{
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 3a5db2f3f..2601fa3be 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -132,7 +132,7 @@ public static class StreamingHelpers
mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
? mediaSources[0]
- : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal));
+ : mediaSources.FirstOrDefault(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal));
if (mediaSource is null && Guid.Parse(streamingRequest.MediaSourceId).Equals(streamingRequest.Id))
{
@@ -210,7 +210,7 @@ public static class StreamingHelpers
&& state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
{
// Don't downscale the resolution if the width/height/MaxWidth/MaxHeight is not requested,
- // and the requested video bitrate is higher than source video bitrate.
+ // and the requested video bitrate is greater than source video bitrate.
if (state.VideoStream.Width.HasValue || state.VideoStream.Height.HasValue)
{
state.VideoRequest.MaxWidth = state.VideoStream?.Width;
@@ -235,6 +235,11 @@ public static class StreamingHelpers
state.VideoRequest.MaxHeight = resolution.MaxHeight;
}
}
+
+ if (state.AudioStream is not null && !EncodingHelper.IsCopyCodec(state.OutputAudioCodec) && string.Equals(state.AudioStream.Codec, state.OutputAudioCodec, StringComparison.OrdinalIgnoreCase) && state.OutputAudioBitrate.HasValue)
+ {
+ state.OutputAudioCodec = state.SupportedAudioCodecs.Where(c => !EncodingHelper.LosslessAudioCodecs.Contains(c)).FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec);
+ }
}
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 5f86a6b6b..25feaa2d7 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedCollectionModelBinder.cs
index 3e3604b2a..25b84cbcc 100644
--- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/CommaDelimitedCollectionModelBinder.cs
@@ -8,18 +8,18 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.ModelBinders;
/// <summary>
-/// Comma delimited array model binder.
+/// Comma delimited collection model binder.
/// Returns an empty array of specified type if there is no query parameter.
/// </summary>
-public class CommaDelimitedArrayModelBinder : IModelBinder
+public class CommaDelimitedCollectionModelBinder : IModelBinder
{
- private readonly ILogger<CommaDelimitedArrayModelBinder> _logger;
+ private readonly ILogger<CommaDelimitedCollectionModelBinder> _logger;
/// <summary>
- /// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinder"/> class.
+ /// Initializes a new instance of the <see cref="CommaDelimitedCollectionModelBinder"/> class.
/// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger{CommaDelimitedArrayModelBinder}"/> interface.</param>
- public CommaDelimitedArrayModelBinder(ILogger<CommaDelimitedArrayModelBinder> logger)
+ /// <param name="logger">Instance of the <see cref="ILogger{CommaDelimitedCollectionModelBinder}"/> interface.</param>
+ public CommaDelimitedCollectionModelBinder(ILogger<CommaDelimitedCollectionModelBinder> logger)
{
_logger = logger;
}
diff --git a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/PipeDelimitedCollectionModelBinder.cs
index ae9f0a8cd..7d0fb2e19 100644
--- a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/PipeDelimitedCollectionModelBinder.cs
@@ -8,18 +8,18 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.ModelBinders;
/// <summary>
-/// Comma delimited array model binder.
-/// Returns an empty array of specified type if there is no query parameter.
+/// Comma delimited collection model binder.
+/// Returns an empty collection of specified type if there is no query parameter.
/// </summary>
-public class PipeDelimitedArrayModelBinder : IModelBinder
+public class PipeDelimitedCollectionModelBinder : IModelBinder
{
- private readonly ILogger<PipeDelimitedArrayModelBinder> _logger;
+ private readonly ILogger<PipeDelimitedCollectionModelBinder> _logger;
/// <summary>
- /// Initializes a new instance of the <see cref="PipeDelimitedArrayModelBinder"/> class.
+ /// Initializes a new instance of the <see cref="PipeDelimitedCollectionModelBinder"/> class.
/// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger{PipeDelimitedArrayModelBinder}"/> interface.</param>
- public PipeDelimitedArrayModelBinder(ILogger<PipeDelimitedArrayModelBinder> logger)
+ /// <param name="logger">Instance of the <see cref="ILogger{PipeDelimitedCollectionModelBinder}"/> interface.</param>
+ public PipeDelimitedCollectionModelBinder(ILogger<PipeDelimitedCollectionModelBinder> logger)
{
_logger = logger;
}
diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
index d07349bdf..c49243668 100644
--- a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
+++ b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
@@ -29,6 +29,11 @@ public class LibraryOptionsResultDto
public IReadOnlyList<LibraryOptionInfoDto> LyricFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
/// <summary>
+ /// Gets or sets the list of MediaSegment Providers.
+ /// </summary>
+ public IReadOnlyList<LibraryOptionInfoDto> MediaSegmentProviders { get; set; } = Array.Empty<LibraryOptionInfoDto>();
+
+ /// <summary>
/// Gets or sets the type options.
/// </summary>
public IReadOnlyList<LibraryTypeOptionsDto> TypeOptions { get; set; } = Array.Empty<LibraryTypeOptionsDto>();
diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
index 190d90681..dece66426 100644
--- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
+++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
@@ -17,7 +17,7 @@ public class GetProgramsDto
/// <summary>
/// Gets or sets the channels to return guide information for.
/// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<Guid>? ChannelIds { get; set; }
/// <summary>
@@ -93,25 +93,25 @@ public class GetProgramsDto
/// <summary>
/// Gets or sets specify one or more sort orders, comma delimited. Options: Name, StartDate.
/// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<ItemSortBy>? SortBy { get; set; }
/// <summary>
/// Gets or sets sort order.
/// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<SortOrder>? SortOrder { get; set; }
/// <summary>
/// Gets or sets the genres to return guide information for.
/// </summary>
- [JsonConverter(typeof(JsonPipeDelimitedArrayConverterFactory))]
+ [JsonConverter(typeof(JsonPipeDelimitedCollectionConverterFactory))]
public IReadOnlyList<string>? Genres { get; set; }
/// <summary>
/// Gets or sets the genre ids to return guide information for.
/// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<Guid>? GenreIds { get; set; }
/// <summary>
@@ -133,7 +133,7 @@ public class GetProgramsDto
/// <summary>
/// Gets or sets the image types to include in the output.
/// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<ImageType>? EnableImageTypes { get; set; }
/// <summary>
@@ -154,6 +154,6 @@ public class GetProgramsDto
/// <summary>
/// Gets or sets specify additional fields of information to return in the output.
/// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<ItemFields>? Fields { get; set; }
}
diff --git a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
index 978e99b35..758c89938 100644
--- a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
+++ b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
@@ -61,7 +61,7 @@ public class OpenLiveStreamDto
public bool? EnableDirectPlay { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether to enale direct stream.
+ /// Gets or sets a value indicating whether to enable direct stream.
/// </summary>
public bool? EnableDirectStream { get; set; }
diff --git a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs
index 82f603ca1..73ab76817 100644
--- a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs
+++ b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs
@@ -4,7 +4,7 @@ using MediaBrowser.Model.Dlna;
namespace Jellyfin.Api.Models.MediaInfoDtos;
/// <summary>
-/// Plabyback info dto.
+/// Playback info dto.
/// </summary>
public class PlaybackInfoDto
{
diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
index 61a3f2ed6..891d758c4 100644
--- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
+++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
@@ -20,7 +20,7 @@ public class CreatePlaylistDto
/// <summary>
/// Gets or sets item ids to add to the playlist.
/// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<Guid> Ids { get; set; } = [];
/// <summary>
diff --git a/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs
index 80e20995c..339a0d5d2 100644
--- a/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs
+++ b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs
@@ -19,7 +19,7 @@ public class UpdatePlaylistDto
/// <summary>
/// Gets or sets item ids of the playlist.
/// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<Guid>? Ids { get; set; }
/// <summary>
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs
index 1ae2cad4b..9c29e372c 100644
--- a/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs
+++ b/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs
@@ -1,3 +1,4 @@
+using System;
using System.ComponentModel.DataAnnotations;
namespace Jellyfin.Api.Models.StartupDtos;
@@ -17,5 +18,6 @@ public class StartupRemoteAccessDto
/// Gets or sets a value indicating whether enable automatic port mapping.
/// </summary>
[Required]
+ [Obsolete("No longer supported")]
public bool EnableAutomaticPortMapping { get; set; }
}
diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
index 99516e938..97f827fde 100644
--- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
@@ -70,7 +70,9 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
/// <param name="message">The message.</param>
protected override void Start(WebSocketMessageInfo message)
{
- if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
+ if (!message.Connection.AuthorizationInfo.IsApiKey
+ && (message.Connection.AuthorizationInfo.User is null
+ || !message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)))
{
throw new AuthenticationException("Only admin users can retrieve the activity log.");
}
diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index a6cfe4d56..6cbab6571 100644
--- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
@@ -79,7 +79,9 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
/// <param name="message">The message.</param>
protected override void Start(WebSocketMessageInfo message)
{
- if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
+ if (!message.Connection.AuthorizationInfo.IsApiKey
+ && (message.Connection.AuthorizationInfo.User is null
+ || !message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)))
{
throw new AuthenticationException("Only admin users can subscribe to session information.");
}
diff --git a/Jellyfin.Data/Entities/AncestorId.cs b/Jellyfin.Data/Entities/AncestorId.cs
new file mode 100644
index 000000000..954416dfe
--- /dev/null
+++ b/Jellyfin.Data/Entities/AncestorId.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Represents the relational information for an <see cref="BaseItemEntity"/>.
+/// </summary>
+public class AncestorId
+{
+ /// <summary>
+ /// Gets or Sets the AncestorId.
+ /// </summary>
+ public required Guid ParentItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the related BaseItem.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the ParentItem.
+ /// </summary>
+ public required BaseItemEntity ParentItem { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the Child item.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/AttachmentStreamInfo.cs b/Jellyfin.Data/Entities/AttachmentStreamInfo.cs
new file mode 100644
index 000000000..19265a011
--- /dev/null
+++ b/Jellyfin.Data/Entities/AttachmentStreamInfo.cs
@@ -0,0 +1,49 @@
+using System;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Provides information about an Attachment to an <see cref="BaseItemEntity"/>.
+/// </summary>
+public class AttachmentStreamInfo
+{
+ /// <summary>
+ /// Gets or Sets the <see cref="BaseItemEntity"/> reference.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the <see cref="BaseItemEntity"/> reference.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the index within the source file.
+ /// </summary>
+ public required int Index { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the codec of the attachment.
+ /// </summary>
+ public required string Codec { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the codec tag of the attachment.
+ /// </summary>
+ public string? CodecTag { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the comment of the attachment.
+ /// </summary>
+ public string? Comment { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the filename of the attachment.
+ /// </summary>
+ public string? Filename { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the attachments mimetype.
+ /// </summary>
+ public string? MimeType { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/BaseItemEntity.cs b/Jellyfin.Data/Entities/BaseItemEntity.cs
new file mode 100644
index 000000000..e3e0e0861
--- /dev/null
+++ b/Jellyfin.Data/Entities/BaseItemEntity.cs
@@ -0,0 +1,186 @@
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable CA2227 // Collection properties should be read only
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities;
+
+public class BaseItemEntity
+{
+ public required Guid Id { get; set; }
+
+ public required string Type { get; set; }
+
+ public string? Data { get; set; }
+
+ public string? Path { get; set; }
+
+ public DateTime? StartDate { get; set; }
+
+ public DateTime? EndDate { get; set; }
+
+ public Guid? ChannelId { get; set; }
+
+ public bool IsMovie { get; set; }
+
+ public float? CommunityRating { get; set; }
+
+ public string? CustomRating { get; set; }
+
+ public int? IndexNumber { get; set; }
+
+ public bool IsLocked { get; set; }
+
+ public string? Name { get; set; }
+
+ public string? OfficialRating { get; set; }
+
+ public string? MediaType { get; set; }
+
+ public string? Overview { get; set; }
+
+ public int? ParentIndexNumber { get; set; }
+
+ public DateTime? PremiereDate { get; set; }
+
+ public int? ProductionYear { get; set; }
+
+ public string? Genres { get; set; }
+
+ public string? SortName { get; set; }
+
+ public string? ForcedSortName { get; set; }
+
+ public long? RunTimeTicks { get; set; }
+
+ public DateTime? DateCreated { get; set; }
+
+ public DateTime? DateModified { get; set; }
+
+ public bool IsSeries { get; set; }
+
+ public string? EpisodeTitle { get; set; }
+
+ public bool IsRepeat { get; set; }
+
+ public string? PreferredMetadataLanguage { get; set; }
+
+ public string? PreferredMetadataCountryCode { get; set; }
+
+ public DateTime? DateLastRefreshed { get; set; }
+
+ public DateTime? DateLastSaved { get; set; }
+
+ public bool IsInMixedFolder { get; set; }
+
+ public string? Studios { get; set; }
+
+ public string? ExternalServiceId { get; set; }
+
+ public string? Tags { get; set; }
+
+ public bool IsFolder { get; set; }
+
+ public int? InheritedParentalRatingValue { get; set; }
+
+ public string? UnratedType { get; set; }
+
+ public float? CriticRating { get; set; }
+
+ public string? CleanName { get; set; }
+
+ public string? PresentationUniqueKey { get; set; }
+
+ public string? OriginalTitle { get; set; }
+
+ public string? PrimaryVersionId { get; set; }
+
+ public DateTime? DateLastMediaAdded { get; set; }
+
+ public string? Album { get; set; }
+
+ public float? LUFS { get; set; }
+
+ public float? NormalizationGain { get; set; }
+
+ public bool IsVirtualItem { get; set; }
+
+ public string? SeriesName { get; set; }
+
+ public string? SeasonName { get; set; }
+
+ public string? ExternalSeriesId { get; set; }
+
+ public string? Tagline { get; set; }
+
+ public string? ProductionLocations { get; set; }
+
+ public string? ExtraIds { get; set; }
+
+ public int? TotalBitrate { get; set; }
+
+ public BaseItemExtraType? ExtraType { get; set; }
+
+ public string? Artists { get; set; }
+
+ public string? AlbumArtists { get; set; }
+
+ public string? ExternalId { get; set; }
+
+ public string? SeriesPresentationUniqueKey { get; set; }
+
+ public string? ShowId { get; set; }
+
+ public string? OwnerId { get; set; }
+
+ public int? Width { get; set; }
+
+ public int? Height { get; set; }
+
+ public long? Size { get; set; }
+
+ public ProgramAudioEntity? Audio { get; set; }
+
+ public Guid? ParentId { get; set; }
+
+ public Guid? TopParentId { get; set; }
+
+ public Guid? SeasonId { get; set; }
+
+ public Guid? SeriesId { get; set; }
+
+ public ICollection<PeopleBaseItemMap>? Peoples { get; set; }
+
+ public ICollection<UserData>? UserData { get; set; }
+
+ public ICollection<ItemValueMap>? ItemValues { get; set; }
+
+ public ICollection<MediaStreamInfo>? MediaStreams { get; set; }
+
+ public ICollection<Chapter>? Chapters { get; set; }
+
+ public ICollection<BaseItemProvider>? Provider { get; set; }
+
+ public ICollection<AncestorId>? ParentAncestors { get; set; }
+
+ public ICollection<AncestorId>? Children { get; set; }
+
+ public ICollection<BaseItemMetadataField>? LockedFields { get; set; }
+
+ public ICollection<BaseItemTrailerType>? TrailerTypes { get; set; }
+
+ public ICollection<BaseItemImageInfo>? Images { get; set; }
+
+ // those are references to __LOCAL__ ids not DB ids ... TODO: Bring the whole folder structure into the DB
+ // public ICollection<BaseItemEntity>? SeriesEpisodes { get; set; }
+ // public BaseItemEntity? Series { get; set; }
+ // public BaseItemEntity? Season { get; set; }
+ // public BaseItemEntity? Parent { get; set; }
+ // public ICollection<BaseItemEntity>? DirectChildren { get; set; }
+ // public BaseItemEntity? TopParent { get; set; }
+ // public ICollection<BaseItemEntity>? AllChildren { get; set; }
+ // public ICollection<BaseItemEntity>? SeasonEpisodes { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/BaseItemExtraType.cs b/Jellyfin.Data/Entities/BaseItemExtraType.cs
new file mode 100644
index 000000000..54aef50e4
--- /dev/null
+++ b/Jellyfin.Data/Entities/BaseItemExtraType.cs
@@ -0,0 +1,18 @@
+#pragma warning disable CS1591
+namespace Jellyfin.Data.Entities;
+
+public enum BaseItemExtraType
+{
+ Unknown = 0,
+ Clip = 1,
+ Trailer = 2,
+ BehindTheScenes = 3,
+ DeletedScene = 4,
+ Interview = 5,
+ Scene = 6,
+ Sample = 7,
+ ThemeSong = 8,
+ ThemeVideo = 9,
+ Featurette = 10,
+ Short = 11
+}
diff --git a/Jellyfin.Data/Entities/BaseItemImageInfo.cs b/Jellyfin.Data/Entities/BaseItemImageInfo.cs
new file mode 100644
index 000000000..37723df11
--- /dev/null
+++ b/Jellyfin.Data/Entities/BaseItemImageInfo.cs
@@ -0,0 +1,59 @@
+#pragma warning disable CA2227
+
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Enum TrailerTypes.
+/// </summary>
+public class BaseItemImageInfo
+{
+ /// <summary>
+ /// Gets or Sets.
+ /// </summary>
+ public required Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the path to the original image.
+ /// </summary>
+ public required string Path { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the time the image was last modified.
+ /// </summary>
+ public DateTime DateModified { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the imagetype.
+ /// </summary>
+ public ImageInfoImageType ImageType { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the width of the original image.
+ /// </summary>
+ public int Width { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the height of the original image.
+ /// </summary>
+ public int Height { get; set; }
+
+#pragma warning disable CA1819 // Properties should not return arrays
+ /// <summary>
+ /// Gets or Sets the blurhash.
+ /// </summary>
+ public byte[]? Blurhash { get; set; }
+#pragma warning restore CA1819
+
+ /// <summary>
+ /// Gets or Sets the reference id to the BaseItem.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the referenced Item.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/BaseItemMetadataField.cs b/Jellyfin.Data/Entities/BaseItemMetadataField.cs
new file mode 100644
index 000000000..27bbfc473
--- /dev/null
+++ b/Jellyfin.Data/Entities/BaseItemMetadataField.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Enum MetadataFields.
+/// </summary>
+public class BaseItemMetadataField
+{
+ /// <summary>
+ /// Gets or Sets Numerical ID of this enumerable.
+ /// </summary>
+ public required int Id { get; set; }
+
+ /// <summary>
+ /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/BaseItemProvider.cs b/Jellyfin.Data/Entities/BaseItemProvider.cs
new file mode 100644
index 000000000..9a1565728
--- /dev/null
+++ b/Jellyfin.Data/Entities/BaseItemProvider.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Represents a Key-Value relation of an BaseItem's provider.
+/// </summary>
+public class BaseItemProvider
+{
+ /// <summary>
+ /// Gets or Sets the reference ItemId.
+ /// </summary>
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the reference BaseItem.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the ProvidersId.
+ /// </summary>
+ public required string ProviderId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the Providers Value.
+ /// </summary>
+ public required string ProviderValue { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/BaseItemTrailerType.cs b/Jellyfin.Data/Entities/BaseItemTrailerType.cs
new file mode 100644
index 000000000..2bb648138
--- /dev/null
+++ b/Jellyfin.Data/Entities/BaseItemTrailerType.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Enum TrailerTypes.
+/// </summary>
+public class BaseItemTrailerType
+{
+ /// <summary>
+ /// Gets or Sets Numerical ID of this enumerable.
+ /// </summary>
+ public required int Id { get; set; }
+
+ /// <summary>
+ /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/Chapter.cs b/Jellyfin.Data/Entities/Chapter.cs
new file mode 100644
index 000000000..579442cdb
--- /dev/null
+++ b/Jellyfin.Data/Entities/Chapter.cs
@@ -0,0 +1,44 @@
+using System;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// The Chapter entity.
+/// </summary>
+public class Chapter
+{
+ /// <summary>
+ /// Gets or Sets the <see cref="BaseItemEntity"/> reference id.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the <see cref="BaseItemEntity"/> reference.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the chapters index in Item.
+ /// </summary>
+ public required int ChapterIndex { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the position within the source file.
+ /// </summary>
+ public required long StartPositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the common name.
+ /// </summary>
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the image path.
+ /// </summary>
+ public string? ImagePath { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the time the image was last modified.
+ /// </summary>
+ public DateTime? ImageDateModified { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/ImageInfoImageType.cs b/Jellyfin.Data/Entities/ImageInfoImageType.cs
new file mode 100644
index 000000000..f78178dd2
--- /dev/null
+++ b/Jellyfin.Data/Entities/ImageInfoImageType.cs
@@ -0,0 +1,76 @@
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Enum ImageType.
+/// </summary>
+public enum ImageInfoImageType
+{
+ /// <summary>
+ /// The primary.
+ /// </summary>
+ Primary = 0,
+
+ /// <summary>
+ /// The art.
+ /// </summary>
+ Art = 1,
+
+ /// <summary>
+ /// The backdrop.
+ /// </summary>
+ Backdrop = 2,
+
+ /// <summary>
+ /// The banner.
+ /// </summary>
+ Banner = 3,
+
+ /// <summary>
+ /// The logo.
+ /// </summary>
+ Logo = 4,
+
+ /// <summary>
+ /// The thumb.
+ /// </summary>
+ Thumb = 5,
+
+ /// <summary>
+ /// The disc.
+ /// </summary>
+ Disc = 6,
+
+ /// <summary>
+ /// The box.
+ /// </summary>
+ Box = 7,
+
+ /// <summary>
+ /// The screenshot.
+ /// </summary>
+ /// <remarks>
+ /// This enum value is obsolete.
+ /// XmlSerializer does not serialize/deserialize objects that are marked as [Obsolete].
+ /// </remarks>
+ Screenshot = 8,
+
+ /// <summary>
+ /// The menu.
+ /// </summary>
+ Menu = 9,
+
+ /// <summary>
+ /// The chapter image.
+ /// </summary>
+ Chapter = 10,
+
+ /// <summary>
+ /// The box rear.
+ /// </summary>
+ BoxRear = 11,
+
+ /// <summary>
+ /// The user profile image.
+ /// </summary>
+ Profile = 12
+}
diff --git a/Jellyfin.Data/Entities/ItemValue.cs b/Jellyfin.Data/Entities/ItemValue.cs
new file mode 100644
index 000000000..11d8e383e
--- /dev/null
+++ b/Jellyfin.Data/Entities/ItemValue.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Represents an ItemValue for a BaseItem.
+/// </summary>
+public class ItemValue
+{
+ /// <summary>
+ /// Gets or Sets the ItemValueId.
+ /// </summary>
+ public required Guid ItemValueId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the Type.
+ /// </summary>
+ public required ItemValueType Type { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the Value.
+ /// </summary>
+ public required string Value { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the sanitized Value.
+ /// </summary>
+ public required string CleanValue { get; set; }
+
+ /// <summary>
+ /// Gets or Sets all associated BaseItems.
+ /// </summary>
+#pragma warning disable CA2227 // Collection properties should be read only
+ public ICollection<ItemValueMap>? BaseItemsMap { get; set; }
+#pragma warning restore CA2227 // Collection properties should be read only
+}
diff --git a/Jellyfin.Data/Entities/ItemValueMap.cs b/Jellyfin.Data/Entities/ItemValueMap.cs
new file mode 100644
index 000000000..94db6a011
--- /dev/null
+++ b/Jellyfin.Data/Entities/ItemValueMap.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Mapping table for the ItemValue BaseItem relation.
+/// </summary>
+public class ItemValueMap
+{
+ /// <summary>
+ /// Gets or Sets the ItemId.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the ItemValueId.
+ /// </summary>
+ public required Guid ItemValueId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the referenced <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the referenced <see cref="ItemValue"/>.
+ /// </summary>
+ public required ItemValue ItemValue { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/ItemValueType.cs b/Jellyfin.Data/Entities/ItemValueType.cs
new file mode 100644
index 000000000..3bae3becc
--- /dev/null
+++ b/Jellyfin.Data/Entities/ItemValueType.cs
@@ -0,0 +1,38 @@
+#pragma warning disable CA1027 // Mark enums with FlagsAttribute
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Provides the Value types for an <see cref="ItemValue"/>.
+/// </summary>
+public enum ItemValueType
+{
+ /// <summary>
+ /// Artists.
+ /// </summary>
+ Artist = 0,
+
+ /// <summary>
+ /// Album.
+ /// </summary>
+ AlbumArtist = 1,
+
+ /// <summary>
+ /// Genre.
+ /// </summary>
+ Genre = 2,
+
+ /// <summary>
+ /// Studios.
+ /// </summary>
+ Studios = 3,
+
+ /// <summary>
+ /// Tags.
+ /// </summary>
+ Tags = 4,
+
+ /// <summary>
+ /// InheritedTags.
+ /// </summary>
+ InheritedTags = 6,
+}
diff --git a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs
index 0cb4716db..15b356a74 100644
--- a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs
+++ b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs
@@ -43,7 +43,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// Gets or sets the next item in the collection.
/// </summary>
/// <remarks>
- /// TODO check if this properly updated Dependant and has the proper principal relationship.
+ /// TODO check if this properly updated Dependent and has the proper principal relationship.
/// </remarks>
public virtual CollectionItem? Next { get; set; }
@@ -51,7 +51,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// Gets or sets the previous item in the collection.
/// </summary>
/// <remarks>
- /// TODO check if this properly updated Dependant and has the proper principal relationship.
+ /// TODO check if this properly updated Dependent and has the proper principal relationship.
/// </remarks>
public virtual CollectionItem? Previous { get; set; }
diff --git a/Jellyfin.Data/Entities/Libraries/Series.cs b/Jellyfin.Data/Entities/Libraries/Series.cs
index 0354433e0..ab484c96d 100644
--- a/Jellyfin.Data/Entities/Libraries/Series.cs
+++ b/Jellyfin.Data/Entities/Libraries/Series.cs
@@ -4,7 +4,7 @@ using System.Collections.Generic;
namespace Jellyfin.Data.Entities.Libraries
{
/// <summary>
- /// An entity representing a a series.
+ /// An entity representing a series.
/// </summary>
public class Series : LibraryItem
{
diff --git a/Jellyfin.Data/Entities/MediaStreamInfo.cs b/Jellyfin.Data/Entities/MediaStreamInfo.cs
new file mode 100644
index 000000000..77816565a
--- /dev/null
+++ b/Jellyfin.Data/Entities/MediaStreamInfo.cs
@@ -0,0 +1,103 @@
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Jellyfin.Data.Entities;
+
+public class MediaStreamInfo
+{
+ public required Guid ItemId { get; set; }
+
+ public required BaseItemEntity Item { get; set; }
+
+ public int StreamIndex { get; set; }
+
+ public required MediaStreamTypeEntity StreamType { get; set; }
+
+ public string? Codec { get; set; }
+
+ public string? Language { get; set; }
+
+ public string? ChannelLayout { get; set; }
+
+ public string? Profile { get; set; }
+
+ public string? AspectRatio { get; set; }
+
+ public string? Path { get; set; }
+
+ public bool? IsInterlaced { get; set; }
+
+ public int? BitRate { get; set; }
+
+ public int? Channels { get; set; }
+
+ public int? SampleRate { get; set; }
+
+ public bool IsDefault { get; set; }
+
+ public bool IsForced { get; set; }
+
+ public bool IsExternal { get; set; }
+
+ public int? Height { get; set; }
+
+ public int? Width { get; set; }
+
+ public float? AverageFrameRate { get; set; }
+
+ public float? RealFrameRate { get; set; }
+
+ public float? Level { get; set; }
+
+ public string? PixelFormat { get; set; }
+
+ public int? BitDepth { get; set; }
+
+ public bool? IsAnamorphic { get; set; }
+
+ public int? RefFrames { get; set; }
+
+ public string? CodecTag { get; set; }
+
+ public string? Comment { get; set; }
+
+ public string? NalLengthSize { get; set; }
+
+ public bool? IsAvc { get; set; }
+
+ public string? Title { get; set; }
+
+ public string? TimeBase { get; set; }
+
+ public string? CodecTimeBase { get; set; }
+
+ public string? ColorPrimaries { get; set; }
+
+ public string? ColorSpace { get; set; }
+
+ public string? ColorTransfer { get; set; }
+
+ public int? DvVersionMajor { get; set; }
+
+ public int? DvVersionMinor { get; set; }
+
+ public int? DvProfile { get; set; }
+
+ public int? DvLevel { get; set; }
+
+ public int? RpuPresentFlag { get; set; }
+
+ public int? ElPresentFlag { get; set; }
+
+ public int? BlPresentFlag { get; set; }
+
+ public int? DvBlSignalCompatibilityId { get; set; }
+
+ public bool? IsHearingImpaired { get; set; }
+
+ public int? Rotation { get; set; }
+
+ public string? KeyFrames { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/MediaStreamTypeEntity.cs b/Jellyfin.Data/Entities/MediaStreamTypeEntity.cs
new file mode 100644
index 000000000..f57672a2c
--- /dev/null
+++ b/Jellyfin.Data/Entities/MediaStreamTypeEntity.cs
@@ -0,0 +1,37 @@
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Enum MediaStreamType.
+/// </summary>
+public enum MediaStreamTypeEntity
+{
+ /// <summary>
+ /// The audio.
+ /// </summary>
+ Audio = 0,
+
+ /// <summary>
+ /// The video.
+ /// </summary>
+ Video = 1,
+
+ /// <summary>
+ /// The subtitle.
+ /// </summary>
+ Subtitle = 2,
+
+ /// <summary>
+ /// The embedded image.
+ /// </summary>
+ EmbeddedImage = 3,
+
+ /// <summary>
+ /// The data.
+ /// </summary>
+ Data = 4,
+
+ /// <summary>
+ /// The lyric.
+ /// </summary>
+ Lyric = 5
+}
diff --git a/Jellyfin.Data/Entities/People.cs b/Jellyfin.Data/Entities/People.cs
new file mode 100644
index 000000000..18c778b17
--- /dev/null
+++ b/Jellyfin.Data/Entities/People.cs
@@ -0,0 +1,32 @@
+#pragma warning disable CA2227 // Collection properties should be read only
+
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// People entity.
+/// </summary>
+public class People
+{
+ /// <summary>
+ /// Gets or Sets the PeopleId.
+ /// </summary>
+ public required Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the Persons Name.
+ /// </summary>
+ public required string Name { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the Type.
+ /// </summary>
+ public string? PersonType { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the mapping of People to BaseItems.
+ /// </summary>
+ public ICollection<PeopleBaseItemMap>? BaseItems { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/PeopleBaseItemMap.cs b/Jellyfin.Data/Entities/PeopleBaseItemMap.cs
new file mode 100644
index 000000000..bfaaf8215
--- /dev/null
+++ b/Jellyfin.Data/Entities/PeopleBaseItemMap.cs
@@ -0,0 +1,44 @@
+using System;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Mapping table for People to BaseItems.
+/// </summary>
+public class PeopleBaseItemMap
+{
+ /// <summary>
+ /// Gets or Sets the SortOrder.
+ /// </summary>
+ public int? SortOrder { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the ListOrder.
+ /// </summary>
+ public int? ListOrder { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the Role name the associated actor played in the <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public string? Role { get; set; }
+
+ /// <summary>
+ /// Gets or Sets The ItemId.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets Reference Item.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+
+ /// <summary>
+ /// Gets or Sets The PeopleId.
+ /// </summary>
+ public required Guid PeopleId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets Reference People.
+ /// </summary>
+ public required People People { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/ProgramAudioEntity.cs b/Jellyfin.Data/Entities/ProgramAudioEntity.cs
new file mode 100644
index 000000000..9d79e5ddb
--- /dev/null
+++ b/Jellyfin.Data/Entities/ProgramAudioEntity.cs
@@ -0,0 +1,37 @@
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Lists types of Audio.
+/// </summary>
+public enum ProgramAudioEntity
+{
+ /// <summary>
+ /// Mono.
+ /// </summary>
+ Mono = 0,
+
+ /// <summary>
+ /// Stereo.
+ /// </summary>
+ Stereo = 1,
+
+ /// <summary>
+ /// Dolby.
+ /// </summary>
+ Dolby = 2,
+
+ /// <summary>
+ /// DolbyDigital.
+ /// </summary>
+ DolbyDigital = 3,
+
+ /// <summary>
+ /// Thx.
+ /// </summary>
+ Thx = 4,
+
+ /// <summary>
+ /// Atmos.
+ /// </summary>
+ Atmos = 5
+}
diff --git a/Jellyfin.Data/Entities/TrickplayInfo.cs b/Jellyfin.Data/Entities/TrickplayInfo.cs
index 64e7da1b5..ff9a68bef 100644
--- a/Jellyfin.Data/Entities/TrickplayInfo.cs
+++ b/Jellyfin.Data/Entities/TrickplayInfo.cs
@@ -66,7 +66,7 @@ public class TrickplayInfo
public int Interval { get; set; }
/// <summary>
- /// Gets or sets peak bandwith usage in bits per second.
+ /// Gets or sets peak bandwidth usage in bits per second.
/// </summary>
/// <remarks>
/// Required.
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index 2c9cc8d78..9bbe9efe8 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -514,7 +514,7 @@ namespace Jellyfin.Data.Entities
/// </summary>
public void AddDefaultPreferences()
{
- foreach (var val in Enum.GetValues(typeof(PreferenceKind)).Cast<PreferenceKind>())
+ foreach (var val in Enum.GetValues<PreferenceKind>())
{
Preferences.Add(new Preference(val, string.Empty));
}
diff --git a/Jellyfin.Data/Entities/UserData.cs b/Jellyfin.Data/Entities/UserData.cs
new file mode 100644
index 000000000..05ab6dd2d
--- /dev/null
+++ b/Jellyfin.Data/Entities/UserData.cs
@@ -0,0 +1,92 @@
+using System;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Provides <see cref="BaseItemEntity"/> and <see cref="User"/> related data.
+/// </summary>
+public class UserData
+{
+ /// <summary>
+ /// Gets or sets the custom data key.
+ /// </summary>
+ /// <value>The rating.</value>
+ public required string CustomDataKey { get; set; }
+
+ /// <summary>
+ /// Gets or sets the users 0-10 rating.
+ /// </summary>
+ /// <value>The rating.</value>
+ public double? Rating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playback position ticks.
+ /// </summary>
+ /// <value>The playback position ticks.</value>
+ public long PlaybackPositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the play count.
+ /// </summary>
+ /// <value>The play count.</value>
+ public int PlayCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is favorite.
+ /// </summary>
+ /// <value><c>true</c> if this instance is favorite; otherwise, <c>false</c>.</value>
+ public bool IsFavorite { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last played date.
+ /// </summary>
+ /// <value>The last played date.</value>
+ public DateTime? LastPlayedDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="UserData" /> is played.
+ /// </summary>
+ /// <value><c>true</c> if played; otherwise, <c>false</c>.</value>
+ public bool Played { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the audio stream.
+ /// </summary>
+ /// <value>The index of the audio stream.</value>
+ public int? AudioStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the subtitle stream.
+ /// </summary>
+ /// <value>The index of the subtitle stream.</value>
+ public int? SubtitleStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the item is liked or not.
+ /// This should never be serialized.
+ /// </summary>
+ /// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value>
+ public bool? Likes { get; set; }
+
+ /// <summary>
+ /// Gets or sets the key.
+ /// </summary>
+ /// <value>The key.</value>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the BaseItem.
+ /// </summary>
+ public required BaseItemEntity? Item { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the UserId.
+ /// </summary>
+ public required Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the User.
+ /// </summary>
+ public required User? User { get; set; }
+}
diff --git a/Jellyfin.Data/Enums/ItemSortBy.cs b/Jellyfin.Data/Enums/ItemSortBy.cs
index 17bf1166d..ef7650294 100644
--- a/Jellyfin.Data/Enums/ItemSortBy.cs
+++ b/Jellyfin.Data/Enums/ItemSortBy.cs
@@ -154,14 +154,4 @@ public enum ItemSortBy
/// The index number.
/// </summary>
IndexNumber = 29,
-
- /// <summary>
- /// The similarity score.
- /// </summary>
- SimilarityScore = 30,
-
- /// <summary>
- /// The search score.
- /// </summary>
- SearchScore = 31,
}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index e24e37740..921cf2d8c 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -18,7 +18,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Data</PackageId>
- <VersionPrefix>10.10.0</VersionPrefix>
+ <VersionPrefix>10.11.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
index ddb393d67..7eee26059 100644
--- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
+++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
@@ -21,7 +21,7 @@ public static class ServiceCollectionExtensions
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
{
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
- opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
+ opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")};Pooling=false");
});
return serviceCollection;
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
new file mode 100644
index 000000000..392b7de74
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -0,0 +1,2178 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+// Do not enforce that because EFCore cannot deal with cultures well.
+#pragma warning disable CA1304 // Specify CultureInfo
+#pragma warning disable CA1311 // Specify a culture or use an invariant version
+#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Common;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Querying;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
+using BaseItemEntity = Jellyfin.Data.Entities.BaseItemEntity;
+
+namespace Jellyfin.Server.Implementations.Item;
+
+/*
+ All queries in this class and all other nullable enabled EFCore repository classes will make liberal use of the null-forgiving operator "!".
+ This is done as the code isn't actually executed client side, but only the expressions are interpret and the compiler cannot know that.
+ This is your only warning/message regarding this topic.
+*/
+
+/// <summary>
+/// Handles all storage logic for BaseItems.
+/// </summary>
+public sealed class BaseItemRepository
+ : IItemRepository
+{
+ /// <summary>
+ /// This holds all the types in the running assemblies
+ /// so that we can de-serialize properly when we don't have strong types.
+ /// </summary>
+ private static readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ private readonly IServerApplicationHost _appHost;
+ private readonly IItemTypeLookup _itemTypeLookup;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly ILogger<BaseItemRepository> _logger;
+
+ private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType.AlbumArtist];
+ private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
+ private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
+ private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
+ private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Studios];
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
+ /// </summary>
+ /// <param name="dbProvider">The db factory.</param>
+ /// <param name="appHost">The Application host.</param>
+ /// <param name="itemTypeLookup">The static type lookup.</param>
+ /// <param name="serverConfigurationManager">The server Configuration manager.</param>
+ /// <param name="logger">System logger.</param>
+ public BaseItemRepository(
+ IDbContextFactory<JellyfinDbContext> dbProvider,
+ IServerApplicationHost appHost,
+ IItemTypeLookup itemTypeLookup,
+ IServerConfigurationManager serverConfigurationManager,
+ ILogger<BaseItemRepository> logger)
+ {
+ _dbProvider = dbProvider;
+ _appHost = appHost;
+ _itemTypeLookup = itemTypeLookup;
+ _serverConfigurationManager = serverConfigurationManager;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public void DeleteItem(Guid id)
+ {
+ if (id.IsEmpty())
+ {
+ throw new ArgumentException("Guid can't be empty", nameof(id));
+ }
+
+ using var context = _dbProvider.CreateDbContext();
+ using var transaction = context.Database.BeginTransaction();
+ context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
+ context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
+ context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
+ context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
+ context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
+ context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
+ context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
+ context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
+ context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
+ context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
+ context.SaveChanges();
+ transaction.Commit();
+ }
+
+ /// <inheritdoc />
+ public void UpdateInheritedValues()
+ {
+ using var context = _dbProvider.CreateDbContext();
+ using var transaction = context.Database.BeginTransaction();
+
+ context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
+ // ItemValue Inheritance is now correctly mapped via AncestorId on demand
+ context.SaveChanges();
+
+ transaction.Commit();
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+ PrepareFilterQuery(filter);
+
+ using var context = _dbProvider.CreateDbContext();
+ return ApplyQueryFilter(context.BaseItems.AsNoTracking(), context, filter).Select(e => e.Id).ToArray();
+ }
+
+ /// <inheritdoc />
+ public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter)
+ {
+ return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
+ }
+
+ /// <inheritdoc />
+ public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter)
+ {
+ return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
+ }
+
+ /// <inheritdoc />
+ public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
+ {
+ return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
+ }
+
+ /// <inheritdoc />
+ public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter)
+ {
+ return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
+ }
+
+ /// <inheritdoc />
+ public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter)
+ {
+ return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
+ }
+
+ /// <inheritdoc />
+ public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
+ {
+ return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyList<string> GetStudioNames()
+ {
+ return GetItemValueNames(_getStudiosValueTypes, [], []);
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyList<string> GetAllArtistNames()
+ {
+ return GetItemValueNames(_getAllArtistsValueTypes, [], []);
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyList<string> GetMusicGenreNames()
+ {
+ return GetItemValueNames(
+ _getGenreValueTypes,
+ _itemTypeLookup.MusicGenreTypes,
+ []);
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyList<string> GetGenreNames()
+ {
+ return GetItemValueNames(
+ _getGenreValueTypes,
+ [],
+ _itemTypeLookup.MusicGenreTypes);
+ }
+
+ /// <inheritdoc />
+ public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+ if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0))
+ {
+ var returnList = GetItemList(filter);
+ return new QueryResult<BaseItemDto>(
+ filter.StartIndex,
+ returnList.Count,
+ returnList);
+ }
+
+ PrepareFilterQuery(filter);
+ var result = new QueryResult<BaseItemDto>();
+
+ using var context = _dbProvider.CreateDbContext();
+
+ IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
+
+ dbQuery = TranslateQuery(dbQuery, context, filter);
+ if (filter.EnableTotalRecordCount)
+ {
+ result.TotalRecordCount = dbQuery.Count();
+ }
+
+ dbQuery = ApplyGroupingFilter(dbQuery, filter);
+ dbQuery = ApplyQueryPaging(dbQuery, filter);
+
+ result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
+ result.StartIndex = filter.StartIndex ?? 0;
+ return result;
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyList<BaseItemDto> GetItemList(InternalItemsQuery filter)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+ PrepareFilterQuery(filter);
+
+ using var context = _dbProvider.CreateDbContext();
+ IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
+
+ dbQuery = TranslateQuery(dbQuery, context, filter);
+
+ dbQuery = ApplyGroupingFilter(dbQuery, filter);
+ dbQuery = ApplyQueryPaging(dbQuery, filter);
+
+ return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
+ }
+
+ private IQueryable<BaseItemEntity> ApplyGroupingFilter(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
+ {
+ // This whole block is needed to filter duplicate entries on request
+ // for the time being it cannot be used because it would destroy the ordering
+ // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions but
+ // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own
+
+ // var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
+ // if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
+ // {
+ // dbQuery = ApplyOrder(dbQuery, filter);
+ // dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.First());
+ // }
+ // else if (enableGroupByPresentationUniqueKey)
+ // {
+ // dbQuery = ApplyOrder(dbQuery, filter);
+ // dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First());
+ // }
+ // else if (filter.GroupBySeriesPresentationUniqueKey)
+ // {
+ // dbQuery = ApplyOrder(dbQuery, filter);
+ // dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.First());
+ // }
+ // else
+ // {
+ // dbQuery = dbQuery.Distinct();
+ // dbQuery = ApplyOrder(dbQuery, filter);
+ // }
+ dbQuery = dbQuery.Distinct();
+ dbQuery = ApplyOrder(dbQuery, filter);
+
+ return dbQuery;
+ }
+
+ private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
+ {
+ if (filter.Limit.HasValue || filter.StartIndex.HasValue)
+ {
+ var offset = filter.StartIndex ?? 0;
+
+ if (offset > 0)
+ {
+ dbQuery = dbQuery.Skip(offset);
+ }
+
+ if (filter.Limit.HasValue)
+ {
+ dbQuery = dbQuery.Take(filter.Limit.Value);
+ }
+ }
+
+ return dbQuery;
+ }
+
+ private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, InternalItemsQuery filter)
+ {
+ dbQuery = TranslateQuery(dbQuery, context, filter);
+ dbQuery = ApplyOrder(dbQuery, filter);
+ dbQuery = ApplyGroupingFilter(dbQuery, filter);
+ dbQuery = ApplyQueryPaging(dbQuery, filter);
+ return dbQuery;
+ }
+
+ private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
+ {
+ IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking().AsSplitQuery()
+ .Include(e => e.TrailerTypes)
+ .Include(e => e.Provider)
+ .Include(e => e.LockedFields);
+
+ if (filter.DtoOptions.EnableImages)
+ {
+ dbQuery = dbQuery.Include(e => e.Images);
+ }
+
+ return dbQuery;
+ }
+
+ /// <inheritdoc/>
+ public int GetCount(InternalItemsQuery filter)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+ // Hack for right now since we currently don't support filtering out these duplicates within a query
+ PrepareFilterQuery(filter);
+
+ using var context = _dbProvider.CreateDbContext();
+ var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
+
+ return dbQuery.Count();
+ }
+
+#pragma warning disable CA1307 // Specify StringComparison for clarity
+ /// <summary>
+ /// Gets the type.
+ /// </summary>
+ /// <param name="typeName">Name of the type.</param>
+ /// <returns>Type.</returns>
+ /// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
+ private static Type? GetType(string typeName)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(typeName);
+
+ // TODO: this isn't great. Refactor later to be both globally handled by a dedicated service not just an static variable and be loaded eagerly.
+ // currently this is done so that plugins may introduce their own type of baseitems as we dont know when we are first called, before or after plugins are loaded
+ return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
+ .Select(a => a.GetType(k))
+ .FirstOrDefault(t => t is not null));
+ }
+
+ /// <inheritdoc />
+ public void SaveImages(BaseItemDto item)
+ {
+ ArgumentNullException.ThrowIfNull(item);
+
+ var images = item.ImageInfos.Select(e => Map(item.Id, e));
+ using var context = _dbProvider.CreateDbContext();
+ using var transaction = context.Database.BeginTransaction();
+ context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete();
+ context.BaseItemImageInfos.AddRange(images);
+ context.SaveChanges();
+ transaction.Commit();
+ }
+
+ /// <inheritdoc />
+ public void SaveItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
+ {
+ UpdateOrInsertItems(items, cancellationToken);
+ }
+
+ /// <inheritdoc cref="IItemRepository"/>
+ public void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(items);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> UserDataKey, List<string> InheritedTags)>();
+ foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()))
+ {
+ var ancestorIds = item.SupportsAncestors ?
+ item.GetAncestorIds().Distinct().ToList() :
+ null;
+
+ var topParent = item.GetTopParent();
+
+ var userdataKey = item.GetUserDataKeys();
+ var inheritedTags = item.GetInheritedTags();
+
+ tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
+ }
+
+ var localItemValueCache = new Dictionary<(int MagicNumber, string Value), Guid>();
+
+ using var context = _dbProvider.CreateDbContext();
+ using var transaction = context.Database.BeginTransaction();
+ foreach (var item in tuples)
+ {
+ var entity = Map(item.Item);
+ // TODO: refactor this "inconsistency"
+ entity.TopParentId = item.TopParent?.Id;
+
+ if (!context.BaseItems.Any(e => e.Id == entity.Id))
+ {
+ context.BaseItems.Add(entity);
+ }
+ else
+ {
+ context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
+ context.BaseItems.Attach(entity).State = EntityState.Modified;
+ }
+
+ context.AncestorIds.Where(e => e.ItemId == entity.Id).ExecuteDelete();
+ if (item.Item.SupportsAncestors && item.AncestorIds != null)
+ {
+ foreach (var ancestorId in item.AncestorIds)
+ {
+ if (!context.BaseItems.Any(f => f.Id == ancestorId))
+ {
+ continue;
+ }
+
+ context.AncestorIds.Add(new AncestorId()
+ {
+ ParentItemId = ancestorId,
+ ItemId = entity.Id,
+ Item = null!,
+ ParentItem = null!
+ });
+ }
+ }
+
+ // Never save duplicate itemValues as they are now mapped anyway.
+ var itemValuesToSave = GetItemValuesToSave(item.Item, item.InheritedTags).DistinctBy(e => (GetCleanValue(e.Value), e.MagicNumber));
+ context.ItemValuesMap.Where(e => e.ItemId == entity.Id).ExecuteDelete();
+ foreach (var itemValue in itemValuesToSave)
+ {
+ if (!localItemValueCache.TryGetValue(itemValue, out var refValue))
+ {
+ refValue = context.ItemValues
+ .Where(f => f.CleanValue == GetCleanValue(itemValue.Value) && (int)f.Type == itemValue.MagicNumber)
+ .Select(e => e.ItemValueId)
+ .FirstOrDefault();
+ }
+
+ if (refValue.IsEmpty())
+ {
+ context.ItemValues.Add(new ItemValue()
+ {
+ CleanValue = GetCleanValue(itemValue.Value),
+ Type = (ItemValueType)itemValue.MagicNumber,
+ ItemValueId = refValue = Guid.NewGuid(),
+ Value = itemValue.Value
+ });
+ localItemValueCache[itemValue] = refValue;
+ }
+
+ context.ItemValuesMap.Add(new ItemValueMap()
+ {
+ Item = null!,
+ ItemId = entity.Id,
+ ItemValue = null!,
+ ItemValueId = refValue
+ });
+ }
+ }
+
+ context.SaveChanges();
+ transaction.Commit();
+ }
+
+ /// <inheritdoc />
+ public BaseItemDto? RetrieveItem(Guid id)
+ {
+ if (id.IsEmpty())
+ {
+ throw new ArgumentException("Guid can't be empty", nameof(id));
+ }
+
+ using var context = _dbProvider.CreateDbContext();
+ var item = PrepareItemQuery(context, new()
+ {
+ DtoOptions = new()
+ {
+ EnableImages = true
+ }
+ }).FirstOrDefault(e => e.Id == id);
+ if (item is null)
+ {
+ return null;
+ }
+
+ return DeserialiseBaseItem(item);
+ }
+
+ /// <summary>
+ /// Maps a Entity to the DTO.
+ /// </summary>
+ /// <param name="entity">The entity.</param>
+ /// <param name="dto">The dto base instance.</param>
+ /// <param name="appHost">The Application server Host.</param>
+ /// <returns>The dto to map.</returns>
+ public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost)
+ {
+ dto.Id = entity.Id;
+ dto.ParentId = entity.ParentId.GetValueOrDefault();
+ dto.Path = appHost?.ExpandVirtualPath(entity.Path) ?? entity.Path;
+ dto.EndDate = entity.EndDate;
+ dto.CommunityRating = entity.CommunityRating;
+ dto.CustomRating = entity.CustomRating;
+ dto.IndexNumber = entity.IndexNumber;
+ dto.IsLocked = entity.IsLocked;
+ dto.Name = entity.Name;
+ dto.OfficialRating = entity.OfficialRating;
+ dto.Overview = entity.Overview;
+ dto.ParentIndexNumber = entity.ParentIndexNumber;
+ dto.PremiereDate = entity.PremiereDate;
+ dto.ProductionYear = entity.ProductionYear;
+ dto.SortName = entity.SortName;
+ dto.ForcedSortName = entity.ForcedSortName;
+ dto.RunTimeTicks = entity.RunTimeTicks;
+ dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage;
+ dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
+ dto.IsInMixedFolder = entity.IsInMixedFolder;
+ dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
+ dto.CriticRating = entity.CriticRating;
+ dto.PresentationUniqueKey = entity.PresentationUniqueKey;
+ dto.OriginalTitle = entity.OriginalTitle;
+ dto.Album = entity.Album;
+ dto.LUFS = entity.LUFS;
+ dto.NormalizationGain = entity.NormalizationGain;
+ dto.IsVirtualItem = entity.IsVirtualItem;
+ dto.ExternalSeriesId = entity.ExternalSeriesId;
+ dto.Tagline = entity.Tagline;
+ dto.TotalBitrate = entity.TotalBitrate;
+ dto.ExternalId = entity.ExternalId;
+ dto.Size = entity.Size;
+ dto.Genres = entity.Genres?.Split('|') ?? [];
+ dto.DateCreated = entity.DateCreated.GetValueOrDefault();
+ dto.DateModified = entity.DateModified.GetValueOrDefault();
+ dto.ChannelId = entity.ChannelId ?? Guid.Empty;
+ dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault();
+ dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault();
+ dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty);
+ dto.Width = entity.Width.GetValueOrDefault();
+ dto.Height = entity.Height.GetValueOrDefault();
+ if (entity.Provider is not null)
+ {
+ dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
+ }
+
+ if (entity.ExtraType is not null)
+ {
+ dto.ExtraType = (ExtraType)entity.ExtraType;
+ }
+
+ if (entity.LockedFields is not null)
+ {
+ dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
+ }
+
+ if (entity.Audio is not null)
+ {
+ dto.Audio = (ProgramAudio)entity.Audio;
+ }
+
+ dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
+ dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
+ dto.Studios = entity.Studios?.Split('|') ?? [];
+ dto.Tags = entity.Tags?.Split('|') ?? [];
+
+ if (dto is IHasProgramAttributes hasProgramAttributes)
+ {
+ hasProgramAttributes.IsMovie = entity.IsMovie;
+ hasProgramAttributes.IsSeries = entity.IsSeries;
+ hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle;
+ hasProgramAttributes.IsRepeat = entity.IsRepeat;
+ }
+
+ if (dto is LiveTvChannel liveTvChannel)
+ {
+ liveTvChannel.ServiceName = entity.ExternalServiceId;
+ }
+
+ if (dto is Trailer trailer)
+ {
+ trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
+ }
+
+ if (dto is Video video)
+ {
+ video.PrimaryVersionId = entity.PrimaryVersionId;
+ }
+
+ if (dto is IHasSeries hasSeriesName)
+ {
+ hasSeriesName.SeriesName = entity.SeriesName;
+ hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault();
+ hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey;
+ }
+
+ if (dto is Episode episode)
+ {
+ episode.SeasonName = entity.SeasonName;
+ episode.SeasonId = entity.SeasonId.GetValueOrDefault();
+ }
+
+ if (dto is IHasArtist hasArtists)
+ {
+ hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
+ }
+
+ if (dto is IHasAlbumArtist hasAlbumArtists)
+ {
+ hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
+ }
+
+ if (dto is LiveTvProgram program)
+ {
+ program.ShowId = entity.ShowId;
+ }
+
+ if (entity.Images is not null)
+ {
+ dto.ImageInfos = entity.Images.Select(e => Map(e, appHost)).ToArray();
+ }
+
+ // dto.Type = entity.Type;
+ // dto.Data = entity.Data;
+ // dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
+ if (dto is IHasStartDate hasStartDate)
+ {
+ hasStartDate.StartDate = entity.StartDate.GetValueOrDefault();
+ }
+
+ // Fields that are present in the DB but are never actually used
+ // dto.UnratedType = entity.UnratedType;
+ // dto.TopParentId = entity.TopParentId;
+ // dto.CleanName = entity.CleanName;
+ // dto.UserDataKey = entity.UserDataKey;
+
+ if (dto is Folder folder)
+ {
+ folder.DateLastMediaAdded = entity.DateLastMediaAdded;
+ }
+
+ return dto;
+ }
+
+ /// <summary>
+ /// Maps a Entity to the DTO.
+ /// </summary>
+ /// <param name="dto">The entity.</param>
+ /// <returns>The dto to map.</returns>
+ public BaseItemEntity Map(BaseItemDto dto)
+ {
+ var dtoType = dto.GetType();
+ var entity = new BaseItemEntity()
+ {
+ Type = dtoType.ToString(),
+ Id = dto.Id
+ };
+
+ if (TypeRequiresDeserialization(dtoType))
+ {
+ entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options);
+ }
+
+ entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
+ entity.Path = GetPathToSave(dto.Path);
+ entity.EndDate = dto.EndDate;
+ entity.CommunityRating = dto.CommunityRating;
+ entity.CustomRating = dto.CustomRating;
+ entity.IndexNumber = dto.IndexNumber;
+ entity.IsLocked = dto.IsLocked;
+ entity.Name = dto.Name;
+ entity.CleanName = GetCleanValue(dto.Name);
+ entity.OfficialRating = dto.OfficialRating;
+ entity.Overview = dto.Overview;
+ entity.ParentIndexNumber = dto.ParentIndexNumber;
+ entity.PremiereDate = dto.PremiereDate;
+ entity.ProductionYear = dto.ProductionYear;
+ entity.SortName = dto.SortName;
+ entity.ForcedSortName = dto.ForcedSortName;
+ entity.RunTimeTicks = dto.RunTimeTicks;
+ entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage;
+ entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
+ entity.IsInMixedFolder = dto.IsInMixedFolder;
+ entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
+ entity.CriticRating = dto.CriticRating;
+ entity.PresentationUniqueKey = dto.PresentationUniqueKey;
+ entity.OriginalTitle = dto.OriginalTitle;
+ entity.Album = dto.Album;
+ entity.LUFS = dto.LUFS;
+ entity.NormalizationGain = dto.NormalizationGain;
+ entity.IsVirtualItem = dto.IsVirtualItem;
+ entity.ExternalSeriesId = dto.ExternalSeriesId;
+ entity.Tagline = dto.Tagline;
+ entity.TotalBitrate = dto.TotalBitrate;
+ entity.ExternalId = dto.ExternalId;
+ entity.Size = dto.Size;
+ entity.Genres = string.Join('|', dto.Genres);
+ entity.DateCreated = dto.DateCreated;
+ entity.DateModified = dto.DateModified;
+ entity.ChannelId = dto.ChannelId;
+ entity.DateLastRefreshed = dto.DateLastRefreshed;
+ entity.DateLastSaved = dto.DateLastSaved;
+ entity.OwnerId = dto.OwnerId.ToString();
+ entity.Width = dto.Width;
+ entity.Height = dto.Height;
+ entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider()
+ {
+ Item = entity,
+ ProviderId = e.Key,
+ ProviderValue = e.Value
+ }).ToList();
+
+ if (dto.Audio.HasValue)
+ {
+ entity.Audio = (ProgramAudioEntity)dto.Audio;
+ }
+
+ if (dto.ExtraType.HasValue)
+ {
+ entity.ExtraType = (BaseItemExtraType)dto.ExtraType;
+ }
+
+ entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
+ entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : null;
+ entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
+ entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
+ entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
+ .Select(e => new BaseItemMetadataField()
+ {
+ Id = (int)e,
+ Item = entity,
+ ItemId = entity.Id
+ })
+ .ToArray() : null;
+
+ if (dto is IHasProgramAttributes hasProgramAttributes)
+ {
+ entity.IsMovie = hasProgramAttributes.IsMovie;
+ entity.IsSeries = hasProgramAttributes.IsSeries;
+ entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle;
+ entity.IsRepeat = hasProgramAttributes.IsRepeat;
+ }
+
+ if (dto is LiveTvChannel liveTvChannel)
+ {
+ entity.ExternalServiceId = liveTvChannel.ServiceName;
+ }
+
+ if (dto is Video video)
+ {
+ entity.PrimaryVersionId = video.PrimaryVersionId;
+ }
+
+ if (dto is IHasSeries hasSeriesName)
+ {
+ entity.SeriesName = hasSeriesName.SeriesName;
+ entity.SeriesId = hasSeriesName.SeriesId;
+ entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey;
+ }
+
+ if (dto is Episode episode)
+ {
+ entity.SeasonName = episode.SeasonName;
+ entity.SeasonId = episode.SeasonId;
+ }
+
+ if (dto is IHasArtist hasArtists)
+ {
+ entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
+ }
+
+ if (dto is IHasAlbumArtist hasAlbumArtists)
+ {
+ entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists) : null;
+ }
+
+ if (dto is LiveTvProgram program)
+ {
+ entity.ShowId = program.ShowId;
+ }
+
+ if (dto.ImageInfos is not null)
+ {
+ entity.Images = dto.ImageInfos.Select(f => Map(dto.Id, f)).ToArray();
+ }
+
+ if (dto is Trailer trailer)
+ {
+ entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType()
+ {
+ Id = (int)e,
+ Item = entity,
+ ItemId = entity.Id
+ }).ToArray() ?? [];
+ }
+
+ // dto.Type = entity.Type;
+ // dto.Data = entity.Data;
+ entity.MediaType = dto.MediaType.ToString();
+ if (dto is IHasStartDate hasStartDate)
+ {
+ entity.StartDate = hasStartDate.StartDate;
+ }
+
+ entity.UnratedType = dto.GetBlockUnratedType().ToString();
+
+ // Fields that are present in the DB but are never actually used
+ // dto.UserDataKey = entity.UserDataKey;
+
+ if (dto is Folder folder)
+ {
+ entity.DateLastMediaAdded = folder.DateLastMediaAdded;
+ entity.IsFolder = folder.IsFolder;
+ }
+
+ return entity;
+ }
+
+ private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes, IReadOnlyList<string> excludeItemTypes)
+ {
+ using var context = _dbProvider.CreateDbContext();
+
+ var query = context.ItemValuesMap
+ .AsNoTracking()
+ .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.Type));
+ if (withItemTypes.Count > 0)
+ {
+ query = query.Where(e => withItemTypes.Contains(e.Item.Type));
+ }
+
+ if (excludeItemTypes.Count > 0)
+ {
+ query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
+ }
+
+ // query = query.DistinctBy(e => e.CleanValue);
+ return query.Select(e => e.ItemValue)
+ .GroupBy(e => e.CleanValue)
+ .Select(e => e.First().Value)
+ .ToArray();
+ }
+
+ private static bool TypeRequiresDeserialization(Type type)
+ {
+ return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
+ }
+
+ private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
+ {
+ ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
+ if (_serverConfigurationManager?.Configuration is null)
+ {
+ throw new InvalidOperationException("Server Configuration manager or configuration is null");
+ }
+
+ var typeToSerialise = GetType(baseItemEntity.Type);
+ return BaseItemRepository.DeserialiseBaseItem(
+ baseItemEntity,
+ _logger,
+ _appHost,
+ skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeToSerialise == typeof(Channel) || typeToSerialise == typeof(UserRootFolder))));
+ }
+
+ /// <summary>
+ /// Deserialises a BaseItemEntity and sets all properties.
+ /// </summary>
+ /// <param name="baseItemEntity">The DB entity.</param>
+ /// <param name="logger">Logger.</param>
+ /// <param name="appHost">The application server Host.</param>
+ /// <param name="skipDeserialization">If only mapping should be processed.</param>
+ /// <returns>A mapped BaseItem.</returns>
+ /// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
+ public static BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
+ {
+ var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unknown type.");
+ BaseItemDto? dto = null;
+ if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
+ {
+ try
+ {
+ dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto;
+ }
+ catch (JsonException ex)
+ {
+ logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data);
+ }
+ }
+
+ if (dto is null)
+ {
+ dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unknown type.");
+ }
+
+ return Map(baseItemEntity, dto, appHost);
+ }
+
+ private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyList<ItemValueType> itemValueTypes, string returnType)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+
+ if (!filter.Limit.HasValue)
+ {
+ filter.EnableTotalRecordCount = false;
+ }
+
+ using var context = _dbProvider.CreateDbContext();
+
+ var innerQuery = new InternalItemsQuery(filter.User)
+ {
+ ExcludeItemTypes = filter.ExcludeItemTypes,
+ IncludeItemTypes = filter.IncludeItemTypes,
+ MediaTypes = filter.MediaTypes,
+ AncestorIds = filter.AncestorIds,
+ ItemIds = filter.ItemIds,
+ TopParentIds = filter.TopParentIds,
+ ParentId = filter.ParentId,
+ IsAiring = filter.IsAiring,
+ IsMovie = filter.IsMovie,
+ IsSports = filter.IsSports,
+ IsKids = filter.IsKids,
+ IsNews = filter.IsNews,
+ IsSeries = filter.IsSeries
+ };
+ var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, innerQuery);
+
+ query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type)));
+
+ if (filter.OrderBy.Count != 0
+ || !string.IsNullOrEmpty(filter.SearchTerm))
+ {
+ query = ApplyOrder(query, filter);
+ }
+ else
+ {
+ query = query.OrderBy(e => e.SortName);
+ }
+
+ if (filter.Limit.HasValue || filter.StartIndex.HasValue)
+ {
+ var offset = filter.StartIndex ?? 0;
+
+ if (offset > 0)
+ {
+ query = query.Skip(offset);
+ }
+
+ if (filter.Limit.HasValue)
+ {
+ query = query.Take(filter.Limit.Value);
+ }
+ }
+
+ var result = new QueryResult<(BaseItemDto, ItemCounts)>();
+ if (filter.EnableTotalRecordCount)
+ {
+ result.TotalRecordCount = query.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First()).Count();
+ }
+
+ var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
+ var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
+ var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
+ var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
+ var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
+ var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
+ var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
+
+ var resultQuery = query.Select(e => new
+ {
+ item = e,
+ // TODO: This is bad refactor!
+ itemCount = new ItemCounts()
+ {
+ SeriesCount = e.ItemValues!.Count(f => f.Item.Type == seriesTypeName),
+ EpisodeCount = e.ItemValues!.Count(f => f.Item.Type == episodeTypeName),
+ MovieCount = e.ItemValues!.Count(f => f.Item.Type == movieTypeName),
+ AlbumCount = e.ItemValues!.Count(f => f.Item.Type == musicAlbumTypeName),
+ ArtistCount = e.ItemValues!.Count(f => f.Item.Type == musicArtistTypeName),
+ SongCount = e.ItemValues!.Count(f => f.Item.Type == audioTypeName),
+ TrailerCount = e.ItemValues!.Count(f => f.Item.Type == trailerTypeName),
+ }
+ });
+
+ result.StartIndex = filter.StartIndex ?? 0;
+ result.Items = resultQuery.ToArray().Where(e => e is not null).Select(e =>
+ {
+ return (DeserialiseBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
+ }).ToArray();
+
+ return result;
+ }
+
+ private static void PrepareFilterQuery(InternalItemsQuery query)
+ {
+ if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
+ {
+ query.Limit = query.Limit.Value + 4;
+ }
+
+ if (query.IsResumable ?? false)
+ {
+ query.IsVirtualItem = false;
+ }
+ }
+
+ private string GetCleanValue(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return value;
+ }
+
+ return value.RemoveDiacritics().ToLowerInvariant();
+ }
+
+ private List<(int MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inheritedTags)
+ {
+ var list = new List<(int, string)>();
+
+ if (item is IHasArtist hasArtist)
+ {
+ list.AddRange(hasArtist.Artists.Select(i => (0, i)));
+ }
+
+ if (item is IHasAlbumArtist hasAlbumArtist)
+ {
+ list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i)));
+ }
+
+ list.AddRange(item.Genres.Select(i => (2, i)));
+ list.AddRange(item.Studios.Select(i => (3, i)));
+ list.AddRange(item.Tags.Select(i => (4, i)));
+
+ // keywords was 5
+
+ list.AddRange(inheritedTags.Select(i => (6, i)));
+
+ // Remove all invalid values.
+ list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
+
+ return list;
+ }
+
+ private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
+ {
+ return new BaseItemImageInfo()
+ {
+ ItemId = baseItemId,
+ Id = Guid.NewGuid(),
+ Path = e.Path,
+ Blurhash = e.BlurHash is null ? null : Encoding.UTF8.GetBytes(e.BlurHash),
+ DateModified = e.DateModified,
+ Height = e.Height,
+ Width = e.Width,
+ ImageType = (ImageInfoImageType)e.Type,
+ Item = null!
+ };
+ }
+
+ private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
+ {
+ return new ItemImageInfo()
+ {
+ Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
+ BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
+ DateModified = e.DateModified,
+ Height = e.Height,
+ Width = e.Width,
+ Type = (ImageType)e.ImageType
+ };
+ }
+
+ private string? GetPathToSave(string path)
+ {
+ if (path is null)
+ {
+ return null;
+ }
+
+ return _appHost.ReverseVirtualPath(path);
+ }
+
+ private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
+ {
+ var list = new List<string>();
+
+ if (IsTypeInQuery(BaseItemKind.Person, query))
+ {
+ list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
+ }
+
+ if (IsTypeInQuery(BaseItemKind.Genre, query))
+ {
+ list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
+ }
+
+ if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
+ {
+ list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
+ }
+
+ if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
+ {
+ list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
+ }
+
+ if (IsTypeInQuery(BaseItemKind.Studio, query))
+ {
+ list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
+ }
+
+ return list;
+ }
+
+ private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
+ {
+ if (query.ExcludeItemTypes.Contains(type))
+ {
+ return false;
+ }
+
+ return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
+ }
+
+ private Expression<Func<BaseItemEntity, object>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query)
+ {
+#pragma warning disable CS8603 // Possible null reference return.
+ return sortBy switch
+ {
+ ItemSortBy.AirTime => e => e.SortName, // TODO
+ ItemSortBy.Runtime => e => e.RunTimeTicks,
+ ItemSortBy.Random => e => EF.Functions.Random(),
+ ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.LastPlayedDate,
+ ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.PlayCount,
+ ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.IsFavorite,
+ ItemSortBy.IsFolder => e => e.IsFolder,
+ ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.Played,
+ ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.Played,
+ ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded,
+ ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
+ ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
+ ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
+ ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue,
+ // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)",
+ ItemSortBy.SeriesSortName => e => e.SeriesName,
+ // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
+ ItemSortBy.Album => e => e.Album,
+ ItemSortBy.DateCreated => e => e.DateCreated,
+ ItemSortBy.PremiereDate => e => e.PremiereDate,
+ ItemSortBy.StartDate => e => e.StartDate,
+ ItemSortBy.Name => e => e.Name,
+ ItemSortBy.CommunityRating => e => e.CommunityRating,
+ ItemSortBy.ProductionYear => e => e.ProductionYear,
+ ItemSortBy.CriticRating => e => e.CriticRating,
+ ItemSortBy.VideoBitRate => e => e.TotalBitrate,
+ ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber,
+ ItemSortBy.IndexNumber => e => e.IndexNumber,
+ _ => e => e.SortName
+ };
+#pragma warning restore CS8603 // Possible null reference return.
+
+ }
+
+ private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
+ {
+ if (!query.GroupByPresentationUniqueKey)
+ {
+ return false;
+ }
+
+ if (query.GroupBySeriesPresentationUniqueKey)
+ {
+ return false;
+ }
+
+ if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey))
+ {
+ return false;
+ }
+
+ if (query.User is null)
+ {
+ return false;
+ }
+
+ if (query.IncludeItemTypes.Length == 0)
+ {
+ return true;
+ }
+
+ return 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);
+ }
+
+ private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter)
+ {
+ var orderBy = filter.OrderBy;
+ var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
+
+ if (hasSearch)
+ {
+ orderBy = filter.OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
+ }
+ else if (orderBy.Count == 0)
+ {
+ return query;
+ }
+
+ IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
+
+ var firstOrdering = orderBy.FirstOrDefault();
+ if (firstOrdering != default)
+ {
+ var expression = MapOrderByField(firstOrdering.OrderBy, filter);
+ if (firstOrdering.SortOrder == SortOrder.Ascending)
+ {
+ orderedQuery = query.OrderBy(expression);
+ }
+ else
+ {
+ orderedQuery = query.OrderByDescending(expression);
+ }
+
+ if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
+ {
+ if (firstOrdering.SortOrder is SortOrder.Ascending)
+ {
+ orderedQuery = orderedQuery.ThenBy(e => e.Name);
+ }
+ else
+ {
+ orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
+ }
+ }
+ }
+
+ foreach (var item in orderBy.Skip(1))
+ {
+ var expression = MapOrderByField(item.OrderBy, filter);
+ if (item.SortOrder == SortOrder.Ascending)
+ {
+ orderedQuery = orderedQuery!.ThenBy(expression);
+ }
+ else
+ {
+ orderedQuery = orderedQuery!.ThenByDescending(expression);
+ }
+ }
+
+ return orderedQuery ?? query;
+ }
+
+ private IQueryable<BaseItemEntity> TranslateQuery(
+ IQueryable<BaseItemEntity> baseQuery,
+ JellyfinDbContext context,
+ InternalItemsQuery filter)
+ {
+ var minWidth = filter.MinWidth;
+ var maxWidth = filter.MaxWidth;
+ var now = DateTime.UtcNow;
+
+ if (filter.IsHD.HasValue)
+ {
+ const int Threshold = 1200;
+ if (filter.IsHD.Value)
+ {
+ minWidth = Threshold;
+ }
+ else
+ {
+ maxWidth = Threshold - 1;
+ }
+ }
+
+ if (filter.Is4K.HasValue)
+ {
+ const int Threshold = 3800;
+ if (filter.Is4K.Value)
+ {
+ minWidth = Threshold;
+ }
+ else
+ {
+ maxWidth = Threshold - 1;
+ }
+ }
+
+ if (minWidth.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.Width >= minWidth);
+ }
+
+ if (filter.MinHeight.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight);
+ }
+
+ if (maxWidth.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.Width >= maxWidth);
+ }
+
+ if (filter.MaxHeight.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight);
+ }
+
+ if (filter.IsLocked.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked);
+ }
+
+ var tags = filter.Tags.ToList();
+ var excludeTags = filter.ExcludeTags.ToList();
+
+ if (filter.IsMovie == true)
+ {
+ if (filter.IncludeItemTypes.Length == 0
+ || filter.IncludeItemTypes.Contains(BaseItemKind.Movie)
+ || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer))
+ {
+ baseQuery = baseQuery.Where(e => e.IsMovie);
+ }
+ }
+ else if (filter.IsMovie.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie);
+ }
+
+ if (filter.IsSeries.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries);
+ }
+
+ if (filter.IsSports.HasValue)
+ {
+ if (filter.IsSports.Value)
+ {
+ tags.Add("Sports");
+ }
+ else
+ {
+ excludeTags.Add("Sports");
+ }
+ }
+
+ if (filter.IsNews.HasValue)
+ {
+ if (filter.IsNews.Value)
+ {
+ tags.Add("News");
+ }
+ else
+ {
+ excludeTags.Add("News");
+ }
+ }
+
+ if (filter.IsKids.HasValue)
+ {
+ if (filter.IsKids.Value)
+ {
+ tags.Add("Kids");
+ }
+ else
+ {
+ excludeTags.Add("Kids");
+ }
+ }
+
+ if (!string.IsNullOrEmpty(filter.SearchTerm))
+ {
+ var searchTerm = filter.SearchTerm.ToLower();
+ baseQuery = baseQuery.Where(e => e.CleanName!.ToLower().Contains(searchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(searchTerm)));
+ }
+
+ if (filter.IsFolder.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
+ }
+
+ var includeTypes = filter.IncludeItemTypes;
+ // Only specify excluded types if no included types are specified
+ if (filter.IncludeItemTypes.Length == 0)
+ {
+ var excludeTypes = filter.ExcludeItemTypes;
+ if (excludeTypes.Length == 1)
+ {
+ if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
+ {
+ baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
+ }
+ }
+ else if (excludeTypes.Length > 1)
+ {
+ var excludeTypeName = new List<string>();
+ foreach (var excludeType in excludeTypes)
+ {
+ if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
+ {
+ excludeTypeName.Add(baseItemKindName!);
+ }
+ }
+
+ baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
+ }
+ }
+ else if (includeTypes.Length == 1)
+ {
+ if (_itemTypeLookup.BaseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName))
+ {
+ baseQuery = baseQuery.Where(e => e.Type == includeTypeName);
+ }
+ }
+ else if (includeTypes.Length > 1)
+ {
+ var includeTypeName = new List<string>();
+ foreach (var includeType in includeTypes)
+ {
+ if (_itemTypeLookup.BaseItemKindNames.TryGetValue(includeType, out var baseItemKindName))
+ {
+ includeTypeName.Add(baseItemKindName!);
+ }
+ }
+
+ baseQuery = baseQuery.Where(e => includeTypeName.Contains(e.Type));
+ }
+
+ if (filter.ChannelIds.Count > 0)
+ {
+ baseQuery = baseQuery.Where(e => e.ChannelId != null && filter.ChannelIds.Contains(e.ChannelId.Value));
+ }
+
+ if (!filter.ParentId.IsEmpty())
+ {
+ baseQuery = baseQuery.Where(e => e.ParentId!.Value == filter.ParentId);
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.Path))
+ {
+ baseQuery = baseQuery.Where(e => e.Path == filter.Path);
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
+ {
+ baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey);
+ }
+
+ if (filter.MinCommunityRating.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating);
+ }
+
+ if (filter.MinIndexNumber.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber);
+ }
+
+ if (filter.MinParentAndIndexNumber.HasValue)
+ {
+ baseQuery = baseQuery
+ .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNumber >= filter.MinParentAndIndexNumber.Value.IndexNumber) || e.ParentIndexNumber > filter.MinParentAndIndexNumber.Value.ParentIndexNumber);
+ }
+
+ if (filter.MinDateCreated.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated);
+ }
+
+ if (filter.MinDateLastSaved.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value);
+ }
+
+ if (filter.MinDateLastSavedForUser.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUser.Value);
+ }
+
+ if (filter.IndexNumber.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value);
+ }
+
+ if (filter.ParentIndexNumber.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value);
+ }
+
+ if (filter.ParentIndexNumberNotEquals.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentIndexNumber == null);
+ }
+
+ var minEndDate = filter.MinEndDate;
+ var maxEndDate = filter.MaxEndDate;
+
+ if (filter.HasAired.HasValue)
+ {
+ if (filter.HasAired.Value)
+ {
+ maxEndDate = DateTime.UtcNow;
+ }
+ else
+ {
+ minEndDate = DateTime.UtcNow;
+ }
+ }
+
+ if (minEndDate.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate);
+ }
+
+ if (maxEndDate.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate);
+ }
+
+ if (filter.MinStartDate.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value);
+ }
+
+ if (filter.MaxStartDate.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value);
+ }
+
+ if (filter.MinPremiereDate.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MinPremiereDate.Value);
+ }
+
+ if (filter.MaxPremiereDate.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value);
+ }
+
+ if (filter.TrailerTypes.Length > 0)
+ {
+ var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray();
+ baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Any(w => w.Id == f)));
+ }
+
+ if (filter.IsAiring.HasValue)
+ {
+ if (filter.IsAiring.Value)
+ {
+ baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now);
+ }
+ else
+ {
+ baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now);
+ }
+ }
+
+ if (filter.PersonIds.Length > 0)
+ {
+ baseQuery = baseQuery
+ .Where(e =>
+ context.PeopleBaseItemMap.Where(w => context.BaseItems.Where(r => filter.PersonIds.Contains(r.Id)).Any(f => f.Name == w.People.Name))
+ .Any(f => f.ItemId == e.Id));
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.Person))
+ {
+ baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.Name == filter.Person));
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.MinSortName))
+ {
+ // this does not makes sense.
+ // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName);
+ // whereClauses.Add("SortName>=@MinSortName");
+ // statement?.TryBind("@MinSortName", query.MinSortName);
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId))
+ {
+ baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId);
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.ExternalId))
+ {
+ baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId);
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.Name))
+ {
+ var cleanName = GetCleanValue(filter.Name);
+ baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
+ }
+
+ // These are the same, for now
+ var nameContains = filter.NameContains;
+ if (!string.IsNullOrWhiteSpace(nameContains))
+ {
+ baseQuery = baseQuery.Where(e =>
+ e.CleanName!.Contains(nameContains)
+ || e.OriginalTitle!.ToLower().Contains(nameContains!));
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
+ {
+ baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith) || e.Name!.StartsWith(filter.NameStartsWith));
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
+ {
+ // i hate this
+ baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() > filter.NameStartsWithOrGreater[0] || e.Name!.FirstOrDefault() > filter.NameStartsWithOrGreater[0]);
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
+ {
+ // i hate this
+ baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() < filter.NameLessThan[0] || e.Name!.FirstOrDefault() < filter.NameLessThan[0]);
+ }
+
+ if (filter.ImageTypes.Length > 0)
+ {
+ var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray();
+ baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Any(w => w.ImageType == f)));
+ }
+
+ if (filter.IsLiked.HasValue)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.Rating >= UserItemData.MinLikeValue);
+ }
+
+ if (filter.IsFavoriteOrLiked.HasValue)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavoriteOrLiked);
+ }
+
+ if (filter.IsFavorite.HasValue)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorite);
+ }
+
+ if (filter.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 (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
+ {
+ baseQuery = baseQuery.Where(e => context.BaseItems
+ .Where(e => e.IsFolder == false && e.IsVirtualItem == false)
+ .Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
+ .Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
+ }
+ else
+ {
+ baseQuery = baseQuery
+ .Select(e => new
+ {
+ IsPlayed = e.UserData!.Where(f => f.UserId == filter.User!.Id).Select(f => (bool?)f.Played).FirstOrDefault() ?? false,
+ Item = e
+ })
+ .Where(e => e.IsPlayed == filter.IsPlayed)
+ .Select(f => f.Item);
+ }
+ }
+
+ if (filter.IsResumable.HasValue)
+ {
+ if (filter.IsResumable.Value)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks > 0);
+ }
+ else
+ {
+ baseQuery = baseQuery
+ .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks == 0);
+ }
+ }
+
+ if (filter.ArtistIds.Length > 0)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type <= ItemValueType.Artist && filter.ArtistIds.Contains(f.ItemId)));
+ }
+
+ if (filter.AlbumArtistIds.Length > 0)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.AlbumArtistIds.Contains(f.ItemId)));
+ }
+
+ if (filter.ContributingArtistIds.Length > 0)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.ContributingArtistIds.Contains(f.ItemId)));
+ }
+
+ if (filter.AlbumIds.Length > 0)
+ {
+ baseQuery = baseQuery.Where(e => context.BaseItems.Where(f => filter.AlbumIds.Contains(f.Id)).Any(f => f.Name == e.Album));
+ }
+
+ if (filter.ExcludeArtistIds.Length > 0)
+ {
+ baseQuery = baseQuery
+ .Where(e => !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.ExcludeArtistIds.Contains(f.ItemId)));
+ }
+
+ if (filter.GenreIds.Count > 0)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Genre && filter.GenreIds.Contains(f.ItemId)));
+ }
+
+ if (filter.Genres.Count > 0)
+ {
+ var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray();
+ baseQuery = baseQuery
+ .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Genre && cleanGenres.Contains(f.ItemValue.CleanValue)));
+ }
+
+ if (tags.Count > 0)
+ {
+ var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray();
+ baseQuery = baseQuery
+ .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && cleanValues.Contains(f.ItemValue.CleanValue)));
+ }
+
+ if (excludeTags.Count > 0)
+ {
+ var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray();
+ baseQuery = baseQuery
+ .Where(e => !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && cleanValues.Contains(f.ItemValue.CleanValue)));
+ }
+
+ if (filter.StudioIds.Length > 0)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Studios && filter.StudioIds.Contains(f.ItemId)));
+ }
+
+ if (filter.OfficialRatings.Length > 0)
+ {
+ baseQuery = baseQuery
+ .Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
+ }
+
+ if (filter.HasParentalRating ?? false)
+ {
+ if (filter.MinParentalRating.HasValue)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.InheritedParentalRatingValue >= filter.MinParentalRating.Value);
+ }
+
+ if (filter.MaxParentalRating.HasValue)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.InheritedParentalRatingValue < filter.MaxParentalRating.Value);
+ }
+ }
+ else if (filter.BlockUnratedItems.Length > 0)
+ {
+ var unratedItems = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
+ if (filter.MinParentalRating.HasValue)
+ {
+ if (filter.MaxParentalRating.HasValue)
+ {
+ baseQuery = baseQuery
+ .Where(e => (e.InheritedParentalRatingValue == null && !unratedItems.Contains(e.UnratedType))
+ || (e.InheritedParentalRatingValue >= filter.MinParentalRating && e.InheritedParentalRatingValue <= filter.MaxParentalRating));
+ }
+ else
+ {
+ baseQuery = baseQuery
+ .Where(e => (e.InheritedParentalRatingValue == null && !unratedItems.Contains(e.UnratedType))
+ || e.InheritedParentalRatingValue >= filter.MinParentalRating);
+ }
+ }
+ else
+ {
+ baseQuery = baseQuery
+ .Where(e => e.InheritedParentalRatingValue != null && !unratedItems.Contains(e.UnratedType));
+ }
+ }
+ else if (filter.MinParentalRating.HasValue)
+ {
+ if (filter.MaxParentalRating.HasValue)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value && e.InheritedParentalRatingValue <= filter.MaxParentalRating.Value);
+ }
+ else
+ {
+ baseQuery = baseQuery
+ .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value);
+ }
+ }
+ else if (filter.MaxParentalRating.HasValue)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MaxParentalRating.Value);
+ }
+ else if (!filter.HasParentalRating ?? false)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.InheritedParentalRatingValue == null);
+ }
+
+ if (filter.HasOfficialRating.HasValue)
+ {
+ if (filter.HasOfficialRating.Value)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty);
+ }
+ else
+ {
+ baseQuery = baseQuery
+ .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty);
+ }
+ }
+
+ if (filter.HasOverview.HasValue)
+ {
+ if (filter.HasOverview.Value)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.Overview != null && e.Overview != string.Empty);
+ }
+ else
+ {
+ baseQuery = baseQuery
+ .Where(e => e.Overview == null || e.Overview == string.Empty);
+ }
+ }
+
+ if (filter.HasOwnerId.HasValue)
+ {
+ if (filter.HasOwnerId.Value)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.OwnerId != null);
+ }
+ else
+ {
+ baseQuery = baseQuery
+ .Where(e => e.OwnerId == null);
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
+ {
+ baseQuery = baseQuery
+ .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && f.Language == filter.HasNoAudioTrackWithLanguage));
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
+ {
+ baseQuery = baseQuery
+ .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal && f.Language == filter.HasNoInternalSubtitleTrackWithLanguage));
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
+ {
+ baseQuery = baseQuery
+ .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && f.Language == filter.HasNoExternalSubtitleTrackWithLanguage));
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
+ {
+ baseQuery = baseQuery
+ .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.Language == filter.HasNoSubtitleTrackWithLanguage));
+ }
+
+ if (filter.HasSubtitles.HasValue)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) == filter.HasSubtitles.Value);
+ }
+
+ if (filter.HasChapterImages.HasValue)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.Chapters!.Any(f => f.ImagePath != null) == filter.HasChapterImages.Value);
+ }
+
+ if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.ParentId.HasValue && !context.BaseItems.Any(f => f.Id == e.ParentId.Value));
+ }
+
+ if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.ItemValues!.Count(f => f.ItemValue.Type == ItemValueType.Artist || f.ItemValue.Type == ItemValueType.AlbumArtist) == 1);
+ }
+
+ if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.ItemValues!.Count(f => f.ItemValue.Type == ItemValueType.Studios) == 1);
+ }
+
+ if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
+ {
+ baseQuery = baseQuery
+ .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
+ }
+
+ if (filter.Years.Length == 1)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.ProductionYear == filter.Years[0]);
+ }
+ else if (filter.Years.Length > 1)
+ {
+ baseQuery = baseQuery
+ .Where(e => filter.Years.Any(f => f == e.ProductionYear));
+ }
+
+ var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
+ if (isVirtualItem.HasValue)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.IsVirtualItem == isVirtualItem.Value);
+ }
+
+ if (filter.IsSpecialSeason.HasValue)
+ {
+ if (filter.IsSpecialSeason.Value)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.IndexNumber == 0);
+ }
+ else
+ {
+ baseQuery = baseQuery
+ .Where(e => e.IndexNumber != 0);
+ }
+ }
+
+ if (filter.IsUnaired.HasValue)
+ {
+ if (filter.IsUnaired.Value)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.PremiereDate >= now);
+ }
+ else
+ {
+ baseQuery = baseQuery
+ .Where(e => e.PremiereDate < now);
+ }
+ }
+
+ if (filter.MediaTypes.Length > 0)
+ {
+ var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
+ baseQuery = baseQuery
+ .Where(e => mediaTypes.Contains(e.MediaType));
+ }
+
+ if (filter.ItemIds.Length > 0)
+ {
+ baseQuery = baseQuery
+ .Where(e => filter.ItemIds.Contains(e.Id));
+ }
+
+ if (filter.ExcludeItemIds.Length > 0)
+ {
+ baseQuery = baseQuery
+ .Where(e => !filter.ItemIds.Contains(e.Id));
+ }
+
+ if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
+ {
+ baseQuery = baseQuery.Where(e => !e.Provider!.All(f => !filter.ExcludeProviderIds.All(w => f.ProviderId == w.Key && f.ProviderValue == w.Value)));
+ }
+
+ if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
+ {
+ baseQuery = baseQuery.Where(e => e.Provider!.Any(f => !filter.HasAnyProviderId.Any(w => f.ProviderId == w.Key && f.ProviderValue == w.Value)));
+ }
+
+ if (filter.HasImdbId.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb"));
+ }
+
+ if (filter.HasTmdbId.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb"));
+ }
+
+ if (filter.HasTvdbId.HasValue)
+ {
+ baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb"));
+ }
+
+ var queryTopParentIds = filter.TopParentIds;
+
+ if (queryTopParentIds.Length > 0)
+ {
+ var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
+ var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
+ if (enableItemsByName && includedItemByNameTypes.Count > 0)
+ {
+ baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => w == e.TopParentId!.Value));
+ }
+ else
+ {
+ baseQuery = baseQuery.Where(e => queryTopParentIds.Contains(e.TopParentId!.Value));
+ }
+ }
+
+ if (filter.AncestorIds.Length > 0)
+ {
+ baseQuery = baseQuery.Where(e => e.Children!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
+ {
+ baseQuery = baseQuery
+ .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.ParentAncestors!.Any(w => w.ItemId == f.Id)));
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
+ {
+ baseQuery = baseQuery
+ .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
+ }
+
+ if (filter.ExcludeInheritedTags.Length > 0)
+ {
+ baseQuery = baseQuery
+ .Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags)
+ .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)));
+ }
+
+ if (filter.IncludeInheritedTags.Length > 0)
+ {
+ // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client.
+ // In addition to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well.
+ if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags)
+ .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
+ ||
+ (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value)!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags)
+ .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
+ }
+
+ // A playlist should be accessible to its owner regardless of allowed tags.
+ else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
+ {
+ baseQuery = baseQuery
+ .Where(e =>
+ e.ParentAncestors!
+ .Any(f =>
+ f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))
+ || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
+ // d ^^ this is stupid it hate this.
+ }
+ else
+ {
+ baseQuery = baseQuery
+ .Where(e => e.ParentAncestors!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))));
+ }
+ }
+
+ if (filter.SeriesStatuses.Length > 0)
+ {
+ var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
+ baseQuery = baseQuery
+ .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
+ }
+
+ if (filter.BoxSetLibraryFolders.Length > 0)
+ {
+ var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).ToArray();
+ baseQuery = baseQuery
+ .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
+ }
+
+ if (filter.VideoTypes.Length > 0)
+ {
+ var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"{e}\"");
+ baseQuery = baseQuery
+ .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f)));
+ }
+
+ if (filter.Is3D.HasValue)
+ {
+ if (filter.Is3D.Value)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.Data!.Contains("Video3DFormat"));
+ }
+ else
+ {
+ baseQuery = baseQuery
+ .Where(e => !e.Data!.Contains("Video3DFormat"));
+ }
+ }
+
+ if (filter.IsPlaceHolder.HasValue)
+ {
+ if (filter.IsPlaceHolder.Value)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.Data!.Contains("IsPlaceHolder\":true"));
+ }
+ else
+ {
+ baseQuery = baseQuery
+ .Where(e => !e.Data!.Contains("IsPlaceHolder\":true"));
+ }
+ }
+
+ if (filter.HasSpecialFeature.HasValue)
+ {
+ if (filter.HasSpecialFeature.Value)
+ {
+ baseQuery = baseQuery
+ .Where(e => e.ExtraIds != null);
+ }
+ else
+ {
+ baseQuery = baseQuery
+ .Where(e => e.ExtraIds == null);
+ }
+ }
+
+ if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue)
+ {
+ if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo.GetValueOrDefault())
+ {
+ baseQuery = baseQuery
+ .Where(e => e.ExtraIds != null);
+ }
+ else
+ {
+ baseQuery = baseQuery
+ .Where(e => e.ExtraIds == null);
+ }
+ }
+
+ return baseQuery;
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs
new file mode 100644
index 000000000..fc6f04d56
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Chapters;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.Item;
+
+/// <summary>
+/// The Chapter manager.
+/// </summary>
+public class ChapterRepository : IChapterRepository
+{
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ private readonly IImageProcessor _imageProcessor;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ChapterRepository"/> class.
+ /// </summary>
+ /// <param name="dbProvider">The EFCore provider.</param>
+ /// <param name="imageProcessor">The Image Processor.</param>
+ public ChapterRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IImageProcessor imageProcessor)
+ {
+ _dbProvider = dbProvider;
+ _imageProcessor = imageProcessor;
+ }
+
+ /// <inheritdoc cref="IChapterRepository"/>
+ public ChapterInfo? GetChapter(BaseItemDto baseItem, int index)
+ {
+ return GetChapter(baseItem.Id, index);
+ }
+
+ /// <inheritdoc cref="IChapterRepository"/>
+ public IReadOnlyList<ChapterInfo> GetChapters(BaseItemDto baseItem)
+ {
+ return GetChapters(baseItem.Id);
+ }
+
+ /// <inheritdoc cref="IChapterRepository"/>
+ public ChapterInfo? GetChapter(Guid baseItemId, int index)
+ {
+ using var context = _dbProvider.CreateDbContext();
+ var chapter = context.Chapters.AsNoTracking()
+ .Select(e => new
+ {
+ chapter = e,
+ baseItemPath = e.Item.Path
+ })
+ .FirstOrDefault(e => e.chapter.ItemId.Equals(baseItemId) && e.chapter.ChapterIndex == index);
+ if (chapter is not null)
+ {
+ return Map(chapter.chapter, chapter.baseItemPath!);
+ }
+
+ return null;
+ }
+
+ /// <inheritdoc cref="IChapterRepository"/>
+ public IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId)
+ {
+ using var context = _dbProvider.CreateDbContext();
+ return context.Chapters.AsNoTracking().Where(e => e.ItemId.Equals(baseItemId))
+ .Select(e => new
+ {
+ chapter = e,
+ baseItemPath = e.Item.Path
+ })
+ .AsEnumerable()
+ .Select(e => Map(e.chapter, e.baseItemPath!))
+ .ToArray();
+ }
+
+ /// <inheritdoc cref="IChapterRepository"/>
+ public void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters)
+ {
+ using var context = _dbProvider.CreateDbContext();
+ using (var transaction = context.Database.BeginTransaction())
+ {
+ context.Chapters.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete();
+ for (var i = 0; i < chapters.Count; i++)
+ {
+ var chapter = chapters[i];
+ context.Chapters.Add(Map(chapter, i, itemId));
+ }
+
+ context.SaveChanges();
+ transaction.Commit();
+ }
+ }
+
+ private Chapter Map(ChapterInfo chapterInfo, int index, Guid itemId)
+ {
+ return new Chapter()
+ {
+ ChapterIndex = index,
+ StartPositionTicks = chapterInfo.StartPositionTicks,
+ ImageDateModified = chapterInfo.ImageDateModified,
+ ImagePath = chapterInfo.ImagePath,
+ ItemId = itemId,
+ Name = chapterInfo.Name,
+ Item = null!
+ };
+ }
+
+ private ChapterInfo Map(Chapter chapterInfo, string baseItemPath)
+ {
+ var chapterEntity = new ChapterInfo()
+ {
+ StartPositionTicks = chapterInfo.StartPositionTicks,
+ ImageDateModified = chapterInfo.ImageDateModified.GetValueOrDefault(),
+ ImagePath = chapterInfo.ImagePath,
+ Name = chapterInfo.Name,
+ };
+ chapterEntity.ImageTag = _imageProcessor.GetImageCacheTag(baseItemPath, chapterEntity.ImageDateModified);
+ return chapterEntity;
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs
new file mode 100644
index 000000000..155798209
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.Item;
+
+/// <summary>
+/// Manager for handling Media Attachments.
+/// </summary>
+/// <param name="dbProvider">Efcore Factory.</param>
+public class MediaAttachmentRepository(IDbContextFactory<JellyfinDbContext> dbProvider) : IMediaAttachmentRepository
+{
+ /// <inheritdoc />
+ public void SaveMediaAttachments(
+ Guid id,
+ IReadOnlyList<MediaAttachment> attachments,
+ CancellationToken cancellationToken)
+ {
+ using var context = dbProvider.CreateDbContext();
+ using var transaction = context.Database.BeginTransaction();
+ context.AttachmentStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete();
+ context.AttachmentStreamInfos.AddRange(attachments.Select(e => Map(e, id)));
+ context.SaveChanges();
+ transaction.Commit();
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery filter)
+ {
+ using var context = dbProvider.CreateDbContext();
+ var query = context.AttachmentStreamInfos.AsNoTracking().Where(e => e.ItemId.Equals(filter.ItemId));
+ if (filter.Index.HasValue)
+ {
+ query = query.Where(e => e.Index == filter.Index);
+ }
+
+ return query.AsEnumerable().Select(Map).ToArray();
+ }
+
+ private MediaAttachment Map(AttachmentStreamInfo attachment)
+ {
+ return new MediaAttachment()
+ {
+ Codec = attachment.Codec,
+ CodecTag = attachment.CodecTag,
+ Comment = attachment.Comment,
+ FileName = attachment.Filename,
+ Index = attachment.Index,
+ MimeType = attachment.MimeType,
+ };
+ }
+
+ private AttachmentStreamInfo Map(MediaAttachment attachment, Guid id)
+ {
+ return new AttachmentStreamInfo()
+ {
+ Codec = attachment.Codec,
+ CodecTag = attachment.CodecTag,
+ Comment = attachment.Comment,
+ Filename = attachment.FileName,
+ Index = attachment.Index,
+ MimeType = attachment.MimeType,
+ ItemId = id,
+ Item = null!
+ };
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
new file mode 100644
index 000000000..f47e3fdfd
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
@@ -0,0 +1,213 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.Item;
+
+/// <summary>
+/// Repository for obtaining MediaStreams.
+/// </summary>
+public class MediaStreamRepository : IMediaStreamRepository
+{
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ private readonly IServerApplicationHost _serverApplicationHost;
+ private readonly ILocalizationManager _localization;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaStreamRepository"/> class.
+ /// </summary>
+ /// <param name="dbProvider">The EFCore db factory.</param>
+ /// <param name="serverApplicationHost">The Application host.</param>
+ /// <param name="localization">The Localisation Provider.</param>
+ public MediaStreamRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IServerApplicationHost serverApplicationHost, ILocalizationManager localization)
+ {
+ _dbProvider = dbProvider;
+ _serverApplicationHost = serverApplicationHost;
+ _localization = localization;
+ }
+
+ /// <inheritdoc />
+ public void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken)
+ {
+ using var context = _dbProvider.CreateDbContext();
+ using var transaction = context.Database.BeginTransaction();
+
+ context.MediaStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete();
+ context.MediaStreamInfos.AddRange(streams.Select(f => Map(f, id)));
+ context.SaveChanges();
+
+ transaction.Commit();
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery filter)
+ {
+ using var context = _dbProvider.CreateDbContext();
+ return TranslateQuery(context.MediaStreamInfos.AsNoTracking(), filter).AsEnumerable().Select(Map).ToArray();
+ }
+
+ private string? GetPathToSave(string? path)
+ {
+ if (path is null)
+ {
+ return null;
+ }
+
+ return _serverApplicationHost.ReverseVirtualPath(path);
+ }
+
+ private string? RestorePath(string? path)
+ {
+ if (path is null)
+ {
+ return null;
+ }
+
+ return _serverApplicationHost.ExpandVirtualPath(path);
+ }
+
+ private IQueryable<MediaStreamInfo> TranslateQuery(IQueryable<MediaStreamInfo> query, MediaStreamQuery filter)
+ {
+ query = query.Where(e => e.ItemId.Equals(filter.ItemId));
+ if (filter.Index.HasValue)
+ {
+ query = query.Where(e => e.StreamIndex == filter.Index);
+ }
+
+ if (filter.Type.HasValue)
+ {
+ var typeValue = (MediaStreamTypeEntity)filter.Type.Value;
+ query = query.Where(e => e.StreamType == typeValue);
+ }
+
+ return query.OrderBy(e => e.StreamIndex);
+ }
+
+ private MediaStream Map(MediaStreamInfo entity)
+ {
+ var dto = new MediaStream();
+ dto.Index = entity.StreamIndex;
+ dto.Type = (MediaStreamType)entity.StreamType;
+
+ dto.IsAVC = entity.IsAvc;
+ dto.Codec = entity.Codec;
+ dto.Language = entity.Language;
+ dto.ChannelLayout = entity.ChannelLayout;
+ dto.Profile = entity.Profile;
+ dto.AspectRatio = entity.AspectRatio;
+ dto.Path = RestorePath(entity.Path);
+ dto.IsInterlaced = entity.IsInterlaced.GetValueOrDefault();
+ dto.BitRate = entity.BitRate;
+ dto.Channels = entity.Channels;
+ dto.SampleRate = entity.SampleRate;
+ dto.IsDefault = entity.IsDefault;
+ dto.IsForced = entity.IsForced;
+ dto.IsExternal = entity.IsExternal;
+ dto.Height = entity.Height;
+ dto.Width = entity.Width;
+ dto.AverageFrameRate = entity.AverageFrameRate;
+ dto.RealFrameRate = entity.RealFrameRate;
+ dto.Level = entity.Level;
+ dto.PixelFormat = entity.PixelFormat;
+ dto.BitDepth = entity.BitDepth;
+ dto.IsAnamorphic = entity.IsAnamorphic;
+ dto.RefFrames = entity.RefFrames;
+ dto.CodecTag = entity.CodecTag;
+ dto.Comment = entity.Comment;
+ dto.NalLengthSize = entity.NalLengthSize;
+ dto.Title = entity.Title;
+ dto.TimeBase = entity.TimeBase;
+ dto.CodecTimeBase = entity.CodecTimeBase;
+ dto.ColorPrimaries = entity.ColorPrimaries;
+ dto.ColorSpace = entity.ColorSpace;
+ dto.ColorTransfer = entity.ColorTransfer;
+ dto.DvVersionMajor = entity.DvVersionMajor;
+ dto.DvVersionMinor = entity.DvVersionMinor;
+ dto.DvProfile = entity.DvProfile;
+ dto.DvLevel = entity.DvLevel;
+ dto.RpuPresentFlag = entity.RpuPresentFlag;
+ dto.ElPresentFlag = entity.ElPresentFlag;
+ dto.BlPresentFlag = entity.BlPresentFlag;
+ dto.DvBlSignalCompatibilityId = entity.DvBlSignalCompatibilityId;
+ dto.IsHearingImpaired = entity.IsHearingImpaired.GetValueOrDefault();
+ dto.Rotation = entity.Rotation;
+
+ if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
+ {
+ dto.LocalizedDefault = _localization.GetLocalizedString("Default");
+ dto.LocalizedExternal = _localization.GetLocalizedString("External");
+
+ if (dto.Type is MediaStreamType.Subtitle)
+ {
+ dto.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
+ dto.LocalizedForced = _localization.GetLocalizedString("Forced");
+ dto.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
+ }
+ }
+
+ return dto;
+ }
+
+ private MediaStreamInfo Map(MediaStream dto, Guid itemId)
+ {
+ var entity = new MediaStreamInfo
+ {
+ Item = null!,
+ ItemId = itemId,
+ StreamIndex = dto.Index,
+ StreamType = (MediaStreamTypeEntity)dto.Type,
+ IsAvc = dto.IsAVC,
+
+ Codec = dto.Codec,
+ Language = dto.Language,
+ ChannelLayout = dto.ChannelLayout,
+ Profile = dto.Profile,
+ AspectRatio = dto.AspectRatio,
+ Path = GetPathToSave(dto.Path) ?? dto.Path,
+ IsInterlaced = dto.IsInterlaced,
+ BitRate = dto.BitRate,
+ Channels = dto.Channels,
+ SampleRate = dto.SampleRate,
+ IsDefault = dto.IsDefault,
+ IsForced = dto.IsForced,
+ IsExternal = dto.IsExternal,
+ Height = dto.Height,
+ Width = dto.Width,
+ AverageFrameRate = dto.AverageFrameRate,
+ RealFrameRate = dto.RealFrameRate,
+ Level = dto.Level.HasValue ? (float)dto.Level : null,
+ PixelFormat = dto.PixelFormat,
+ BitDepth = dto.BitDepth,
+ IsAnamorphic = dto.IsAnamorphic,
+ RefFrames = dto.RefFrames,
+ CodecTag = dto.CodecTag,
+ Comment = dto.Comment,
+ NalLengthSize = dto.NalLengthSize,
+ Title = dto.Title,
+ TimeBase = dto.TimeBase,
+ CodecTimeBase = dto.CodecTimeBase,
+ ColorPrimaries = dto.ColorPrimaries,
+ ColorSpace = dto.ColorSpace,
+ ColorTransfer = dto.ColorTransfer,
+ DvVersionMajor = dto.DvVersionMajor,
+ DvVersionMinor = dto.DvVersionMinor,
+ DvProfile = dto.DvProfile,
+ DvLevel = dto.DvLevel,
+ RpuPresentFlag = dto.RpuPresentFlag,
+ ElPresentFlag = dto.ElPresentFlag,
+ BlPresentFlag = dto.BlPresentFlag,
+ DvBlSignalCompatibilityId = dto.DvBlSignalCompatibilityId,
+ IsHearingImpaired = dto.IsHearingImpaired,
+ Rotation = dto.Rotation
+ };
+ return entity;
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
new file mode 100644
index 000000000..a8dfd4cd3
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
@@ -0,0 +1,190 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Persistence;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.Item;
+#pragma warning disable RS0030 // Do not use banned APIs
+#pragma warning disable CA1304 // Specify CultureInfo
+#pragma warning disable CA1311 // Specify a culture or use an invariant version
+#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
+
+/// <summary>
+/// Manager for handling people.
+/// </summary>
+/// <param name="dbProvider">Efcore Factory.</param>
+/// <param name="itemTypeLookup">Items lookup service.</param>
+/// <remarks>
+/// Initializes a new instance of the <see cref="PeopleRepository"/> class.
+/// </remarks>
+public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IItemTypeLookup itemTypeLookup) : IPeopleRepository
+{
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider = dbProvider;
+
+ /// <inheritdoc/>
+ public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery filter)
+ {
+ using var context = _dbProvider.CreateDbContext();
+ var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter);
+
+ // dbQuery = dbQuery.OrderBy(e => e.ListOrder);
+ if (filter.Limit > 0)
+ {
+ dbQuery = dbQuery.Take(filter.Limit);
+ }
+
+ return dbQuery.AsEnumerable().Select(Map).ToArray();
+ }
+
+ /// <inheritdoc/>
+ public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery filter)
+ {
+ using var context = _dbProvider.CreateDbContext();
+ var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter);
+
+ // dbQuery = dbQuery.OrderBy(e => e.ListOrder);
+ if (filter.Limit > 0)
+ {
+ dbQuery = dbQuery.Take(filter.Limit);
+ }
+
+ return dbQuery.Select(e => e.Name).ToArray();
+ }
+
+ /// <inheritdoc />
+ public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people)
+ {
+ using var context = _dbProvider.CreateDbContext();
+ using var transaction = context.Database.BeginTransaction();
+
+ context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).ExecuteDelete();
+ // TODO: yes for __SOME__ reason there can be duplicates.
+ foreach (var item in people.DistinctBy(e => e.Id))
+ {
+ var personEntity = Map(item);
+ var existingEntity = context.Peoples.FirstOrDefault(e => e.Id == personEntity.Id);
+ if (existingEntity is null)
+ {
+ context.Peoples.Add(personEntity);
+ existingEntity = personEntity;
+ }
+
+ context.PeopleBaseItemMap.Add(new PeopleBaseItemMap()
+ {
+ Item = null!,
+ ItemId = itemId,
+ People = existingEntity,
+ PeopleId = existingEntity.Id,
+ ListOrder = item.SortOrder,
+ SortOrder = item.SortOrder,
+ Role = item.Role
+ });
+ }
+
+ context.SaveChanges();
+ transaction.Commit();
+ }
+
+ private PersonInfo Map(People people)
+ {
+ var personInfo = new PersonInfo()
+ {
+ Id = people.Id,
+ Name = people.Name,
+ };
+ if (Enum.TryParse<PersonKind>(people.PersonType, out var kind))
+ {
+ personInfo.Type = kind;
+ }
+
+ return personInfo;
+ }
+
+ private People Map(PersonInfo people)
+ {
+ var personInfo = new People()
+ {
+ Name = people.Name,
+ PersonType = people.Type.ToString(),
+ Id = people.Id,
+ };
+
+ return personInfo;
+ }
+
+ private IQueryable<People> TranslateQuery(IQueryable<People> query, JellyfinDbContext context, InternalPeopleQuery filter)
+ {
+ if (filter.User is not null && filter.IsFavorite.HasValue)
+ {
+ var personType = itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
+ query = query.Where(e => e.PersonType == personType)
+ .Where(e => context.BaseItems.Where(d => d.UserData!.Any(w => w.IsFavorite == filter.IsFavorite && w.UserId.Equals(filter.User.Id)))
+ .Select(f => f.Name).Contains(e.Name));
+ }
+
+ if (!filter.ItemId.IsEmpty())
+ {
+ query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.ItemId)));
+ }
+
+ if (!filter.AppearsInItemId.IsEmpty())
+ {
+ query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.AppearsInItemId)));
+ }
+
+ var queryPersonTypes = filter.PersonTypes.Where(IsValidPersonType).ToList();
+ if (queryPersonTypes.Count > 0)
+ {
+ query = query.Where(e => queryPersonTypes.Contains(e.PersonType));
+ }
+
+ var queryExcludePersonTypes = filter.ExcludePersonTypes.Where(IsValidPersonType).ToList();
+
+ if (queryExcludePersonTypes.Count > 0)
+ {
+ query = query.Where(e => !queryPersonTypes.Contains(e.PersonType));
+ }
+
+ if (filter.MaxListOrder.HasValue && !filter.ItemId.IsEmpty())
+ {
+ query = query.Where(e => e.BaseItems!.First(w => w.ItemId == filter.ItemId).ListOrder <= filter.MaxListOrder.Value);
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.NameContains))
+ {
+ var nameContainsUpper = filter.NameContains.ToUpper();
+ query = query.Where(e => e.Name.ToUpper().Contains(nameContainsUpper));
+ }
+
+ return query;
+ }
+
+ private bool IsAlphaNumeric(string str)
+ {
+ if (string.IsNullOrWhiteSpace(str))
+ {
+ return false;
+ }
+
+ for (int i = 0; i < str.Length; i++)
+ {
+ if (!char.IsLetter(str[i]) && !char.IsNumber(str[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private bool IsValidPersonType(string value)
+ {
+ return IsAlphaNumeric(value);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 20944ee4b..31cf24fb2 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
index 150bc8bb4..34d9e3960 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
@@ -4,21 +4,19 @@ using Jellyfin.Data.Entities;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Interfaces;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations;
/// <inheritdoc/>
-public class JellyfinDbContext : DbContext
+/// <summary>
+/// Initializes a new instance of the <see cref="JellyfinDbContext"/> class.
+/// </summary>
+/// <param name="options">The database context options.</param>
+/// <param name="logger">Logger.</param>
+public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILogger<JellyfinDbContext> logger) : DbContext(options)
{
/// <summary>
- /// Initializes a new instance of the <see cref="JellyfinDbContext"/> class.
- /// </summary>
- /// <param name="options">The database context options.</param>
- public JellyfinDbContext(DbContextOptions<JellyfinDbContext> options) : base(options)
- {
- }
-
- /// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the access schedules.
/// </summary>
public DbSet<AccessSchedule> AccessSchedules => Set<AccessSchedule>();
@@ -88,6 +86,76 @@ public class JellyfinDbContext : DbContext
/// </summary>
public DbSet<MediaSegment> MediaSegments => Set<MediaSegment>();
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
+ /// </summary>
+ public DbSet<UserData> UserData => Set<UserData>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
+ /// </summary>
+ public DbSet<AncestorId> AncestorIds => Set<AncestorId>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
+ /// </summary>
+ public DbSet<AttachmentStreamInfo> AttachmentStreamInfos => Set<AttachmentStreamInfo>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
+ /// </summary>
+ public DbSet<BaseItemEntity> BaseItems => Set<BaseItemEntity>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
+ /// </summary>
+ public DbSet<Chapter> Chapters => Set<Chapter>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<ItemValue> ItemValues => Set<ItemValue>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<ItemValueMap> ItemValuesMap => Set<ItemValueMap>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<MediaStreamInfo> MediaStreamInfos => Set<MediaStreamInfo>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<People> Peoples => Set<People>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<PeopleBaseItemMap> PeopleBaseItemMap => Set<PeopleBaseItemMap>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the referenced Providers with ids.
+ /// </summary>
+ public DbSet<BaseItemProvider> BaseItemProviders => Set<BaseItemProvider>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<BaseItemImageInfo> BaseItemImageInfos => Set<BaseItemImageInfo>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<BaseItemMetadataField> BaseItemMetadataFields => Set<BaseItemMetadataField>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<BaseItemTrailerType> BaseItemTrailerTypes => Set<BaseItemTrailerType>();
+
/*public DbSet<Artwork> Artwork => Set<Artwork>();
public DbSet<Book> Books => Set<Book>();
@@ -183,7 +251,15 @@ public class JellyfinDbContext : DbContext
saveEntity.OnSavingChanges();
}
- return base.SaveChanges();
+ try
+ {
+ return base.SaveChanges();
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "Error trying to save changes.");
+ throw;
+ }
}
/// <inheritdoc />
@@ -192,7 +268,7 @@ public class JellyfinDbContext : DbContext
modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);
base.OnModelCreating(modelBuilder);
- // Configuration for each entity is in it's own class inside 'ModelConfiguration'.
+ // Configuration for each entity is in its own class inside 'ModelConfiguration'.
modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDbContext).Assembly);
}
}
diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
index d641f521b..59ec418ce 100644
--- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
+++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
@@ -22,7 +22,7 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations.MediaSegments;
/// <summary>
-/// Manages media segments retrival and storage.
+/// Manages media segments retrieval and storage.
/// </summary>
public class MediaSegmentManager : IMediaSegmentManager
{
@@ -61,7 +61,7 @@ public class MediaSegmentManager : IMediaSegmentManager
.Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
.OrderBy(i =>
{
- var index = libraryOptions.MediaSegmentProvideOrder.IndexOf(i.Name);
+ var index = libraryOptions.MediaSegmentProviderOrder.IndexOf(i.Name);
return index == -1 ? int.MaxValue : index;
})
.ToList();
@@ -139,23 +139,53 @@ public class MediaSegmentManager : IMediaSegmentManager
}
/// <inheritdoc />
- public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter)
+ public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true)
+ {
+ var baseItem = _libraryManager.GetItemById(itemId);
+
+ if (baseItem is null)
+ {
+ _logger.LogError("Tried to request segments for an invalid item");
+ return [];
+ }
+
+ return await GetSegmentsAsync(baseItem, typeFilter, filterByProvider).ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true)
{
using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
var query = db.MediaSegments
- .Where(e => e.ItemId.Equals(itemId));
+ .Where(e => e.ItemId.Equals(item.Id));
if (typeFilter is not null)
{
query = query.Where(e => typeFilter.Contains(e.Type));
}
+ if (filterByProvider)
+ {
+ var libraryOptions = _libraryManager.GetLibraryOptions(item);
+ var providerIds = _segmentProviders
+ .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
+ .Select(f => GetProviderId(f.Name))
+ .ToArray();
+ if (providerIds.Length == 0)
+ {
+ return [];
+ }
+
+ query = query.Where(e => providerIds.Contains(e.SegmentProviderId));
+ }
+
return query
.OrderBy(e => e.StartTicks)
.AsNoTracking()
- .ToImmutableList()
- .Select(Map);
+ .AsEnumerable()
+ .Select(Map)
+ .ToArray();
}
private static MediaSegmentDto Map(MediaSegment segment)
diff --git a/Jellyfin.Server.Implementations/Migrations/.gitattributes b/Jellyfin.Server.Implementations/Migrations/.gitattributes
new file mode 100644
index 000000000..da5c26f40
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/.gitattributes
@@ -0,0 +1 @@
+JellyfinDbModelSnapshot.cs binary
diff --git a/Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.Designer.cs
new file mode 100644
index 000000000..27745f601
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.Designer.cs
@@ -0,0 +1,1607 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20241020103111_LibraryDbMigration")]
+ partial class LibraryDbMigration
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("BaseItemEntityId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("BaseItemEntityId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Album")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property<float?>("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property<string>("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Data")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraIds")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property<float?>("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property<string>("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property<string>("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<byte[]>("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+ b.ToTable("BaseItemProviders");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId");
+
+ b.HasIndex("Type", "CleanValue");
+
+ b.ToTable("ItemValues");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("ItemValuesMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamIndex");
+
+ b.HasIndex("StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType", "Language");
+
+ b.ToTable("MediaStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.ToTable("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null)
+ .WithMany("AncestorIds")
+ .HasForeignKey("BaseItemEntityId");
+
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+ .WithMany()
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("AncestorIds");
+
+ b.Navigation("Chapters");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.cs b/Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.cs
new file mode 100644
index 000000000..8cc7fb452
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.cs
@@ -0,0 +1,639 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class LibraryDbMigration : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "BaseItems",
+ columns: table => new
+ {
+ Id = table.Column<Guid>(type: "TEXT", nullable: false),
+ Type = table.Column<string>(type: "TEXT", nullable: false),
+ Data = table.Column<string>(type: "TEXT", nullable: true),
+ Path = table.Column<string>(type: "TEXT", nullable: true),
+ StartDate = table.Column<DateTime>(type: "TEXT", nullable: false),
+ EndDate = table.Column<DateTime>(type: "TEXT", nullable: false),
+ ChannelId = table.Column<string>(type: "TEXT", nullable: true),
+ IsMovie = table.Column<bool>(type: "INTEGER", nullable: false),
+ CommunityRating = table.Column<float>(type: "REAL", nullable: true),
+ CustomRating = table.Column<string>(type: "TEXT", nullable: true),
+ IndexNumber = table.Column<int>(type: "INTEGER", nullable: true),
+ IsLocked = table.Column<bool>(type: "INTEGER", nullable: false),
+ Name = table.Column<string>(type: "TEXT", nullable: true),
+ OfficialRating = table.Column<string>(type: "TEXT", nullable: true),
+ MediaType = table.Column<string>(type: "TEXT", nullable: true),
+ Overview = table.Column<string>(type: "TEXT", nullable: true),
+ ParentIndexNumber = table.Column<int>(type: "INTEGER", nullable: true),
+ PremiereDate = table.Column<DateTime>(type: "TEXT", nullable: true),
+ ProductionYear = table.Column<int>(type: "INTEGER", nullable: true),
+ Genres = table.Column<string>(type: "TEXT", nullable: true),
+ SortName = table.Column<string>(type: "TEXT", nullable: true),
+ ForcedSortName = table.Column<string>(type: "TEXT", nullable: true),
+ RunTimeTicks = table.Column<long>(type: "INTEGER", nullable: true),
+ DateCreated = table.Column<DateTime>(type: "TEXT", nullable: true),
+ DateModified = table.Column<DateTime>(type: "TEXT", nullable: true),
+ IsSeries = table.Column<bool>(type: "INTEGER", nullable: false),
+ EpisodeTitle = table.Column<string>(type: "TEXT", nullable: true),
+ IsRepeat = table.Column<bool>(type: "INTEGER", nullable: false),
+ PreferredMetadataLanguage = table.Column<string>(type: "TEXT", nullable: true),
+ PreferredMetadataCountryCode = table.Column<string>(type: "TEXT", nullable: true),
+ DateLastRefreshed = table.Column<DateTime>(type: "TEXT", nullable: true),
+ DateLastSaved = table.Column<DateTime>(type: "TEXT", nullable: true),
+ IsInMixedFolder = table.Column<bool>(type: "INTEGER", nullable: false),
+ Studios = table.Column<string>(type: "TEXT", nullable: true),
+ ExternalServiceId = table.Column<string>(type: "TEXT", nullable: true),
+ Tags = table.Column<string>(type: "TEXT", nullable: true),
+ IsFolder = table.Column<bool>(type: "INTEGER", nullable: false),
+ InheritedParentalRatingValue = table.Column<int>(type: "INTEGER", nullable: true),
+ UnratedType = table.Column<string>(type: "TEXT", nullable: true),
+ CriticRating = table.Column<float>(type: "REAL", nullable: true),
+ CleanName = table.Column<string>(type: "TEXT", nullable: true),
+ PresentationUniqueKey = table.Column<string>(type: "TEXT", nullable: true),
+ OriginalTitle = table.Column<string>(type: "TEXT", nullable: true),
+ PrimaryVersionId = table.Column<string>(type: "TEXT", nullable: true),
+ DateLastMediaAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
+ Album = table.Column<string>(type: "TEXT", nullable: true),
+ LUFS = table.Column<float>(type: "REAL", nullable: true),
+ NormalizationGain = table.Column<float>(type: "REAL", nullable: true),
+ IsVirtualItem = table.Column<bool>(type: "INTEGER", nullable: false),
+ SeriesName = table.Column<string>(type: "TEXT", nullable: true),
+ SeasonName = table.Column<string>(type: "TEXT", nullable: true),
+ ExternalSeriesId = table.Column<string>(type: "TEXT", nullable: true),
+ Tagline = table.Column<string>(type: "TEXT", nullable: true),
+ ProductionLocations = table.Column<string>(type: "TEXT", nullable: true),
+ ExtraIds = table.Column<string>(type: "TEXT", nullable: true),
+ TotalBitrate = table.Column<int>(type: "INTEGER", nullable: true),
+ ExtraType = table.Column<int>(type: "INTEGER", nullable: true),
+ Artists = table.Column<string>(type: "TEXT", nullable: true),
+ AlbumArtists = table.Column<string>(type: "TEXT", nullable: true),
+ ExternalId = table.Column<string>(type: "TEXT", nullable: true),
+ SeriesPresentationUniqueKey = table.Column<string>(type: "TEXT", nullable: true),
+ ShowId = table.Column<string>(type: "TEXT", nullable: true),
+ OwnerId = table.Column<string>(type: "TEXT", nullable: true),
+ Width = table.Column<int>(type: "INTEGER", nullable: true),
+ Height = table.Column<int>(type: "INTEGER", nullable: true),
+ Size = table.Column<long>(type: "INTEGER", nullable: true),
+ Audio = table.Column<int>(type: "INTEGER", nullable: true),
+ ParentId = table.Column<Guid>(type: "TEXT", nullable: true),
+ TopParentId = table.Column<Guid>(type: "TEXT", nullable: true),
+ SeasonId = table.Column<Guid>(type: "TEXT", nullable: true),
+ SeriesId = table.Column<Guid>(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_BaseItems", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "ItemValues",
+ columns: table => new
+ {
+ ItemValueId = table.Column<Guid>(type: "TEXT", nullable: false),
+ Type = table.Column<int>(type: "INTEGER", nullable: false),
+ Value = table.Column<string>(type: "TEXT", nullable: false),
+ CleanValue = table.Column<string>(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ItemValues", x => x.ItemValueId);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Peoples",
+ columns: table => new
+ {
+ Id = table.Column<Guid>(type: "TEXT", nullable: false),
+ Name = table.Column<string>(type: "TEXT", nullable: false),
+ PersonType = table.Column<string>(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Peoples", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AncestorIds",
+ columns: table => new
+ {
+ ParentItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+ BaseItemEntityId = table.Column<Guid>(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AncestorIds", x => new { x.ItemId, x.ParentItemId });
+ table.ForeignKey(
+ name: "FK_AncestorIds_BaseItems_BaseItemEntityId",
+ column: x => x.BaseItemEntityId,
+ principalTable: "BaseItems",
+ principalColumn: "Id");
+ table.ForeignKey(
+ name: "FK_AncestorIds_BaseItems_ItemId",
+ column: x => x.ItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_AncestorIds_BaseItems_ParentItemId",
+ column: x => x.ParentItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AttachmentStreamInfos",
+ columns: table => new
+ {
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+ Index = table.Column<int>(type: "INTEGER", nullable: false),
+ Codec = table.Column<string>(type: "TEXT", nullable: false),
+ CodecTag = table.Column<string>(type: "TEXT", nullable: true),
+ Comment = table.Column<string>(type: "TEXT", nullable: true),
+ Filename = table.Column<string>(type: "TEXT", nullable: true),
+ MimeType = table.Column<string>(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AttachmentStreamInfos", x => new { x.ItemId, x.Index });
+ table.ForeignKey(
+ name: "FK_AttachmentStreamInfos_BaseItems_ItemId",
+ column: x => x.ItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "BaseItemImageInfos",
+ columns: table => new
+ {
+ Id = table.Column<Guid>(type: "TEXT", nullable: false),
+ Path = table.Column<string>(type: "TEXT", nullable: false),
+ DateModified = table.Column<DateTime>(type: "TEXT", nullable: false),
+ ImageType = table.Column<int>(type: "INTEGER", nullable: false),
+ Width = table.Column<int>(type: "INTEGER", nullable: false),
+ Height = table.Column<int>(type: "INTEGER", nullable: false),
+ Blurhash = table.Column<byte[]>(type: "BLOB", nullable: true),
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_BaseItemImageInfos", x => x.Id);
+ table.ForeignKey(
+ name: "FK_BaseItemImageInfos_BaseItems_ItemId",
+ column: x => x.ItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "BaseItemMetadataFields",
+ columns: table => new
+ {
+ Id = table.Column<int>(type: "INTEGER", nullable: false),
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_BaseItemMetadataFields", x => new { x.Id, x.ItemId });
+ table.ForeignKey(
+ name: "FK_BaseItemMetadataFields_BaseItems_ItemId",
+ column: x => x.ItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "BaseItemProviders",
+ columns: table => new
+ {
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+ ProviderId = table.Column<string>(type: "TEXT", nullable: false),
+ ProviderValue = table.Column<string>(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_BaseItemProviders", x => new { x.ItemId, x.ProviderId });
+ table.ForeignKey(
+ name: "FK_BaseItemProviders_BaseItems_ItemId",
+ column: x => x.ItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "BaseItemTrailerTypes",
+ columns: table => new
+ {
+ Id = table.Column<int>(type: "INTEGER", nullable: false),
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_BaseItemTrailerTypes", x => new { x.Id, x.ItemId });
+ table.ForeignKey(
+ name: "FK_BaseItemTrailerTypes_BaseItems_ItemId",
+ column: x => x.ItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Chapters",
+ columns: table => new
+ {
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+ ChapterIndex = table.Column<int>(type: "INTEGER", nullable: false),
+ StartPositionTicks = table.Column<long>(type: "INTEGER", nullable: false),
+ Name = table.Column<string>(type: "TEXT", nullable: true),
+ ImagePath = table.Column<string>(type: "TEXT", nullable: true),
+ ImageDateModified = table.Column<DateTime>(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Chapters", x => new { x.ItemId, x.ChapterIndex });
+ table.ForeignKey(
+ name: "FK_Chapters_BaseItems_ItemId",
+ column: x => x.ItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "MediaStreamInfos",
+ columns: table => new
+ {
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+ StreamIndex = table.Column<int>(type: "INTEGER", nullable: false),
+ StreamType = table.Column<int>(type: "INTEGER", nullable: true),
+ Codec = table.Column<string>(type: "TEXT", nullable: true),
+ Language = table.Column<string>(type: "TEXT", nullable: true),
+ ChannelLayout = table.Column<string>(type: "TEXT", nullable: true),
+ Profile = table.Column<string>(type: "TEXT", nullable: true),
+ AspectRatio = table.Column<string>(type: "TEXT", nullable: true),
+ Path = table.Column<string>(type: "TEXT", nullable: true),
+ IsInterlaced = table.Column<bool>(type: "INTEGER", nullable: false),
+ BitRate = table.Column<int>(type: "INTEGER", nullable: false),
+ Channels = table.Column<int>(type: "INTEGER", nullable: false),
+ SampleRate = table.Column<int>(type: "INTEGER", nullable: false),
+ IsDefault = table.Column<bool>(type: "INTEGER", nullable: false),
+ IsForced = table.Column<bool>(type: "INTEGER", nullable: false),
+ IsExternal = table.Column<bool>(type: "INTEGER", nullable: false),
+ Height = table.Column<int>(type: "INTEGER", nullable: false),
+ Width = table.Column<int>(type: "INTEGER", nullable: false),
+ AverageFrameRate = table.Column<float>(type: "REAL", nullable: false),
+ RealFrameRate = table.Column<float>(type: "REAL", nullable: false),
+ Level = table.Column<float>(type: "REAL", nullable: false),
+ PixelFormat = table.Column<string>(type: "TEXT", nullable: true),
+ BitDepth = table.Column<int>(type: "INTEGER", nullable: false),
+ IsAnamorphic = table.Column<bool>(type: "INTEGER", nullable: false),
+ RefFrames = table.Column<int>(type: "INTEGER", nullable: false),
+ CodecTag = table.Column<string>(type: "TEXT", nullable: false),
+ Comment = table.Column<string>(type: "TEXT", nullable: false),
+ NalLengthSize = table.Column<string>(type: "TEXT", nullable: false),
+ IsAvc = table.Column<bool>(type: "INTEGER", nullable: false),
+ Title = table.Column<string>(type: "TEXT", nullable: false),
+ TimeBase = table.Column<string>(type: "TEXT", nullable: false),
+ CodecTimeBase = table.Column<string>(type: "TEXT", nullable: false),
+ ColorPrimaries = table.Column<string>(type: "TEXT", nullable: false),
+ ColorSpace = table.Column<string>(type: "TEXT", nullable: false),
+ ColorTransfer = table.Column<string>(type: "TEXT", nullable: false),
+ DvVersionMajor = table.Column<int>(type: "INTEGER", nullable: false),
+ DvVersionMinor = table.Column<int>(type: "INTEGER", nullable: false),
+ DvProfile = table.Column<int>(type: "INTEGER", nullable: false),
+ DvLevel = table.Column<int>(type: "INTEGER", nullable: false),
+ RpuPresentFlag = table.Column<int>(type: "INTEGER", nullable: false),
+ ElPresentFlag = table.Column<int>(type: "INTEGER", nullable: false),
+ BlPresentFlag = table.Column<int>(type: "INTEGER", nullable: false),
+ DvBlSignalCompatibilityId = table.Column<int>(type: "INTEGER", nullable: false),
+ IsHearingImpaired = table.Column<bool>(type: "INTEGER", nullable: false),
+ Rotation = table.Column<int>(type: "INTEGER", nullable: false),
+ KeyFrames = table.Column<string>(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_MediaStreamInfos", x => new { x.ItemId, x.StreamIndex });
+ table.ForeignKey(
+ name: "FK_MediaStreamInfos_BaseItems_ItemId",
+ column: x => x.ItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "UserData",
+ columns: table => new
+ {
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+ UserId = table.Column<Guid>(type: "TEXT", nullable: false),
+ Rating = table.Column<double>(type: "REAL", nullable: true),
+ PlaybackPositionTicks = table.Column<long>(type: "INTEGER", nullable: false),
+ PlayCount = table.Column<int>(type: "INTEGER", nullable: false),
+ IsFavorite = table.Column<bool>(type: "INTEGER", nullable: false),
+ LastPlayedDate = table.Column<DateTime>(type: "TEXT", nullable: true),
+ Played = table.Column<bool>(type: "INTEGER", nullable: false),
+ AudioStreamIndex = table.Column<int>(type: "INTEGER", nullable: true),
+ SubtitleStreamIndex = table.Column<int>(type: "INTEGER", nullable: true),
+ Likes = table.Column<bool>(type: "INTEGER", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_UserData", x => new { x.ItemId, x.UserId });
+ table.ForeignKey(
+ name: "FK_UserData_BaseItems_ItemId",
+ column: x => x.ItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_UserData_Users_UserId",
+ column: x => x.UserId,
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "ItemValuesMap",
+ columns: table => new
+ {
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+ ItemValueId = table.Column<Guid>(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ItemValuesMap", x => new { x.ItemValueId, x.ItemId });
+ table.ForeignKey(
+ name: "FK_ItemValuesMap_BaseItems_ItemId",
+ column: x => x.ItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_ItemValuesMap_ItemValues_ItemValueId",
+ column: x => x.ItemValueId,
+ principalTable: "ItemValues",
+ principalColumn: "ItemValueId",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "PeopleBaseItemMap",
+ columns: table => new
+ {
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+ PeopleId = table.Column<Guid>(type: "TEXT", nullable: false),
+ SortOrder = table.Column<int>(type: "INTEGER", nullable: true),
+ ListOrder = table.Column<int>(type: "INTEGER", nullable: true),
+ Role = table.Column<string>(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_PeopleBaseItemMap", x => new { x.ItemId, x.PeopleId });
+ table.ForeignKey(
+ name: "FK_PeopleBaseItemMap_BaseItems_ItemId",
+ column: x => x.ItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_PeopleBaseItemMap_Peoples_PeopleId",
+ column: x => x.PeopleId,
+ principalTable: "Peoples",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AncestorIds_BaseItemEntityId",
+ table: "AncestorIds",
+ column: "BaseItemEntityId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AncestorIds_ParentItemId",
+ table: "AncestorIds",
+ column: "ParentItemId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItemImageInfos_ItemId",
+ table: "BaseItemImageInfos",
+ column: "ItemId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItemMetadataFields_ItemId",
+ table: "BaseItemMetadataFields",
+ column: "ItemId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItemProviders_ProviderId_ProviderValue_ItemId",
+ table: "BaseItemProviders",
+ columns: new[] { "ProviderId", "ProviderValue", "ItemId" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItems_Id_Type_IsFolder_IsVirtualItem",
+ table: "BaseItems",
+ columns: new[] { "Id", "Type", "IsFolder", "IsVirtualItem" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItems_IsFolder_TopParentId_IsVirtualItem_PresentationUniqueKey_DateCreated",
+ table: "BaseItems",
+ columns: new[] { "IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItems_MediaType_TopParentId_IsVirtualItem_PresentationUniqueKey",
+ table: "BaseItems",
+ columns: new[] { "MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItems_ParentId",
+ table: "BaseItems",
+ column: "ParentId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItems_Path",
+ table: "BaseItems",
+ column: "Path");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItems_PresentationUniqueKey",
+ table: "BaseItems",
+ column: "PresentationUniqueKey");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItems_TopParentId_Id",
+ table: "BaseItems",
+ columns: new[] { "TopParentId", "Id" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItems_Type_SeriesPresentationUniqueKey_IsFolder_IsVirtualItem",
+ table: "BaseItems",
+ columns: new[] { "Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItems_Type_SeriesPresentationUniqueKey_PresentationUniqueKey_SortName",
+ table: "BaseItems",
+ columns: new[] { "Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItems_Type_TopParentId_Id",
+ table: "BaseItems",
+ columns: new[] { "Type", "TopParentId", "Id" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItems_Type_TopParentId_IsVirtualItem_PresentationUniqueKey_DateCreated",
+ table: "BaseItems",
+ columns: new[] { "Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItems_Type_TopParentId_PresentationUniqueKey",
+ table: "BaseItems",
+ columns: new[] { "Type", "TopParentId", "PresentationUniqueKey" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItems_Type_TopParentId_StartDate",
+ table: "BaseItems",
+ columns: new[] { "Type", "TopParentId", "StartDate" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItemTrailerTypes_ItemId",
+ table: "BaseItemTrailerTypes",
+ column: "ItemId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ItemValues_Type_CleanValue",
+ table: "ItemValues",
+ columns: new[] { "Type", "CleanValue" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ItemValuesMap_ItemId",
+ table: "ItemValuesMap",
+ column: "ItemId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_MediaStreamInfos_StreamIndex",
+ table: "MediaStreamInfos",
+ column: "StreamIndex");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_MediaStreamInfos_StreamIndex_StreamType",
+ table: "MediaStreamInfos",
+ columns: new[] { "StreamIndex", "StreamType" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_MediaStreamInfos_StreamIndex_StreamType_Language",
+ table: "MediaStreamInfos",
+ columns: new[] { "StreamIndex", "StreamType", "Language" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_MediaStreamInfos_StreamType",
+ table: "MediaStreamInfos",
+ column: "StreamType");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_PeopleBaseItemMap_ItemId_ListOrder",
+ table: "PeopleBaseItemMap",
+ columns: new[] { "ItemId", "ListOrder" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_PeopleBaseItemMap_ItemId_SortOrder",
+ table: "PeopleBaseItemMap",
+ columns: new[] { "ItemId", "SortOrder" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_PeopleBaseItemMap_PeopleId",
+ table: "PeopleBaseItemMap",
+ column: "PeopleId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Peoples_Name",
+ table: "Peoples",
+ column: "Name");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_UserData_ItemId_UserId_IsFavorite",
+ table: "UserData",
+ columns: new[] { "ItemId", "UserId", "IsFavorite" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_UserData_ItemId_UserId_LastPlayedDate",
+ table: "UserData",
+ columns: new[] { "ItemId", "UserId", "LastPlayedDate" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_UserData_ItemId_UserId_PlaybackPositionTicks",
+ table: "UserData",
+ columns: new[] { "ItemId", "UserId", "PlaybackPositionTicks" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_UserData_ItemId_UserId_Played",
+ table: "UserData",
+ columns: new[] { "ItemId", "UserId", "Played" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_UserData_UserId",
+ table: "UserData",
+ column: "UserId");
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "AncestorIds");
+
+ migrationBuilder.DropTable(
+ name: "AttachmentStreamInfos");
+
+ migrationBuilder.DropTable(
+ name: "BaseItemImageInfos");
+
+ migrationBuilder.DropTable(
+ name: "BaseItemMetadataFields");
+
+ migrationBuilder.DropTable(
+ name: "BaseItemProviders");
+
+ migrationBuilder.DropTable(
+ name: "BaseItemTrailerTypes");
+
+ migrationBuilder.DropTable(
+ name: "Chapters");
+
+ migrationBuilder.DropTable(
+ name: "ItemValuesMap");
+
+ migrationBuilder.DropTable(
+ name: "MediaStreamInfos");
+
+ migrationBuilder.DropTable(
+ name: "PeopleBaseItemMap");
+
+ migrationBuilder.DropTable(
+ name: "UserData");
+
+ migrationBuilder.DropTable(
+ name: "ItemValues");
+
+ migrationBuilder.DropTable(
+ name: "Peoples");
+
+ migrationBuilder.DropTable(
+ name: "BaseItems");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.Designer.cs
new file mode 100644
index 000000000..1fbf21492
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.Designer.cs
@@ -0,0 +1,1610 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20241111131257_AddedCustomDataKey")]
+ partial class AddedCustomDataKey
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("BaseItemEntityId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("BaseItemEntityId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Album")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property<float?>("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property<string>("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Data")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraIds")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property<float?>("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property<string>("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property<string>("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<byte[]>("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+ b.ToTable("BaseItemProviders");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId");
+
+ b.HasIndex("Type", "CleanValue");
+
+ b.ToTable("ItemValues");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("ItemValuesMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamIndex");
+
+ b.HasIndex("StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType", "Language");
+
+ b.ToTable("MediaStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.ToTable("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null)
+ .WithMany("AncestorIds")
+ .HasForeignKey("BaseItemEntityId");
+
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+ .WithMany()
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("AncestorIds");
+
+ b.Navigation("Chapters");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.cs b/Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.cs
new file mode 100644
index 000000000..ac78019ed
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddedCustomDataKey : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<string>(
+ name: "CustomDataKey",
+ table: "UserData",
+ type: "TEXT",
+ nullable: true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "CustomDataKey",
+ table: "UserData");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs
new file mode 100644
index 000000000..bac6fd5b5
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs
@@ -0,0 +1,1610 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20241111135439_AddedCustomDataKeyKey")]
+ partial class AddedCustomDataKeyKey
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("BaseItemEntityId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("BaseItemEntityId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Album")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property<float?>("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property<string>("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Data")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraIds")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property<float?>("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property<string>("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property<string>("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<byte[]>("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+ b.ToTable("BaseItemProviders");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId");
+
+ b.HasIndex("Type", "CleanValue");
+
+ b.ToTable("ItemValues");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("ItemValuesMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamIndex");
+
+ b.HasIndex("StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType", "Language");
+
+ b.ToTable("MediaStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.ToTable("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null)
+ .WithMany("AncestorIds")
+ .HasForeignKey("BaseItemEntityId");
+
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+ .WithMany()
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("AncestorIds");
+
+ b.Navigation("Chapters");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.cs b/Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.cs
new file mode 100644
index 000000000..4558d7c49
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.cs
@@ -0,0 +1,54 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddedCustomDataKeyKey : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropPrimaryKey(
+ name: "PK_UserData",
+ table: "UserData");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "CustomDataKey",
+ table: "UserData",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AddPrimaryKey(
+ name: "PK_UserData",
+ table: "UserData",
+ columns: new[] { "ItemId", "UserId", "CustomDataKey" });
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropPrimaryKey(
+ name: "PK_UserData",
+ table: "UserData");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "CustomDataKey",
+ table: "UserData",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AddPrimaryKey(
+ name: "PK_UserData",
+ table: "UserData",
+ columns: new[] { "ItemId", "UserId" });
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs
new file mode 100644
index 000000000..ad622d44c
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs
@@ -0,0 +1,1603 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20241112152323_FixAncestorIdConfig")]
+ partial class FixAncestorIdConfig
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Album")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property<float?>("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property<string>("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Data")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraIds")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property<float?>("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property<string>("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property<string>("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<byte[]>("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+ b.ToTable("BaseItemProviders");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId");
+
+ b.HasIndex("Type", "CleanValue");
+
+ b.ToTable("ItemValues");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("ItemValuesMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamIndex");
+
+ b.HasIndex("StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType", "Language");
+
+ b.ToTable("MediaStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.ToTable("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Children")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("ParentAncestors")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("ParentAncestors");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.cs b/Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.cs
new file mode 100644
index 000000000..70e81f367
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.cs
@@ -0,0 +1,49 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class FixAncestorIdConfig : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_AncestorIds_BaseItems_BaseItemEntityId",
+ table: "AncestorIds");
+
+ migrationBuilder.DropIndex(
+ name: "IX_AncestorIds_BaseItemEntityId",
+ table: "AncestorIds");
+
+ migrationBuilder.DropColumn(
+ name: "BaseItemEntityId",
+ table: "AncestorIds");
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<Guid>(
+ name: "BaseItemEntityId",
+ table: "AncestorIds",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AncestorIds_BaseItemEntityId",
+ table: "AncestorIds",
+ column: "BaseItemEntityId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_AncestorIds_BaseItems_BaseItemEntityId",
+ table: "AncestorIds",
+ column: "BaseItemEntityId",
+ principalTable: "BaseItems",
+ principalColumn: "Id");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.Designer.cs
new file mode 100644
index 000000000..dc4c8212b
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.Designer.cs
@@ -0,0 +1,1600 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20241112232041_FixMediaStreams")]
+ partial class FixMediaStreams
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Album")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property<float?>("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property<string>("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Data")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraIds")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property<float?>("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property<string>("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property<string>("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<byte[]>("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+ b.ToTable("BaseItemProviders");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId");
+
+ b.HasIndex("Type", "CleanValue");
+
+ b.ToTable("ItemValues");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("ItemValuesMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamIndex");
+
+ b.HasIndex("StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType", "Language");
+
+ b.ToTable("MediaStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.ToTable("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Children")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("ParentAncestors")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("ParentAncestors");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.cs b/Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.cs
new file mode 100644
index 000000000..d57ea81b3
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.cs
@@ -0,0 +1,702 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class FixMediaStreams : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn<int>(
+ name: "Width",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Title",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "TimeBase",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "StreamType",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "SampleRate",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "RpuPresentFlag",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "Rotation",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "RefFrames",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<float>(
+ name: "RealFrameRate",
+ table: "MediaStreamInfos",
+ type: "REAL",
+ nullable: true,
+ oldClrType: typeof(float),
+ oldType: "REAL");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Profile",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Path",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "NalLengthSize",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<float>(
+ name: "Level",
+ table: "MediaStreamInfos",
+ type: "REAL",
+ nullable: true,
+ oldClrType: typeof(float),
+ oldType: "REAL");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Language",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<bool>(
+ name: "IsHearingImpaired",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(bool),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<bool>(
+ name: "IsAvc",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(bool),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<bool>(
+ name: "IsAnamorphic",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(bool),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "Height",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "ElPresentFlag",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "DvVersionMinor",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "DvVersionMajor",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "DvProfile",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "DvLevel",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "DvBlSignalCompatibilityId",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Comment",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "ColorTransfer",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "ColorSpace",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "ColorPrimaries",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "CodecTimeBase",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "CodecTag",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Codec",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "Channels",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "ChannelLayout",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "BlPresentFlag",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "BitRate",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "BitDepth",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<float>(
+ name: "AverageFrameRate",
+ table: "MediaStreamInfos",
+ type: "REAL",
+ nullable: true,
+ oldClrType: typeof(float),
+ oldType: "REAL");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "AspectRatio",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn<int>(
+ name: "Width",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Title",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "TimeBase",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "StreamType",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "SampleRate",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "RpuPresentFlag",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "Rotation",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "RefFrames",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<float>(
+ name: "RealFrameRate",
+ table: "MediaStreamInfos",
+ type: "REAL",
+ nullable: false,
+ defaultValue: 0f,
+ oldClrType: typeof(float),
+ oldType: "REAL",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Profile",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Path",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "NalLengthSize",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<float>(
+ name: "Level",
+ table: "MediaStreamInfos",
+ type: "REAL",
+ nullable: false,
+ defaultValue: 0f,
+ oldClrType: typeof(float),
+ oldType: "REAL",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Language",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<bool>(
+ name: "IsHearingImpaired",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false,
+ oldClrType: typeof(bool),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<bool>(
+ name: "IsAvc",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false,
+ oldClrType: typeof(bool),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<bool>(
+ name: "IsAnamorphic",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false,
+ oldClrType: typeof(bool),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "Height",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "ElPresentFlag",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "DvVersionMinor",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "DvVersionMajor",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "DvProfile",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "DvLevel",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "DvBlSignalCompatibilityId",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Comment",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "ColorTransfer",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "ColorSpace",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "ColorPrimaries",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "CodecTimeBase",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "CodecTag",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Codec",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "Channels",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "ChannelLayout",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "BlPresentFlag",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "BitRate",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<int>(
+ name: "BitDepth",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<float>(
+ name: "AverageFrameRate",
+ table: "MediaStreamInfos",
+ type: "REAL",
+ nullable: false,
+ defaultValue: 0f,
+ oldClrType: typeof(float),
+ oldType: "REAL",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "AspectRatio",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.Designer.cs
new file mode 100644
index 000000000..5714120b5
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.Designer.cs
@@ -0,0 +1,1594 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20241112234144_FixMediaStreams2")]
+ partial class FixMediaStreams2
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Album")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property<float?>("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property<string>("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Data")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraIds")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property<float?>("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property<string>("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property<string>("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<byte[]>("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+ b.ToTable("BaseItemProviders");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId");
+
+ b.HasIndex("Type", "CleanValue");
+
+ b.ToTable("ItemValues");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("ItemValuesMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamIndex");
+
+ b.HasIndex("StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType", "Language");
+
+ b.ToTable("MediaStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.ToTable("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Children")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("ParentAncestors")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("ParentAncestors");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.cs b/Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.cs
new file mode 100644
index 000000000..78611b9e4
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.cs
@@ -0,0 +1,144 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class FixMediaStreams2 : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn<string>(
+ name: "Profile",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Path",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Language",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<bool>(
+ name: "IsInterlaced",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(bool),
+ oldType: "INTEGER");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Codec",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "ChannelLayout",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<string>(
+ name: "AspectRatio",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn<string>(
+ name: "Profile",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Path",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Language",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<bool>(
+ name: "IsInterlaced",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false,
+ oldClrType: typeof(bool),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "Codec",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "ChannelLayout",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<string>(
+ name: "AspectRatio",
+ table: "MediaStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs
new file mode 100644
index 000000000..855f02fd3
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs
@@ -0,0 +1,1595 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20241113133548_EnforceUniqueItemValue")]
+ partial class EnforceUniqueItemValue
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Album")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property<float?>("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property<string>("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Data")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraIds")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property<float?>("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property<string>("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property<string>("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<byte[]>("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+ b.ToTable("BaseItemProviders");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId");
+
+ b.HasIndex("Type", "CleanValue")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("ItemValuesMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamIndex");
+
+ b.HasIndex("StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType", "Language");
+
+ b.ToTable("MediaStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.ToTable("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Children")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("ParentAncestors")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("ParentAncestors");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.cs b/Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.cs
new file mode 100644
index 000000000..d1b06ceae
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.cs
@@ -0,0 +1,37 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class EnforceUniqueItemValue : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_ItemValues_Type_CleanValue",
+ table: "ItemValues");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ItemValues_Type_CleanValue",
+ table: "ItemValues",
+ columns: new[] { "Type", "CleanValue" },
+ unique: true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_ItemValues_Type_CleanValue",
+ table: "ItemValues");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ItemValues_Type_CleanValue",
+ table: "ItemValues",
+ columns: new[] { "Type", "CleanValue" });
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs
new file mode 100644
index 000000000..a329f1ef1
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs
@@ -0,0 +1,1595 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20250204092455_MakeStartEndDateNullable")]
+ partial class MakeStartEndDateNullable
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.1");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Album")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property<float?>("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property<string>("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Data")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraIds")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property<float?>("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property<string>("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property<string>("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<byte[]>("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+ b.ToTable("BaseItemProviders");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId");
+
+ b.HasIndex("Type", "CleanValue")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("ItemValuesMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamIndex");
+
+ b.HasIndex("StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType", "Language");
+
+ b.ToTable("MediaStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.ToTable("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Children")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("ParentAncestors")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("ParentAncestors");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20250204092455_MakeStartEndDateNullable.cs b/Jellyfin.Server.Implementations/Migrations/20250204092455_MakeStartEndDateNullable.cs
new file mode 100644
index 000000000..2c60dd7a6
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20250204092455_MakeStartEndDateNullable.cs
@@ -0,0 +1,55 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class MakeStartEndDateNullable : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "StartDate",
+ table: "BaseItems",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(DateTime),
+ oldType: "TEXT");
+
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "EndDate",
+ table: "BaseItems",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(DateTime),
+ oldType: "TEXT");
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "StartDate",
+ table: "BaseItems",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ oldClrType: typeof(DateTime),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<DateTime>(
+ name: "EndDate",
+ table: "BaseItems",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ oldClrType: typeof(DateTime),
+ oldType: "TEXT",
+ oldNullable: true);
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20250214031148_ChannelIdGuid.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20250214031148_ChannelIdGuid.Designer.cs
new file mode 100644
index 000000000..48919c9b5
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20250214031148_ChannelIdGuid.Designer.cs
@@ -0,0 +1,1595 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20250214031148_ChannelIdGuid")]
+ partial class ChannelIdGuid
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.2");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Album")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property<float?>("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property<string>("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Data")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraIds")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property<float?>("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property<string>("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property<string>("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<byte[]>("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+ b.ToTable("BaseItemProviders");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId");
+
+ b.HasIndex("Type", "CleanValue")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("ItemValuesMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamIndex");
+
+ b.HasIndex("StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType", "Language");
+
+ b.ToTable("MediaStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.ToTable("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Children")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("ParentAncestors")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("ParentAncestors");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20250214031148_ChannelIdGuid.cs b/Jellyfin.Server.Implementations/Migrations/20250214031148_ChannelIdGuid.cs
new file mode 100644
index 000000000..1e904e833
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20250214031148_ChannelIdGuid.cs
@@ -0,0 +1,22 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class ChannelIdGuid : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ // NOOP, Guids and strings are stored the same in SQLite.
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ // NOOP, Guids and strings are stored the same in SQLite.
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs
index 940cf7c5d..500c4a1c7 100644
--- a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs
+++ b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs
@@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.Extensions.Logging.Abstractions;
namespace Jellyfin.Server.Implementations.Migrations
{
@@ -14,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
var optionsBuilder = new DbContextOptionsBuilder<JellyfinDbContext>();
optionsBuilder.UseSqlite("Data Source=jellyfin.db");
- return new JellyfinDbContext(optionsBuilder.Options);
+ return new JellyfinDbContext(optionsBuilder.Options, NullLogger<JellyfinDbContext>.Instance);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index 6e1f985ba..fef122886 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.2");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -90,6 +90,407 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("ActivityLogs");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Album")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property<float?>("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property<string>("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Data")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraIds")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property<float?>("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property<string>("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property<string>("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<byte[]>("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+ b.ToTable("BaseItemProviders");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
{
b.Property<int>("Id")
@@ -270,6 +671,46 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("ItemDisplayPreferences");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId");
+
+ b.HasIndex("Type", "CleanValue")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("ItemValuesMap");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
{
b.Property<Guid>("Id")
@@ -297,6 +738,207 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("MediaSegments");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamIndex");
+
+ b.HasIndex("StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType", "Language");
+
+ b.ToTable("MediaStreamInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.Property<int>("Id")
@@ -613,6 +1255,59 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("Users");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.ToTable("UserData");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
@@ -622,6 +1317,91 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsRequired();
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Children")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("ParentAncestors")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
@@ -657,6 +1437,55 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsRequired();
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
@@ -684,11 +1513,65 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("User");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("ParentAncestors");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Navigation("HomeSections");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Navigation("AccessSchedules");
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs
new file mode 100644
index 000000000..8cc817fb8
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs
@@ -0,0 +1,21 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// AncestorId configuration.
+/// </summary>
+public class AncestorIdConfiguration : IEntityTypeConfiguration<AncestorId>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<AncestorId> builder)
+ {
+ builder.HasKey(e => new { e.ItemId, e.ParentItemId });
+ builder.HasIndex(e => e.ParentItemId);
+ builder.HasOne(e => e.ParentItem).WithMany(e => e.ParentAncestors).HasForeignKey(f => f.ParentItemId);
+ builder.HasOne(e => e.Item).WithMany(e => e.Children).HasForeignKey(f => f.ItemId);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs
new file mode 100644
index 000000000..057b6689a
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs
@@ -0,0 +1,17 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// FluentAPI configuration for the AttachmentStreamInfo entity.
+/// </summary>
+public class AttachmentStreamInfoConfiguration : IEntityTypeConfiguration<AttachmentStreamInfo>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<AttachmentStreamInfo> builder)
+ {
+ builder.HasKey(e => new { e.ItemId, e.Index });
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs
new file mode 100644
index 000000000..eaf48981c
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs
@@ -0,0 +1,59 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using SQLitePCL;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// Configuration for BaseItem.
+/// </summary>
+public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<BaseItemEntity> builder)
+ {
+ builder.HasKey(e => e.Id);
+ // TODO: See rant in entity file.
+ // builder.HasOne(e => e.Parent).WithMany(e => e.DirectChildren).HasForeignKey(e => e.ParentId);
+ // builder.HasOne(e => e.TopParent).WithMany(e => e.AllChildren).HasForeignKey(e => e.TopParentId);
+ // builder.HasOne(e => e.Season).WithMany(e => e.SeasonEpisodes).HasForeignKey(e => e.SeasonId);
+ // builder.HasOne(e => e.Series).WithMany(e => e.SeriesEpisodes).HasForeignKey(e => e.SeriesId);
+ builder.HasMany(e => e.Peoples);
+ builder.HasMany(e => e.UserData);
+ builder.HasMany(e => e.ItemValues);
+ builder.HasMany(e => e.MediaStreams);
+ builder.HasMany(e => e.Chapters);
+ builder.HasMany(e => e.Provider);
+ builder.HasMany(e => e.ParentAncestors);
+ builder.HasMany(e => e.Children);
+ builder.HasMany(e => e.LockedFields);
+ builder.HasMany(e => e.TrailerTypes);
+ builder.HasMany(e => e.Images);
+
+ builder.HasIndex(e => e.Path);
+ builder.HasIndex(e => e.ParentId);
+ builder.HasIndex(e => e.PresentationUniqueKey);
+ builder.HasIndex(e => new { e.Id, e.Type, e.IsFolder, e.IsVirtualItem });
+
+ // covering index
+ builder.HasIndex(e => new { e.TopParentId, e.Id });
+ // series
+ builder.HasIndex(e => new { e.Type, e.SeriesPresentationUniqueKey, e.PresentationUniqueKey, e.SortName });
+ // series counts
+ // seriesdateplayed sort order
+ builder.HasIndex(e => new { e.Type, e.SeriesPresentationUniqueKey, e.IsFolder, e.IsVirtualItem });
+ // live tv programs
+ builder.HasIndex(e => new { e.Type, e.TopParentId, e.StartDate });
+ // covering index for getitemvalues
+ builder.HasIndex(e => new { e.Type, e.TopParentId, e.Id });
+ // used by movie suggestions
+ builder.HasIndex(e => new { e.Type, e.TopParentId, e.PresentationUniqueKey });
+ // latest items
+ builder.HasIndex(e => new { e.Type, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey, e.DateCreated });
+ builder.HasIndex(e => new { e.IsFolder, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey, e.DateCreated });
+ // resume
+ builder.HasIndex(e => new { e.MediaType, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey });
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs
new file mode 100644
index 000000000..137f4a883
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Linq;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Model.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using SQLitePCL;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// Provides configuration for the BaseItemMetadataField entity.
+/// </summary>
+public class BaseItemMetadataFieldConfiguration : IEntityTypeConfiguration<BaseItemMetadataField>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<BaseItemMetadataField> builder)
+ {
+ builder.HasKey(e => new { e.Id, e.ItemId });
+ builder.HasOne(e => e.Item);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs
new file mode 100644
index 000000000..d15049a1f
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs
@@ -0,0 +1,20 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// BaseItemProvider configuration.
+/// </summary>
+public class BaseItemProviderConfiguration : IEntityTypeConfiguration<BaseItemProvider>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<BaseItemProvider> builder)
+ {
+ builder.HasKey(e => new { e.ItemId, e.ProviderId });
+ builder.HasOne(e => e.Item);
+ builder.HasIndex(e => new { e.ProviderId, e.ProviderValue, e.ItemId });
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs
new file mode 100644
index 000000000..f03d99c29
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Linq;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Model.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using SQLitePCL;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// Provides configuration for the BaseItemMetadataField entity.
+/// </summary>
+public class BaseItemTrailerTypeConfiguration : IEntityTypeConfiguration<BaseItemTrailerType>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<BaseItemTrailerType> builder)
+ {
+ builder.HasKey(e => new { e.Id, e.ItemId });
+ builder.HasOne(e => e.Item);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs
new file mode 100644
index 000000000..5a84f7750
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs
@@ -0,0 +1,19 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// Chapter configuration.
+/// </summary>
+public class ChapterConfiguration : IEntityTypeConfiguration<Chapter>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<Chapter> builder)
+ {
+ builder.HasKey(e => new { e.ItemId, e.ChapterIndex });
+ builder.HasOne(e => e.Item);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs
new file mode 100644
index 000000000..abeeb09c9
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs
@@ -0,0 +1,19 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// itemvalues Configuration.
+/// </summary>
+public class ItemValuesConfiguration : IEntityTypeConfiguration<ItemValue>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<ItemValue> builder)
+ {
+ builder.HasKey(e => e.ItemValueId);
+ builder.HasIndex(e => new { e.Type, e.CleanValue }).IsUnique();
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs
new file mode 100644
index 000000000..9c22b114c
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs
@@ -0,0 +1,20 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// itemvalues Configuration.
+/// </summary>
+public class ItemValuesMapConfiguration : IEntityTypeConfiguration<ItemValueMap>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<ItemValueMap> builder)
+ {
+ builder.HasKey(e => new { e.ItemValueId, e.ItemId });
+ builder.HasOne(e => e.Item);
+ builder.HasOne(e => e.ItemValue);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs
new file mode 100644
index 000000000..7e572f9a3
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs
@@ -0,0 +1,22 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// People configuration.
+/// </summary>
+public class MediaStreamInfoConfiguration : IEntityTypeConfiguration<MediaStreamInfo>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<MediaStreamInfo> builder)
+ {
+ builder.HasKey(e => new { e.ItemId, e.StreamIndex });
+ builder.HasIndex(e => e.StreamIndex);
+ builder.HasIndex(e => e.StreamType);
+ builder.HasIndex(e => new { e.StreamIndex, e.StreamType });
+ builder.HasIndex(e => new { e.StreamIndex, e.StreamType, e.Language });
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs
new file mode 100644
index 000000000..cdaee9161
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs
@@ -0,0 +1,22 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// People configuration.
+/// </summary>
+public class PeopleBaseItemMapConfiguration : IEntityTypeConfiguration<PeopleBaseItemMap>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<PeopleBaseItemMap> builder)
+ {
+ builder.HasKey(e => new { e.ItemId, e.PeopleId });
+ builder.HasIndex(e => new { e.ItemId, e.SortOrder });
+ builder.HasIndex(e => new { e.ItemId, e.ListOrder });
+ builder.HasOne(e => e.Item);
+ builder.HasOne(e => e.People);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs
new file mode 100644
index 000000000..f3cccb13f
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs
@@ -0,0 +1,20 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// People configuration.
+/// </summary>
+public class PeopleConfiguration : IEntityTypeConfiguration<People>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<People> builder)
+ {
+ builder.HasKey(e => e.Id);
+ builder.HasIndex(e => e.Name);
+ builder.HasMany(e => e.BaseItems);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs
new file mode 100644
index 000000000..7bbb28d43
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs
@@ -0,0 +1,23 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// FluentAPI configuration for the UserData entity.
+/// </summary>
+public class UserDataConfiguration : IEntityTypeConfiguration<UserData>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<UserData> builder)
+ {
+ builder.HasKey(d => new { d.ItemId, d.UserId, d.CustomDataKey });
+ builder.HasIndex(d => new { d.ItemId, d.UserId, d.Played });
+ builder.HasIndex(d => new { d.ItemId, d.UserId, d.PlaybackPositionTicks });
+ builder.HasIndex(d => new { d.ItemId, d.UserId, d.IsFavorite });
+ builder.HasIndex(d => new { d.ItemId, d.UserId, d.LastPlayedDate });
+ builder.HasOne(e => e.Item);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index 2ae722982..9e225393c 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Queries;
using Jellyfin.Extensions;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
@@ -22,17 +23,20 @@ namespace Jellyfin.Server.Implementations.Security
private readonly IUserManager _userManager;
private readonly IDeviceManager _deviceManager;
private readonly IServerApplicationHost _serverApplicationHost;
+ private readonly IServerConfigurationManager _configurationManager;
public AuthorizationContext(
IDbContextFactory<JellyfinDbContext> jellyfinDb,
IUserManager userManager,
IDeviceManager deviceManager,
- IServerApplicationHost serverApplicationHost)
+ IServerApplicationHost serverApplicationHost,
+ IServerConfigurationManager configurationManager)
{
_jellyfinDbProvider = jellyfinDb;
_userManager = userManager;
_deviceManager = deviceManager;
_serverApplicationHost = serverApplicationHost;
+ _configurationManager = configurationManager;
}
public Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext)
@@ -85,12 +89,12 @@ namespace Jellyfin.Server.Implementations.Security
auth.TryGetValue("Token", out token);
}
- if (string.IsNullOrEmpty(token))
+ if (_configurationManager.Configuration.EnableLegacyAuthorization && string.IsNullOrEmpty(token))
{
token = headers["X-Emby-Token"];
}
- if (string.IsNullOrEmpty(token))
+ if (_configurationManager.Configuration.EnableLegacyAuthorization && string.IsNullOrEmpty(token))
{
token = headers["X-MediaBrowser-Token"];
}
@@ -100,8 +104,7 @@ namespace Jellyfin.Server.Implementations.Security
token = queryString["ApiKey"];
}
- // TODO deprecate this query parameter.
- if (string.IsNullOrEmpty(token))
+ if (_configurationManager.Configuration.EnableLegacyAuthorization && string.IsNullOrEmpty(token))
{
token = queryString["api_key"];
}
@@ -113,25 +116,20 @@ namespace Jellyfin.Server.Implementations.Security
DeviceId = deviceId,
Version = version,
Token = token,
- IsAuthenticated = false,
- HasToken = false
+ IsAuthenticated = false
};
- if (string.IsNullOrWhiteSpace(token))
+ if (!authInfo.HasToken)
{
// Request doesn't contain a token.
return authInfo;
}
- authInfo.HasToken = true;
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var device = _deviceManager.GetDevices(
- new DeviceQuery
- {
- AccessToken = token
- }).Items.FirstOrDefault();
+ new DeviceQuery { AccessToken = token }).Items.FirstOrDefault();
if (device is not null)
{
@@ -227,13 +225,13 @@ namespace Jellyfin.Server.Implementations.Security
/// </summary>
/// <param name="httpReq">The HTTP request.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
- private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
+ private Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
{
- var auth = httpReq.Headers["X-Emby-Authorization"];
+ var auth = httpReq.Headers[HeaderNames.Authorization];
- if (string.IsNullOrEmpty(auth))
+ if (_configurationManager.Configuration.EnableLegacyAuthorization && string.IsNullOrEmpty(auth))
{
- auth = httpReq.Headers[HeaderNames.Authorization];
+ auth = httpReq.Headers["X-Emby-Authorization"];
}
return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
@@ -244,7 +242,7 @@ namespace Jellyfin.Server.Implementations.Security
/// </summary>
/// <param name="authorizationHeader">The authorization header.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
- private static Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
+ private Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
{
var firstSpace = authorizationHeader.IndexOf(' ');
@@ -256,8 +254,10 @@ namespace Jellyfin.Server.Implementations.Security
var name = authorizationHeader[..firstSpace];
- if (!name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase)
- && !name.Equals("Emby", StringComparison.OrdinalIgnoreCase))
+ var validName = name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase);
+ validName = validName || (_configurationManager.Configuration.EnableLegacyAuthorization && name.Equals("Emby", StringComparison.OrdinalIgnoreCase));
+
+ if (!validName)
{
return null;
}
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
index cfe385106..5d209b0af 100644
--- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -46,7 +46,7 @@ public class TrickplayManager : ITrickplayManager
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="mediaEncoder">The media encoder.</param>
- /// <param name="fileSystem">The file systen.</param>
+ /// <param name="fileSystem">The file system.</param>
/// <param name="encodingHelper">The encoding helper.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="config">The server configuration manager.</param>
@@ -179,7 +179,7 @@ public class TrickplayManager : ITrickplayManager
{
// Extract images
// Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
- var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
+ var mediaSource = video.GetMediaSources(false).FirstOrDefault(source => Guid.Parse(source.Id).Equals(video.Id));
if (mediaSource is null)
{
@@ -194,6 +194,14 @@ public class TrickplayManager : ITrickplayManager
return;
}
+ // We support video backdrops, but we should not generate trickplay images for them
+ var parentDirectory = Directory.GetParent(mediaPath);
+ if (parentDirectory is not null && string.Equals(parentDirectory.Name, "backdrops", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", mediaPath, video.Id);
+ return;
+ }
+
// The width has to be even, otherwise a lot of filters will not be able to sample it
var actualWidth = 2 * (width / 2);
@@ -238,7 +246,7 @@ public class TrickplayManager : ITrickplayManager
foreach (var tile in existingFiles)
{
var image = _imageEncoder.GetImageSize(tile);
- localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, image.Height);
+ localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, (int)Math.Ceiling((double)image.Height / localTrickplayInfo.TileHeight));
var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tile).Length * 8 / localTrickplayInfo.TileWidth / localTrickplayInfo.TileHeight / (localTrickplayInfo.Interval / 1000));
localTrickplayInfo.Bandwidth = Math.Max(localTrickplayInfo.Bandwidth, bitrate);
}
@@ -536,7 +544,7 @@ public class TrickplayManager : ITrickplayManager
if (trickplayInfo.ThumbnailCount > 0)
{
- const string urlFormat = "{0}.jpg?MediaSourceId={1}&api_key={2}";
+ const string urlFormat = "{0}.jpg?MediaSourceId={1}&ApiKey={2}";
const string decimalFormat = "{0:0.###}";
var resolution = $"{trickplayInfo.Width}x{trickplayInfo.Height}";
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 1b6635938..c7ae0f4db 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -113,7 +113,7 @@ namespace Jellyfin.Server.Implementations.Users
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
- [GeneratedRegex(@"^[\w\ \-'._@]+$")]
+ [GeneratedRegex(@"^[\w\ \-'._@+]+$")]
private static partial Regex ValidUsernameRegex();
/// <inheritdoc/>
diff --git a/Jellyfin.Server/Helpers/StartupHelpers.cs b/Jellyfin.Server/Helpers/StartupHelpers.cs
index 0802b23ad..bbf6d31f1 100644
--- a/Jellyfin.Server/Helpers/StartupHelpers.cs
+++ b/Jellyfin.Server/Helpers/StartupHelpers.cs
@@ -292,13 +292,5 @@ public static class StartupHelpers
// Make sure we have all the code pages we can get
// Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
-
- // Increase the max http request limit
- // The default connection limit is 10 for ASP.NET hosted applications and 2 for all others.
- ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
-
- // Disable the "Expect: 100-Continue" header by default
- // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
- ServicePointManager.Expect100Continue = false;
}
}
diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
index 801026c54..910b5c467 100644
--- a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
+++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
@@ -67,41 +67,43 @@ namespace Jellyfin.Server.Infrastructure
}
/// <inheritdoc />
- protected override Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
+ protected override async Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(result);
if (range is not null && rangeLength == 0)
{
- return Task.CompletedTask;
+ return;
}
// It's a bit of wasted IO to perform this check again, but non-symlinks shouldn't use this code
if (!IsSymLink(result.FileName))
{
- return base.WriteFileAsync(context, result, range, rangeLength);
+ await base.WriteFileAsync(context, result, range, rangeLength).ConfigureAwait(false);
+ return;
}
var response = context.HttpContext.Response;
if (range is not null)
{
- return SendFileAsync(
+ await SendFileAsync(
result.FileName,
response,
offset: range.From ?? 0L,
- count: rangeLength);
+ count: rangeLength).ConfigureAwait(false);
+ return;
}
- return SendFileAsync(
+ await SendFileAsync(
result.FileName,
response,
offset: 0,
- count: null);
+ count: null).ConfigureAwait(false);
}
- private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count)
+ private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count, CancellationToken cancellationToken = default)
{
var fileInfo = GetFileInfo(filePath);
if (offset < 0 || offset > fileInfo.Length)
@@ -118,6 +120,9 @@ namespace Jellyfin.Server.Infrastructure
// Copied from SendFileFallback.SendFileAsync
const int BufferSize = 1024 * 16;
+ var useRequestAborted = !cancellationToken.CanBeCanceled;
+ var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken;
+
var fileStream = new FileStream(
filePath,
FileMode.Open,
@@ -127,10 +132,17 @@ namespace Jellyfin.Server.Infrastructure
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
await using (fileStream.ConfigureAwait(false))
{
- fileStream.Seek(offset, SeekOrigin.Begin);
- await StreamCopyOperation
- .CopyToAsync(fileStream, response.Body, count, BufferSize, CancellationToken.None)
- .ConfigureAwait(true);
+ try
+ {
+ localCancel.ThrowIfCancellationRequested();
+ fileStream.Seek(offset, SeekOrigin.Begin);
+ await StreamCopyOperation
+ .CopyToAsync(fileStream, response.Body, count, BufferSize, localCancel)
+ .ConfigureAwait(true);
+ }
+ catch (OperationCanceledException) when (useRequestAborted)
+ {
+ }
}
}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index e18212908..ebb12ba4e 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -8,7 +8,7 @@
<PropertyGroup>
<AssemblyName>jellyfin</AssemblyName>
<OutputType>Exe</OutputType>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<ServerGarbageCollection>false</ServerGarbageCollection>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 9d4441ac3..fa799ae6e 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -47,7 +47,9 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.AddDefaultCastReceivers),
typeof(Routines.UpdateDefaultPluginRepository),
typeof(Routines.FixAudioData),
- typeof(Routines.MoveTrickplayFiles)
+ typeof(Routines.MoveTrickplayFiles),
+ typeof(Routines.RemoveDuplicatePlaylistChildren),
+ typeof(Routines.MigrateLibraryDb),
};
/// <summary>
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
index 49960f430..09b292171 100644
--- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS0618 // obsolete
+
using System;
using System.IO;
using System.Xml;
diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
index ee4f8b0ba..5a8ef2e1c 100644
--- a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
+++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
@@ -46,7 +46,7 @@ namespace Jellyfin.Server.Migrations.Routines
public Guid Id => Guid.Parse("{EF103419-8451-40D8-9F34-D1A8E93A1679}");
/// <inheritdoc/>
- public string Name => "CreateLoggingConfigHeirarchy";
+ public string Name => "CreateLoggingConfigHierarchy";
/// <inheritdoc/>
public bool PerformOnNewInstall => false;
diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
index 3655a610d..192c170b2 100644
--- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
+++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
@@ -15,12 +15,12 @@ namespace Jellyfin.Server.Migrations.Routines;
/// </summary>
internal class FixPlaylistOwner : IMigrationRoutine
{
- private readonly ILogger<RemoveDuplicateExtras> _logger;
+ private readonly ILogger<FixPlaylistOwner> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IPlaylistManager _playlistManager;
public FixPlaylistOwner(
- ILogger<RemoveDuplicateExtras> logger,
+ ILogger<FixPlaylistOwner> logger,
ILibraryManager libraryManager,
IPlaylistManager playlistManager)
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
new file mode 100644
index 000000000..3289484f9
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
@@ -0,0 +1,1201 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Data;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Emby.Server.Implementations.Data;
+using Jellyfin.Data.Entities;
+using Jellyfin.Extensions;
+using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Implementations.Item;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Chapter = Jellyfin.Data.Entities.Chapter;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// The migration routine for migrating the userdata database to EF Core.
+/// </summary>
+public class MigrateLibraryDb : IMigrationRoutine
+{
+ private const string DbFilename = "library.db";
+
+ private readonly ILogger<MigrateLibraryDb> _logger;
+ private readonly IServerApplicationPaths _paths;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MigrateLibraryDb"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="provider">The database provider.</param>
+ /// <param name="paths">The server application paths.</param>
+ public MigrateLibraryDb(
+ ILogger<MigrateLibraryDb> logger,
+ IDbContextFactory<JellyfinDbContext> provider,
+ IServerApplicationPaths paths)
+ {
+ _logger = logger;
+ _provider = provider;
+ _paths = paths;
+ }
+
+ /// <inheritdoc/>
+ public Guid Id => Guid.Parse("36445464-849f-429f-9ad0-bb130efa0664");
+
+ /// <inheritdoc/>
+ public string Name => "MigrateLibraryDbData";
+
+ /// <inheritdoc/>
+ public bool PerformOnNewInstall => false; // TODO Change back after testing
+
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ _logger.LogInformation("Migrating the userdata from library.db may take a while, do not stop Jellyfin.");
+
+ var dataPath = _paths.DataPath;
+ var libraryDbPath = Path.Combine(dataPath, DbFilename);
+ using var connection = new SqliteConnection($"Filename={libraryDbPath}");
+ var migrationTotalTime = TimeSpan.Zero;
+
+ var stopwatch = new Stopwatch();
+ stopwatch.Start();
+
+ connection.Open();
+ using var dbContext = _provider.CreateDbContext();
+
+ migrationTotalTime += stopwatch.Elapsed;
+ _logger.LogInformation("Saving UserData entries took {0}.", stopwatch.Elapsed);
+ stopwatch.Restart();
+
+ _logger.LogInformation("Start moving TypedBaseItem.");
+ const string typedBaseItemsQuery = """
+ SELECT guid, type, data, StartDate, EndDate, ChannelId, IsMovie,
+ IsSeries, EpisodeTitle, IsRepeat, CommunityRating, CustomRating, IndexNumber, IsLocked, PreferredMetadataLanguage,
+ PreferredMetadataCountryCode, Width, Height, DateLastRefreshed, Name, Path, PremiereDate, Overview, ParentIndexNumber,
+ ProductionYear, OfficialRating, ForcedSortName, RunTimeTicks, Size, DateCreated, DateModified, Genres, ParentId, TopParentId,
+ Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId,
+ DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId,
+ PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate,
+ ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType FROM TypedBaseItems
+ """;
+ dbContext.BaseItems.ExecuteDelete();
+
+ var legacyBaseItemWithUserKeys = new Dictionary<string, BaseItemEntity>();
+ foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery))
+ {
+ var baseItem = GetItem(dto);
+ dbContext.BaseItems.Add(baseItem.BaseItem);
+ foreach (var dataKey in baseItem.LegacyUserDataKey)
+ {
+ legacyBaseItemWithUserKeys[dataKey] = baseItem.BaseItem;
+ }
+ }
+
+ _logger.LogInformation("Try saving {0} BaseItem entries.", dbContext.BaseItems.Local.Count);
+ dbContext.SaveChanges();
+ migrationTotalTime += stopwatch.Elapsed;
+ _logger.LogInformation("Saving BaseItems entries took {0}.", stopwatch.Elapsed);
+ stopwatch.Restart();
+
+ _logger.LogInformation("Start moving ItemValues.");
+ // do not migrate inherited types as they are now properly mapped in search and lookup.
+ const string itemValueQuery =
+ """
+ SELECT ItemId, Type, Value, CleanValue FROM ItemValues
+ WHERE Type <> 6 AND EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = ItemValues.ItemId)
+ """;
+ dbContext.ItemValues.ExecuteDelete();
+
+ // EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
+ var localItems = new Dictionary<(int Type, string CleanValue), (ItemValue ItemValue, List<Guid> ItemIds)>();
+
+ foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
+ {
+ var itemId = dto.GetGuid(0);
+ var entity = GetItemValue(dto);
+ var key = ((int)entity.Type, entity.CleanValue);
+ if (!localItems.TryGetValue(key, out var existing))
+ {
+ localItems[key] = existing = (entity, []);
+ }
+
+ existing.ItemIds.Add(itemId);
+ }
+
+ foreach (var item in localItems)
+ {
+ dbContext.ItemValues.Add(item.Value.ItemValue);
+ dbContext.ItemValuesMap.AddRange(item.Value.ItemIds.Distinct().Select(f => new ItemValueMap()
+ {
+ Item = null!,
+ ItemValue = null!,
+ ItemId = f,
+ ItemValueId = item.Value.ItemValue.ItemValueId
+ }));
+ }
+
+ _logger.LogInformation("Try saving {0} ItemValues entries.", dbContext.ItemValues.Local.Count);
+ dbContext.SaveChanges();
+ migrationTotalTime += stopwatch.Elapsed;
+ _logger.LogInformation("Saving People ItemValues took {0}.", stopwatch.Elapsed);
+ stopwatch.Restart();
+
+ _logger.LogInformation("Start moving UserData.");
+ var queryResult = connection.Query("""
+ SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex FROM UserDatas
+
+ WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
+ """);
+
+ dbContext.UserData.ExecuteDelete();
+
+ var users = dbContext.Users.AsNoTracking().ToImmutableArray();
+ var oldUserdata = new Dictionary<string, UserData>();
+
+ foreach (var entity in queryResult)
+ {
+ var userData = GetUserData(users, entity);
+ if (userData is null)
+ {
+ _logger.LogError("Was not able to migrate user data with key {0}", entity.GetString(0));
+ continue;
+ }
+
+ if (!legacyBaseItemWithUserKeys.TryGetValue(userData.CustomDataKey!, out var refItem))
+ {
+ _logger.LogError("Was not able to migrate user data with key {0} because it does not reference a valid BaseItem.", entity.GetString(0));
+ continue;
+ }
+
+ userData.ItemId = refItem.Id;
+ dbContext.UserData.Add(userData);
+ }
+
+ _logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count);
+ dbContext.SaveChanges();
+
+ _logger.LogInformation("Start moving MediaStreamInfos.");
+ const string mediaStreamQuery = """
+ SELECT ItemId, StreamIndex, StreamType, Codec, Language, ChannelLayout, Profile, AspectRatio, Path,
+ IsInterlaced, BitRate, Channels, SampleRate, IsDefault, IsForced, IsExternal, Height, Width,
+ AverageFrameRate, RealFrameRate, Level, PixelFormat, BitDepth, IsAnamorphic, RefFrames, CodecTag,
+ Comment, NalLengthSize, IsAvc, Title, TimeBase, CodecTimeBase, ColorPrimaries, ColorSpace, ColorTransfer,
+ DvVersionMajor, DvVersionMinor, DvProfile, DvLevel, RpuPresentFlag, ElPresentFlag, BlPresentFlag, DvBlSignalCompatibilityId, IsHearingImpaired
+ FROM MediaStreams
+ WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = MediaStreams.ItemId)
+ """;
+ dbContext.MediaStreamInfos.ExecuteDelete();
+
+ foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
+ {
+ dbContext.MediaStreamInfos.Add(GetMediaStream(dto));
+ }
+
+ _logger.LogInformation("Try saving {0} MediaStreamInfos entries.", dbContext.MediaStreamInfos.Local.Count);
+ dbContext.SaveChanges();
+
+ migrationTotalTime += stopwatch.Elapsed;
+ _logger.LogInformation("Saving MediaStreamInfos entries took {0}.", stopwatch.Elapsed);
+ stopwatch.Restart();
+
+ _logger.LogInformation("Start moving People.");
+ const string personsQuery = """
+ SELECT ItemId, Name, Role, PersonType, SortOrder FROM People
+ WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = People.ItemId)
+ """;
+ dbContext.Peoples.ExecuteDelete();
+ dbContext.PeopleBaseItemMap.ExecuteDelete();
+
+ var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
+
+ foreach (SqliteDataReader reader in connection.Query(personsQuery))
+ {
+ var itemId = reader.GetGuid(0);
+ if (!dbContext.BaseItems.Any(f => f.Id == itemId))
+ {
+ _logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1));
+ continue;
+ }
+
+ var entity = GetPerson(reader);
+ if (!peopleCache.TryGetValue(entity.Name, out var personCache))
+ {
+ peopleCache[entity.Name] = personCache = (entity, []);
+ }
+
+ if (reader.TryGetString(2, out var role))
+ {
+ }
+
+ if (reader.TryGetInt32(4, out var sortOrder))
+ {
+ }
+
+ personCache.Items.Add(new PeopleBaseItemMap()
+ {
+ Item = null!,
+ ItemId = itemId,
+ People = null!,
+ PeopleId = personCache.Person.Id,
+ ListOrder = sortOrder,
+ SortOrder = sortOrder,
+ Role = role
+ });
+ }
+
+ foreach (var item in peopleCache)
+ {
+ dbContext.Peoples.Add(item.Value.Person);
+ dbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId)));
+ }
+
+ _logger.LogInformation("Try saving {0} People entries.", dbContext.MediaStreamInfos.Local.Count);
+ dbContext.SaveChanges();
+ migrationTotalTime += stopwatch.Elapsed;
+ _logger.LogInformation("Saving People entries took {0}.", stopwatch.Elapsed);
+ stopwatch.Restart();
+
+ _logger.LogInformation("Start moving Chapters.");
+ const string chapterQuery = """
+ SELECT ItemId,StartPositionTicks,Name,ImagePath,ImageDateModified,ChapterIndex from Chapters2
+ WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId)
+ """;
+ dbContext.Chapters.ExecuteDelete();
+
+ foreach (SqliteDataReader dto in connection.Query(chapterQuery))
+ {
+ var chapter = GetChapter(dto);
+ dbContext.Chapters.Add(chapter);
+ }
+
+ _logger.LogInformation("Try saving {0} Chapters entries.", dbContext.Chapters.Local.Count);
+ dbContext.SaveChanges();
+ migrationTotalTime += stopwatch.Elapsed;
+ _logger.LogInformation("Saving Chapters took {0}.", stopwatch.Elapsed);
+ stopwatch.Restart();
+
+ _logger.LogInformation("Start moving AncestorIds.");
+ const string ancestorIdsQuery = """
+ SELECT ItemId, AncestorId, AncestorIdText FROM AncestorIds
+ WHERE
+ EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.ItemId)
+ AND
+ EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId)
+ """;
+ dbContext.Chapters.ExecuteDelete();
+
+ foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
+ {
+ var ancestorId = GetAncestorId(dto);
+ dbContext.AncestorIds.Add(ancestorId);
+ }
+
+ _logger.LogInformation("Try saving {0} AncestorIds entries.", dbContext.Chapters.Local.Count);
+
+ dbContext.SaveChanges();
+ migrationTotalTime += stopwatch.Elapsed;
+ _logger.LogInformation("Saving AncestorIds took {0}.", stopwatch.Elapsed);
+ stopwatch.Restart();
+
+ connection.Close();
+ _logger.LogInformation("Migration of the Library.db done.");
+ _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
+
+ SqliteConnection.ClearAllPools();
+ File.Move(libraryDbPath, libraryDbPath + ".old");
+
+ _logger.LogInformation("Migrating Library db took {0}.", migrationTotalTime);
+
+ if (dbContext.Database.IsSqlite())
+ {
+ _logger.LogInformation("Vacuum and Optimise jellyfin.db now.");
+ dbContext.Database.ExecuteSqlRaw("PRAGMA optimize");
+ dbContext.Database.ExecuteSqlRaw("VACUUM");
+ _logger.LogInformation("jellyfin.db optimized successfully!");
+ }
+ else
+ {
+ _logger.LogInformation("This database doesn't support optimization");
+ }
+ }
+
+ private UserData? GetUserData(ImmutableArray<User> users, SqliteDataReader dto)
+ {
+ var internalUserId = dto.GetInt32(1);
+ var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
+
+ if (user is null)
+ {
+ _logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId, users.Length);
+ return null;
+ }
+
+ var oldKey = dto.GetString(0);
+
+ return new UserData()
+ {
+ ItemId = Guid.NewGuid(),
+ CustomDataKey = oldKey,
+ UserId = user.Id,
+ Rating = dto.IsDBNull(2) ? null : dto.GetDouble(2),
+ Played = dto.GetBoolean(3),
+ PlayCount = dto.GetInt32(4),
+ IsFavorite = dto.GetBoolean(5),
+ PlaybackPositionTicks = dto.GetInt64(6),
+ LastPlayedDate = dto.IsDBNull(7) ? null : dto.GetDateTime(7),
+ AudioStreamIndex = dto.IsDBNull(8) ? null : dto.GetInt32(8),
+ SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9),
+ Likes = null,
+ User = null!,
+ Item = null!
+ };
+ }
+
+ private AncestorId GetAncestorId(SqliteDataReader reader)
+ {
+ return new AncestorId()
+ {
+ ItemId = reader.GetGuid(0),
+ ParentItemId = reader.GetGuid(1),
+ Item = null!,
+ ParentItem = null!
+ };
+ }
+
+ /// <summary>
+ /// Gets the chapter.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <returns>ChapterInfo.</returns>
+ private Chapter GetChapter(SqliteDataReader reader)
+ {
+ var chapter = new Chapter
+ {
+ StartPositionTicks = reader.GetInt64(1),
+ ChapterIndex = reader.GetInt32(5),
+ Item = null!,
+ ItemId = reader.GetGuid(0),
+ };
+
+ if (reader.TryGetString(2, out var chapterName))
+ {
+ chapter.Name = chapterName;
+ }
+
+ if (reader.TryGetString(3, out var imagePath))
+ {
+ chapter.ImagePath = imagePath;
+ }
+
+ if (reader.TryReadDateTime(4, out var imageDateModified))
+ {
+ chapter.ImageDateModified = imageDateModified;
+ }
+
+ return chapter;
+ }
+
+ private ItemValue GetItemValue(SqliteDataReader reader)
+ {
+ return new ItemValue
+ {
+ ItemValueId = Guid.NewGuid(),
+ Type = (ItemValueType)reader.GetInt32(1),
+ Value = reader.GetString(2),
+ CleanValue = reader.GetString(3),
+ };
+ }
+
+ private People GetPerson(SqliteDataReader reader)
+ {
+ var item = new People
+ {
+ Id = Guid.NewGuid(),
+ Name = reader.GetString(1),
+ };
+
+ if (reader.TryGetString(3, out var type))
+ {
+ item.PersonType = type;
+ }
+
+ return item;
+ }
+
+ /// <summary>
+ /// Gets the media stream.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <returns>MediaStream.</returns>
+ private MediaStreamInfo GetMediaStream(SqliteDataReader reader)
+ {
+ var item = new MediaStreamInfo
+ {
+ StreamIndex = reader.GetInt32(1),
+ StreamType = Enum.Parse<MediaStreamTypeEntity>(reader.GetString(2)),
+ Item = null!,
+ ItemId = reader.GetGuid(0),
+ AspectRatio = null!,
+ ChannelLayout = null!,
+ Codec = null!,
+ IsInterlaced = false,
+ Language = null!,
+ Path = null!,
+ Profile = null!,
+ };
+
+ if (reader.TryGetString(3, out var codec))
+ {
+ item.Codec = codec;
+ }
+
+ if (reader.TryGetString(4, out var language))
+ {
+ item.Language = language;
+ }
+
+ if (reader.TryGetString(5, out var channelLayout))
+ {
+ item.ChannelLayout = channelLayout;
+ }
+
+ if (reader.TryGetString(6, out var profile))
+ {
+ item.Profile = profile;
+ }
+
+ if (reader.TryGetString(7, out var aspectRatio))
+ {
+ item.AspectRatio = aspectRatio;
+ }
+
+ if (reader.TryGetString(8, out var path))
+ {
+ item.Path = path;
+ }
+
+ item.IsInterlaced = reader.GetBoolean(9);
+
+ if (reader.TryGetInt32(10, out var bitrate))
+ {
+ item.BitRate = bitrate;
+ }
+
+ if (reader.TryGetInt32(11, out var channels))
+ {
+ item.Channels = channels;
+ }
+
+ if (reader.TryGetInt32(12, out var sampleRate))
+ {
+ item.SampleRate = sampleRate;
+ }
+
+ item.IsDefault = reader.GetBoolean(13);
+ item.IsForced = reader.GetBoolean(14);
+ item.IsExternal = reader.GetBoolean(15);
+
+ if (reader.TryGetInt32(16, out var width))
+ {
+ item.Width = width;
+ }
+
+ if (reader.TryGetInt32(17, out var height))
+ {
+ item.Height = height;
+ }
+
+ if (reader.TryGetSingle(18, out var averageFrameRate))
+ {
+ item.AverageFrameRate = averageFrameRate;
+ }
+
+ if (reader.TryGetSingle(19, out var realFrameRate))
+ {
+ item.RealFrameRate = realFrameRate;
+ }
+
+ if (reader.TryGetSingle(20, out var level))
+ {
+ item.Level = level;
+ }
+
+ if (reader.TryGetString(21, out var pixelFormat))
+ {
+ item.PixelFormat = pixelFormat;
+ }
+
+ if (reader.TryGetInt32(22, out var bitDepth))
+ {
+ item.BitDepth = bitDepth;
+ }
+
+ if (reader.TryGetBoolean(23, out var isAnamorphic))
+ {
+ item.IsAnamorphic = isAnamorphic;
+ }
+
+ if (reader.TryGetInt32(24, out var refFrames))
+ {
+ item.RefFrames = refFrames;
+ }
+
+ if (reader.TryGetString(25, out var codecTag))
+ {
+ item.CodecTag = codecTag;
+ }
+
+ if (reader.TryGetString(26, out var comment))
+ {
+ item.Comment = comment;
+ }
+
+ if (reader.TryGetString(27, out var nalLengthSize))
+ {
+ item.NalLengthSize = nalLengthSize;
+ }
+
+ if (reader.TryGetBoolean(28, out var isAVC))
+ {
+ item.IsAvc = isAVC;
+ }
+
+ if (reader.TryGetString(29, out var title))
+ {
+ item.Title = title;
+ }
+
+ if (reader.TryGetString(30, out var timeBase))
+ {
+ item.TimeBase = timeBase;
+ }
+
+ if (reader.TryGetString(31, out var codecTimeBase))
+ {
+ item.CodecTimeBase = codecTimeBase;
+ }
+
+ if (reader.TryGetString(32, out var colorPrimaries))
+ {
+ item.ColorPrimaries = colorPrimaries;
+ }
+
+ if (reader.TryGetString(33, out var colorSpace))
+ {
+ item.ColorSpace = colorSpace;
+ }
+
+ if (reader.TryGetString(34, out var colorTransfer))
+ {
+ item.ColorTransfer = colorTransfer;
+ }
+
+ if (reader.TryGetInt32(35, out var dvVersionMajor))
+ {
+ item.DvVersionMajor = dvVersionMajor;
+ }
+
+ if (reader.TryGetInt32(36, out var dvVersionMinor))
+ {
+ item.DvVersionMinor = dvVersionMinor;
+ }
+
+ if (reader.TryGetInt32(37, out var dvProfile))
+ {
+ item.DvProfile = dvProfile;
+ }
+
+ if (reader.TryGetInt32(38, out var dvLevel))
+ {
+ item.DvLevel = dvLevel;
+ }
+
+ if (reader.TryGetInt32(39, out var rpuPresentFlag))
+ {
+ item.RpuPresentFlag = rpuPresentFlag;
+ }
+
+ if (reader.TryGetInt32(40, out var elPresentFlag))
+ {
+ item.ElPresentFlag = elPresentFlag;
+ }
+
+ if (reader.TryGetInt32(41, out var blPresentFlag))
+ {
+ item.BlPresentFlag = blPresentFlag;
+ }
+
+ if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
+ {
+ item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
+ }
+
+ item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
+
+ // if (reader.TryGetInt32(44, out var rotation))
+ // {
+ // item.Rotation = rotation;
+ // }
+
+ return item;
+ }
+
+ private (BaseItemEntity BaseItem, string[] LegacyUserDataKey) GetItem(SqliteDataReader reader)
+ {
+ var entity = new BaseItemEntity()
+ {
+ Id = reader.GetGuid(0),
+ Type = reader.GetString(1),
+ };
+
+ var index = 2;
+
+ if (reader.TryGetString(index++, out var data))
+ {
+ entity.Data = data;
+ }
+
+ if (reader.TryReadDateTime(index++, out var startDate))
+ {
+ entity.StartDate = startDate;
+ }
+
+ if (reader.TryReadDateTime(index++, out var endDate))
+ {
+ entity.EndDate = endDate;
+ }
+
+ if (reader.TryGetGuid(index++, out var guid))
+ {
+ entity.ChannelId = guid;
+ }
+
+ if (reader.TryGetBoolean(index++, out var isMovie))
+ {
+ entity.IsMovie = isMovie;
+ }
+
+ if (reader.TryGetBoolean(index++, out var isSeries))
+ {
+ entity.IsSeries = isSeries;
+ }
+
+ if (reader.TryGetString(index++, out var episodeTitle))
+ {
+ entity.EpisodeTitle = episodeTitle;
+ }
+
+ if (reader.TryGetBoolean(index++, out var isRepeat))
+ {
+ entity.IsRepeat = isRepeat;
+ }
+
+ if (reader.TryGetSingle(index++, out var communityRating))
+ {
+ entity.CommunityRating = communityRating;
+ }
+
+ if (reader.TryGetString(index++, out var customRating))
+ {
+ entity.CustomRating = customRating;
+ }
+
+ if (reader.TryGetInt32(index++, out var indexNumber))
+ {
+ entity.IndexNumber = indexNumber;
+ }
+
+ if (reader.TryGetBoolean(index++, out var isLocked))
+ {
+ entity.IsLocked = isLocked;
+ }
+
+ if (reader.TryGetString(index++, out var preferredMetadataLanguage))
+ {
+ entity.PreferredMetadataLanguage = preferredMetadataLanguage;
+ }
+
+ if (reader.TryGetString(index++, out var preferredMetadataCountryCode))
+ {
+ entity.PreferredMetadataCountryCode = preferredMetadataCountryCode;
+ }
+
+ if (reader.TryGetInt32(index++, out var width))
+ {
+ entity.Width = width;
+ }
+
+ if (reader.TryGetInt32(index++, out var height))
+ {
+ entity.Height = height;
+ }
+
+ if (reader.TryReadDateTime(index++, out var dateLastRefreshed))
+ {
+ entity.DateLastRefreshed = dateLastRefreshed;
+ }
+
+ if (reader.TryGetString(index++, out var name))
+ {
+ entity.Name = name;
+ }
+
+ if (reader.TryGetString(index++, out var restorePath))
+ {
+ entity.Path = restorePath;
+ }
+
+ if (reader.TryReadDateTime(index++, out var premiereDate))
+ {
+ entity.PremiereDate = premiereDate;
+ }
+
+ if (reader.TryGetString(index++, out var overview))
+ {
+ entity.Overview = overview;
+ }
+
+ if (reader.TryGetInt32(index++, out var parentIndexNumber))
+ {
+ entity.ParentIndexNumber = parentIndexNumber;
+ }
+
+ if (reader.TryGetInt32(index++, out var productionYear))
+ {
+ entity.ProductionYear = productionYear;
+ }
+
+ if (reader.TryGetString(index++, out var officialRating))
+ {
+ entity.OfficialRating = officialRating;
+ }
+
+ if (reader.TryGetString(index++, out var forcedSortName))
+ {
+ entity.ForcedSortName = forcedSortName;
+ }
+
+ if (reader.TryGetInt64(index++, out var runTimeTicks))
+ {
+ entity.RunTimeTicks = runTimeTicks;
+ }
+
+ if (reader.TryGetInt64(index++, out var size))
+ {
+ entity.Size = size;
+ }
+
+ if (reader.TryReadDateTime(index++, out var dateCreated))
+ {
+ entity.DateCreated = dateCreated;
+ }
+
+ if (reader.TryReadDateTime(index++, out var dateModified))
+ {
+ entity.DateModified = dateModified;
+ }
+
+ if (reader.TryGetString(index++, out var genres))
+ {
+ entity.Genres = genres;
+ }
+
+ if (reader.TryGetGuid(index++, out var parentId))
+ {
+ entity.ParentId = parentId;
+ }
+
+ if (reader.TryGetGuid(index++, out var topParentId))
+ {
+ entity.TopParentId = topParentId;
+ }
+
+ if (reader.TryGetString(index++, out var audioString) && Enum.TryParse<ProgramAudioEntity>(audioString, out var audioType))
+ {
+ entity.Audio = audioType;
+ }
+
+ if (reader.TryGetString(index++, out var serviceName))
+ {
+ entity.ExternalServiceId = serviceName;
+ }
+
+ if (reader.TryGetBoolean(index++, out var isInMixedFolder))
+ {
+ entity.IsInMixedFolder = isInMixedFolder;
+ }
+
+ if (reader.TryReadDateTime(index++, out var dateLastSaved))
+ {
+ entity.DateLastSaved = dateLastSaved;
+ }
+
+ if (reader.TryGetString(index++, out var lockedFields))
+ {
+ entity.LockedFields = lockedFields.Split('|').Select(Enum.Parse<MetadataField>)
+ .Select(e => new BaseItemMetadataField()
+ {
+ Id = (int)e,
+ Item = entity,
+ ItemId = entity.Id
+ })
+ .ToArray();
+ }
+
+ if (reader.TryGetString(index++, out var studios))
+ {
+ entity.Studios = studios;
+ }
+
+ if (reader.TryGetString(index++, out var tags))
+ {
+ entity.Tags = tags;
+ }
+
+ if (reader.TryGetString(index++, out var trailerTypes))
+ {
+ entity.TrailerTypes = trailerTypes.Split('|').Select(Enum.Parse<TrailerType>)
+ .Select(e => new BaseItemTrailerType()
+ {
+ Id = (int)e,
+ Item = entity,
+ ItemId = entity.Id
+ })
+ .ToArray();
+ }
+
+ if (reader.TryGetString(index++, out var originalTitle))
+ {
+ entity.OriginalTitle = originalTitle;
+ }
+
+ if (reader.TryGetString(index++, out var primaryVersionId))
+ {
+ entity.PrimaryVersionId = primaryVersionId;
+ }
+
+ if (reader.TryReadDateTime(index++, out var dateLastMediaAdded))
+ {
+ entity.DateLastMediaAdded = dateLastMediaAdded;
+ }
+
+ if (reader.TryGetString(index++, out var album))
+ {
+ entity.Album = album;
+ }
+
+ if (reader.TryGetSingle(index++, out var lUFS))
+ {
+ entity.LUFS = lUFS;
+ }
+
+ if (reader.TryGetSingle(index++, out var normalizationGain))
+ {
+ entity.NormalizationGain = normalizationGain;
+ }
+
+ if (reader.TryGetSingle(index++, out var criticRating))
+ {
+ entity.CriticRating = criticRating;
+ }
+
+ if (reader.TryGetBoolean(index++, out var isVirtualItem))
+ {
+ entity.IsVirtualItem = isVirtualItem;
+ }
+
+ if (reader.TryGetString(index++, out var seriesName))
+ {
+ entity.SeriesName = seriesName;
+ }
+
+ var userDataKeys = new List<string>();
+ if (reader.TryGetString(index++, out var directUserDataKey))
+ {
+ userDataKeys.Add(directUserDataKey);
+ }
+
+ if (reader.TryGetString(index++, out var seasonName))
+ {
+ entity.SeasonName = seasonName;
+ }
+
+ if (reader.TryGetGuid(index++, out var seasonId))
+ {
+ entity.SeasonId = seasonId;
+ }
+
+ if (reader.TryGetGuid(index++, out var seriesId))
+ {
+ entity.SeriesId = seriesId;
+ }
+
+ if (reader.TryGetString(index++, out var presentationUniqueKey))
+ {
+ entity.PresentationUniqueKey = presentationUniqueKey;
+ }
+
+ if (reader.TryGetInt32(index++, out var parentalRating))
+ {
+ entity.InheritedParentalRatingValue = parentalRating;
+ }
+
+ if (reader.TryGetString(index++, out var externalSeriesId))
+ {
+ entity.ExternalSeriesId = externalSeriesId;
+ }
+
+ if (reader.TryGetString(index++, out var tagLine))
+ {
+ entity.Tagline = tagLine;
+ }
+
+ if (reader.TryGetString(index++, out var providerIds))
+ {
+ entity.Provider = providerIds.Split('|').Select(e => e.Split("="))
+ .Select(e => new BaseItemProvider()
+ {
+ Item = null!,
+ ProviderId = e[0],
+ ProviderValue = e[1]
+ }).ToArray();
+ }
+
+ if (reader.TryGetString(index++, out var imageInfos))
+ {
+ entity.Images = DeserializeImages(imageInfos).Select(f => Map(entity.Id, f)).ToArray();
+ }
+
+ if (reader.TryGetString(index++, out var productionLocations))
+ {
+ entity.ProductionLocations = productionLocations;
+ }
+
+ if (reader.TryGetString(index++, out var extraIds))
+ {
+ entity.ExtraIds = extraIds;
+ }
+
+ if (reader.TryGetInt32(index++, out var totalBitrate))
+ {
+ entity.TotalBitrate = totalBitrate;
+ }
+
+ if (reader.TryGetString(index++, out var extraTypeString) && Enum.TryParse<BaseItemExtraType>(extraTypeString, out var extraType))
+ {
+ entity.ExtraType = extraType;
+ }
+
+ if (reader.TryGetString(index++, out var artists))
+ {
+ entity.Artists = artists;
+ }
+
+ if (reader.TryGetString(index++, out var albumArtists))
+ {
+ entity.AlbumArtists = albumArtists;
+ }
+
+ if (reader.TryGetString(index++, out var externalId))
+ {
+ entity.ExternalId = externalId;
+ }
+
+ if (reader.TryGetString(index++, out var seriesPresentationUniqueKey))
+ {
+ entity.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
+ }
+
+ if (reader.TryGetString(index++, out var showId))
+ {
+ entity.ShowId = showId;
+ }
+
+ if (reader.TryGetString(index++, out var ownerId))
+ {
+ entity.OwnerId = ownerId;
+ }
+
+ if (reader.TryGetString(index++, out var mediaType))
+ {
+ entity.MediaType = mediaType;
+ }
+
+ var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false);
+ var dataKeys = baseItem.GetUserDataKeys();
+ userDataKeys.AddRange(dataKeys);
+
+ return (entity, userDataKeys.ToArray());
+ }
+
+ private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
+ {
+ return new BaseItemImageInfo()
+ {
+ ItemId = baseItemId,
+ Id = Guid.NewGuid(),
+ Path = e.Path,
+ Blurhash = e.BlurHash != null ? Encoding.UTF8.GetBytes(e.BlurHash) : null,
+ DateModified = e.DateModified,
+ Height = e.Height,
+ Width = e.Width,
+ ImageType = (ImageInfoImageType)e.Type,
+ Item = null!
+ };
+ }
+
+ internal ItemImageInfo[] DeserializeImages(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return Array.Empty<ItemImageInfo>();
+ }
+
+ // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed
+ var valueSpan = value.AsSpan();
+ var count = valueSpan.Count('|') + 1;
+
+ var position = 0;
+ var result = new ItemImageInfo[count];
+ foreach (var part in valueSpan.Split('|'))
+ {
+ var image = ItemImageInfoFromValueString(part);
+
+ if (image is not null)
+ {
+ result[position++] = image;
+ }
+ }
+
+ if (position == count)
+ {
+ return result;
+ }
+
+ if (position == 0)
+ {
+ return Array.Empty<ItemImageInfo>();
+ }
+
+ // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
+ return result[..position];
+ }
+
+ internal ItemImageInfo? ItemImageInfoFromValueString(ReadOnlySpan<char> value)
+ {
+ const char Delimiter = '*';
+
+ var nextSegment = value.IndexOf(Delimiter);
+ if (nextSegment == -1)
+ {
+ return null;
+ }
+
+ ReadOnlySpan<char> path = value[..nextSegment];
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf(Delimiter);
+ if (nextSegment == -1)
+ {
+ return null;
+ }
+
+ ReadOnlySpan<char> dateModified = value[..nextSegment];
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf(Delimiter);
+ if (nextSegment == -1)
+ {
+ nextSegment = value.Length;
+ }
+
+ ReadOnlySpan<char> imageType = value[..nextSegment];
+
+ var image = new ItemImageInfo
+ {
+ Path = path.ToString()
+ };
+
+ if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks)
+ && ticks >= DateTime.MinValue.Ticks
+ && ticks <= DateTime.MaxValue.Ticks)
+ {
+ image.DateModified = new DateTime(ticks, DateTimeKind.Utc);
+ }
+ else
+ {
+ return null;
+ }
+
+ if (Enum.TryParse(imageType, true, out ImageType type))
+ {
+ image.Type = type;
+ }
+ else
+ {
+ return null;
+ }
+
+ // Optional parameters: width*height*blurhash
+ if (nextSegment + 1 < value.Length - 1)
+ {
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf(Delimiter);
+ if (nextSegment == -1 || nextSegment == value.Length)
+ {
+ return image;
+ }
+
+ ReadOnlySpan<char> widthSpan = value[..nextSegment];
+
+ value = value[(nextSegment + 1)..];
+ nextSegment = value.IndexOf(Delimiter);
+ if (nextSegment == -1)
+ {
+ nextSegment = value.Length;
+ }
+
+ ReadOnlySpan<char> heightSpan = value[..nextSegment];
+
+ if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width)
+ && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height))
+ {
+ image.Width = width;
+ image.Height = height;
+ }
+
+ if (nextSegment < value.Length - 1)
+ {
+ value = value[(nextSegment + 1)..];
+ var length = value.Length;
+
+ Span<char> blurHashSpan = stackalloc char[length];
+ for (int i = 0; i < length; i++)
+ {
+ var c = value[i];
+ blurHashSpan[i] = c switch
+ {
+ '/' => Delimiter,
+ '\\' => '|',
+ _ => c
+ };
+ }
+
+ image.BlurHash = new string(blurHashSpan);
+ }
+ }
+
+ return image;
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
index 247e1d845..9c2184029 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
@@ -30,7 +30,7 @@ namespace Jellyfin.Server.Migrations.Routines
}
/// <inheritdoc/>
- public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}");
+ public Guid Id => Guid.Parse("{73DAB92A-178B-48CD-B05B-FE18733ACDC8}");
/// <inheritdoc/>
public string Name => "MigrateRatingLevels";
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
new file mode 100644
index 000000000..f84bccc25
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Linq;
+using System.Threading;
+
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Remove duplicate playlist entries.
+/// </summary>
+internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine
+{
+ private readonly ILogger<RemoveDuplicatePlaylistChildren> _logger;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IPlaylistManager _playlistManager;
+
+ public RemoveDuplicatePlaylistChildren(
+ ILogger<RemoveDuplicatePlaylistChildren> logger,
+ ILibraryManager libraryManager,
+ IPlaylistManager playlistManager)
+ {
+ _logger = logger;
+ _libraryManager = libraryManager;
+ _playlistManager = playlistManager;
+ }
+
+ /// <inheritdoc/>
+ public Guid Id => Guid.Parse("{96C156A2-7A13-4B3B-A8B8-FB80C94D20C0}");
+
+ /// <inheritdoc/>
+ public string Name => "RemoveDuplicatePlaylistChildren";
+
+ /// <inheritdoc/>
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc/>
+ public void Perform()
+ {
+ var playlists = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.Playlist]
+ })
+ .Cast<Playlist>()
+ .Where(p => !p.OpenAccess || !p.OwnerUserId.Equals(Guid.Empty))
+ .ToArray();
+
+ if (playlists.Length > 0)
+ {
+ foreach (var playlist in playlists)
+ {
+ var linkedChildren = playlist.LinkedChildren;
+ if (linkedChildren.Length > 0)
+ {
+ var nullItemChildren = linkedChildren.Where(c => c.ItemId is null);
+ var deduplicatedChildren = linkedChildren.DistinctBy(c => c.ItemId);
+ var newLinkedChildren = nullItemChildren.Concat(deduplicatedChildren);
+ playlist.LinkedChildren = linkedChildren;
+ playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
+ _playlistManager.SavePlaylistFile(playlist);
+ }
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 3d92caac4..7e50100b0 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -15,6 +15,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -203,6 +204,7 @@ namespace Jellyfin.Server
// Don't throw additional exception if startup failed.
if (appHost.ServiceProvider is not null)
{
+ var isSqlite = false;
_logger.LogInformation("Running query planner optimizations in the database... This might take a while");
// Run before disposing the application
var context = await appHost.ServiceProvider.GetRequiredService<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false);
@@ -210,9 +212,15 @@ namespace Jellyfin.Server
{
if (context.Database.IsSqlite())
{
+ isSqlite = true;
await context.Database.ExecuteSqlRawAsync("PRAGMA optimize").ConfigureAwait(false);
}
}
+
+ if (isSqlite)
+ {
+ SqliteConnection.ClearAllPools();
+ }
}
_appHost = null;
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index e9fb3e4c2..c68661469 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -129,7 +129,6 @@ namespace Jellyfin.Server
services.AddHostedService<RecordingsHost>();
services.AddHostedService<AutoDiscoveryHost>();
- services.AddHostedService<PortForwardingHost>();
services.AddHostedService<NfoUserDataSaver>();
services.AddHostedService<LibraryChangedNotifier>();
services.AddHostedService<UserDataChangeNotifier>();
diff --git a/Jellyfin.sln b/Jellyfin.sln
index 30eab6cc2..edef9b7a5 100644
--- a/Jellyfin.sln
+++ b/Jellyfin.sln
@@ -30,7 +30,6 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{41093F42-C7CC-4D07-956B-6182CBEDE2EC}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
- jellyfin.ruleset = jellyfin.ruleset
SharedVersion.cs = SharedVersion.cs
EndProjectSection
EndProject
@@ -39,9 +38,6 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Api", "Jellyfin.Api\Jellyfin.Api.csproj", "{DFBEFB4C-DA19-4143-98B7-27320C7F7163}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}"
- ProjectSection(SolutionItems) = preProject
- tests\jellyfin-tests.ruleset = tests\jellyfin-tests.ruleset
- EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Common.Tests", "tests\Jellyfin.Common.Tests\Jellyfin.Common.Tests.csproj", "{DF194677-DFD3-42AF-9F75-D44D5A416478}"
EndProject
diff --git a/MediaBrowser.Common/Configuration/IConfigurationManager.cs b/MediaBrowser.Common/Configuration/IConfigurationManager.cs
index e6696a571..18a8d3e7b 100644
--- a/MediaBrowser.Common/Configuration/IConfigurationManager.cs
+++ b/MediaBrowser.Common/Configuration/IConfigurationManager.cs
@@ -61,7 +61,7 @@ namespace MediaBrowser.Common.Configuration
object GetConfiguration(string key);
/// <summary>
- /// Gets the array of coniguration stores.
+ /// Gets the array of configuration stores.
/// </summary>
/// <returns>Array of ConfigurationStore.</returns>
ConfigurationStore[] GetConfigurationStores();
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index c1945bf93..de6be4707 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Common</PackageId>
- <VersionPrefix>10.10.0</VersionPrefix>
+ <VersionPrefix>10.11.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
@@ -28,7 +28,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
diff --git a/MediaBrowser.Common/Net/NetworkConfiguration.cs b/MediaBrowser.Common/Net/NetworkConfiguration.cs
index 61a51c99e..053357296 100644
--- a/MediaBrowser.Common/Net/NetworkConfiguration.cs
+++ b/MediaBrowser.Common/Net/NetworkConfiguration.cs
@@ -110,6 +110,7 @@ public class NetworkConfiguration
/// <summary>
/// Gets or sets a value indicating whether to enable automatic port forwarding.
/// </summary>
+ [Obsolete("No longer supported")]
public bool EnableUPnP { get; set; }
/// <summary>
diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs
index e482089f0..738096352 100644
--- a/MediaBrowser.Common/Net/NetworkUtils.cs
+++ b/MediaBrowser.Common/Net/NetworkUtils.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
using System.Net;
using System.Net.Sockets;
using System.Text.RegularExpressions;
@@ -102,7 +103,7 @@ public static partial class NetworkUtils
Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.IPv4MaskBytes : NetworkConstants.IPv6MaskBytes];
if (!mask.TryWriteBytes(bytes, out var bytesWritten))
{
- Console.WriteLine("Unable to write address bytes, only ${bytesWritten} bytes written.");
+ Console.WriteLine("Unable to write address bytes, only {0} bytes written.", bytesWritten.ToString(CultureInfo.InvariantCulture));
}
var zeroed = false;
diff --git a/MediaBrowser.Common/Plugins/BasePluginOfT.cs b/MediaBrowser.Common/Plugins/BasePluginOfT.cs
index 116e9cef8..58992ecd7 100644
--- a/MediaBrowser.Common/Plugins/BasePluginOfT.cs
+++ b/MediaBrowser.Common/Plugins/BasePluginOfT.cs
@@ -4,6 +4,7 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
+using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
@@ -20,12 +21,12 @@ namespace MediaBrowser.Common.Plugins
/// <summary>
/// The configuration sync lock.
/// </summary>
- private readonly object _configurationSyncLock = new object();
+ private readonly Lock _configurationSyncLock = new();
/// <summary>
/// The configuration save lock.
/// </summary>
- private readonly object _configurationSaveLock = new object();
+ private readonly Lock _configurationSaveLock = new();
/// <summary>
/// The configuration.
@@ -193,7 +194,7 @@ namespace MediaBrowser.Common.Plugins
}
catch
{
- var config = (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
+ var config = Activator.CreateInstance<TConfigurationType>();
SaveConfiguration(config);
return config;
}
diff --git a/MediaBrowser.Common/RequiresSourceSerialisationAttribute.cs b/MediaBrowser.Common/RequiresSourceSerialisationAttribute.cs
new file mode 100644
index 000000000..b22e7cba1
--- /dev/null
+++ b/MediaBrowser.Common/RequiresSourceSerialisationAttribute.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace MediaBrowser.Common;
+
+/// <summary>
+/// Marks a BaseItem as needing custom serialisation from the Data field of the db.
+/// </summary>
+[System.AttributeUsage(System.AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
+public sealed class RequiresSourceSerialisationAttribute : System.Attribute
+{
+}
diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs
index f186523b9..9e07000bc 100644
--- a/MediaBrowser.Controller/Channels/Channel.cs
+++ b/MediaBrowser.Controller/Channels/Channel.cs
@@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Channels
[JsonIgnore]
public override SourceType SourceType => SourceType.Channel;
- public override bool IsVisible(User user)
+ public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels);
if (blockedChannelsPreference.Length != 0)
@@ -41,7 +41,7 @@ namespace MediaBrowser.Controller.Channels
}
}
- return base.IsVisible(user);
+ return base.IsVisible(user, skipAllowedTagsCheck);
}
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs
deleted file mode 100644
index c049bb97e..000000000
--- a/MediaBrowser.Controller/Chapters/IChapterManager.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Controller.Chapters
-{
- /// <summary>
- /// Interface IChapterManager.
- /// </summary>
- public interface IChapterManager
- {
- /// <summary>
- /// Saves the chapters.
- /// </summary>
- /// <param name="itemId">The item.</param>
- /// <param name="chapters">The set of chapters.</param>
- void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters);
- }
-}
diff --git a/MediaBrowser.Controller/Chapters/IChapterRepository.cs b/MediaBrowser.Controller/Chapters/IChapterRepository.cs
new file mode 100644
index 000000000..e22cb0f58
--- /dev/null
+++ b/MediaBrowser.Controller/Chapters/IChapterRepository.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Chapters;
+
+/// <summary>
+/// Interface IChapterManager.
+/// </summary>
+public interface IChapterRepository
+{
+ /// <summary>
+ /// Saves the chapters.
+ /// </summary>
+ /// <param name="itemId">The item.</param>
+ /// <param name="chapters">The set of chapters.</param>
+ void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters);
+
+ /// <summary>
+ /// Gets all chapters associated with the baseItem.
+ /// </summary>
+ /// <param name="baseItem">The baseitem.</param>
+ /// <returns>A readonly list of chapter instances.</returns>
+ IReadOnlyList<ChapterInfo> GetChapters(BaseItemDto baseItem);
+
+ /// <summary>
+ /// Gets a single chapter of a BaseItem on a specific index.
+ /// </summary>
+ /// <param name="baseItem">The baseitem.</param>
+ /// <param name="index">The index of that chapter.</param>
+ /// <returns>A chapter instance.</returns>
+ ChapterInfo? GetChapter(BaseItemDto baseItem, int index);
+
+ /// <summary>
+ /// Gets all chapters associated with the baseItem.
+ /// </summary>
+ /// <param name="baseItemId">The BaseItems id.</param>
+ /// <returns>A readonly list of chapter instances.</returns>
+ IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId);
+
+ /// <summary>
+ /// Gets a single chapter of a BaseItem on a specific index.
+ /// </summary>
+ /// <param name="baseItemId">The BaseItems id.</param>
+ /// <param name="index">The index of that chapter.</param>
+ /// <returns>A chapter instance.</returns>
+ ChapterInfo? GetChapter(Guid baseItemId, int index);
+}
diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs
index cade53d99..fe7dc1cf9 100644
--- a/MediaBrowser.Controller/Devices/IDeviceManager.cs
+++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs
@@ -58,7 +58,7 @@ public interface IDeviceManager
QueryResult<Device> GetDevices(DeviceQuery query);
/// <summary>
- /// Gets device infromation based on the provided query.
+ /// Gets device information based on the provided query.
/// </summary>
/// <param name="query">The device query.</param>
/// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the device information.</returns>
@@ -109,7 +109,7 @@ public interface IDeviceManager
DeviceOptionsDto? GetDeviceOptions(string deviceId);
/// <summary>
- /// Gets the dto for client capabilites.
+ /// Gets the dto for client capabilities.
/// </summary>
/// <param name="capabilities">The client capabilities.</param>
/// <returns><see cref="ClientCapabilitiesDto"/> of the device.</returns>
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index 0d1e2a5a0..702ce39a2 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Drawing
@@ -60,11 +61,35 @@ namespace MediaBrowser.Controller.Drawing
/// <summary>
/// Gets the image cache tag.
/// </summary>
+ /// <param name="baseItemPath">The items basePath.</param>
+ /// <param name="imageDateModified">The image last modification date.</param>
+ /// <returns>Guid.</returns>
+ string? GetImageCacheTag(string baseItemPath, DateTime imageDateModified);
+
+ /// <summary>
+ /// Gets the image cache tag.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="image">The image.</param>
+ /// <returns>Guid.</returns>
+ string? GetImageCacheTag(BaseItemDto item, ChapterInfo image);
+
+ /// <summary>
+ /// Gets the image cache tag.
+ /// </summary>
/// <param name="item">The item.</param>
/// <param name="image">The image.</param>
/// <returns>Guid.</returns>
string GetImageCacheTag(BaseItem item, ItemImageInfo image);
+ /// <summary>
+ /// Gets the image cache tag.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="image">The image.</param>
+ /// <returns>Guid.</returns>
+ string GetImageCacheTag(BaseItemDto item, ItemImageInfo image);
+
string? GetImageCacheTag(BaseItem item, ChapterInfo chapter);
string? GetImageCacheTag(User user);
diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs
index 40cdd6c91..a02802f41 100644
--- a/MediaBrowser.Controller/Entities/AggregateFolder.cs
+++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs
@@ -23,7 +23,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class AggregateFolder : Folder
{
- private readonly object _childIdsLock = new object();
+ private readonly Lock _childIdsLock = new();
/// <summary>
/// The _virtual children.
@@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Entities
return CreateResolveArgs(directoryService, true).FileSystemChildren;
}
- protected override List<BaseItem> LoadChildren()
+ protected override IReadOnlyList<BaseItem> LoadChildren()
{
lock (_childIdsLock)
{
diff --git a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
index 1625c748a..b085398c5 100644
--- a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
@@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Entities.Audio
IReadOnlyList<string> Artists { get; set; }
}
- public static class Extentions
+ public static class Extensions
{
public static IEnumerable<string> GetAllArtists<T>(this T item)
where T : IHasArtist, IHasAlbumArtist
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
index a0aae8769..f3873775b 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
@@ -21,6 +21,7 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <summary>
/// Class MusicAlbum.
/// </summary>
+ [Common.RequiresSourceSerialisation]
public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer
{
public MusicAlbum()
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index 1ab6c9706..ecb3ac3a6 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -21,6 +21,7 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <summary>
/// Class MusicArtist.
/// </summary>
+ [Common.RequiresSourceSerialisation]
public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo>
{
[JsonIgnore]
@@ -84,7 +85,7 @@ namespace MediaBrowser.Controller.Entities.Audio
return !IsAccessedByName;
}
- public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+ public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
if (query.IncludeItemTypes.Length == 0)
{
@@ -110,15 +111,15 @@ namespace MediaBrowser.Controller.Entities.Audio
return base.IsSaveLocalMetadataEnabled();
}
- protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
+ protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
{
if (IsAccessedByName)
{
// Should never get in here anyway
- return Task.CompletedTask;
+ return;
}
- return base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, false, refreshOptions, directoryService, cancellationToken);
+ await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, false, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false);
}
public override List<string> GetUserDataKeys()
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
index 7448d02ea..65669e680 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
@@ -14,6 +14,7 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <summary>
/// Class MusicGenre.
/// </summary>
+ [Common.RequiresSourceSerialisation]
public class MusicGenre : BaseItem, IItemByName
{
[JsonIgnore]
@@ -64,7 +65,7 @@ namespace MediaBrowser.Controller.Entities.Audio
return true;
}
- public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+ public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
query.GenreIds = new[] { Id };
query.IncludeItemTypes = new[] { BaseItemKind.MusicVideo, BaseItemKind.Audio, BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist };
diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs
index 782481fbc..666bf2a75 100644
--- a/MediaBrowser.Controller/Entities/AudioBook.cs
+++ b/MediaBrowser.Controller/Entities/AudioBook.cs
@@ -9,6 +9,7 @@ using MediaBrowser.Controller.Providers;
namespace MediaBrowser.Controller.Entities
{
+ [Common.RequiresSourceSerialisation]
public class AudioBook : Audio.Audio, IHasSeries, IHasLookupInfo<SongInfo>
{
[JsonIgnore]
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index eb605f6c8..55553da49 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -16,6 +17,7 @@ using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities.Audio;
@@ -479,6 +481,8 @@ namespace MediaBrowser.Controller.Entities
public static IItemRepository ItemRepository { get; set; }
+ public static IChapterRepository ChapterRepository { get; set; }
+
public static IFileSystem FileSystem { get; set; }
public static IUserDataManager UserDataManager { get; set; }
@@ -1041,7 +1045,7 @@ namespace MediaBrowser.Controller.Entities
return PlayAccess.Full;
}
- public virtual List<MediaStream> GetMediaStreams()
+ public virtual IReadOnlyList<MediaStream> GetMediaStreams()
{
return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
{
@@ -1054,7 +1058,7 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- public virtual List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
+ public virtual IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
{
if (SourceType == SourceType.Channel)
{
@@ -1088,7 +1092,7 @@ namespace MediaBrowser.Controller.Entities
return 1;
}).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
.ThenByDescending(i => i, new MediaSourceWidthComparator())
- .ToList();
+ .ToArray();
}
protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
@@ -1299,7 +1303,7 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- if (GetParents().Any(i => !i.IsVisible(user)))
+ if (GetParents().Any(i => !i.IsVisible(user, true)))
{
return false;
}
@@ -1521,13 +1525,14 @@ namespace MediaBrowser.Controller.Entities
/// Determines if a given user has access to this item.
/// </summary>
/// <param name="user">The user.</param>
+ /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
/// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException">If user is null.</exception>
- public bool IsParentalAllowed(User user)
+ public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
{
ArgumentNullException.ThrowIfNull(user);
- if (!IsVisibleViaTags(user))
+ if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
{
return false;
}
@@ -1599,7 +1604,7 @@ namespace MediaBrowser.Controller.Entities
return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
}
- private bool IsVisibleViaTags(User user)
+ private bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
{
var allTags = GetInheritedTags();
if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
@@ -1614,7 +1619,7 @@ namespace MediaBrowser.Controller.Entities
}
var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
- if (allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
+ if (!skipAllowedTagsCheck && allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
@@ -1654,13 +1659,14 @@ namespace MediaBrowser.Controller.Entities
/// Default is just parental allowed. Can be overridden for more functionality.
/// </summary>
/// <param name="user">The user.</param>
+ /// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
/// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
- public virtual bool IsVisible(User user)
+ public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
ArgumentNullException.ThrowIfNull(user);
- return IsParentalAllowed(user);
+ return IsParentalAllowed(user, skipAllowedTagsCheck);
}
public virtual bool IsVisibleStandalone(User user)
@@ -1781,7 +1787,7 @@ namespace MediaBrowser.Controller.Entities
}
else
{
- Studios = [..current, name];
+ Studios = [.. current, name];
}
}
}
@@ -1795,7 +1801,7 @@ namespace MediaBrowser.Controller.Entities
/// Adds a genre to the item.
/// </summary>
/// <param name="name">The name.</param>
- /// <exception cref="ArgumentNullException">Throwns if name is null.</exception>
+ /// <exception cref="ArgumentNullException">Throws if name is null.</exception>
public void AddGenre(string name)
{
ArgumentException.ThrowIfNullOrEmpty(name);
@@ -1803,7 +1809,7 @@ namespace MediaBrowser.Controller.Entities
var genres = Genres;
if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase))
{
- Genres = [..genres, name];
+ Genres = [.. genres, name];
}
}
@@ -1821,7 +1827,10 @@ namespace MediaBrowser.Controller.Entities
{
ArgumentNullException.ThrowIfNull(user);
- var data = UserDataManager.GetUserData(user, this);
+ var data = UserDataManager.GetUserData(user, this) ?? new UserItemData()
+ {
+ Key = GetUserDataKeys().First(),
+ };
if (datePlayed.HasValue)
{
@@ -1974,11 +1983,11 @@ namespace MediaBrowser.Controller.Entities
public void AddImage(ItemImageInfo image)
{
- ImageInfos = [..ImageInfos, image];
+ ImageInfos = [.. ImageInfos, image];
}
- public virtual Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
- => LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken);
+ public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
+ => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false);
/// <summary>
/// Validates that images within the item are still on the filesystem.
@@ -2031,7 +2040,7 @@ namespace MediaBrowser.Controller.Entities
{
if (imageType == ImageType.Chapter)
{
- var chapter = ItemRepository.GetChapter(this, imageIndex);
+ var chapter = ChapterRepository.GetChapter(this.Id, imageIndex);
if (chapter is null)
{
@@ -2081,7 +2090,7 @@ namespace MediaBrowser.Controller.Entities
if (image.Type == ImageType.Chapter)
{
- var chapters = ItemRepository.GetChapters(this);
+ var chapters = ChapterRepository.GetChapters(this.Id);
for (var i = 0; i < chapters.Count; i++)
{
if (chapters[i].ImagePath == image.Path)
@@ -2367,7 +2376,7 @@ namespace MediaBrowser.Controller.Entities
}
}
- protected Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
var newOptions = new MetadataRefreshOptions(options)
{
@@ -2428,10 +2437,10 @@ namespace MediaBrowser.Controller.Entities
}
}
- return ownedItem.RefreshMetadata(newOptions, cancellationToken);
+ await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false);
}
- protected Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken)
+ protected async Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken)
{
var newOptions = new MetadataRefreshOptions(options)
{
@@ -2441,9 +2450,7 @@ namespace MediaBrowser.Controller.Entities
var id = LibraryManager.GetNewItemId(path, typeof(Video));
// Try to retrieve it from the db. If we don't find it, use the resolved version
- var video = LibraryManager.GetItemById(id) as Video;
-
- if (video is null)
+ if (LibraryManager.GetItemById(id) is not Video video)
{
video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video;
@@ -2452,15 +2459,15 @@ namespace MediaBrowser.Controller.Entities
if (video is null)
{
- return Task.FromResult(true);
+ return;
}
if (video.OwnerId.IsEmpty())
{
- video.OwnerId = this.Id;
+ video.OwnerId = Id;
}
- return RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken);
+ await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(false);
}
public string GetEtag(User user)
@@ -2524,7 +2531,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="children">Media children.</param>
/// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns>
- public bool UpdateRatingToItems(IList<BaseItem> children)
+ public bool UpdateRatingToItems(IReadOnlyList<BaseItem> children)
{
var currentOfficialRating = OfficialRating;
diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs
index 66dea1084..518766937 100644
--- a/MediaBrowser.Controller/Entities/Book.cs
+++ b/MediaBrowser.Controller/Entities/Book.cs
@@ -10,6 +10,7 @@ using MediaBrowser.Controller.Providers;
namespace MediaBrowser.Controller.Entities
{
+ [Common.RequiresSourceSerialisation]
public class Book : BaseItem, IHasLookupInfo<BookInfo>, IHasSeries
{
public Book()
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 4ead477f8..b7b5dac03 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -96,11 +96,11 @@ namespace MediaBrowser.Controller.Entities
return GetLibraryOptions(Path);
}
- public override bool IsVisible(User user)
+ public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
if (GetLibraryOptions().Enabled)
{
- return base.IsVisible(user);
+ return base.IsVisible(user, skipAllowedTagsCheck);
}
return false;
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 83c19a54e..957e8b319 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Security;
@@ -11,6 +12,7 @@ using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
+using J2N.Collections.Generic.Extensions;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@@ -217,7 +219,7 @@ namespace MediaBrowser.Controller.Entities
LibraryManager.CreateItem(item, this);
}
- public override bool IsVisible(User user)
+ public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
if (this is ICollectionFolder && this is not BasePluginFolder)
{
@@ -239,7 +241,7 @@ namespace MediaBrowser.Controller.Entities
}
}
- return base.IsVisible(user);
+ return base.IsVisible(user, skipAllowedTagsCheck);
}
/// <summary>
@@ -247,7 +249,7 @@ namespace MediaBrowser.Controller.Entities
/// We want this synchronous.
/// </summary>
/// <returns>Returns children.</returns>
- protected virtual List<BaseItem> LoadChildren()
+ protected virtual IReadOnlyList<BaseItem> LoadChildren()
{
// logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path);
// just load our children from the repo - the library will be validated and maintained in other processes
@@ -528,13 +530,13 @@ namespace MediaBrowser.Controller.Entities
}
}
- private Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken)
+ private async Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken)
{
- return RunTasks(
+ await RunTasks(
(baseItem, innerProgress) => RefreshChildMetadata(baseItem, refreshOptions, recursive && baseItem.IsFolder, innerProgress, cancellationToken),
children,
progress,
- cancellationToken);
+ cancellationToken).ConfigureAwait(false);
}
private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
@@ -575,13 +577,13 @@ namespace MediaBrowser.Controller.Entities
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- private Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken)
+ private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken)
{
- return RunTasks(
+ await RunTasks(
(folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, false, null, directoryService, cancellationToken),
children,
progress,
- cancellationToken);
+ cancellationToken).ConfigureAwait(false);
}
/// <summary>
@@ -659,7 +661,7 @@ namespace MediaBrowser.Controller.Entities
/// Get our children from the repo - stubbed for now.
/// </summary>
/// <returns>IEnumerable{BaseItem}.</returns>
- protected List<BaseItem> GetCachedChildren()
+ protected IReadOnlyList<BaseItem> GetCachedChildren()
{
return ItemRepository.GetItemList(new InternalItemsQuery
{
@@ -1240,11 +1242,6 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- if (request.GenreIds.Count > 0)
- {
- return false;
- }
-
if (request.VideoTypes.Length > 0)
{
return false;
@@ -1283,14 +1280,14 @@ namespace MediaBrowser.Controller.Entities
return true;
}
- public List<BaseItem> GetChildren(User user, bool includeLinkedChildren)
+ public IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren)
{
ArgumentNullException.ThrowIfNull(user);
return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user));
}
- public virtual List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
{
ArgumentNullException.ThrowIfNull(user);
@@ -1304,7 +1301,7 @@ namespace MediaBrowser.Controller.Entities
AddChildren(user, includeLinkedChildren, result, false, query);
- return result.Values.ToList();
+ return result.Values.ToArray();
}
protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
@@ -1369,7 +1366,7 @@ namespace MediaBrowser.Controller.Entities
}
}
- public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
+ public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
{
ArgumentNullException.ThrowIfNull(user);
@@ -1377,35 +1374,35 @@ namespace MediaBrowser.Controller.Entities
AddChildren(user, true, result, true, query);
- return result.Values;
+ return result.Values.ToArray();
}
/// <summary>
/// Gets the recursive children.
/// </summary>
/// <returns>IList{BaseItem}.</returns>
- public IList<BaseItem> GetRecursiveChildren()
+ public IReadOnlyList<BaseItem> GetRecursiveChildren()
{
return GetRecursiveChildren(true);
}
- public IList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren)
+ public IReadOnlyList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren)
{
return GetRecursiveChildren(i => true, includeLinkedChildren);
}
- public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter)
+ public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter)
{
return GetRecursiveChildren(filter, true);
}
- public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren)
+ public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren)
{
var result = new Dictionary<Guid, BaseItem>();
AddChildrenToList(result, includeLinkedChildren, true, filter);
- return result.Values.ToList();
+ return result.Values.ToArray();
}
/// <summary>
@@ -1556,11 +1553,12 @@ namespace MediaBrowser.Controller.Entities
/// Gets the linked children.
/// </summary>
/// <returns>IEnumerable{BaseItem}.</returns>
- public IEnumerable<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos()
+ public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos()
{
return LinkedChildren
.Select(i => new Tuple<LinkedChild, BaseItem>(i, GetLinkedChild(i)))
- .Where(i => i.Item2 is not null);
+ .Where(i => i.Item2 is not null)
+ .ToArray();
}
protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs
index ddf62dd4c..6ec78a270 100644
--- a/MediaBrowser.Controller/Entities/Genre.cs
+++ b/MediaBrowser.Controller/Entities/Genre.cs
@@ -14,6 +14,7 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Class Genre.
/// </summary>
+ [Common.RequiresSourceSerialisation]
public class Genre : BaseItem, IItemByName
{
/// <summary>
@@ -61,7 +62,7 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+ public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
query.GenreIds = new[] { Id };
query.ExcludeItemTypes = new[]
diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs
index 90d9bdd2d..ad35494c2 100644
--- a/MediaBrowser.Controller/Entities/IHasMediaSources.cs
+++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs
@@ -22,8 +22,8 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <param name="enablePathSubstitution"><c>true</c> to enable path substitution, <c>false</c> to not.</param>
/// <returns>A list of media sources.</returns>
- List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution);
+ IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution);
- List<MediaStream> GetMediaStreams();
+ IReadOnlyList<MediaStream> GetMediaStreams();
}
}
diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs
index cac8aa61a..4928bda7a 100644
--- a/MediaBrowser.Controller/Entities/IItemByName.cs
+++ b/MediaBrowser.Controller/Entities/IItemByName.cs
@@ -9,7 +9,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public interface IItemByName
{
- IList<BaseItem> GetTaggedItems(InternalItemsQuery query);
+ IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query);
}
public interface IHasDualAccess : IItemByName
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 1461a3680..43f02fb72 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -37,7 +37,6 @@ namespace MediaBrowser.Controller.Entities
IncludeItemTypes = Array.Empty<BaseItemKind>();
ItemIds = Array.Empty<Guid>();
MediaTypes = Array.Empty<MediaType>();
- MinSimilarityScore = 20;
OfficialRatings = Array.Empty<string>();
OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
PersonIds = Array.Empty<Guid>();
@@ -71,8 +70,6 @@ namespace MediaBrowser.Controller.Entities
public User? User { get; set; }
- public BaseItem? SimilarTo { get; set; }
-
public bool? IsFolder { get; set; }
public bool? IsFavorite { get; set; }
@@ -295,8 +292,6 @@ namespace MediaBrowser.Controller.Entities
public DtoOptions DtoOptions { get; set; }
- public int MinSimilarityScore { get; set; }
-
public string? HasNoAudioTrackWithLanguage { get; set; }
public string? HasNoInternalSubtitleTrackWithLanguage { get; set; }
diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs
index fd5fef3dc..98e4f525f 100644
--- a/MediaBrowser.Controller/Entities/LinkedChild.cs
+++ b/MediaBrowser.Controller/Entities/LinkedChild.cs
@@ -4,7 +4,6 @@
using System;
using System.Globalization;
-using System.Text.Json.Serialization;
namespace MediaBrowser.Controller.Entities
{
@@ -12,7 +11,6 @@ namespace MediaBrowser.Controller.Entities
{
public LinkedChild()
{
- Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
}
public string Path { get; set; }
@@ -21,9 +19,6 @@ namespace MediaBrowser.Controller.Entities
public string LibraryItemId { get; set; }
- [JsonIgnore]
- public string Id { get; set; }
-
/// <summary>
/// Gets or sets the linked item id.
/// </summary>
@@ -31,6 +26,8 @@ namespace MediaBrowser.Controller.Entities
public static LinkedChild Create(BaseItem item)
{
+ ArgumentNullException.ThrowIfNull(item);
+
var child = new LinkedChild
{
Path = item.Path,
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index a07187d2f..c9a93d0f5 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
@@ -91,7 +92,7 @@ namespace MediaBrowser.Controller.Entities.Movies
return Enumerable.Empty<BaseItem>();
}
- protected override List<BaseItem> LoadChildren()
+ protected override IReadOnlyList<BaseItem> LoadChildren()
{
if (IsLegacyBoxSet)
{
@@ -99,7 +100,7 @@ namespace MediaBrowser.Controller.Entities.Movies
}
// Save a trip to the database
- return new List<BaseItem>();
+ return [];
}
public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
@@ -127,16 +128,16 @@ namespace MediaBrowser.Controller.Entities.Movies
return LibraryManager.Sort(items, user, new[] { sortBy }, SortOrder.Ascending);
}
- public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
{
var children = base.GetChildren(user, includeLinkedChildren, query);
- return Sort(children, user).ToList();
+ return Sort(children, user).ToArray();
}
- public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
+ public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
{
var children = base.GetRecursiveChildren(user, query);
- return Sort(children, user).ToList();
+ return Sort(children, user).ToArray();
}
public BoxSetInfo GetLookupInfo()
@@ -144,14 +145,14 @@ namespace MediaBrowser.Controller.Entities.Movies
return GetItemLookupInfo<BoxSetInfo>();
}
- public override bool IsVisible(User user)
+ public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
if (IsLegacyBoxSet)
{
- return base.IsVisible(user);
+ return base.IsVisible(user, skipAllowedTagsCheck);
}
- if (base.IsVisible(user))
+ if (base.IsVisible(user, skipAllowedTagsCheck))
{
if (LinkedChildren.Length == 0)
{
diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs
index 5292bd772..4141b1712 100644
--- a/MediaBrowser.Controller/Entities/PeopleHelper.cs
+++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs
@@ -10,7 +10,7 @@ namespace MediaBrowser.Controller.Entities
{
public static class PeopleHelper
{
- public static void AddPerson(List<PersonInfo> people, PersonInfo person)
+ public static void AddPerson(ICollection<PersonInfo> people, PersonInfo person)
{
ArgumentNullException.ThrowIfNull(person);
ArgumentException.ThrowIfNullOrEmpty(person.Name);
diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs
index 7f265084f..5cc4d322f 100644
--- a/MediaBrowser.Controller/Entities/Person.cs
+++ b/MediaBrowser.Controller/Entities/Person.cs
@@ -14,6 +14,7 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// This is the full Person object that can be retrieved with all of it's data.
/// </summary>
+ [Common.RequiresSourceSerialisation]
public class Person : BaseItem, IItemByName, IHasLookupInfo<PersonLookupInfo>
{
/// <summary>
@@ -62,7 +63,7 @@ namespace MediaBrowser.Controller.Entities
return value;
}
- public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+ public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
query.PersonIds = new[] { Id };
diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs
index 3df0b0b78..0ed870bac 100644
--- a/MediaBrowser.Controller/Entities/PersonInfo.cs
+++ b/MediaBrowser.Controller/Entities/PersonInfo.cs
@@ -17,8 +17,14 @@ namespace MediaBrowser.Controller.Entities
public PersonInfo()
{
ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ Id = Guid.NewGuid();
}
+ /// <summary>
+ /// Gets or Sets the PersonId.
+ /// </summary>
+ public Guid Id { get; set; }
+
public Guid ItemId { get; set; }
/// <summary>
diff --git a/MediaBrowser.Controller/Entities/PhotoAlbum.cs b/MediaBrowser.Controller/Entities/PhotoAlbum.cs
index a7ecb9061..5b31b4f11 100644
--- a/MediaBrowser.Controller/Entities/PhotoAlbum.cs
+++ b/MediaBrowser.Controller/Entities/PhotoAlbum.cs
@@ -4,6 +4,7 @@ using System.Text.Json.Serialization;
namespace MediaBrowser.Controller.Entities
{
+ [Common.RequiresSourceSerialisation]
public class PhotoAlbum : Folder
{
[JsonIgnore]
diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs
index a3736a4bf..9103b09a9 100644
--- a/MediaBrowser.Controller/Entities/Studio.cs
+++ b/MediaBrowser.Controller/Entities/Studio.cs
@@ -13,6 +13,7 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Class Studio.
/// </summary>
+ [Common.RequiresSourceSerialisation]
public class Studio : BaseItem, IItemByName
{
/// <summary>
@@ -63,7 +64,7 @@ namespace MediaBrowser.Controller.Entities
return true;
}
- public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+ public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
query.StudioIds = new[] { Id };
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index 181b9be2b..e3fbe8e4d 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -10,6 +10,7 @@ using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
+using MediaBrowser.Common;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Querying;
@@ -19,6 +20,7 @@ namespace MediaBrowser.Controller.Entities.TV
/// <summary>
/// Class Season.
/// </summary>
+ [RequiresSourceSerialisation]
public class Season : Folder, IHasSeries, IHasLookupInfo<SeasonInfo>
{
[JsonIgnore]
@@ -132,7 +134,7 @@ namespace MediaBrowser.Controller.Entities.TV
var series = Series;
if (series is not null)
{
- return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture);
+ return series.PresentationUniqueKey + "-" + IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture);
}
}
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index a324f79ef..137d91f1c 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -189,12 +189,12 @@ namespace MediaBrowser.Controller.Entities.TV
return list;
}
- public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
{
return GetSeasons(user, new DtoOptions(true));
}
- public List<BaseItem> GetSeasons(User user, DtoOptions options)
+ public IReadOnlyList<BaseItem> GetSeasons(User user, DtoOptions options)
{
var query = new InternalItemsQuery(user)
{
diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs
index a687adedd..7ae4a4a2c 100644
--- a/MediaBrowser.Controller/Entities/UserRootFolder.cs
+++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs
@@ -21,7 +21,7 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class UserRootFolder : Folder
{
- private readonly object _childIdsLock = new object();
+ private readonly Lock _childIdsLock = new();
private List<Guid> _childrenIds = null;
/// <summary>
@@ -52,7 +52,7 @@ namespace MediaBrowser.Controller.Entities
}
}
- protected override List<BaseItem> LoadChildren()
+ protected override IReadOnlyList<BaseItem> LoadChildren()
{
lock (_childIdsLock)
{
diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs
index e4fb340f7..f5ca3737c 100644
--- a/MediaBrowser.Controller/Entities/UserView.cs
+++ b/MediaBrowser.Controller/Entities/UserView.cs
@@ -134,7 +134,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <inheritdoc />
- public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
+ public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
{
query.SetUser(user);
query.Recursive = true;
@@ -145,7 +145,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <inheritdoc />
- protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
+ protected override IReadOnlyList<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{
return GetChildren(user, false);
}
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 420349f35..4ec2e4c0a 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -236,7 +236,7 @@ namespace MediaBrowser.Controller.Entities
return ConvertToResult(_libraryManager.GetItemList(query));
}
- private QueryResult<BaseItem> ConvertToResult(List<BaseItem> items)
+ private QueryResult<BaseItem> ConvertToResult(IReadOnlyList<BaseItem> items)
{
return new QueryResult<BaseItem>(items);
}
diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs
index afdaf448b..37820296c 100644
--- a/MediaBrowser.Controller/Entities/Year.cs
+++ b/MediaBrowser.Controller/Entities/Year.cs
@@ -13,6 +13,7 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Class Year.
/// </summary>
+ [Common.RequiresSourceSerialisation]
public class Year : BaseItem, IItemByName
{
[JsonIgnore]
@@ -55,7 +56,7 @@ namespace MediaBrowser.Controller.Entities
return true;
}
- public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+ public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
if (!int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
{
diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
index f8049cd48..e4806109a 100644
--- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
@@ -50,11 +50,6 @@ namespace MediaBrowser.Controller.Extensions
public const string FfmpegPathKey = "ffmpeg";
/// <summary>
- /// The key for a setting that indicates whether playlists should allow duplicate entries.
- /// </summary>
- public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates";
-
- /// <summary>
/// The key for a setting that indicates whether kestrel should bind to a unix socket.
/// </summary>
public const string BindToUnixSocketKey = "kestrel:socket";
@@ -121,14 +116,6 @@ namespace MediaBrowser.Controller.Extensions
=> configuration.GetValue<bool>(FfmpegImgExtractPerfTradeoffKey);
/// <summary>
- /// Gets a value indicating whether playlists should allow duplicate entries from the <see cref="IConfiguration"/>.
- /// </summary>
- /// <param name="configuration">The configuration to read the setting from.</param>
- /// <returns>True if playlists should allow duplicates, otherwise false.</returns>
- public static bool DoPlaylistsAllowDuplicates(this IConfiguration configuration)
- => configuration.GetValue<bool>(PlaylistsAllowDuplicatesKey);
-
- /// <summary>
/// Gets a value indicating whether kestrel should bind to a unix socket from the <see cref="IConfiguration" />.
/// </summary>
/// <param name="configuration">The configuration to read the setting from.</param>
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index b802b7e6e..47b1cb16e 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -483,21 +483,21 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="item">The item.</param>
/// <returns>List&lt;PersonInfo&gt;.</returns>
- List<PersonInfo> GetPeople(BaseItem item);
+ IReadOnlyList<PersonInfo> GetPeople(BaseItem item);
/// <summary>
/// Gets the people.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>List&lt;PersonInfo&gt;.</returns>
- List<PersonInfo> GetPeople(InternalPeopleQuery query);
+ IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query);
/// <summary>
/// Gets the people items.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>List&lt;Person&gt;.</returns>
- List<Person> GetPeopleItems(InternalPeopleQuery query);
+ IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query);
/// <summary>
/// Updates the people.
@@ -513,21 +513,21 @@ namespace MediaBrowser.Controller.Library
/// <param name="people">The people.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The async task.</returns>
- Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken);
+ Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellationToken);
/// <summary>
/// Gets the item ids.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>List&lt;Guid&gt;.</returns>
- List<Guid> GetItemIds(InternalItemsQuery query);
+ IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query);
/// <summary>
/// Gets the people names.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>List&lt;System.String&gt;.</returns>
- List<string> GetPeopleNames(InternalPeopleQuery query);
+ IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query);
/// <summary>
/// Queries the items.
@@ -553,9 +553,9 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="query">The query.</param>
/// <returns>QueryResult&lt;BaseItem&gt;.</returns>
- List<BaseItem> GetItemList(InternalItemsQuery query);
+ IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query);
- List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent);
+ IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent);
/// <summary>
/// Gets the items.
@@ -563,7 +563,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="query">The query to use.</param>
/// <param name="parents">Items to use for query.</param>
/// <returns>List of items.</returns>
- List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents);
+ IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents);
/// <summary>
/// Gets the items result.
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
index 44a1a85e3..eb697268c 100644
--- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -29,31 +29,31 @@ namespace MediaBrowser.Controller.Library
/// </summary>
/// <param name="itemId">The item identifier.</param>
/// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
- List<MediaStream> GetMediaStreams(Guid itemId);
+ IReadOnlyList<MediaStream> GetMediaStreams(Guid itemId);
/// <summary>
/// Gets the media streams.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
- List<MediaStream> GetMediaStreams(MediaStreamQuery query);
+ IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery query);
/// <summary>
/// Gets the media attachments.
/// </summary>
/// <param name="itemId">The item identifier.</param>
/// <returns>IEnumerable&lt;MediaAttachment&gt;.</returns>
- List<MediaAttachment> GetMediaAttachments(Guid itemId);
+ IReadOnlyList<MediaAttachment> GetMediaAttachments(Guid itemId);
/// <summary>
/// Gets the media attachments.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>IEnumerable&lt;MediaAttachment&gt;.</returns>
- List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query);
+ IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query);
/// <summary>
- /// Gets the playack media sources.
+ /// Gets the playback media sources.
/// </summary>
/// <param name="item">Item to use.</param>
/// <param name="user">User to use for operation.</param>
@@ -61,7 +61,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="enablePathSubstitution">Option to enable path substitution.</param>
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
/// <returns>List of media sources wrapped in an awaitable task.</returns>
- Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken);
+ Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken);
/// <summary>
/// Gets the static media sources.
@@ -70,7 +70,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="enablePathSubstitution">Option to enable path substitution.</param>
/// <param name="user">User to use for operation.</param>
/// <returns>List of media sources.</returns>
- List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null);
+ IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null);
/// <summary>
/// Gets the static media source.
@@ -123,7 +123,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="info">The <see cref="ActiveRecordingInfo"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <returns>A task containing the <see cref="MediaSourceInfo"/>'s for the recording.</returns>
- Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken);
+ Task<IReadOnlyList<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken);
/// <summary>
/// Closes the media source.
diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs
index 93073cc79..7ba8fc20c 100644
--- a/MediaBrowser.Controller/Library/IMusicManager.cs
+++ b/MediaBrowser.Controller/Library/IMusicManager.cs
@@ -17,7 +17,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user to use.</param>
/// <param name="dtoOptions">The options to use.</param>
/// <returns>List of items.</returns>
- List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions);
+ IReadOnlyList<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions);
/// <summary>
/// Gets the instant mix from artist.
@@ -26,7 +26,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user to use.</param>
/// <param name="dtoOptions">The options to use.</param>
/// <returns>List of items.</returns>
- List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions);
+ IReadOnlyList<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions);
/// <summary>
/// Gets the instant mix from genre.
@@ -35,6 +35,6 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">The user to use.</param>
/// <param name="dtoOptions">The options to use.</param>
/// <returns>List of items.</returns>
- List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions);
+ IReadOnlyList<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions);
}
}
diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs
index f36fd393f..5a2deda66 100644
--- a/MediaBrowser.Controller/Library/IUserDataManager.cs
+++ b/MediaBrowser.Controller/Library/IUserDataManager.cs
@@ -44,7 +44,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">User to use.</param>
/// <param name="item">Item to use.</param>
/// <returns>User data.</returns>
- UserItemData GetUserData(User user, BaseItem item);
+ UserItemData? GetUserData(User user, BaseItem item);
/// <summary>
/// Gets the user data dto.
@@ -52,7 +52,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="item">Item to use.</param>
/// <param name="user">User to use.</param>
/// <returns>User data dto.</returns>
- UserItemDataDto GetUserDataDto(BaseItem item, User user);
+ UserItemDataDto? GetUserDataDto(BaseItem item, User user);
/// <summary>
/// Gets the user data dto.
@@ -62,7 +62,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="user">User to use.</param>
/// <param name="options">Dto options to use.</param>
/// <returns>User data dto.</returns>
- UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options);
+ UserItemDataDto? GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options);
/// <summary>
/// Updates playstate for an item and returns true or false indicating if it was played to completion.
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
index 3c2cf8e3d..b10e77e10 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
@@ -119,13 +120,10 @@ namespace MediaBrowser.Controller.LiveTv
return "TvChannel";
}
- public IEnumerable<BaseItem> GetTaggedItems()
- => Enumerable.Empty<BaseItem>();
+ public IEnumerable<BaseItem> GetTaggedItems() => [];
- public override List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
+ public override IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
{
- var list = new List<MediaSourceInfo>();
-
var info = new MediaSourceInfo
{
Id = Id.ToString("N", CultureInfo.InvariantCulture),
@@ -138,14 +136,12 @@ namespace MediaBrowser.Controller.LiveTv
IsInfiniteStream = RunTimeTicks is null
};
- list.Add(info);
-
- return list;
+ return [info];
}
- public override List<MediaStream> GetMediaStreams()
+ public override IReadOnlyList<MediaStream> GetMediaStreams()
{
- return new List<MediaStream>();
+ return [];
}
protected override string GetInternalMetadataPath(string basePath)
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
index 2ac6f9963..83944f741 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
@@ -18,6 +18,7 @@ using MediaBrowser.Model.Providers;
namespace MediaBrowser.Controller.LiveTv
{
+ [Common.RequiresSourceSerialisation]
public class LiveTvProgram : BaseItem, IHasLookupInfo<ItemLookupInfo>, IHasStartDate, IHasProgramAttributes
{
private const string EmbyServiceName = "Emby";
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 1ef2eb343..ba4a2a59c 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId>
- <VersionPrefix>10.10.0</VersionPrefix>
+ <VersionPrefix>10.11.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
@@ -34,7 +34,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 28f0d1fff..a9e419df4 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -60,7 +60,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18);
private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15);
- private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
+ private readonly Version _minFFmpegImplicitHwaccel = new Version(6, 0);
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
@@ -309,7 +309,6 @@ namespace MediaBrowser.Controller.MediaEncoding
private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
{
if (state.VideoStream is null
- || !options.EnableTonemapping
|| GetVideoColorBitDepth(state) < 10
|| !_mediaEncoder.SupportsFilter("tonemapx"))
{
@@ -631,7 +630,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (string.IsNullOrWhiteSpace(container))
{
- // this may not work, but if the client is that broken we can not do anything better
+ // this may not work, but if the client is that broken we cannot do anything better
return "aac";
}
@@ -2061,7 +2060,13 @@ namespace MediaBrowser.Controller.MediaEncoding
// libx265 only accept level option in -x265-params.
// level option may cause libx265 to fail.
// libx265 cannot adjust the given level, just throw an error.
- param += " -x265-params:0 subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1:no-scenecut=1:no-open-gop=1:no-info=1";
+ param += " -x265-params:0 no-scenecut=1:no-open-gop=1:no-info=1";
+
+ if (encodingOptions.EncoderPreset < EncoderPreset.ultrafast)
+ {
+ // The following params are slower than the ultrafast preset, don't use when ultrafast is selected.
+ param += ":subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1";
+ }
}
if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)
@@ -2196,7 +2201,10 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var videoFrameRate = videoStream.ReferenceFrameRate;
- if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value)
+ // Add a little tolerance to the framerate check because some videos might record a framerate
+ // that is slightly greater than the intended framerate, but the device can still play it correctly.
+ // 0.05 fps tolerance should be safe enough.
+ if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value + 0.05f)
{
return false;
}
@@ -3318,24 +3326,25 @@ namespace MediaBrowser.Controller.MediaEncoding
&& options.VppTonemappingBrightness >= -100
&& options.VppTonemappingBrightness <= 100)
{
- procampParams += $"=b={options.VppTonemappingBrightness}";
+ procampParams += "procamp_vaapi=b={0}";
doVaVppProcamp = true;
}
if (options.VppTonemappingContrast > 1
&& options.VppTonemappingContrast <= 10)
{
- procampParams += doVaVppProcamp ? ":" : "=";
- procampParams += $"c={options.VppTonemappingContrast}";
+ procampParams += doVaVppProcamp ? ":c={1}" : "procamp_vaapi=c={1}";
doVaVppProcamp = true;
}
- args = "{0}tonemap_vaapi=format={1}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32";
+ args = procampParams + "{2}tonemap_vaapi=format={3}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32";
return string.Format(
CultureInfo.InvariantCulture,
args,
- doVaVppProcamp ? $"procamp_vaapi{procampParams}," : string.Empty,
+ options.VppTonemappingBrightness,
+ options.VppTonemappingContrast,
+ doVaVppProcamp ? "," : string.Empty,
videoFormat ?? "nv12");
}
else
@@ -3523,20 +3532,29 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// tonemapx requires yuv420p10 input for dovi reshaping, let ffmpeg convert the frame when necessary
var tonemapFormat = requireDoviReshaping ? "yuv420p" : outFormat;
-
- var tonemapArgs = $"tonemapx=tonemap={options.TonemappingAlgorithm}:desat={options.TonemappingDesat}:peak={options.TonemappingPeak}:t=bt709:m=bt709:p=bt709:format={tonemapFormat}";
+ var tonemapArgString = "tonemapx=tonemap={0}:desat={1}:peak={2}:t=bt709:m=bt709:p=bt709:format={3}";
if (options.TonemappingParam != 0)
{
- tonemapArgs += $":param={options.TonemappingParam}";
+ tonemapArgString += ":param={4}";
}
var range = options.TonemappingRange;
if (range == TonemappingRange.tv || range == TonemappingRange.pc)
{
- tonemapArgs += $":range={options.TonemappingRange}";
+ tonemapArgString += ":range={5}";
}
+ var tonemapArgs = string.Format(
+ CultureInfo.InvariantCulture,
+ tonemapArgString,
+ options.TonemappingAlgorithm,
+ options.TonemappingDesat,
+ options.TonemappingPeak,
+ tonemapFormat,
+ options.TonemappingParam,
+ options.TonemappingRange);
+
mainFilters.Add(tonemapArgs);
}
else
@@ -3595,7 +3613,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetSwVidFilterChain(state, options, vidEncoder);
}
- // prefered nvdec/cuvid + cuda filters + nvenc pipeline
+ // preferred nvdec/cuvid + cuda filters + nvenc pipeline
return GetNvidiaVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
}
@@ -3636,8 +3654,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var subH = state.SubtitleStream?.Height;
var rotation = state.VideoStream?.Rotation ?? 0;
- var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
- var doCuTranspose = !string.IsNullOrEmpty(tranposeDir) && _mediaEncoder.SupportsFilter("transpose_cuda");
+ var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doCuTranspose = !string.IsNullOrEmpty(transposeDir) && _mediaEncoder.SupportsFilter("transpose_cuda");
var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isNvDecoder && doCuTranspose));
var swpInW = swapWAndH ? inH : inW;
var swpInH = swapWAndH ? inW : inH;
@@ -3683,7 +3701,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// hw transpose
if (doCuTranspose)
{
- mainFilters.Add($"transpose_cuda=dir={tranposeDir}");
+ mainFilters.Add($"transpose_cuda=dir={transposeDir}");
}
var isRext = IsVideoStreamHevcRext(state);
@@ -3803,7 +3821,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetSwVidFilterChain(state, options, vidEncoder);
}
- // prefered d3d11va + opencl filters + amf pipeline
+ // preferred d3d11va + opencl filters + amf pipeline
return GetAmdDx11VidFiltersPrefered(state, options, vidDecoder, vidEncoder);
}
@@ -3843,8 +3861,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var subH = state.SubtitleStream?.Height;
var rotation = state.VideoStream?.Rotation ?? 0;
- var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
- var doOclTranspose = !string.IsNullOrEmpty(tranposeDir)
+ var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doOclTranspose = !string.IsNullOrEmpty(transposeDir)
&& _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TransposeOpenclReversal);
var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isD3d11vaDecoder && doOclTranspose));
var swpInW = swapWAndH ? inH : inW;
@@ -3888,12 +3906,12 @@ namespace MediaBrowser.Controller.MediaEncoding
// map from d3d11va to opencl via d3d11-opencl interop.
mainFilters.Add("hwmap=derive_device=opencl:mode=read");
- // hw deint <= TODO: finsh the 'yadif_opencl' filter
+ // hw deint <= TODO: finish the 'yadif_opencl' filter
// hw transpose
if (doOclTranspose)
{
- mainFilters.Add($"transpose_opencl=dir={tranposeDir}");
+ mainFilters.Add($"transpose_opencl=dir={transposeDir}");
}
var outFormat = doOclTonemap ? string.Empty : "nv12";
@@ -4029,13 +4047,13 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetSwVidFilterChain(state, options, vidEncoder);
}
- // prefered qsv(vaapi) + opencl filters pipeline
+ // preferred qsv(vaapi) + opencl filters pipeline
if (isIntelVaapiOclSupported)
{
return GetIntelQsvVaapiVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
}
- // prefered qsv(d3d11) + opencl filters pipeline
+ // preferred qsv(d3d11) + opencl filters pipeline
if (isIntelDx11OclSupported)
{
return GetIntelQsvDx11VidFiltersPrefered(state, options, vidDecoder, vidEncoder);
@@ -4084,8 +4102,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var subH = state.SubtitleStream?.Height;
var rotation = state.VideoStream?.Rotation ?? 0;
- var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
- var doVppTranspose = !string.IsNullOrEmpty(tranposeDir);
+ var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doVppTranspose = !string.IsNullOrEmpty(transposeDir);
var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || ((isD3d11vaDecoder || isQsvDecoder) && doVppTranspose));
var swpInW = swapWAndH ? inH : inW;
var swpInH = swapWAndH ? inW : inH;
@@ -4128,31 +4146,46 @@ namespace MediaBrowser.Controller.MediaEncoding
else if (isD3d11vaDecoder || isQsvDecoder)
{
var isRext = IsVideoStreamHevcRext(state);
- var twoPassVppTonemap = isRext;
+ var twoPassVppTonemap = false;
var doVppFullRangeOut = isMjpegEncoder
&& _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption;
var doVppScaleModeHq = isMjpegEncoder
&& _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppScaleModeOption;
var doVppProcamp = false;
var procampParams = string.Empty;
+ var procampParamsString = string.Empty;
if (doVppTonemap)
{
+ if (isRext)
+ {
+ // VPP tonemap requires p010 input
+ twoPassVppTonemap = true;
+ }
+
if (options.VppTonemappingBrightness != 0
&& options.VppTonemappingBrightness >= -100
&& options.VppTonemappingBrightness <= 100)
{
- procampParams += $":brightness={options.VppTonemappingBrightness}";
+ procampParamsString += ":brightness={0}";
twoPassVppTonemap = doVppProcamp = true;
}
if (options.VppTonemappingContrast > 1
&& options.VppTonemappingContrast <= 10)
{
- procampParams += $":contrast={options.VppTonemappingContrast}";
+ procampParamsString += ":contrast={1}";
twoPassVppTonemap = doVppProcamp = true;
}
- procampParams += doVppProcamp ? ":procamp=1:async_depth=2" : string.Empty;
+ if (doVppProcamp)
+ {
+ procampParamsString += ":procamp=1:async_depth=2";
+ procampParams = string.Format(
+ CultureInfo.InvariantCulture,
+ procampParamsString,
+ options.VppTonemappingBrightness,
+ options.VppTonemappingContrast);
+ }
}
var outFormat = doOclTonemap ? ((doVppTranspose || isRext) ? "p010" : string.Empty) : "nv12";
@@ -4163,7 +4196,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTranspose)
{
- hwScaleFilter += $":transpose={tranposeDir}";
+ hwScaleFilter += $":transpose={transposeDir}";
}
if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder)
@@ -4356,8 +4389,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var subH = state.SubtitleStream?.Height;
var rotation = state.VideoStream?.Rotation ?? 0;
- var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
- var doVppTranspose = !string.IsNullOrEmpty(tranposeDir);
+ var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doVppTranspose = !string.IsNullOrEmpty(transposeDir);
var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || ((isVaapiDecoder || isQsvDecoder) && doVppTranspose));
var swpInW = swapWAndH ? inH : inW;
var swpInH = swapWAndH ? inW : inH;
@@ -4417,7 +4450,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// hw transpose(vaapi vpp)
if (isVaapiDecoder && doVppTranspose)
{
- mainFilters.Add($"transpose_vaapi=dir={tranposeDir}");
+ mainFilters.Add($"transpose_vaapi=dir={transposeDir}");
}
var outFormat = doTonemap ? (((isQsvDecoder && doVppTranspose) || isRext) ? "p010" : string.Empty) : "nv12";
@@ -4427,7 +4460,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(hwScaleFilter) && isQsvDecoder && doVppTranspose)
{
- hwScaleFilter += $":transpose={tranposeDir}";
+ hwScaleFilter += $":transpose={transposeDir}";
}
if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder)
@@ -4628,14 +4661,14 @@ namespace MediaBrowser.Controller.MediaEncoding
return swFilterChain;
}
- // prefered vaapi + opencl filters pipeline
+ // preferred vaapi + opencl filters pipeline
if (_mediaEncoder.IsVaapiDeviceInteliHD)
{
// Intel iHD path, with extra vpp tonemap and overlay support.
return GetIntelVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
}
- // prefered vaapi + vulkan filters pipeline
+ // preferred vaapi + vulkan filters pipeline
if (_mediaEncoder.IsVaapiDeviceAmd
&& isVaapiVkSupported
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop
@@ -4687,8 +4720,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var subH = state.SubtitleStream?.Height;
var rotation = state.VideoStream?.Rotation ?? 0;
- var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
- var doVaVppTranspose = !string.IsNullOrEmpty(tranposeDir);
+ var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doVaVppTranspose = !string.IsNullOrEmpty(transposeDir);
var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isVaapiDecoder && doVaVppTranspose));
var swpInW = swapWAndH ? inH : inW;
var swpInH = swapWAndH ? inW : inH;
@@ -4743,7 +4776,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// hw transpose
if (doVaVppTranspose)
{
- mainFilters.Add($"transpose_vaapi=dir={tranposeDir}");
+ mainFilters.Add($"transpose_vaapi=dir={transposeDir}");
}
var outFormat = doTonemap ? (isRext ? "p010" : string.Empty) : "nv12";
@@ -4920,8 +4953,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
var rotation = state.VideoStream?.Rotation ?? 0;
- var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
- var doVkTranspose = isVaapiDecoder && !string.IsNullOrEmpty(tranposeDir);
+ var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doVkTranspose = isVaapiDecoder && !string.IsNullOrEmpty(transposeDir);
var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isVaapiDecoder && doVkTranspose));
var swpInW = swapWAndH ? inH : inW;
var swpInH = swapWAndH ? inW : inH;
@@ -5014,13 +5047,13 @@ namespace MediaBrowser.Controller.MediaEncoding
// vk transpose
if (doVkTranspose)
{
- if (string.Equals(tranposeDir, "reversal", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(transposeDir, "reversal", StringComparison.OrdinalIgnoreCase))
{
mainFilters.Add("flip_vulkan");
}
else
{
- mainFilters.Add($"transpose_vulkan=dir={tranposeDir}");
+ mainFilters.Add($"transpose_vulkan=dir={transposeDir}");
}
}
@@ -5388,8 +5421,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var usingHwSurface = isVtDecoder && (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface);
var rotation = state.VideoStream?.Rotation ?? 0;
- var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
- var doVtTranspose = !string.IsNullOrEmpty(tranposeDir) && _mediaEncoder.SupportsFilter("transpose_vt");
+ var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doVtTranspose = !string.IsNullOrEmpty(transposeDir) && _mediaEncoder.SupportsFilter("transpose_vt");
var swapWAndH = Math.Abs(rotation) == 90 && doVtTranspose;
var swpInW = swapWAndH ? inH : inW;
var swpInH = swapWAndH ? inW : inH;
@@ -5433,7 +5466,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// hw transpose
if (doVtTranspose)
{
- mainFilters.Add($"transpose_vt=dir={tranposeDir}");
+ mainFilters.Add($"transpose_vt=dir={transposeDir}");
}
if (doVtTonemap)
@@ -5548,7 +5581,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetSwVidFilterChain(state, options, vidEncoder);
}
- // prefered rkmpp + rkrga + opencl filters pipeline
+ // preferred rkmpp + rkrga + opencl filters pipeline
if (isRkmppOclSupported)
{
return GetRkmppVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
@@ -5596,8 +5629,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var subH = state.SubtitleStream?.Height;
var rotation = state.VideoStream?.Rotation ?? 0;
- var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
- var doRkVppTranspose = !string.IsNullOrEmpty(tranposeDir);
+ var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
+ var doRkVppTranspose = !string.IsNullOrEmpty(transposeDir);
var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isRkmppDecoder && doRkVppTranspose));
var swpInW = swapWAndH ? inH : inW;
var swpInH = swapWAndH ? inW : inH;
@@ -5662,13 +5695,17 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(doScaling)
&& !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f))
{
- var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={outFormat}:afbc=1";
+ // Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format.
+ // Use NV15 instead of P010 to avoid the issue.
+ // SDR inputs are using BGRA formats already which is not affected.
+ var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat;
+ var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_divisible_by=4:afbc=1";
mainFilters.Add(hwScaleFilterFirstPass);
}
if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose)
{
- hwScaleFilter += $":transpose={tranposeDir}";
+ hwScaleFilter += $":transpose={transposeDir}";
}
// try enabling AFBC to save DDR bandwidth
@@ -6142,7 +6179,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var ffmpegVersion = _mediaEncoder.EncoderVersion;
// Set the av1 codec explicitly to trigger hw accelerator, otherwise libdav1d will be used.
- var isAv1 = ffmpegVersion < _minFFmpegImplictHwaccel
+ var isAv1 = ffmpegVersion < _minFFmpegImplicitHwaccel
&& string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase);
// Allow profile mismatch if decoding H.264 baseline with d3d11va and vaapi hwaccels.
@@ -7036,7 +7073,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// DTS and TrueHD are not supported by HLS
// Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
- shiftAudioCodecs.Add("dca");
+ shiftAudioCodecs.Add("dts");
shiftAudioCodecs.Add("truehd");
}
else
diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs
index fefa66cdb..56990d0b8 100644
--- a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs
+++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs
@@ -12,8 +12,8 @@ namespace MediaBrowser.Controller.MediaEncoding;
public sealed class TranscodingJob : IDisposable
{
private readonly ILogger<TranscodingJob> _logger;
- private readonly object _processLock = new();
- private readonly object _timerLock = new();
+ private readonly Lock _processLock = new();
+ private readonly Lock _timerLock = new();
private Timer? _killTimer;
diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs
index 010d7edb4..570d2bace 100644
--- a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs
+++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs
@@ -46,12 +46,22 @@ public interface IMediaSegmentManager
Task DeleteSegmentAsync(Guid segmentId);
/// <summary>
- /// Obtains all segments accociated with the itemId.
+ /// Obtains all segments associated with the itemId.
/// </summary>
/// <param name="itemId">The id of the <see cref="BaseItem"/>.</param>
- /// <param name="typeFilter">filteres all media segments of the given type to be included. If null all types are included.</param>
+ /// <param name="typeFilter">filters all media segments of the given type to be included. If null all types are included.</param>
+ /// <param name="filterByProvider">When set filters the segments to only return those that which providers are currently enabled on their library.</param>
/// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns>
- Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter);
+ Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true);
+
+ /// <summary>
+ /// Obtains all segments associated with the itemId.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/>.</param>
+ /// <param name="typeFilter">filters all media segments of the given type to be included. If null all types are included.</param>
+ /// <param name="filterByProvider">When set filters the segments to only return those that which providers are currently enabled on their library.</param>
+ /// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns>
+ Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true);
/// <summary>
/// Gets information about any media segments stored for the given itemId.
diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs
index 39bb58bef..39bb58bef 100644
--- a/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs
+++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs
diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
index 2452b25ab..e452f2649 100644
--- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs
+++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
@@ -1,6 +1,5 @@
-#nullable disable
-
using System;
+using System.Diagnostics.CodeAnalysis;
using Jellyfin.Data.Entities;
namespace MediaBrowser.Controller.Net
@@ -20,31 +19,31 @@ namespace MediaBrowser.Controller.Net
/// Gets or sets the device identifier.
/// </summary>
/// <value>The device identifier.</value>
- public string DeviceId { get; set; }
+ public string? DeviceId { get; set; }
/// <summary>
/// Gets or sets the device.
/// </summary>
/// <value>The device.</value>
- public string Device { get; set; }
+ public string? Device { get; set; }
/// <summary>
/// Gets or sets the client.
/// </summary>
/// <value>The client.</value>
- public string Client { get; set; }
+ public string? Client { get; set; }
/// <summary>
/// Gets or sets the version.
/// </summary>
/// <value>The version.</value>
- public string Version { get; set; }
+ public string? Version { get; set; }
/// <summary>
/// Gets or sets the token.
/// </summary>
/// <value>The token.</value>
- public string Token { get; set; }
+ public string? Token { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the authorization is from an api key.
@@ -54,7 +53,7 @@ namespace MediaBrowser.Controller.Net
/// <summary>
/// Gets or sets the user making the request.
/// </summary>
- public User User { get; set; }
+ public User? User { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the token is authenticated.
@@ -62,8 +61,9 @@ namespace MediaBrowser.Controller.Net
public bool IsAuthenticated { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether the request has a token.
+ /// Gets a value indicating whether the request has a token.
/// </summary>
- public bool HasToken { get; set; }
+ [MemberNotNullWhen(true, nameof(Token))]
+ public bool HasToken => !string.IsNullOrWhiteSpace(Token);
}
}
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index a47d2fa45..4757bfa30 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -33,7 +33,7 @@ namespace MediaBrowser.Controller.Net
SingleWriter = false
});
- private readonly object _activeConnectionsLock = new();
+ private readonly Lock _activeConnectionsLock = new();
/// <summary>
/// The _active connections.
diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
index bba5a6b85..bdc0f9a10 100644
--- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs
+++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
@@ -24,9 +24,9 @@ namespace MediaBrowser.Controller.Net
DateTime LastActivityDate { get; }
/// <summary>
- /// Gets or sets the date of last Keeplive received.
+ /// Gets or sets the date of last Keepalive received.
/// </summary>
- /// <value>The date of last Keeplive received.</value>
+ /// <value>The date of last Keepalive received.</value>
DateTime LastKeepAliveDate { get; set; }
/// <summary>
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index 2c52b2b45..afe2d833d 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -7,157 +7,82 @@ using System.Collections.Generic;
using System.Threading;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
-namespace MediaBrowser.Controller.Persistence
+namespace MediaBrowser.Controller.Persistence;
+
+/// <summary>
+/// Provides an interface to implement an Item repository.
+/// </summary>
+public interface IItemRepository
{
/// <summary>
- /// Provides an interface to implement an Item repository.
+ /// Deletes the item.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ void DeleteItem(Guid id);
+
+ /// <summary>
+ /// Saves the items.
+ /// </summary>
+ /// <param name="items">The items.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken);
+
+ void SaveImages(BaseItem item);
+
+ /// <summary>
+ /// Retrieves the item.
/// </summary>
- public interface IItemRepository : IDisposable
- {
- /// <summary>
- /// Deletes the item.
- /// </summary>
- /// <param name="id">The identifier.</param>
- void DeleteItem(Guid id);
-
- /// <summary>
- /// Saves the items.
- /// </summary>
- /// <param name="items">The items.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken);
-
- void SaveImages(BaseItem item);
-
- /// <summary>
- /// Retrieves the item.
- /// </summary>
- /// <param name="id">The id.</param>
- /// <returns>BaseItem.</returns>
- BaseItem RetrieveItem(Guid id);
-
- /// <summary>
- /// Gets chapters for an item.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <returns>The list of chapter info.</returns>
- List<ChapterInfo> GetChapters(BaseItem item);
-
- /// <summary>
- /// Gets a single chapter for an item.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="index">The chapter index.</param>
- /// <returns>The chapter info at the specified index.</returns>
- ChapterInfo GetChapter(BaseItem item, int index);
-
- /// <summary>
- /// Saves the chapters.
- /// </summary>
- /// <param name="id">The item id.</param>
- /// <param name="chapters">The list of chapters to save.</param>
- void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters);
-
- /// <summary>
- /// Gets the media streams.
- /// </summary>
- /// <param name="query">The query.</param>
- /// <returns>IEnumerable{MediaStream}.</returns>
- List<MediaStream> GetMediaStreams(MediaStreamQuery query);
-
- /// <summary>
- /// Saves the media streams.
- /// </summary>
- /// <param name="id">The identifier.</param>
- /// <param name="streams">The streams.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken);
-
- /// <summary>
- /// Gets the media attachments.
- /// </summary>
- /// <param name="query">The query.</param>
- /// <returns>IEnumerable{MediaAttachment}.</returns>
- List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query);
-
- /// <summary>
- /// Saves the media attachments.
- /// </summary>
- /// <param name="id">The identifier.</param>
- /// <param name="attachments">The attachments.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- void SaveMediaAttachments(Guid id, IReadOnlyList<MediaAttachment> attachments, CancellationToken cancellationToken);
-
- /// <summary>
- /// Gets the items.
- /// </summary>
- /// <param name="query">The query.</param>
- /// <returns>QueryResult&lt;BaseItem&gt;.</returns>
- QueryResult<BaseItem> GetItems(InternalItemsQuery query);
-
- /// <summary>
- /// Gets the item ids list.
- /// </summary>
- /// <param name="query">The query.</param>
- /// <returns>List&lt;Guid&gt;.</returns>
- List<Guid> GetItemIdsList(InternalItemsQuery query);
-
- /// <summary>
- /// Gets the people.
- /// </summary>
- /// <param name="query">The query.</param>
- /// <returns>List&lt;PersonInfo&gt;.</returns>
- List<PersonInfo> GetPeople(InternalPeopleQuery query);
-
- /// <summary>
- /// Updates the people.
- /// </summary>
- /// <param name="itemId">The item identifier.</param>
- /// <param name="people">The people.</param>
- void UpdatePeople(Guid itemId, List<PersonInfo> people);
-
- /// <summary>
- /// Gets the people names.
- /// </summary>
- /// <param name="query">The query.</param>
- /// <returns>List&lt;System.String&gt;.</returns>
- List<string> GetPeopleNames(InternalPeopleQuery query);
-
- /// <summary>
- /// Gets the item list.
- /// </summary>
- /// <param name="query">The query.</param>
- /// <returns>List&lt;BaseItem&gt;.</returns>
- List<BaseItem> GetItemList(InternalItemsQuery query);
-
- /// <summary>
- /// Updates the inherited values.
- /// </summary>
- void UpdateInheritedValues();
-
- int GetCount(InternalItemsQuery query);
-
- QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query);
-
- QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query);
-
- QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query);
-
- QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query);
-
- QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query);
-
- QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query);
-
- List<string> GetMusicGenreNames();
-
- List<string> GetStudioNames();
-
- List<string> GetGenreNames();
-
- List<string> GetAllArtistNames();
- }
+ /// <param name="id">The id.</param>
+ /// <returns>BaseItem.</returns>
+ BaseItem RetrieveItem(Guid id);
+
+ /// <summary>
+ /// Gets the items.
+ /// </summary>
+ /// <param name="filter">The query.</param>
+ /// <returns>QueryResult&lt;BaseItem&gt;.</returns>
+ QueryResult<BaseItem> GetItems(InternalItemsQuery filter);
+
+ /// <summary>
+ /// Gets the item ids list.
+ /// </summary>
+ /// <param name="filter">The query.</param>
+ /// <returns>List&lt;Guid&gt;.</returns>
+ IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter);
+
+ /// <summary>
+ /// Gets the item list.
+ /// </summary>
+ /// <param name="filter">The query.</param>
+ /// <returns>List&lt;BaseItem&gt;.</returns>
+ IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery filter);
+
+ /// <summary>
+ /// Updates the inherited values.
+ /// </summary>
+ void UpdateInheritedValues();
+
+ int GetCount(InternalItemsQuery filter);
+
+ QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter);
+
+ QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter);
+
+ QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter);
+
+ QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter);
+
+ QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter);
+
+ QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter);
+
+ IReadOnlyList<string> GetMusicGenreNames();
+
+ IReadOnlyList<string> GetStudioNames();
+
+ IReadOnlyList<string> GetGenreNames();
+
+ IReadOnlyList<string> GetAllArtistNames();
}
diff --git a/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs
new file mode 100644
index 000000000..6699d3a4d
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Model.Querying;
+
+namespace MediaBrowser.Controller.Persistence;
+
+/// <summary>
+/// Provides static lookup data for <see cref="ItemFields"/> and <see cref="BaseItemKind"/> for the domain.
+/// </summary>
+public interface IItemTypeLookup
+{
+ /// <summary>
+ /// Gets all serialisation target types for music related kinds.
+ /// </summary>
+ IReadOnlyList<string> MusicGenreTypes { get; }
+
+ /// <summary>
+ /// Gets mapping for all BaseItemKinds and their expected serialization target.
+ /// </summary>
+ IReadOnlyDictionary<BaseItemKind, string> BaseItemKindNames { get; }
+}
diff --git a/MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.cs b/MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.cs
new file mode 100644
index 000000000..4773f4058
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.cs
@@ -0,0 +1,28 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Persistence;
+
+public interface IMediaAttachmentRepository
+{
+ /// <summary>
+ /// Gets the media attachments.
+ /// </summary>
+ /// <param name="filter">The query.</param>
+ /// <returns>IEnumerable{MediaAttachment}.</returns>
+ IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery filter);
+
+ /// <summary>
+ /// Saves the media attachments.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="attachments">The attachments.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void SaveMediaAttachments(Guid id, IReadOnlyList<MediaAttachment> attachments, CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs b/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs
new file mode 100644
index 000000000..665129eaf
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs
@@ -0,0 +1,31 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Persistence;
+
+/// <summary>
+/// Provides methods for accessing MediaStreams.
+/// </summary>
+public interface IMediaStreamRepository
+{
+ /// <summary>
+ /// Gets the media streams.
+ /// </summary>
+ /// <param name="filter">The query.</param>
+ /// <returns>IEnumerable{MediaStream}.</returns>
+ IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery filter);
+
+ /// <summary>
+ /// Saves the media streams.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="streams">The streams.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs
new file mode 100644
index 000000000..418289cb4
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs
@@ -0,0 +1,33 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Persistence;
+
+public interface IPeopleRepository
+{
+ /// <summary>
+ /// Gets the people.
+ /// </summary>
+ /// <param name="filter">The query.</param>
+ /// <returns>The list of people matching the filter.</returns>
+ IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery filter);
+
+ /// <summary>
+ /// Updates the people.
+ /// </summary>
+ /// <param name="itemId">The item identifier.</param>
+ /// <param name="people">The people.</param>
+ void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people);
+
+ /// <summary>
+ /// Gets the people names.
+ /// </summary>
+ /// <param name="filter">The query.</param>
+ /// <returns>The list of people names matching the filter.</returns>
+ IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery filter);
+}
diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
deleted file mode 100644
index f2fb2826a..000000000
--- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-#nullable disable
-
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using MediaBrowser.Controller.Entities;
-
-namespace MediaBrowser.Controller.Persistence
-{
- /// <summary>
- /// Provides an interface to implement a UserData repository.
- /// </summary>
- public interface IUserDataRepository : IDisposable
- {
- /// <summary>
- /// Saves the user data.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="key">The key.</param>
- /// <param name="userData">The user data.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken);
-
- /// <summary>
- /// Gets the user data.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="key">The key.</param>
- /// <returns>The user data.</returns>
- UserItemData GetUserData(long userId, string key);
-
- /// <summary>
- /// Gets the user data.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="keys">The keys.</param>
- /// <returns>The user data.</returns>
- UserItemData GetUserData(long userId, List<string> keys);
-
- /// <summary>
- /// Return all user data associated with the given user.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <returns>The list of user item data.</returns>
- List<UserItemData> GetAllUserData(long userId);
-
- /// <summary>
- /// Save all user data associated with the given user.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="userData">The user item data.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken);
- }
-}
diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
index 038cbd2d6..497c4a511 100644
--- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
+++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
@@ -92,8 +92,9 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="playlistId">The playlist identifier.</param>
/// <param name="entryId">The entry identifier.</param>
/// <param name="newIndex">The new index.</param>
+ /// <param name="callingUserId">The calling user.</param>
/// <returns>Task.</returns>
- Task MoveItemAsync(string playlistId, string entryId, int newIndex);
+ Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId);
/// <summary>
/// Removed all playlists of a user.
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index 45aefacf6..edea54291 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -137,27 +137,27 @@ namespace MediaBrowser.Controller.Playlists
return Task.CompletedTask;
}
- public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
{
return GetPlayableItems(user, query);
}
- protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
+ protected override IReadOnlyList<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
{
return [];
}
- public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
+ public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
{
return GetPlayableItems(user, query);
}
- public IEnumerable<Tuple<LinkedChild, BaseItem>> GetManageableItems()
+ public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetManageableItems()
{
return GetLinkedChildrenInfos();
}
- private List<BaseItem> GetPlayableItems(User user, InternalItemsQuery query)
+ private IReadOnlyList<BaseItem> GetPlayableItems(User user, InternalItemsQuery query)
{
query ??= new InternalItemsQuery(user);
@@ -227,11 +227,11 @@ namespace MediaBrowser.Controller.Playlists
return [item];
}
- public override bool IsVisible(User user)
+ public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
if (!IsSharedItem)
{
- return base.IsVisible(user);
+ return base.IsVisible(user, skipAllowedTagsCheck);
}
if (OpenAccess)
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index 38fc5f2cc..0d3a334df 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -77,7 +77,8 @@ namespace MediaBrowser.Controller.Providers
Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken);
/// <summary>
- /// Saves the image.
+ /// Saves the image by giving the image path on filesystem.
+ /// This method will remove the image on the source path after saving it to the destination.
/// </summary>
/// <param name="item">Image to save.</param>
/// <param name="source">Source of image.</param>
diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs
index cfff3eb14..ef69885fc 100644
--- a/MediaBrowser.Controller/Providers/MetadataResult.cs
+++ b/MediaBrowser.Controller/Providers/MetadataResult.cs
@@ -3,6 +3,7 @@
#pragma warning disable CA1002, CA2227, CS1591
using System.Collections.Generic;
+using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
@@ -13,6 +14,7 @@ namespace MediaBrowser.Controller.Providers
// Images aren't always used so the allocation is a waste a lot of the time
private List<LocalImageInfo> _images;
private List<(string Url, ImageType Type)> _remoteImages;
+ private List<PersonInfo> _people;
public MetadataResult()
{
@@ -21,17 +23,21 @@ namespace MediaBrowser.Controller.Providers
public List<LocalImageInfo> Images
{
- get => _images ??= new List<LocalImageInfo>();
+ get => _images ??= [];
set => _images = value;
}
public List<(string Url, ImageType Type)> RemoteImages
{
- get => _remoteImages ??= new List<(string Url, ImageType Type)>();
+ get => _remoteImages ??= [];
set => _remoteImages = value;
}
- public List<PersonInfo> People { get; set; }
+ public IReadOnlyList<PersonInfo> People
+ {
+ get => _people;
+ set => _people = value?.ToList();
+ }
public bool HasMetadata { get; set; }
@@ -47,7 +53,7 @@ namespace MediaBrowser.Controller.Providers
{
People ??= new List<PersonInfo>();
- PeopleHelper.AddPerson(People, p);
+ PeopleHelper.AddPerson(_people, p);
}
/// <summary>
@@ -61,7 +67,7 @@ namespace MediaBrowser.Controller.Providers
}
else
{
- People.Clear();
+ _people.Clear();
}
}
}
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 462a62455..5dd0413b4 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -324,7 +324,7 @@ namespace MediaBrowser.Controller.Session
Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, string appVersion);
/// <summary>
- /// Logouts the specified access token.
+ /// Logs out the specified access token.
/// </summary>
/// <param name="accessToken">The access token.</param>
/// <returns>A <see cref="Task"/> representing the log out process.</returns>
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 3ba1bfce4..96783f607 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -25,7 +25,7 @@ namespace MediaBrowser.Controller.Session
private readonly ISessionManager _sessionManager;
private readonly ILogger _logger;
- private readonly object _progressLock = new();
+ private readonly Lock _progressLock = new();
private Timer _progressTimer;
private PlaybackProgressInfo _lastProgressInfo;
@@ -286,7 +286,7 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Gets or sets the playlist item id.
/// </summary>
- /// <value>The splaylist item id.</value>
+ /// <value>The playlist item id.</value>
public string PlaylistItemId { get; set; }
/// <summary>
diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
index bd47db39a..66a0c5254 100644
--- a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
+++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
@@ -5,7 +5,7 @@ using MediaBrowser.Controller.Library;
namespace MediaBrowser.Controller.Sorting
{
/// <summary>
- /// Represents a BaseItem comparer that requires a User to perform it's comparison.
+ /// Represents a BaseItem comparer that requires a User to perform its comparison.
/// </summary>
public interface IUserBaseItemComparer : IBaseItemComparer
{
diff --git a/MediaBrowser.Controller/Streaming/StreamState.cs b/MediaBrowser.Controller/Streaming/StreamState.cs
index b5dbe29ec..195dda5fe 100644
--- a/MediaBrowser.Controller/Streaming/StreamState.cs
+++ b/MediaBrowser.Controller/Streaming/StreamState.cs
@@ -51,7 +51,7 @@ public class StreamState : EncodingJobInfo, IDisposable
public VideoRequestDto? VideoRequest => Request as VideoRequestDto;
/// <summary>
- /// Gets or sets the direct stream provicer.
+ /// Gets or sets the direct stream provider.
/// </summary>
/// <remarks>
/// Deprecated.
diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
index 9aa9c3548..0bb341da1 100644
--- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
+++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
@@ -320,7 +320,7 @@ namespace MediaBrowser.LocalMetadata.Images
{
AddImage(files, images, name + "-fanart", ImageType.Backdrop, imagePrefix);
- // Support without the prefix if it's in it's own folder
+ // Support without the prefix if it's in its own folder
if (!isInMixedFolder)
{
AddImage(files, images, name + "-fanart", ImageType.Backdrop);
diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
index 05177ac39..8e3c8cf7f 100644
--- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
+++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
@@ -11,7 +11,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs
index 952ed3aac..00634de5b 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs
@@ -15,7 +15,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
/// <summary>
/// Initializes a new instance of the <see cref="BoxSetXmlParser"/> class.
/// </summary>
- /// <param name="logger">Instance of the <see cref="ILogger{BoxSetXmlParset}"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{BoxSetXmlParser}"/> interface.</param>
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
public BoxSetXmlParser(ILogger<BoxSetXmlParser> logger, IProviderManager providerManager)
: base(logger, providerManager)
diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
index ee0d10bea..c25adb774 100644
--- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
@@ -96,7 +96,7 @@ namespace MediaBrowser.LocalMetadata.Savers
var directory = Path.GetDirectoryName(path) ?? throw new InvalidDataException($"Provided path ({path}) is not valid.");
Directory.CreateDirectory(directory);
- // On Windows, savint the file will fail if the file is hidden or readonly
+ // On Windows, saving the file will fail if the file is hidden or readonly
FileSystem.SetAttributes(path, false, false);
var fileStreamOptions = new FileStreamOptions()
diff --git a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
index 3f018cae9..ae767a72a 100644
--- a/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/PlaylistXmlSaver.cs
@@ -45,16 +45,16 @@ namespace MediaBrowser.LocalMetadata.Savers
}
/// <inheritdoc />
- protected override Task WriteCustomElementsAsync(BaseItem item, XmlWriter writer)
+ protected override async Task WriteCustomElementsAsync(BaseItem item, XmlWriter writer)
{
var game = (Playlist)item;
if (game.PlaylistMediaType == MediaType.Unknown)
{
- return Task.CompletedTask;
+ return;
}
- return writer.WriteElementStringAsync(null, "PlaylistMediaType", null, game.PlaylistMediaType.ToString());
+ await writer.WriteElementStringAsync(null, "PlaylistMediaType", null, game.PlaylistMediaType.ToString()).ConfigureAwait(false);
}
/// <inheritdoc />
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
index fca17d4c0..7c0be5a9f 100644
--- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
+++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
@@ -84,7 +84,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
/// Gets the files matching a pattern.
/// </summary>
/// <param name="searchPattern">The search pattern.</param>
- /// <returns>All files of the directory matchign the search pattern.</returns>
+ /// <returns>All files of the directory matching the search pattern.</returns>
public IFileInfo[] GetFiles(string searchPattern)
{
return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false)
@@ -96,8 +96,8 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
/// Gets the files matching a pattern and search options.
/// </summary>
/// <param name="searchPattern">The search pattern.</param>
- /// <param name="searchOption">The search optin.</param>
- /// <returns>All files of the directory matchign the search pattern and options.</returns>
+ /// <param name="searchOption">The search option.</param>
+ /// <returns>All files of the directory matching the search pattern and options.</returns>
public IFileInfo[] GetFiles(string searchPattern, SearchOption searchOption)
{
return _fileSystem.GetFiles(
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 826ffd0b7..14cf869f9 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -62,7 +62,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly AsyncNonKeyedLocker _thumbnailResourcePool;
- private readonly object _runningProcessesLock = new object();
+ private readonly Lock _runningProcessesLock = new();
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
// MediaEncoder is registered as a Singleton
@@ -122,7 +122,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
_jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
_jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
- var semaphoreCount = 2 * Environment.ProcessorCount;
+ // Although the type is not nullable, this might still be null during unit tests
+ var semaphoreCount = serverConfig.Configuration?.ParallelImageEncodingLimit ?? 0;
+ if (semaphoreCount < 1)
+ {
+ semaphoreCount = Environment.ProcessorCount;
+ }
+
_thumbnailResourcePool = new(semaphoreCount);
}
@@ -1035,6 +1041,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (exitCode == -1)
{
_logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
+ // Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed ffmpeg process is not possible for caller.
+ // Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed.
+ try
+ {
+ Directory.Delete(targetDirectory, true);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Failed to delete ffmpeg temp directory {TargetDirectory}", targetDirectory);
+ }
throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", processDescription));
}
@@ -1091,14 +1107,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
private void StopProcesses()
{
- List<ProcessWrapper> proceses;
+ List<ProcessWrapper> processes;
lock (_runningProcessesLock)
{
- proceses = _runningProcesses.ToList();
+ processes = _runningProcesses.ToList();
_runningProcesses.Clear();
}
- foreach (var process in proceses)
+ foreach (var process in processes)
{
if (!process.HasExited)
{
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index be63513a7..be7eeda92 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
index 1b5b5262a..6f51e1a6a 100644
--- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
+++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.MediaEncoding.Probing
if (result.Streams is not null)
{
- // Convert all dictionaries to case insensitive
+ // Convert all dictionaries to case-insensitive
foreach (var stream in result.Streams)
{
if (stream.Tags is not null)
@@ -70,7 +70,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
/// <summary>
- /// Converts a dictionary to case insensitive.
+ /// Converts a dictionary to case-insensitive.
/// </summary>
/// <param name="dict">The dict.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 334796f58..c730f4cda 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -1648,7 +1648,7 @@ namespace MediaBrowser.MediaEncoding.Probing
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 1))
{
- fs.Read(packetBuffer);
+ fs.ReadExactly(packetBuffer);
}
if (packetBuffer[0] == 71)
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
index a79d801fb..d060b247d 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
@@ -17,7 +17,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
public class SubtitleEditParser : ISubtitleParser
{
private readonly ILogger<SubtitleEditParser> _logger;
- private readonly Dictionary<string, SubtitleFormat[]> _subtitleFormats;
+ private readonly Dictionary<string, List<Type>> _subtitleFormatTypes;
/// <summary>
/// Initializes a new instance of the <see cref="SubtitleEditParser"/> class.
@@ -26,10 +26,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
public SubtitleEditParser(ILogger<SubtitleEditParser> logger)
{
_logger = logger;
- _subtitleFormats = GetSubtitleFormats()
- .Where(subtitleFormat => !string.IsNullOrEmpty(subtitleFormat.Extension))
- .GroupBy(subtitleFormat => subtitleFormat.Extension.TrimStart('.'), StringComparer.OrdinalIgnoreCase)
- .ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase);
+ _subtitleFormatTypes = GetSubtitleFormatTypes();
}
/// <inheritdoc />
@@ -38,13 +35,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var subtitle = new Subtitle();
var lines = stream.ReadAllLines().ToList();
- if (!_subtitleFormats.TryGetValue(fileExtension, out var subtitleFormats))
+ if (!_subtitleFormatTypes.TryGetValue(fileExtension, out var subtitleFormatTypesForExtension))
{
throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension));
}
- foreach (var subtitleFormat in subtitleFormats)
+ foreach (var subtitleFormatType in subtitleFormatTypesForExtension)
{
+ var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(subtitleFormatType, true)!;
_logger.LogDebug(
"Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
fileExtension,
@@ -97,11 +95,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <inheritdoc />
public bool SupportsFileExtension(string fileExtension)
- => _subtitleFormats.ContainsKey(fileExtension);
+ => _subtitleFormatTypes.ContainsKey(fileExtension);
- private List<SubtitleFormat> GetSubtitleFormats()
+ private Dictionary<string, List<Type>> GetSubtitleFormatTypes()
{
- var subtitleFormats = new List<SubtitleFormat>();
+ var subtitleFormatTypes = new Dictionary<string, List<Type>>(StringComparer.OrdinalIgnoreCase);
var assembly = typeof(SubtitleFormat).Assembly;
foreach (var type in assembly.GetTypes())
@@ -113,9 +111,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles
try
{
- // It shouldn't be null, but the exception is caught if it is
- var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(type, true)!;
- subtitleFormats.Add(subtitleFormat);
+ var tempInstance = (SubtitleFormat)Activator.CreateInstance(type, true)!;
+ var extension = tempInstance.Extension.TrimStart('.');
+ if (!string.IsNullOrEmpty(extension))
+ {
+ // Store only the type, we will instantiate from it later
+ if (!subtitleFormatTypes.TryGetValue(extension, out var subtitleFormatTypesForExtension))
+ {
+ subtitleFormatTypes[extension] = [type];
+ }
+ else
+ {
+ subtitleFormatTypesForExtension.Add(type);
+ }
+ }
}
catch (Exception ex)
{
@@ -123,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
- return subtitleFormats;
+ return subtitleFormatTypes;
}
}
}
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index 6054ba34e..590b74304 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -15,7 +15,7 @@ namespace MediaBrowser.Model.Configuration
TypeOptions = Array.Empty<TypeOptions>();
DisabledSubtitleFetchers = Array.Empty<string>();
DisabledMediaSegmentProviders = Array.Empty<string>();
- MediaSegmentProvideOrder = Array.Empty<string>();
+ MediaSegmentProviderOrder = Array.Empty<string>();
SubtitleFetcherOrder = Array.Empty<string>();
DisabledLocalMetadataReaders = Array.Empty<string>();
DisabledLyricFetchers = Array.Empty<string>();
@@ -99,7 +99,7 @@ namespace MediaBrowser.Model.Configuration
public string[] DisabledMediaSegmentProviders { get; set; }
- public string[] MediaSegmentProvideOrder { get; set; }
+ public string[] MediaSegmentProviderOrder { get; set; }
public bool SkipSubtitlesIfEmbeddedSubtitlesPresent { get; set; }
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 5ad588200..693bf90e7 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -83,9 +83,9 @@ public class ServerConfiguration : BaseApplicationConfiguration
public bool QuickConnectAvailable { get; set; } = true;
/// <summary>
- /// Gets or sets a value indicating whether [enable case sensitive item ids].
+ /// 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>
+ /// <value><c>true</c> if [enable case-sensitive item ids]; otherwise, <c>false</c>.</value>
public bool EnableCaseSensitiveItemIds { get; set; } = true;
public bool DisableLiveTvChannelUserDataName { get; set; } = true;
@@ -244,17 +244,12 @@ public class ServerConfiguration : BaseApplicationConfiguration
public int LibraryMetadataRefreshConcurrency { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
- /// </summary>
- public bool RemoveOldPlugins { get; set; }
-
- /// <summary>
/// Gets or sets a value indicating whether clients should be allowed to upload logs.
/// </summary>
public bool AllowClientLogUpload { get; set; } = true;
/// <summary>
- /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation alltogether.
+ /// Gets or sets the dummy chapter duration in seconds, use 0 (zero) or less to disable generation altogether.
/// </summary>
/// <value>The dummy chapters duration.</value>
public int DummyChapterDuration { get; set; }
@@ -281,4 +276,9 @@ public class ServerConfiguration : BaseApplicationConfiguration
/// </summary>
/// <value>The trickplay options.</value>
public TrickplayOptions TrickplayOptions { get; set; } = new TrickplayOptions();
+
+ /// <summary>
+ /// Gets or sets a value indicating whether old authorization methods are allowed.
+ /// </summary>
+ public bool EnableLegacyAuthorization { get; set; } = true;
}
diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
index af0787990..1b046f54e 100644
--- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs
+++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
@@ -25,8 +25,8 @@ namespace MediaBrowser.Model.Dlna
/// <param name="videoFramerate">The framerate.</param>
/// <param name="packetLength">The packet length.</param>
/// <param name="timestamp">The <see cref="TransportStreamTimestamp"/>.</param>
- /// <param name="isAnamorphic">A value indicating whether tthe video is anamorphic.</param>
- /// <param name="isInterlaced">A value indicating whether tthe video is interlaced.</param>
+ /// <param name="isAnamorphic">A value indicating whether the video is anamorphic.</param>
+ /// <param name="isInterlaced">A value indicating whether the video is interlaced.</param>
/// <param name="refFrames">The reference frames.</param>
/// <param name="numVideoStreams">The number of video streams.</param>
/// <param name="numAudioStreams">The number of audio streams.</param>
diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
index 438df3441..553ccfc64 100644
--- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
+++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
@@ -59,7 +59,7 @@ public class DirectPlayProfile
/// <returns>True if supported.</returns>
public bool SupportsAudioCodec(string? codec)
{
- // Video profiles can have audio codec restrictions too, therefore incude Video as valid type.
+ // Video profiles can have audio codec restrictions too, therefore include Video as valid type.
return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerHelper.ContainsContainer(AudioCodec, codec);
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index a25ddc367..1ed493708 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -30,7 +30,7 @@ namespace MediaBrowser.Model.Dlna
private readonly ITranscoderSupport _transcoderSupport;
private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"];
private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"];
- private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd"];
+ private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dts", "truehd"];
/// <summary>
/// Initializes a new instance of the <see cref="StreamBuilder"/> class.
@@ -208,6 +208,14 @@ namespace MediaBrowser.Model.Dlna
var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate);
playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
+
+ // Pure audio transcoding does not support comma separated list of transcoding codec at the moment.
+ // So just use the AudioCodec as is would be safe enough as the _transcoderSupport.CanEncodeToAudioCodec
+ // would fail so this profile will not even be picked up.
+ if (playlistItem.AudioCodecs.Count == 0 && !string.IsNullOrWhiteSpace(transcodingProfile.AudioCodec))
+ {
+ playlistItem.AudioCodecs = [transcodingProfile.AudioCodec];
+ }
}
playlistItem.TranscodeReasons = transcodeReasons;
@@ -854,18 +862,37 @@ namespace MediaBrowser.Model.Dlna
if (options.AllowAudioStreamCopy)
{
- if (ContainerHelper.ContainsContainer(transcodingProfile.AudioCodec, audioCodec))
+ // For Audio stream, we prefer the audio codec that can be directly copied, then the codec that can otherwise satisfies
+ // the transcoding conditions, then the one does not satisfy the transcoding conditions.
+ // For example: A client can support both aac and flac, but flac only supports 2 channels while aac supports 6.
+ // When the source audio is 6 channel flac, we should transcode to 6 channel aac, instead of down-mix to 2 channel flac.
+ var transcodingAudioCodecs = ContainerHelper.Split(transcodingProfile.AudioCodec);
+
+ foreach (var transcodingAudioCodec in transcodingAudioCodecs)
{
var appliedVideoConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.VideoAudio &&
- i.ContainsAnyCodec(audioCodec, container) &&
+ i.ContainsAnyCodec(transcodingAudioCodec, container) &&
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)))
.Select(i =>
i.Conditions.All(condition => ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)));
// An empty appliedVideoConditions means that the codec has no conditions for the current audio stream
var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
- rank.Audio = conditionsSatisfied ? 1 : 2;
+
+ var rankAudio = 3;
+
+ if (conditionsSatisfied)
+ {
+ rankAudio = string.Equals(transcodingAudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ? 1 : 2;
+ }
+
+ rank.Audio = Math.Min(rank.Audio, rankAudio);
+
+ if (rank.Audio == 1)
+ {
+ break;
+ }
}
}
@@ -955,9 +982,28 @@ namespace MediaBrowser.Model.Dlna
var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(audioCodecs, false, stream.Codec)).FirstOrDefault();
- var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null;
+ var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && audioStreamWithSupportedCodec.Channels > (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue);
+
+ var directAudioStreamSatisfied = audioStreamWithSupportedCodec is not null && !channelsExceedsLimit
+ && options.Profile.CodecProfiles
+ .Where(i => i.Type == CodecType.VideoAudio
+ && i.ContainsAnyCodec(audioStreamWithSupportedCodec.Codec, container)
+ && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioStreamWithSupportedCodec.Channels, audioStreamWithSupportedCodec.BitRate, audioStreamWithSupportedCodec.SampleRate, audioStreamWithSupportedCodec.BitDepth, audioStreamWithSupportedCodec.Profile, false)))
+ .Select(i => i.Conditions.All(condition =>
+ {
+ var satisfied = ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioStreamWithSupportedCodec.Channels, audioStreamWithSupportedCodec.BitRate, audioStreamWithSupportedCodec.SampleRate, audioStreamWithSupportedCodec.BitDepth, audioStreamWithSupportedCodec.Profile, false);
+ if (!satisfied)
+ {
+ playlistItem.TranscodeReasons |= GetTranscodeReasonForFailedCondition(condition);
+ }
+
+ return satisfied;
+ }))
+ .All(satisfied => satisfied);
+
+ directAudioStreamSatisfied = directAudioStreamSatisfied && !playlistItem.TranscodeReasons.HasFlag(TranscodeReason.ContainerBitrateExceedsLimit);
- var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && directAudioStream is null;
+ var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : null;
if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
{
@@ -1079,12 +1125,12 @@ namespace MediaBrowser.Model.Dlna
_logger.LogDebug(
"Transcode Result for Profile: {Profile}, Path: {Path}, PlayMethod: {PlayMethod}, AudioStreamIndex: {AudioStreamIndex}, SubtitleStreamIndex: {SubtitleStreamIndex}, Reasons: {TranscodeReason}",
- options.Profile?.Name ?? "Anonymous Profile",
+ options.Profile.Name ?? "Anonymous Profile",
item.Path ?? "Unknown path",
- playlistItem?.PlayMethod,
+ playlistItem.PlayMethod,
audioStream?.Index,
- playlistItem?.SubtitleStreamIndex,
- playlistItem?.TranscodeReasons);
+ playlistItem.SubtitleStreamIndex,
+ playlistItem.TranscodeReasons);
}
private static int GetDefaultAudioBitrate(string? audioCodec, int? audioChannels)
@@ -2205,7 +2251,7 @@ namespace MediaBrowser.Model.Dlna
}
}
- private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
+ private static bool IsAudioContainerSupported(DirectPlayProfile profile, MediaSourceInfo item)
{
// Check container type
if (!profile.SupportsContainer(item.Container))
@@ -2213,6 +2259,20 @@ namespace MediaBrowser.Model.Dlna
return false;
}
+ // Never direct play audio in matroska when the device only declare support for webm.
+ // The first check is not enough because mkv is assumed can be webm.
+ // See https://github.com/jellyfin/jellyfin/issues/13344
+ return !ContainerHelper.ContainsContainer("mkv", item.Container)
+ || profile.SupportsContainer("mkv");
+ }
+
+ private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
+ {
+ if (!IsAudioContainerSupported(profile, item))
+ {
+ return false;
+ }
+
// Check audio codec
string? audioCodec = audioStream?.Codec;
if (!profile.SupportsAudioCodec(audioCodec))
@@ -2227,19 +2287,16 @@ namespace MediaBrowser.Model.Dlna
{
// Check container type, this should NOT be supported
// If the container is supported, the file should be directly played
- if (!profile.SupportsContainer(item.Container))
+ if (IsAudioContainerSupported(profile, item))
{
- // Check audio codec, we cannot use the SupportsAudioCodec here
- // Because that one assumes empty container supports all codec, which is just useless
- string? audioCodec = audioStream?.Codec;
- if (string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
+ return false;
}
- return false;
+ // Check audio codec, we cannot use the SupportsAudioCodec here
+ // Because that one assumes empty container supports all codec, which is just useless
+ string? audioCodec = audioStream?.Codec;
+ return string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase);
}
private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings)
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index 1ae4e1962..e44152213 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -979,7 +979,7 @@ public class StreamInfo
}
list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
- list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
+ list.Add(new NameValuePair("ApiKey", accessToken ?? string.Empty));
string? liveStreamId = item.MediaSource?.LiveStreamId;
list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
@@ -1189,7 +1189,7 @@ public class StreamInfo
if (!string.IsNullOrEmpty(accessToken))
{
- info.Url += "?api_key=" + accessToken;
+ info.Url += "?ApiKey=" + accessToken;
}
info.IsExternalUrl = false;
diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
index 5a9fa22ae..5797d4250 100644
--- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs
+++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
@@ -1,3 +1,4 @@
+using System;
using System.ComponentModel;
using System.Xml.Serialization;
using Jellyfin.Data.Enums;
@@ -6,6 +7,7 @@ namespace MediaBrowser.Model.Dlna;
/// <summary>
/// A class for transcoding profile information.
+/// Note for client developers: Conditions defined in <see cref="CodecProfile"/> has higher priority and can override values defined here.
/// </summary>
public class TranscodingProfile
{
@@ -18,6 +20,33 @@ public class TranscodingProfile
}
/// <summary>
+ /// Initializes a new instance of the <see cref="TranscodingProfile" /> class copying the values from another instance.
+ /// </summary>
+ /// <param name="other">Another instance of <see cref="TranscodingProfile" /> to be copied.</param>
+ public TranscodingProfile(TranscodingProfile other)
+ {
+ ArgumentNullException.ThrowIfNull(other);
+
+ Container = other.Container;
+ Type = other.Type;
+ VideoCodec = other.VideoCodec;
+ AudioCodec = other.AudioCodec;
+ Protocol = other.Protocol;
+ EstimateContentLength = other.EstimateContentLength;
+ EnableMpegtsM2TsMode = other.EnableMpegtsM2TsMode;
+ TranscodeSeekInfo = other.TranscodeSeekInfo;
+ CopyTimestamps = other.CopyTimestamps;
+ Context = other.Context;
+ EnableSubtitlesInManifest = other.EnableSubtitlesInManifest;
+ MaxAudioChannels = other.MaxAudioChannels;
+ MinSegments = other.MinSegments;
+ SegmentLength = other.SegmentLength;
+ BreakOnNonKeyFrames = other.BreakOnNonKeyFrames;
+ Conditions = other.Conditions;
+ EnableAudioVbrEncoding = other.EnableAudioVbrEncoding;
+ }
+
+ /// <summary>
/// Gets or sets the container.
/// </summary>
[XmlAttribute("container")]
diff --git a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs
index 1c60ba460..53b9b1fad 100644
--- a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs
+++ b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs
@@ -17,12 +17,12 @@ public static class ImageFormatExtensions
public static string GetMimeType(this ImageFormat format)
=> format switch
{
- ImageFormat.Bmp => "image/bmp",
+ ImageFormat.Bmp => MediaTypeNames.Image.Bmp,
ImageFormat.Gif => MediaTypeNames.Image.Gif,
ImageFormat.Jpg => MediaTypeNames.Image.Jpeg,
- ImageFormat.Png => "image/png",
- ImageFormat.Webp => "image/webp",
- ImageFormat.Svg => "image/svg+xml",
+ ImageFormat.Png => MediaTypeNames.Image.Png,
+ ImageFormat.Webp => MediaTypeNames.Image.Webp,
+ ImageFormat.Svg => MediaTypeNames.Image.Svg,
_ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
};
diff --git a/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs b/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs
index 5963ed270..d481593cd 100644
--- a/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs
+++ b/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs
@@ -15,13 +15,13 @@ public class ClientCapabilitiesDto
/// <summary>
/// Gets or sets the list of playable media types.
/// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<MediaType> PlayableMediaTypes { get; set; } = [];
/// <summary>
/// Gets or sets the list of supported commands.
/// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; } = [];
/// <summary>
diff --git a/MediaBrowser.Model/Dto/SessionInfoDto.cs b/MediaBrowser.Model/Dto/SessionInfoDto.cs
index 2496c933a..d727cd874 100644
--- a/MediaBrowser.Model/Dto/SessionInfoDto.cs
+++ b/MediaBrowser.Model/Dto/SessionInfoDto.cs
@@ -163,7 +163,7 @@ public class SessionInfoDto
/// <summary>
/// Gets or sets the playlist item id.
/// </summary>
- /// <value>The splaylist item id.</value>
+ /// <value>The playlist item id.</value>
public string? PlaylistItemId { get; set; }
/// <summary>
diff --git a/MediaBrowser.Model/Entities/HardwareAccelerationType.cs b/MediaBrowser.Model/Entities/HardwareAccelerationType.cs
index 198a2e00f..ece18ec3e 100644
--- a/MediaBrowser.Model/Entities/HardwareAccelerationType.cs
+++ b/MediaBrowser.Model/Entities/HardwareAccelerationType.cs
@@ -8,7 +8,7 @@ namespace MediaBrowser.Model.Entities;
public enum HardwareAccelerationType
{
/// <summary>
- /// Software accelleration.
+ /// Software acceleration.
/// </summary>
none = 0,
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index 85c1f797b..400768ef3 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -383,7 +383,7 @@ namespace MediaBrowser.Model.Entities
attributes.Add(string.IsNullOrEmpty(LocalizedUndefined) ? "Und" : LocalizedUndefined);
}
- if (IsHearingImpaired)
+ if (IsHearingImpaired == true)
{
attributes.Add(string.IsNullOrEmpty(LocalizedHearingImpaired) ? "Hearing Impaired" : LocalizedHearingImpaired);
}
@@ -537,7 +537,7 @@ namespace MediaBrowser.Model.Entities
get
{
// In some cases AverageFrameRate for videos will be read as 1000fps even if it is not.
- // This is probably due to a library compatability issue.
+ // This is probably due to a library compatibility issue.
// See https://github.com/jellyfin/jellyfin/pull/12603#discussion_r1748044018 for more info.
return AverageFrameRate < 1000 ? AverageFrameRate : RealFrameRate;
}
diff --git a/MediaBrowser.Model/Entities/MetadataProvider.cs b/MediaBrowser.Model/Entities/MetadataProvider.cs
index bd8db9941..dcc4ae88c 100644
--- a/MediaBrowser.Model/Entities/MetadataProvider.cs
+++ b/MediaBrowser.Model/Entities/MetadataProvider.cs
@@ -27,7 +27,7 @@ namespace MediaBrowser.Model.Entities
Tvdb = 4,
/// <summary>
- /// The tvcom providerd.
+ /// The tvcom provider.
/// </summary>
Tvcom = 5,
diff --git a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
index 479ec7712..385a86d31 100644
--- a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
+++ b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
@@ -11,7 +11,7 @@ namespace MediaBrowser.Model.Entities;
public static class ProviderIdsExtensions
{
/// <summary>
- /// Case insensitive dictionary of <see cref="MetadataProvider"/> string representation.
+ /// Case-insensitive dictionary of <see cref="MetadataProvider"/> string representation.
/// </summary>
private static readonly Dictionary<string, string> _metadataProviderEnumDictionary =
Enum.GetValues<MetadataProvider>()
@@ -107,7 +107,7 @@ public static class ProviderIdsExtensions
/// <param name="instance">The instance.</param>
/// <param name="name">The name, this should not contain a '=' character.</param>
/// <param name="value">The value.</param>
- /// <remarks>Due to how deserialization from the database works the name can not contain '='.</remarks>
+ /// <remarks>Due to how deserialization from the database works the name cannot contain '='.</remarks>
/// <returns><c>true</c> if the provider id got set successfully; otherwise, <c>false</c>.</returns>
public static bool TrySetProviderId(this IHasProviderIds instance, string? name, string? value)
{
@@ -153,7 +153,7 @@ public static class ProviderIdsExtensions
/// <param name="instance">The instance.</param>
/// <param name="name">The name, this should not contain a '=' character.</param>
/// <param name="value">The value.</param>
- /// <remarks>Due to how deserialization from the database works the name can not contain '='.</remarks>
+ /// <remarks>Due to how deserialization from the database works the name cannot contain '='.</remarks>
public static void SetProviderId(this IHasProviderIds instance, string name, string value)
{
ArgumentNullException.ThrowIfNull(instance);
diff --git a/MediaBrowser.Model/Extensions/ContainerHelper.cs b/MediaBrowser.Model/Extensions/ContainerHelper.cs
index c86328ba6..39e5358ba 100644
--- a/MediaBrowser.Model/Extensions/ContainerHelper.cs
+++ b/MediaBrowser.Model/Extensions/ContainerHelper.cs
@@ -14,7 +14,8 @@ public static class ContainerHelper
/// in <paramref name="profileContainers"/>.
/// </summary>
/// <param name="profileContainers">The comma-delimited string being searched.
- /// If the parameter begins with the <c>-</c> character, the operation is reversed.</param>
+ /// If the parameter begins with the <c>-</c> character, the operation is reversed.
+ /// If the parameter is empty or null, all containers in <paramref name="inputContainer"/> will be accepted.</param>
/// <param name="inputContainer">The comma-delimited string being matched.</param>
/// <returns>The result of the operation.</returns>
public static bool ContainsContainer(string? profileContainers, string? inputContainer)
@@ -34,7 +35,8 @@ public static class ContainerHelper
/// in <paramref name="profileContainers"/>.
/// </summary>
/// <param name="profileContainers">The comma-delimited string being searched.
- /// If the parameter begins with the <c>-</c> character, the operation is reversed.</param>
+ /// If the parameter begins with the <c>-</c> character, the operation is reversed.
+ /// If the parameter is empty or null, all containers in <paramref name="inputContainer"/> will be accepted.</param>
/// <param name="inputContainer">The comma-delimited string being matched.</param>
/// <returns>The result of the operation.</returns>
public static bool ContainsContainer(string? profileContainers, ReadOnlySpan<char> inputContainer)
@@ -53,7 +55,8 @@ public static class ContainerHelper
/// Compares two containers, returning <paramref name="isNegativeList"/> if an item in <paramref name="inputContainer"/>
/// does not exist in <paramref name="profileContainers"/>.
/// </summary>
- /// <param name="profileContainers">The comma-delimited string being searched.</param>
+ /// <param name="profileContainers">The comma-delimited string being searched.
+ /// If the parameter is empty or null, all containers in <paramref name="inputContainer"/> will be accepted.</param>
/// <param name="isNegativeList">The boolean result to return if a match is not found.</param>
/// <param name="inputContainer">The comma-delimited string being matched.</param>
/// <returns>The result of the operation.</returns>
@@ -71,7 +74,8 @@ public static class ContainerHelper
/// Compares two containers, returning <paramref name="isNegativeList"/> if an item in <paramref name="inputContainer"/>
/// does not exist in <paramref name="profileContainers"/>.
/// </summary>
- /// <param name="profileContainers">The comma-delimited string being searched.</param>
+ /// <param name="profileContainers">The comma-delimited string being searched.
+ /// If the parameter is empty or null, all containers in <paramref name="inputContainer"/> will be accepted.</param>
/// <param name="isNegativeList">The boolean result to return if a match is not found.</param>
/// <param name="inputContainer">The comma-delimited string being matched.</param>
/// <returns>The result of the operation.</returns>
@@ -106,7 +110,8 @@ public static class ContainerHelper
/// Compares two containers, returning <paramref name="isNegativeList"/> if an item in <paramref name="inputContainer"/>
/// does not exist in <paramref name="profileContainers"/>.
/// </summary>
- /// <param name="profileContainers">The profile containers being matched searched.</param>
+ /// <param name="profileContainers">The profile containers being matched searched.
+ /// If the parameter is empty or null, all containers in <paramref name="inputContainer"/> will be accepted.</param>
/// <param name="isNegativeList">The boolean result to return if a match is not found.</param>
/// <param name="inputContainer">The comma-delimited string being matched.</param>
/// <returns>The result of the operation.</returns>
diff --git a/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs b/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs
index 4a814f22a..b088cfb53 100644
--- a/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs
+++ b/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs
@@ -18,7 +18,7 @@ public static class LibraryOptionsExtension
{
ArgumentNullException.ThrowIfNull(options);
- return options.CustomTagDelimiters.Select<string, char?>(x =>
+ var delimiterList = options.CustomTagDelimiters.Select<string, char?>(x =>
{
var isChar = char.TryParse(x, out var c);
if (isChar)
@@ -27,6 +27,8 @@ public static class LibraryOptionsExtension
}
return null;
- }).Where(x => x is not null).Select(x => x!.Value).ToArray();
+ }).Where(x => x is not null).Select(x => x!.Value).ToList();
+ delimiterList.Add('\0');
+ return delimiterList.ToArray();
}
}
diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
index 02a29e7fa..20deaa505 100644
--- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs
+++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
@@ -52,7 +52,7 @@ namespace MediaBrowser.Model.Globalization
/// <summary>
/// Gets the localization options.
/// </summary>
- /// <returns><see cref="IEnumerable{LocalizatonOption}" />.</returns>
+ /// <returns><see cref="IEnumerable{LocalizationOption}" />.</returns>
IEnumerable<LocalizationOption> GetLocalizationOptions();
/// <summary>
diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs
index 2085328dd..229368d00 100644
--- a/MediaBrowser.Model/IO/IFileSystem.cs
+++ b/MediaBrowser.Model/IO/IFileSystem.cs
@@ -145,7 +145,7 @@ namespace MediaBrowser.Model.IO
/// Gets the directories.
/// </summary>
/// <param name="path">The path.</param>
- /// <param name="recursive">If set to <c>true</c> also searches in subdirectiories.</param>
+ /// <param name="recursive">If set to <c>true</c> also searches in subdirectories.</param>
/// <returns>All found directories.</returns>
IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false);
@@ -153,7 +153,7 @@ namespace MediaBrowser.Model.IO
/// Gets the files.
/// </summary>
/// <param name="path">The path in which to search.</param>
- /// <param name="recursive">If set to <c>true</c> also searches in subdirectiories.</param>
+ /// <param name="recursive">If set to <c>true</c> also searches in subdirectories.</param>
/// <returns>All found files.</returns>
IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false);
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 9489fe190..e9dab6bc8 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -8,13 +8,13 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Model</PackageId>
- <VersionPrefix>10.10.0</VersionPrefix>
+ <VersionPrefix>10.11.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -35,7 +35,7 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
-
+
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="MimeTypes">
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
index 5d65b0f9b..de087d0e7 100644
--- a/MediaBrowser.Model/Net/MimeTypes.cs
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
+using System.Net.Mime;
using Jellyfin.Extensions;
namespace MediaBrowser.Model.Net
@@ -125,6 +126,7 @@ namespace MediaBrowser.Model.Net
new("audio/vorbis", ".vorbis"),
new("audio/x-ape", ".ape"),
new("audio/xsp", ".xsp"),
+ new("audio/x-aac", ".aac"),
new("audio/x-wavpack", ".wv"),
// Type image
@@ -143,7 +145,7 @@ namespace MediaBrowser.Model.Net
new("video/x-matroska", ".mkv"),
}.ToFrozenDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase);
- public static string GetMimeType(string path) => GetMimeType(path, "application/octet-stream");
+ public static string GetMimeType(string path) => GetMimeType(path, MediaTypeNames.Application.Octet);
/// <summary>
/// Gets the type of the MIME.
diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
index ec54b1afd..98f7c6ce1 100644
--- a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
+++ b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
@@ -38,5 +38,5 @@ public class PlaylistCreationRequest
/// <summary>
/// Gets or sets a value indicating whether the playlist is public.
/// </summary>
- public bool? Public { get; set; } = true;
+ public bool? Public { get; set; } = false;
}
diff --git a/MediaBrowser.Model/Plugins/PluginStatus.cs b/MediaBrowser.Model/Plugins/PluginStatus.cs
index bd420d7b4..9c7a8f0c2 100644
--- a/MediaBrowser.Model/Plugins/PluginStatus.cs
+++ b/MediaBrowser.Model/Plugins/PluginStatus.cs
@@ -34,7 +34,12 @@ namespace MediaBrowser.Model.Plugins
Malfunctioned = -3,
/// <summary>
- /// This plugin has been superceded by another version.
+ /// This plugin has been superseded by another version.
+ /// </summary>
+ Superseded = -4,
+
+ /// <summary>
+ /// [DEPRECATED] See Superseded.
/// </summary>
Superceded = -4,
diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs
index ae25267ac..11e83844b 100644
--- a/MediaBrowser.Model/Session/TranscodingInfo.cs
+++ b/MediaBrowser.Model/Session/TranscodingInfo.cs
@@ -5,7 +5,7 @@ using MediaBrowser.Model.Entities;
namespace MediaBrowser.Model.Session;
/// <summary>
-/// Class holding information on a runnning transcode.
+/// Class holding information on a running transcode.
/// </summary>
public class TranscodingInfo
{
diff --git a/MediaBrowser.Model/System/PublicSystemInfo.cs b/MediaBrowser.Model/System/PublicSystemInfo.cs
index 31a895642..c26cfb667 100644
--- a/MediaBrowser.Model/System/PublicSystemInfo.cs
+++ b/MediaBrowser.Model/System/PublicSystemInfo.cs
@@ -47,7 +47,7 @@ namespace MediaBrowser.Model.System
/// Gets or sets a value indicating whether the startup wizard is completed.
/// </summary>
/// <remarks>
- /// Nullable for OpenAPI specification only to retain backwards compatibility in apiclients.
+ /// Nullable for OpenAPI specification only to retain backwards compatibility in api clients.
/// </remarks>
/// <value>The startup completion status.</value>]
public bool? StartupWizardCompleted { get; set; }
diff --git a/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs b/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs
index 63709557d..186c0aed3 100644
--- a/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs
+++ b/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs
@@ -9,30 +9,10 @@ namespace MediaBrowser.Model.Tasks
public class TaskTriggerInfo
{
/// <summary>
- /// The daily trigger.
- /// </summary>
- public const string TriggerDaily = "DailyTrigger";
-
- /// <summary>
- /// The weekly trigger.
- /// </summary>
- public const string TriggerWeekly = "WeeklyTrigger";
-
- /// <summary>
- /// The interval trigger.
- /// </summary>
- public const string TriggerInterval = "IntervalTrigger";
-
- /// <summary>
- /// The startup trigger.
- /// </summary>
- public const string TriggerStartup = "StartupTrigger";
-
- /// <summary>
/// Gets or sets the type.
/// </summary>
/// <value>The type.</value>
- public string Type { get; set; }
+ public TaskTriggerInfoType Type { get; set; }
/// <summary>
/// Gets or sets the time of day.
diff --git a/MediaBrowser.Model/Tasks/TaskTriggerInfoType.cs b/MediaBrowser.Model/Tasks/TaskTriggerInfoType.cs
new file mode 100644
index 000000000..b596cf580
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/TaskTriggerInfoType.cs
@@ -0,0 +1,28 @@
+namespace MediaBrowser.Model.Tasks
+{
+ /// <summary>
+ /// Enum TaskTriggerInfoType.
+ /// </summary>
+ public enum TaskTriggerInfoType
+ {
+ /// <summary>
+ /// The daily trigger.
+ /// </summary>
+ DailyTrigger,
+
+ /// <summary>
+ /// The weekly trigger.
+ /// </summary>
+ WeeklyTrigger,
+
+ /// <summary>
+ /// The interval trigger.
+ /// </summary>
+ IntervalTrigger,
+
+ /// <summary>
+ /// The startup trigger.
+ /// </summary>
+ StartupTrigger
+ }
+}
diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
index 32ab7716f..b51ab4c08 100644
--- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
+++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
@@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.BoxSets
protected override bool EnableUpdatingPremiereDateFromChildren => true;
/// <inheritdoc />
- protected override IList<BaseItem> GetChildrenForMetadataUpdates(BoxSet item)
+ protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(BoxSet item)
{
return item.GetLinkedChildren();
}
diff --git a/MediaBrowser.Providers/Chapters/ChapterManager.cs b/MediaBrowser.Providers/Chapters/ChapterManager.cs
deleted file mode 100644
index 3cbfe7d4d..000000000
--- a/MediaBrowser.Providers/Chapters/ChapterManager.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Chapters;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Providers.Chapters
-{
- public class ChapterManager : IChapterManager
- {
- private readonly IItemRepository _itemRepo;
-
- public ChapterManager(IItemRepository itemRepo)
- {
- _itemRepo = itemRepo;
- }
-
- /// <inheritdoc />
- public void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters)
- {
- _itemRepo.SaveChapters(itemId, chapters);
- }
- }
-}
diff --git a/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs b/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs
index 89d71e172..73912b579 100644
--- a/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs
+++ b/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs
@@ -162,7 +162,7 @@ public class LyricScheduledTask : IScheduledTask
[
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval,
+ Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromHours(24).Ticks
}
];
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index 9a676cb2e..8f6aa2db3 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -291,6 +291,7 @@ namespace MediaBrowser.Providers.Manager
var fileStreamOptions = AsyncFile.WriteOptions;
fileStreamOptions.Mode = FileMode.Create;
+ fileStreamOptions.Options = FileOptions.WriteThrough;
if (source.CanSeek)
{
fileStreamOptions.PreallocationSize = source.Length;
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index 36a7c2fab..ee22b4bc6 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
@@ -229,9 +230,7 @@ namespace MediaBrowser.Providers.Manager
{
var mimeType = MimeTypes.GetMimeType(response.Path);
- var stream = AsyncFile.OpenRead(response.Path);
-
- await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
+ await _providerManager.SaveImage(item, response.Path, mimeType, imageType, null, null, cancellationToken).ConfigureAwait(false);
}
}
@@ -387,8 +386,8 @@ namespace MediaBrowser.Providers.Manager
item.RemoveImages(images);
- // Cleanup old metadata directory for episodes if empty
- if (item is Episode)
+ // Cleanup old metadata directory for episodes if empty, as long as it's not a virtual item
+ if (item is Episode && !item.IsVirtualItem)
{
var oldLocalMetadataDirectory = Path.Combine(item.ContainingFolderPath, "metadata");
if (_fileSystem.DirectoryExists(oldLocalMetadataDirectory) && !_fileSystem.GetFiles(oldLocalMetadataDirectory).Any())
@@ -553,10 +552,16 @@ namespace MediaBrowser.Providers.Manager
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
+ var mimetype = response.Content.Headers.ContentType?.MediaType;
+ if (mimetype is null || mimetype.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
+ {
+ mimetype = MimeTypes.GetMimeType(response.RequestMessage.RequestUri.GetLeftPart(UriPartial.Path));
+ }
+
await _providerManager.SaveImage(
item,
stream,
- response.Content.Headers.ContentType?.MediaType,
+ mimetype,
type,
null,
cancellationToken).ConfigureAwait(false);
@@ -679,10 +684,16 @@ namespace MediaBrowser.Providers.Manager
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
+ var mimetype = response.Content.Headers.ContentType?.MediaType;
+ if (mimetype is null || mimetype.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
+ {
+ mimetype = MimeTypes.GetMimeType(response.RequestMessage.RequestUri.GetLeftPart(UriPartial.Path));
+ }
+
await _providerManager.SaveImage(
item,
stream,
- response.Content.Headers.ContentType?.MediaType,
+ mimetype,
imageType,
null,
cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 7203bf115..778fbc712 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -74,10 +74,11 @@ namespace MediaBrowser.Providers.Manager
public virtual async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
{
var itemOfType = (TItemType)item;
-
var updateType = ItemUpdateType.None;
-
var libraryOptions = LibraryManager.GetLibraryOptions(item);
+ var isFirstRefresh = item.DateLastRefreshed == default;
+ var hasRefreshedMetadata = true;
+ var hasRefreshedImages = true;
var requiresRefresh = libraryOptions.AutomaticRefreshIntervalDays > 0 && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= libraryOptions.AutomaticRefreshIntervalDays;
@@ -131,9 +132,10 @@ namespace MediaBrowser.Providers.Manager
People = LibraryManager.GetPeople(item)
};
- bool hasRefreshedMetadata = true;
- bool hasRefreshedImages = true;
- var isFirstRefresh = item.DateLastRefreshed == default;
+ var beforeSaveResult = BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh || refreshOptions.ForceSave, updateType);
+ updateType |= beforeSaveResult;
+
+ updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false);
// Next run metadata providers
if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
@@ -188,43 +190,43 @@ namespace MediaBrowser.Providers.Manager
}
}
- var beforeSaveResult = BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh || refreshOptions.ForceSave, updateType);
- updateType |= beforeSaveResult;
+ if (hasRefreshedMetadata && hasRefreshedImages)
+ {
+ item.DateLastRefreshed = DateTime.UtcNow;
+ }
+
+ updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false);
+
+ await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false);
- // Save if changes were made, or it's never been saved before
- if (refreshOptions.ForceSave || updateType > ItemUpdateType.None || isFirstRefresh || refreshOptions.ReplaceAllMetadata || requiresRefresh)
+ return updateType;
+
+ async Task<ItemUpdateType> SaveInternal(BaseItem item, MetadataRefreshOptions refreshOptions, ItemUpdateType updateType, bool isFirstRefresh, bool requiresRefresh, MetadataResult<TItemType> metadataResult, CancellationToken cancellationToken)
{
- if (item.IsFileProtocol)
+ // Save if changes were made, or it's never been saved before
+ if (refreshOptions.ForceSave || updateType > ItemUpdateType.None || isFirstRefresh || refreshOptions.ReplaceAllMetadata || requiresRefresh)
{
- var file = TryGetFile(item.Path, refreshOptions.DirectoryService);
- if (file is not null)
+ if (item.IsFileProtocol)
{
- item.DateModified = file.LastWriteTimeUtc;
+ var file = TryGetFile(item.Path, refreshOptions.DirectoryService);
+ if (file is not null)
+ {
+ item.DateModified = file.LastWriteTimeUtc;
+ }
}
- }
- // If any of these properties are set then make sure the updateType is not None, just to force everything to save
- if (refreshOptions.ForceSave || refreshOptions.ReplaceAllMetadata)
- {
- updateType |= ItemUpdateType.MetadataDownload;
- }
+ // If any of these properties are set then make sure the updateType is not None, just to force everything to save
+ if (refreshOptions.ForceSave || refreshOptions.ReplaceAllMetadata)
+ {
+ updateType |= ItemUpdateType.MetadataDownload;
+ }
- if (hasRefreshedMetadata && hasRefreshedImages)
- {
- item.DateLastRefreshed = DateTime.UtcNow;
- }
- else
- {
- item.DateLastRefreshed = default;
+ // Save to database
+ await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false);
}
- // Save to database
- await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false);
+ return updateType;
}
-
- await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false);
-
- return updateType;
}
private void ApplySearchResult(ItemLookupInfo lookupInfo, RemoteSearchResult result)
@@ -322,17 +324,17 @@ namespace MediaBrowser.Providers.Manager
return false;
}
- protected virtual IList<BaseItem> GetChildrenForMetadataUpdates(TItemType item)
+ protected virtual IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(TItemType item)
{
if (item is Folder folder)
{
return folder.GetRecursiveChildren();
}
- return Array.Empty<BaseItem>();
+ return [];
}
- protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
+ protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
{
var updateType = ItemUpdateType.None;
@@ -371,7 +373,7 @@ namespace MediaBrowser.Providers.Manager
return updateType;
}
- private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IList<BaseItem> children)
+ private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IReadOnlyList<BaseItem> children)
{
if (item is Folder folder && folder.SupportsCumulativeRunTimeTicks)
{
@@ -395,7 +397,7 @@ namespace MediaBrowser.Providers.Manager
return ItemUpdateType.None;
}
- private ItemUpdateType UpdateDateLastMediaAdded(TItemType item, IList<BaseItem> children)
+ private ItemUpdateType UpdateDateLastMediaAdded(TItemType item, IReadOnlyList<BaseItem> children)
{
var updateType = ItemUpdateType.None;
@@ -429,7 +431,7 @@ namespace MediaBrowser.Providers.Manager
return updateType;
}
- private ItemUpdateType UpdatePremiereDate(TItemType item, IList<BaseItem> children)
+ private ItemUpdateType UpdatePremiereDate(TItemType item, IReadOnlyList<BaseItem> children)
{
var updateType = ItemUpdateType.None;
@@ -467,7 +469,7 @@ namespace MediaBrowser.Providers.Manager
return updateType;
}
- private ItemUpdateType UpdateGenres(TItemType item, IList<BaseItem> children)
+ private ItemUpdateType UpdateGenres(TItemType item, IReadOnlyList<BaseItem> children)
{
var updateType = ItemUpdateType.None;
@@ -488,7 +490,7 @@ namespace MediaBrowser.Providers.Manager
return updateType;
}
- private ItemUpdateType UpdateStudios(TItemType item, IList<BaseItem> children)
+ private ItemUpdateType UpdateStudios(TItemType item, IReadOnlyList<BaseItem> children)
{
var updateType = ItemUpdateType.None;
@@ -509,7 +511,7 @@ namespace MediaBrowser.Providers.Manager
return updateType;
}
- private ItemUpdateType UpdateOfficialRating(TItemType item, IList<BaseItem> children)
+ private ItemUpdateType UpdateOfficialRating(TItemType item, IReadOnlyList<BaseItem> children)
{
var updateType = ItemUpdateType.None;
@@ -1142,13 +1144,8 @@ namespace MediaBrowser.Providers.Manager
}
}
- private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target)
+ private static void MergePeople(IReadOnlyList<PersonInfo> source, IReadOnlyList<PersonInfo> target)
{
- if (target is null)
- {
- target = new List<PersonInfo>();
- }
-
foreach (var person in target)
{
var normalizedName = person.Name.RemoveDiacritics();
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 81a9af68b..8c45abe25 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
+using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
@@ -47,7 +48,7 @@ namespace MediaBrowser.Providers.Manager
/// </summary>
public class ProviderManager : IProviderManager, IDisposable
{
- private readonly object _refreshQueueLock = new();
+ private readonly Lock _refreshQueueLock = new();
private readonly ILogger<ProviderManager> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryMonitor _libraryMonitor;
@@ -199,24 +200,15 @@ namespace MediaBrowser.Providers.Manager
// TODO: Isolate this hack into the tvh plugin
if (string.IsNullOrEmpty(contentType))
{
+ // Special case for imagecache
if (url.Contains("/imagecache/", StringComparison.OrdinalIgnoreCase))
{
contentType = MediaTypeNames.Image.Png;
}
- else
- {
- throw new HttpRequestException("Invalid image received: contentType not set.", null, response.StatusCode);
- }
}
- // TVDb will sometimes serve a rubbish 404 html page with a 200 OK code, because reasons...
- if (contentType.Equals(MediaTypeNames.Text.Html, StringComparison.OrdinalIgnoreCase))
- {
- throw new HttpRequestException("Invalid image received.", null, HttpStatusCode.NotFound);
- }
-
- // some iptv/epg providers don't correctly report media type, extract from url if no extension found
- if (string.IsNullOrWhiteSpace(MimeTypes.ToExtension(contentType)))
+ // some providers don't correctly report media type, extract from url if no extension found
+ if (contentType is null || contentType.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
{
// Strip query parameters from url to get actual path.
contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path));
@@ -224,7 +216,7 @@ namespace MediaBrowser.Providers.Manager
if (!contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
{
- throw new HttpRequestException($"Request returned {contentType} instead of an image type", null, HttpStatusCode.NotFound);
+ throw new HttpRequestException($"Request returned '{contentType}' instead of an image type", null, HttpStatusCode.NotFound);
}
var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
@@ -251,15 +243,31 @@ namespace MediaBrowser.Providers.Manager
}
/// <inheritdoc/>
- public Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken)
+ public async Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(source))
{
throw new ArgumentNullException(nameof(source));
}
- var fileStream = AsyncFile.OpenRead(source);
- return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
+ try
+ {
+ var fileStream = AsyncFile.OpenRead(source);
+ await new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger)
+ .SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ finally
+ {
+ try
+ {
+ File.Delete(source);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Source file {Source} not found or in use, skip removing", source);
+ }
+ }
}
/// <inheritdoc/>
@@ -1016,7 +1024,6 @@ namespace MediaBrowser.Providers.Manager
/// <inheritdoc/>
public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority)
{
- ArgumentNullException.ThrowIfNull(itemId);
if (itemId.IsEmpty())
{
throw new ArgumentException("Guid can't be empty", nameof(itemId));
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 9a65852f0..34b3104b0 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -23,15 +23,14 @@
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="PlaylistsNET" />
- <PackageReference Include="z440.atl.core"/>
+ <PackageReference Include="z440.atl.core" />
<PackageReference Include="TMDbLib" />
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<!-- Code Analyzers -->
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 27f6d120f..a0481a642 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -36,6 +36,7 @@ namespace MediaBrowser.Providers.MediaInfo
private readonly IMediaSourceManager _mediaSourceManager;
private readonly LyricResolver _lyricResolver;
private readonly ILyricManager _lyricManager;
+ private readonly IMediaStreamRepository _mediaStreamRepository;
/// <summary>
/// Initializes a new instance of the <see cref="AudioFileProber"/> class.
@@ -47,6 +48,7 @@ namespace MediaBrowser.Providers.MediaInfo
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="lyricResolver">Instance of the <see cref="LyricResolver"/> interface.</param>
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
+ /// <param name="mediaStreamRepository">Instance of the <see cref="IMediaStreamRepository"/>.</param>
public AudioFileProber(
ILogger<AudioFileProber> logger,
IMediaSourceManager mediaSourceManager,
@@ -54,7 +56,8 @@ namespace MediaBrowser.Providers.MediaInfo
IItemRepository itemRepo,
ILibraryManager libraryManager,
LyricResolver lyricResolver,
- ILyricManager lyricManager)
+ ILyricManager lyricManager,
+ IMediaStreamRepository mediaStreamRepository)
{
_mediaEncoder = mediaEncoder;
_itemRepo = itemRepo;
@@ -63,6 +66,7 @@ namespace MediaBrowser.Providers.MediaInfo
_mediaSourceManager = mediaSourceManager;
_lyricResolver = lyricResolver;
_lyricManager = lyricManager;
+ _mediaStreamRepository = mediaStreamRepository;
ATL.Settings.DisplayValueSeparator = InternalValueSeparator;
ATL.Settings.UseFileNameWhenNoTitle = false;
ATL.Settings.ID3v2_separatev2v3Values = false;
@@ -149,7 +153,7 @@ namespace MediaBrowser.Providers.MediaInfo
audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric);
- _itemRepo.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken);
+ _mediaStreamRepository.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken);
}
/// <summary>
@@ -172,14 +176,14 @@ namespace MediaBrowser.Providers.MediaInfo
track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
- track.Year ??= mediaInfo.ProductionYear;
- track.TrackNumber ??= mediaInfo.IndexNumber;
- track.DiscNumber ??= mediaInfo.ParentIndexNumber;
+ track.Year = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year;
+ track.TrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber;
+ track.DiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber;
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
{
var people = new List<PersonInfo>();
- var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split(InternalValueSeparator);
+ var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? [] : track.AlbumArtist.Split(InternalValueSeparator);
if (libraryOptions.UseCustomTagDelimiters)
{
@@ -210,7 +214,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (performers is null || performers.Length == 0)
{
- performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator);
+ performers = string.IsNullOrEmpty(track.Artist) ? [] : track.Artist.Split(InternalValueSeparator);
}
if (libraryOptions.UseCustomTagDelimiters)
@@ -314,7 +318,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (!audio.LockedFields.Contains(MetadataField.Genres))
{
- var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+ var genres = string.IsNullOrEmpty(track.Genre) ? [] : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
if (libraryOptions.UseCustomTagDelimiters)
{
@@ -347,7 +351,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag))
&& !string.IsNullOrEmpty(musicBrainzArtistTag))
{
- audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzArtistTag);
+ var id = GetFirstMusicBrainzId(musicBrainzArtistTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, id);
}
}
@@ -357,7 +362,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag))
&& !string.IsNullOrEmpty(musicBrainzReleaseArtistIdTag))
{
- audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, musicBrainzReleaseArtistIdTag);
+ var id = GetFirstMusicBrainzId(musicBrainzReleaseArtistIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, id);
}
}
@@ -367,7 +373,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag))
&& !string.IsNullOrEmpty(musicBrainzReleaseIdTag))
{
- audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, musicBrainzReleaseIdTag);
+ var id = GetFirstMusicBrainzId(musicBrainzReleaseIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, id);
}
}
@@ -377,7 +384,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag))
&& !string.IsNullOrEmpty(musicBrainzReleaseGroupIdTag))
{
- audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, musicBrainzReleaseGroupIdTag);
+ var id = GetFirstMusicBrainzId(musicBrainzReleaseGroupIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, id);
}
}
@@ -387,7 +395,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId))
&& !string.IsNullOrEmpty(trackMbId))
{
- audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
+ var id = GetFirstMusicBrainzId(trackMbId, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, id);
}
}
@@ -441,5 +450,18 @@ namespace MediaBrowser.Providers.MediaInfo
return items;
}
+
+ // MusicBrainz IDs are multi-value tags, so we need to split them
+ // However, our current provider can only have one single ID, which means we need to pick the first one
+ private string? GetFirstMusicBrainzId(string tag, bool useCustomTagDelimiters, char[] tagDelimiters, string[] whitelist)
+ {
+ var val = tag.Split(InternalValueSeparator).FirstOrDefault();
+ if (val is not null && useCustomTagDelimiters)
+ {
+ val = SplitWithCustomDelimiter(val, tagDelimiters, whitelist).FirstOrDefault();
+ }
+
+ return val;
+ }
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
index d1c0ddb37..cc2b3face 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
@@ -1,4 +1,4 @@
-#nullable disable
+#pragma warning disable CA1826 // CA1826 Do not use Enumerable methods on Indexable collections.
using System;
using System.Collections.Generic;
@@ -74,18 +74,17 @@ namespace MediaBrowser.Providers.MediaInfo
return GetImage((Audio)item, imageStreams, cancellationToken);
}
- private async Task<DynamicImageResponse> GetImage(Audio item, List<MediaStream> imageStreams, CancellationToken cancellationToken)
+ private async Task<DynamicImageResponse> GetImage(Audio item, IReadOnlyList<MediaStream> imageStreams, CancellationToken cancellationToken)
{
var path = GetAudioImagePath(item);
if (!File.Exists(path))
{
- Directory.CreateDirectory(Path.GetDirectoryName(path));
-
+ var directoryName = Path.GetDirectoryName(path) ?? throw new InvalidOperationException($"Invalid path '{path}'");
+ Directory.CreateDirectory(directoryName);
var imageStream = imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).Contains("front", StringComparison.OrdinalIgnoreCase)) ??
imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).Contains("cover", StringComparison.OrdinalIgnoreCase)) ??
imageStreams.FirstOrDefault();
-
var imageStreamIndex = imageStream?.Index;
var tempFile = await _mediaEncoder.ExtractAudioImage(item.Path, imageStreamIndex, cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 246ba2733..16f3175d2 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -31,6 +31,7 @@ namespace MediaBrowser.Providers.MediaInfo
public class FFProbeVideoInfo
{
private readonly ILogger<FFProbeVideoInfo> _logger;
+ private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IItemRepository _itemRepo;
private readonly IBlurayExaminer _blurayExaminer;
@@ -38,11 +39,12 @@ namespace MediaBrowser.Providers.MediaInfo
private readonly IEncodingManager _encodingManager;
private readonly IServerConfigurationManager _config;
private readonly ISubtitleManager _subtitleManager;
- private readonly IChapterManager _chapterManager;
+ private readonly IChapterRepository _chapterManager;
private readonly ILibraryManager _libraryManager;
private readonly AudioResolver _audioResolver;
private readonly SubtitleResolver _subtitleResolver;
- private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IMediaAttachmentRepository _mediaAttachmentRepository;
+ private readonly IMediaStreamRepository _mediaStreamRepository;
public FFProbeVideoInfo(
ILogger<FFProbeVideoInfo> logger,
@@ -54,10 +56,12 @@ namespace MediaBrowser.Providers.MediaInfo
IEncodingManager encodingManager,
IServerConfigurationManager config,
ISubtitleManager subtitleManager,
- IChapterManager chapterManager,
+ IChapterRepository chapterManager,
ILibraryManager libraryManager,
AudioResolver audioResolver,
- SubtitleResolver subtitleResolver)
+ SubtitleResolver subtitleResolver,
+ IMediaAttachmentRepository mediaAttachmentRepository,
+ IMediaStreamRepository mediaStreamRepository)
{
_logger = logger;
_mediaSourceManager = mediaSourceManager;
@@ -72,6 +76,9 @@ namespace MediaBrowser.Providers.MediaInfo
_libraryManager = libraryManager;
_audioResolver = audioResolver;
_subtitleResolver = subtitleResolver;
+ _mediaAttachmentRepository = mediaAttachmentRepository;
+ _mediaStreamRepository = mediaStreamRepository;
+ _mediaStreamRepository = mediaStreamRepository;
}
public async Task<ItemUpdateType> ProbeVideo<T>(
@@ -267,11 +274,11 @@ namespace MediaBrowser.Providers.MediaInfo
video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle);
- _itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken);
+ _mediaStreamRepository.SaveMediaStreams(video.Id, mediaStreams, cancellationToken);
if (mediaAttachments.Any())
{
- _itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
+ _mediaAttachmentRepository.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
}
if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh
@@ -354,7 +361,7 @@ namespace MediaBrowser.Providers.MediaInfo
blurayVideoStream.Codec = ffmpegVideoStream.Codec;
blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate;
blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width;
- blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Height;
+ blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Height : blurayVideoStream.Height;
blurayVideoStream.ColorRange = ffmpegVideoStream.ColorRange;
blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace;
blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer;
@@ -627,7 +634,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
var runtime = video.RunTimeTicks.GetValueOrDefault();
- // Only process files with a runtime higher than 0 and lower than 12h. The latter are likely corrupted.
+ // Only process files with a runtime greater than 0 and less than 12h. The latter are likely corrupted.
if (runtime < 0 || runtime > TimeSpan.FromHours(12).Ticks)
{
throw new ArgumentException(
diff --git a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs
index fbec4e963..0716cdfa0 100644
--- a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs
+++ b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs
@@ -119,7 +119,7 @@ namespace MediaBrowser.Providers.MediaInfo
|| (mediaStream.Type == MediaStreamType.Subtitle && _type == DlnaProfileType.Subtitle))
{
mediaStream.Index = startIndex++;
- mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault;
+ mediaStream.IsDefault = pathInfo.IsDefault;
mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced;
mediaStream.IsHearingImpaired = pathInfo.IsHearingImpaired || mediaStream.IsHearingImpaired;
diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
index 04da8fb88..1c2f8b913 100644
--- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
@@ -61,12 +61,14 @@ namespace MediaBrowser.Providers.MediaInfo
/// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param>
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="subtitleManager">Instance of the <see cref="ISubtitleManager"/> interface.</param>
- /// <param name="chapterManager">Instance of the <see cref="IChapterManager"/> interface.</param>
+ /// <param name="chapterManager">Instance of the <see cref="IChapterRepository"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/>.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="namingOptions">The <see cref="NamingOptions"/>.</param>
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
+ /// <param name="mediaAttachmentRepository">Instance of the <see cref="IMediaAttachmentRepository"/> interface.</param>
+ /// <param name="mediaStreamRepository">Instance of the <see cref="IMediaStreamRepository"/> interface.</param>
public ProbeProvider(
IMediaSourceManager mediaSourceManager,
IMediaEncoder mediaEncoder,
@@ -76,12 +78,14 @@ namespace MediaBrowser.Providers.MediaInfo
IEncodingManager encodingManager,
IServerConfigurationManager config,
ISubtitleManager subtitleManager,
- IChapterManager chapterManager,
+ IChapterRepository chapterManager,
ILibraryManager libraryManager,
IFileSystem fileSystem,
ILoggerFactory loggerFactory,
NamingOptions namingOptions,
- ILyricManager lyricManager)
+ ILyricManager lyricManager,
+ IMediaAttachmentRepository mediaAttachmentRepository,
+ IMediaStreamRepository mediaStreamRepository)
{
_logger = loggerFactory.CreateLogger<ProbeProvider>();
_audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
@@ -101,7 +105,9 @@ namespace MediaBrowser.Providers.MediaInfo
chapterManager,
libraryManager,
_audioResolver,
- _subtitleResolver);
+ _subtitleResolver,
+ mediaAttachmentRepository,
+ mediaStreamRepository);
_audioProber = new AudioFileProber(
loggerFactory.CreateLogger<AudioFileProber>(),
@@ -110,7 +116,8 @@ namespace MediaBrowser.Providers.MediaInfo
itemRepo,
libraryManager,
_lyricResolver,
- lyricManager);
+ lyricManager,
+ mediaStreamRepository);
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
index 20fb4dab9..227f31025 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
@@ -31,7 +31,7 @@ namespace MediaBrowser.Providers.MediaInfo
public async Task<List<string>> DownloadSubtitles(
Video video,
- List<MediaStream> mediaStreams,
+ IReadOnlyList<MediaStream> mediaStreams,
bool skipIfEmbeddedSubtitlesPresent,
bool skipIfAudioTrackMatches,
bool requirePerfectMatch,
@@ -68,7 +68,7 @@ namespace MediaBrowser.Providers.MediaInfo
public Task<bool> DownloadSubtitles(
Video video,
- List<MediaStream> mediaStreams,
+ IReadOnlyList<MediaStream> mediaStreams,
bool skipIfEmbeddedSubtitlesPresent,
bool skipIfAudioTrackMatches,
bool requirePerfectMatch,
@@ -120,7 +120,7 @@ namespace MediaBrowser.Providers.MediaInfo
private async Task<bool> DownloadSubtitles(
Video video,
- List<MediaStream> mediaStreams,
+ IReadOnlyList<MediaStream> mediaStreams,
bool skipIfEmbeddedSubtitlesPresent,
bool skipIfAudioTrackMatches,
bool requirePerfectMatch,
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
index 6eb75891a..938f3cb32 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
@@ -217,7 +217,7 @@ namespace MediaBrowser.Providers.MediaInfo
return new[]
{
// Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
+ new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks }
};
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
index ba7ad4072..3d446053b 100644
--- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CA1826 // CA1826 Do not use Enumerable methods on Indexable collections.
+
using System;
using System.Collections.Generic;
using System.Linq;
diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
index a39bd16ce..25698d8cb 100644
--- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs
+++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
@@ -47,11 +47,11 @@ namespace MediaBrowser.Providers.Music
protected override bool EnableUpdatingStudiosFromChildren => true;
/// <inheritdoc />
- protected override IList<BaseItem> GetChildrenForMetadataUpdates(MusicAlbum item)
+ protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(MusicAlbum item)
=> item.GetRecursiveChildren(i => i is Audio);
/// <inheritdoc />
- protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
+ protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
{
var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType);
diff --git a/MediaBrowser.Providers/Music/ArtistMetadataService.cs b/MediaBrowser.Providers/Music/ArtistMetadataService.cs
index 1f342c0db..c47f9a500 100644
--- a/MediaBrowser.Providers/Music/ArtistMetadataService.cs
+++ b/MediaBrowser.Providers/Music/ArtistMetadataService.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System.Collections.Generic;
+using System.Collections.Immutable;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -28,7 +29,7 @@ namespace MediaBrowser.Providers.Music
protected override bool EnableUpdatingGenresFromChildren => true;
/// <inheritdoc />
- protected override IList<BaseItem> GetChildrenForMetadataUpdates(MusicArtist item)
+ protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(MusicArtist item)
{
return item.IsAccessedByName
? item.GetTaggedItems(new InternalItemsQuery
diff --git a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
index 43889bfbf..7be54453f 100644
--- a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
+++ b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
@@ -36,7 +36,7 @@ namespace MediaBrowser.Providers.Playlists
protected override bool EnableUpdatingStudiosFromChildren => true;
/// <inheritdoc />
- protected override IList<BaseItem> GetChildrenForMetadataUpdates(Playlist item)
+ protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Playlist item)
=> item.GetLinkedChildren();
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
index daad9706c..ff30af879 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
@@ -148,21 +148,19 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
item.Overview = (overview ?? string.Empty).StripHtml();
}
- internal Task EnsureInfo(string musicBrainzReleaseGroupId, CancellationToken cancellationToken)
+ internal async Task EnsureInfo(string musicBrainzReleaseGroupId, CancellationToken cancellationToken)
{
var xmlPath = GetAlbumInfoPath(_config.ApplicationPaths, musicBrainzReleaseGroupId);
var fileInfo = _fileSystem.GetFileSystemInfo(xmlPath);
- if (fileInfo.Exists)
+ if (fileInfo.Exists
+ && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
{
- if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
- {
- return Task.CompletedTask;
- }
+ return;
}
- return DownloadInfo(musicBrainzReleaseGroupId, cancellationToken);
+ await DownloadInfo(musicBrainzReleaseGroupId, cancellationToken).ConfigureAwait(false);
}
internal async Task DownloadInfo(string musicBrainzReleaseGroupId, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
index 92742b1aa..00bd96282 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
@@ -131,7 +131,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
item.Overview = (overview ?? string.Empty).StripHtml();
}
- internal Task EnsureArtistInfo(string musicBrainzId, CancellationToken cancellationToken)
+ internal async Task EnsureArtistInfo(string musicBrainzId, CancellationToken cancellationToken)
{
var xmlPath = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId);
@@ -140,10 +140,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
if (fileInfo.Exists
&& (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
{
- return Task.CompletedTask;
+ return;
}
- return DownloadArtistInfo(musicBrainzId, cancellationToken);
+ await DownloadArtistInfo(musicBrainzId, cancellationToken).ConfigureAwait(false);
}
internal async Task DownloadArtistInfo(string musicBrainzId, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
index 5ca9f6f9a..a50d69df5 100644
--- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
@@ -101,11 +101,11 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
return string.Format(CultureInfo.InvariantCulture, "{0}/images/{1}/{2}.jpg", GetRepositoryUrl(), image, filename);
}
- private Task EnsureThumbsList(string file, CancellationToken cancellationToken)
+ private async Task EnsureThumbsList(string file, CancellationToken cancellationToken)
{
string url = string.Format(CultureInfo.InvariantCulture, "{0}/thumbs.txt", GetRepositoryUrl());
- return EnsureList(url, file, _fileSystem, cancellationToken);
+ await EnsureList(url, file, _fileSystem, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index 8d68e2dcf..eef08b251 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -198,7 +198,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
};
movie.SetProviderId(MetadataProvider.Tmdb, tmdbId);
- movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
+ movie.TrySetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
if (movieResult.BelongsToCollection is not null)
{
movie.SetProviderId(MetadataProvider.TmdbCollection, movieResult.BelongsToCollection.Id.ToString(CultureInfo.InvariantCulture));
diff --git a/MediaBrowser.Providers/TV/SeasonMetadataService.cs b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
index 8b690193e..b27ccaa6a 100644
--- a/MediaBrowser.Providers/TV/SeasonMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
@@ -80,11 +80,11 @@ namespace MediaBrowser.Providers.TV
}
/// <inheritdoc />
- protected override IList<BaseItem> GetChildrenForMetadataUpdates(Season item)
+ protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Season item)
=> item.GetEpisodes();
/// <inheritdoc />
- protected override ItemUpdateType UpdateMetadataFromChildren(Season item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
+ protected override ItemUpdateType UpdateMetadataFromChildren(Season item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
{
var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType);
@@ -96,7 +96,7 @@ namespace MediaBrowser.Providers.TV
return updateType;
}
- private ItemUpdateType SaveIsVirtualItem(Season item, IList<BaseItem> episodes)
+ private ItemUpdateType SaveIsVirtualItem(Season item, IReadOnlyList<BaseItem> episodes)
{
var isVirtualItem = item.LocationType == LocationType.Virtual && (episodes.Count == 0 || episodes.All(i => i.LocationType == LocationType.Virtual));
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index f4aede463..284415dce 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -140,38 +140,39 @@ namespace MediaBrowser.Providers.TV
private void RemoveObsoleteEpisodes(Series series)
{
- var episodes = series.GetEpisodes(null, new DtoOptions(), true).OfType<Episode>().ToList();
- var numberOfEpisodes = episodes.Count;
- // TODO: O(n^2), but can it be done faster without overcomplicating it?
- for (var i = 0; i < numberOfEpisodes; i++)
+ var episodesBySeason = series.GetEpisodes(null, new DtoOptions(), true)
+ .OfType<Episode>()
+ .GroupBy(e => e.ParentIndexNumber)
+ .ToList();
+
+ foreach (var seasonEpisodes in episodesBySeason)
{
- var currentEpisode = episodes[i];
- // The outer loop only examines virtual episodes
- if (!currentEpisode.IsVirtualItem)
+ List<Episode> nonPhysicalEpisodes = [];
+ List<Episode> physicalEpisodes = [];
+ foreach (var episode in seasonEpisodes)
{
- continue;
- }
+ if (episode.IsVirtualItem || episode.IsMissingEpisode)
+ {
+ nonPhysicalEpisodes.Add(episode);
+ continue;
+ }
- // Virtual episodes without an episode number are practically orphaned and should be deleted
- if (!currentEpisode.IndexNumber.HasValue)
- {
- DeleteEpisode(currentEpisode);
- continue;
+ physicalEpisodes.Add(episode);
}
- for (var j = i + 1; j < numberOfEpisodes; j++)
+ // Only consider non-physical episodes
+ foreach (var episode in nonPhysicalEpisodes)
{
- var comparisonEpisode = episodes[j];
- // The inner loop is only for "physical" episodes
- if (comparisonEpisode.IsVirtualItem
- || currentEpisode.ParentIndexNumber != comparisonEpisode.ParentIndexNumber
- || !comparisonEpisode.ContainsEpisodeNumber(currentEpisode.IndexNumber.Value))
+ // Episodes without an episode number are practically orphaned and should be deleted
+ // Episodes with a physical equivalent should be deleted (they are no longer missing)
+ var shouldKeep = episode.IndexNumber.HasValue && !physicalEpisodes.Any(e => e.ContainsEpisodeNumber(episode.IndexNumber.Value));
+
+ if (shouldKeep)
{
continue;
}
- DeleteEpisode(currentEpisode);
- break;
+ DeleteEpisode(episode);
}
}
}
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
index 31c0eeb31..4310f93d4 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
@@ -63,7 +63,7 @@ public class TrickplayImagesTask : IScheduledTask
{
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerDaily,
+ Type = TaskTriggerInfoType.DailyTrigger,
TimeOfDayTicks = TimeSpan.FromHours(3).Ticks
}
};
diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
index c20073eea..b195af96c 100644
--- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -15,7 +15,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index a8800431e..75ad0d58c 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -73,7 +73,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
protected IProviderManager ProviderManager { get; }
/// <summary>
- /// Gets a value indicating whether URLs after a closing XML tag are supporrted.
+ /// Gets a value indicating whether URLs after a closing XML tag are supported.
/// </summary>
protected virtual bool SupportsUrlAfterClosingXmlTag => false;
@@ -312,8 +312,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (user is not null)
{
userData = _userDataManager.GetUserData(user, item);
- userData.Played = played;
- _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
+ if (userData is not null)
+ {
+ userData.Played = played;
+ _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
+ }
}
}
@@ -326,8 +329,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (user is not null)
{
userData = _userDataManager.GetUserData(user, item);
- userData.PlayCount = count;
- _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
+ if (userData is not null)
+ {
+ userData.PlayCount = count;
+ _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
+ }
}
}
@@ -340,8 +346,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (user is not null)
{
userData = _userDataManager.GetUserData(user, item);
- userData.LastPlayedDate = lastPlayed;
- _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
+ if (userData is not null)
+ {
+ userData.LastPlayedDate = lastPlayed;
+ _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
+ }
}
}
@@ -663,7 +672,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
var fileSystemMetadata = _directoryService.GetFile(val);
- // non existing file returns null
+ // nonexistent file returns null
if (fileSystemMetadata is null || !fileSystemMetadata.Exists)
{
Logger.LogWarning("Artwork file {Path} specified in nfo file for {ItemName} does not exist.", uri, itemResult.Item.Name);
diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
index 2d65188b6..137abff47 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
@@ -50,23 +50,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
case "id":
{
- // get ids from attributes
+ // Get ids from attributes
+ item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
+ item.TrySetProviderId(MetadataProvider.Tvdb, reader.GetAttribute("TVDB"));
string? imdbId = reader.GetAttribute("IMDB");
- string? tmdbId = reader.GetAttribute("TMDB");
- // read id from content
+ // Read id from content
+ // Content can be arbitrary according to Kodi wiki, so only parse if we are sure it matches a provider-specific schema
var contentId = reader.ReadElementContentAsString();
- if (contentId.Contains("tt", StringComparison.Ordinal) && string.IsNullOrEmpty(imdbId))
+ if (string.IsNullOrEmpty(imdbId) && contentId.StartsWith("tt", StringComparison.Ordinal))
{
imdbId = contentId;
}
- else if (string.IsNullOrEmpty(tmdbId))
- {
- tmdbId = contentId;
- }
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
- item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
break;
}
@@ -82,21 +79,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val) && movie is not null)
{
- // TODO Handle this better later
- if (!val.Contains('<', StringComparison.Ordinal))
+ try
{
- movie.CollectionName = val;
+ ParseSetXml(val, movie);
}
- else
+ catch (Exception ex)
{
- try
- {
- ParseSetXml(val, movie);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error parsing set node");
- }
+ Logger.LogError(ex, "Error parsing set node");
}
}
@@ -139,7 +128,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
- if (reader.NodeType == XmlNodeType.Element)
+ if (reader.NodeType == XmlNodeType.Text && reader.Depth == 1)
+ {
+ movie.CollectionName = reader.Value;
+ break;
+ }
+ else if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
index 59abef919..b0944515b 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
@@ -1,3 +1,4 @@
+using System;
using System.Globalization;
using System.Xml;
using Emby.Naming.TV;
@@ -48,16 +49,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
case "id":
{
- item.TrySetProviderId(MetadataProvider.Imdb, reader.GetAttribute("IMDB"));
+ // Get ids from attributes
item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
+ item.TrySetProviderId(MetadataProvider.Tvdb, reader.GetAttribute("TVDB"));
+ string? imdbId = reader.GetAttribute("IMDB");
- string? tvdbId = reader.GetAttribute("TVDB");
- if (string.IsNullOrWhiteSpace(tvdbId))
+ // Read id from content
+ // Content can be arbitrary according to Kodi wiki, so only parse if we are sure it matches a provider-specific schema
+ var contentId = reader.ReadElementContentAsString();
+ if (string.IsNullOrEmpty(imdbId) && contentId.StartsWith("tt", StringComparison.Ordinal))
{
- tvdbId = reader.ReadElementContentAsString();
+ imdbId = contentId;
}
- item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
+ item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
break;
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
index 813d75f6c..df72ff044 100644
--- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
@@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.XbmcMetadata.Savers
{
/// <summary>
- /// Nfo saver for artsist.
+ /// Nfo saver for artist.
/// </summary>
public class ArtistNfoSaver : BaseNfoSaver
{
@@ -67,7 +67,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
AddAlbums(albums, writer);
}
- private void AddAlbums(IList<BaseItem> albums, XmlWriter writer)
+ private void AddAlbums(IReadOnlyList<BaseItem> albums, XmlWriter writer)
{
foreach (var album in albums)
{
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index 2afec3f6c..51c5a2080 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -869,49 +869,52 @@ namespace MediaBrowser.XbmcMetadata.Savers
var userdata = userDataRepo.GetUserData(user, item);
- writer.WriteElementString(
- "isuserfavorite",
- userdata.IsFavorite.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
-
- if (userdata.Rating.HasValue)
- {
- writer.WriteElementString(
- "userrating",
- userdata.Rating.Value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
- }
-
- if (!item.IsFolder)
+ if (userdata is not null)
{
writer.WriteElementString(
- "playcount",
- userdata.PlayCount.ToString(CultureInfo.InvariantCulture));
- writer.WriteElementString(
- "watched",
- userdata.Played.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+ "isuserfavorite",
+ userdata.IsFavorite.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
- if (userdata.LastPlayedDate.HasValue)
+ if (userdata.Rating.HasValue)
{
writer.WriteElementString(
- "lastplayed",
- userdata.LastPlayedDate.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture).ToLowerInvariant());
+ "userrating",
+ userdata.Rating.Value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
}
- writer.WriteStartElement("resume");
+ if (!item.IsFolder)
+ {
+ writer.WriteElementString(
+ "playcount",
+ userdata.PlayCount.ToString(CultureInfo.InvariantCulture));
+ writer.WriteElementString(
+ "watched",
+ userdata.Played.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
- var runTimeTicks = item.RunTimeTicks ?? 0;
+ if (userdata.LastPlayedDate.HasValue)
+ {
+ writer.WriteElementString(
+ "lastplayed",
+ userdata.LastPlayedDate.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture).ToLowerInvariant());
+ }
- writer.WriteElementString(
- "position",
- TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
- writer.WriteElementString(
- "total",
- TimeSpan.FromTicks(runTimeTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
+ writer.WriteStartElement("resume");
+
+ var runTimeTicks = item.RunTimeTicks ?? 0;
+
+ writer.WriteElementString(
+ "position",
+ TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
+ writer.WriteElementString(
+ "total",
+ TimeSpan.FromTicks(runTimeTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
+ }
}
writer.WriteEndElement();
}
- private void AddActors(List<PersonInfo> people, XmlWriter writer, ILibraryManager libraryManager, bool saveImagePath)
+ private void AddActors(IReadOnlyList<PersonInfo> people, XmlWriter writer, ILibraryManager libraryManager, bool saveImagePath)
{
foreach (var person in people)
{
diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
index bc344d87e..e85e369d9 100644
--- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
@@ -115,7 +115,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
if (!string.IsNullOrEmpty(movie.CollectionName))
{
- writer.WriteElementString("set", movie.CollectionName);
+ writer.WriteStartElement("set");
+ writer.WriteElementString("name", movie.CollectionName);
+ writer.WriteEndElement();
}
}
}
diff --git a/README.md b/README.md
index 7da0cb30d..a07fd32ce 100644
--- a/README.md
+++ b/README.md
@@ -74,7 +74,7 @@ These instructions will help you get set up with a local development environment
### Prerequisites
-Before the project can be built, you must first install the [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet) on your system.
+Before the project can be built, you must first install the [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet) on your system.
Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET 6 development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2022) and [Visual Studio Code](https://code.visualstudio.com/Download).
@@ -131,7 +131,7 @@ A second option is to build the project and then run the resulting executable fi
```bash
dotnet build # Build the project
-cd Jellyfin.Server/bin/Debug/net8.0 # Change into the build output directory
+cd Jellyfin.Server/bin/Debug/net9.0 # Change into the build output directory
```
2. Execute the build output. On Linux, Mac, etc. use `./jellyfin` and on Windows use `jellyfin.exe`.
diff --git a/SharedVersion.cs b/SharedVersion.cs
index f98cfbc74..d26eb31ae 100644
--- a/SharedVersion.cs
+++ b/SharedVersion.cs
@@ -1,4 +1,4 @@
using System.Reflection;
-[assembly: AssemblyVersion("10.10.0")]
-[assembly: AssemblyFileVersion("10.10.0")]
+[assembly: AssemblyVersion("10.11.0")]
+[assembly: AssemblyFileVersion("10.11.0")]
diff --git a/bump_version b/bump_version
index 72bbbfbf5..6d08dc72f 100755
--- a/bump_version
+++ b/bump_version
@@ -28,6 +28,7 @@ jellyfin_subprojects=(
Emby.Naming/Emby.Naming.csproj
src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
)
+issue_template_file="./.github/ISSUE_TEMPLATE/issue report.yml"
new_version="$1"
new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
@@ -56,6 +57,9 @@ for subproject in ${jellyfin_subprojects[@]}; do
sed -i "s|${old_version}|${new_version_sed}|g" ${subproject}
done
+# Set the version in the GitHub issue template file
+sed -i "s|${old_version}|${new_version_sed}|g" ${issue_template_file}
+
# Stage the changed files for commit
git add .
git status -v
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
index 73aae3f3d..1373d2fe0 100644
--- a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh
index 80a5cd7c1..8183bb37a 100755
--- a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh
+++ b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh
@@ -8,4 +8,4 @@ cp bin/Emby.Server.Implementations.dll .
dotnet build
mkdir -p Findings
-AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net8.0/Emby.Server.Implementations.Fuzz "$1"
+AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net9.0/Emby.Server.Implementations.Fuzz "$1"
diff --git a/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj
index faac7d976..04c7be11d 100644
--- a/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj
+++ b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
diff --git a/fuzz/Jellyfin.Api.Fuzz/fuzz.sh b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh
index 96b0192cf..15148e1bb 100755
--- a/fuzz/Jellyfin.Api.Fuzz/fuzz.sh
+++ b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh
@@ -8,4 +8,4 @@ cp bin/Jellyfin.Api.dll .
dotnet build
mkdir -p Findings
-AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net8.0/Jellyfin.Api.Fuzz "$1"
+AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net9.0/Jellyfin.Api.Fuzz "$1"
diff --git a/global.json b/global.json
index dbf2988d5..2e13a6387 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "8.0.0",
+ "version": "9.0.0",
"rollForward": "latestMinor"
}
}
diff --git a/jellyfin.code-workspace b/jellyfin.code-workspace
index 7882b3821..844c69dbc 100644
--- a/jellyfin.code-workspace
+++ b/jellyfin.code-workspace
@@ -12,6 +12,6 @@
"**/jellyfin-web": true,
"**/obj": true
},
- "formatFiles.excludePattern": "**/node_modules,**/.vscode,**/dist/**,**/.chrome,ThirdParty,RSSDP,Mono.Nat,unRaid,debian"
+ "formatFiles.excludePattern": "**/node_modules,**/.vscode,**/dist/**,**/.chrome,ThirdParty,unRaid,debian"
}
}
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
deleted file mode 100644
index 67ffd9a37..000000000
--- a/jellyfin.ruleset
+++ /dev/null
@@ -1,221 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RuleSet Name="Rules for Jellyfin.Server" Description="Code analysis rules for Jellyfin.Server.csproj" ToolsVersion="14.0">
- <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
- <!-- error on SA1000: The keyword 'new' should be followed by a space -->
- <Rule Id="SA1000" Action="Error" />
- <!-- error on SA1001: Commas should not be preceded by whitespace -->
- <Rule Id="SA1001" Action="Error" />
- <!-- error on SA1106: Code should not contain empty statements -->
- <Rule Id="SA1106" Action="Error" />
- <!-- error on SA1107: Code should not contain multiple statements on one line -->
- <Rule Id="SA1107" Action="Error" />
- <!-- error on SA1028: Code should not contain trailing whitespace -->
- <Rule Id="SA1028" Action="Error" />
- <!-- error on SA1117: The parameters should all be placed on the same line or each parameter should be placed on its own line -->
- <Rule Id="SA1117" Action="Error" />
- <!-- error on SA1137: Elements should have the same indentation -->
- <Rule Id="SA1137" Action="Error" />
- <!-- error on SA1142: Refer to tuple fields by name -->
- <Rule Id="SA1142" Action="Error" />
- <!-- error on SA1210: Using directives should be ordered alphabetically by the namespaces -->
- <Rule Id="SA1210" Action="Error" />
- <!-- error on SA1316: Tuple element names should use correct casing -->
- <Rule Id="SA1316" Action="Error" />
- <!-- error on SA1414: Tuple types in signatures should have element names -->
- <Rule Id="SA1414" Action="Error" />
- <!-- disable warning SA1513: Closing brace should be followed by blank line -->
- <Rule Id="SA1513" Action="Error" />
- <!-- error on SA1518: File is required to end with a single newline character -->
- <Rule Id="SA1518" Action="Error" />
- <!-- error on SA1629: Documentation text should end with a period -->
- <Rule Id="SA1629" Action="Error" />
-
- <!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
- <Rule Id="SA1009" Action="None" />
- <!-- disable warning SA1011: Closing square bracket should be followed by a space. -->
- <Rule Id="SA1011" Action="None" />
- <!-- disable warning SA1101: Prefix local calls with 'this.' -->
- <Rule Id="SA1101" Action="None" />
- <!-- disable warning SA1108: Block statements should not contain embedded comments -->
- <Rule Id="SA1108" Action="None" />
- <!-- disable warning SA1118: Parameter must not span multiple lines. -->
- <Rule Id="SA1118" Action="None" />
- <!-- disable warning SA1128:: Put constructor initializers on their own line -->
- <Rule Id="SA1128" Action="None" />
- <!-- disable warning SA1130: Use lambda syntax -->
- <Rule Id="SA1130" Action="None" />
- <!-- disable warning SA1200: 'using' directive must appear within a namespace declaration -->
- <Rule Id="SA1200" Action="None" />
- <!-- disable warning SA1202: 'public' members must come before 'private' members -->
- <Rule Id="SA1202" Action="None" />
- <!-- disable warning SA1204: Static members must appear before non-static members -->
- <Rule Id="SA1204" Action="None" />
- <!-- disable warning SA1309: Fields must not begin with an underscore -->
- <Rule Id="SA1309" Action="None" />
- <!-- disable warning SA1311: Static readonly fields should begin with upper-case letter -->
- <Rule Id="SA1311" Action="None" />
- <!-- disable warning SA1413: Use trailing comma in multi-line initializers -->
- <Rule Id="SA1413" Action="None" />
- <!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
- <Rule Id="SA1512" Action="None" />
- <!-- disable warning SA1515: Single-line comment should be preceded by blank line -->
- <Rule Id="SA1515" Action="None" />
- <!-- disable warning SA1600: Elements should be documented -->
- <Rule Id="SA1600" Action="None" />
- <!-- disable warning SA1601: Partial elements should be documented -->
- <Rule Id="SA1601" Action="None" />
- <!-- disable warning SA1602: Enumeration items should be documented -->
- <Rule Id="SA1602" Action="None" />
- <!-- disable warning SA1633: The file header is missing or not located at the top of the file -->
- <Rule Id="SA1633" Action="None" />
- </Rules>
-
- <Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.Design">
- <!-- error on CA1001: Types that own disposable fields should be disposable -->
- <Rule Id="CA1001" Action="Error" />
- <!-- error on CA1012: Abstract types should not have public constructors -->
- <Rule Id="CA1012" Action="Error" />
- <!-- 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 CA1310: Specify StringComparison for correctness -->
- <Rule Id="CA1310" Action="Error" />
- <!-- error on CA1513: Use 'ObjectDisposedException.ThrowIf' instead of explicitly throwing a new exception instance -->
- <Rule Id="CA1513" Action="Error" />
- <!-- error on CA1725: Parameter names should match base declaration -->
- <Rule Id="CA1725" Action="Error" />
- <!-- error on CA1725: Call async methods when in an async method -->
- <Rule Id="CA1727" Action="Error" />
- <!-- error on CA1813: Avoid unsealed attributes -->
- <Rule Id="CA1813" Action="Error" />
- <!-- error on CA1834: Use 'StringBuilder.Append(char)' instead of 'StringBuilder.Append(string)' when the input is a constant unit string -->
- <Rule Id="CA1834" 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 CA1849: Call async methods when in an async method -->
- <Rule Id="CA1849" Action="Error" />
- <!-- error on CA1851: Possible multiple enumerations of IEnumerable collection -->
- <Rule Id="CA1851" Action="Error" />
- <!-- error on CA1854: Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup -->
- <Rule Id="CA1854" Action="Error" />
- <!-- error on CA1860: Avoid using 'Enumerable.Any()' extension method -->
- <Rule Id="CA1860" Action="Error" />
- <!-- error on CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons -->
- <Rule Id="CA1862" Action="Error" />
- <!-- error on CA1863: Use 'CompositeFormat' -->
- <Rule Id="CA1863" Action="Error" />
- <!-- error on CA1864: Prefer the 'IDictionary.TryAdd(TKey, TValue)' method -->
- <Rule Id="CA1864" Action="Error" />
- <!-- error on CA1865-CA1867: Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char -->
- <Rule Id="CA1865" Action="Error" />
- <Rule Id="CA1866" Action="Error" />
- <Rule Id="CA1867" Action="Error" />
- <!-- error on CA1868: Unnecessary call to 'Contains' for sets -->
- <Rule Id="CA1868" Action="Error" />
- <!-- error on CA1869: Cache and reuse 'JsonSerializerOptions' instances -->
- <Rule Id="CA1869" Action="Error" />
- <!-- error on CA1870: Use a cached 'SearchValues' instance -->
- <Rule Id="CA1870" Action="Error" />
- <!-- error on CA1871: Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull' -->
- <Rule Id="CA1871" Action="Error" />
- <!-- error on CA1872: Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString' -->
- <Rule Id="CA1872" Action="Error" />
- <!-- error on CA2016: Forward the CancellationToken parameter to methods that take one
- or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token -->
- <Rule Id="CA2016" Action="Error" />
- <!-- error on CA2201: Exception type System.Exception is not sufficiently specific -->
- <Rule Id="CA2201" Action="Error" />
- <!-- error on CA2215: Dispose methods should call base class dispose -->
- <Rule Id="CA2215" Action="Error" />
- <!-- error on CA2249: Use 'string.Contains' instead of 'string.IndexOf' to improve readability -->
- <Rule Id="CA2249" Action="Error" />
- <!-- error on CA2254: Template should be a static expression -->
- <Rule Id="CA2254" Action="Error" />
-
- <!-- disable warning CA1014: Mark assemblies with CLSCompliantAttribute -->
- <Rule Id="CA1014" Action="Info" />
- <!-- disable warning CA1024: Use properties where appropriate -->
- <Rule Id="CA1024" Action="Info" />
- <!-- disable warning CA1031: Do not catch general exception types -->
- <Rule Id="CA1031" Action="Info" />
- <!-- disable warning CA1032: Implement standard exception constructors -->
- <Rule Id="CA1032" Action="Info" />
- <!-- disable warning CA1040: Avoid empty interfaces -->
- <Rule Id="CA1040" Action="Info" />
- <!-- disable warning CA1062: Validate arguments of public methods -->
- <Rule Id="CA1062" Action="Info" />
- <!-- TODO: enable when false positives are fixed -->
- <!-- disable warning CA1508: Avoid dead conditional code -->
- <Rule Id="CA1508" Action="Info" />
- <!-- disable warning CA1716: Identifiers should not match keywords -->
- <Rule Id="CA1716" Action="Info" />
- <!-- disable warning CA1720: Identifiers should not contain type names -->
- <Rule Id="CA1720" Action="Info" />
- <!-- disable warning CA1724: Type names should not match namespaces -->
- <Rule Id="CA1724" Action="Info" />
- <!-- disable warning CA1805: Do not initialize unnecessarily -->
- <Rule Id="CA1805" Action="Info" />
- <!-- disable warning CA1812: internal class that is apparently never instantiated.
- If so, remove the code from the assembly.
- If this class is intended to contain only static members, make it static -->
- <Rule Id="CA1812" Action="Info" />
- <!-- disable warning CA1822: Member does not access instance data and can be marked as static -->
- <Rule Id="CA1822" Action="Info" />
- <!-- TODO: Enable -->
- <!-- CA1861: Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array -->
- <Rule Id="CA1861" Action="Info" />
- <!-- disable warning CA2000: Dispose objects before losing scope -->
- <Rule Id="CA2000" Action="Info" />
- <!-- disable warning CA2253: Named placeholders should not be numeric values -->
- <Rule Id="CA2253" Action="Info" />
- <!-- disable warning CA5394: Do not use insecure randomness -->
- <Rule Id="CA5394" Action="Info" />
- <!-- error on CA3003: Review code for file path injection vulnerabilities -->
- <Rule Id="CA3003" Action="Info" />
- <!-- error on CA3006: Review code for process command injection vulnerabilities -->
- <Rule Id="CA3006" Action="Info" />
-
- <!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
- <Rule Id="CA1054" Action="None" />
- <!-- disable warning CA1055: URI return values should not be strings -->
- <Rule Id="CA1055" Action="None" />
- <!-- disable warning CA1056: URI properties should not be strings -->
- <Rule Id="CA1056" Action="None" />
- <!-- disable warning CA1303: Do not pass literals as localized parameters -->
- <Rule Id="CA1303" Action="None" />
- <!-- disable warning CA1308: Normalize strings to uppercase -->
- <Rule Id="CA1308" Action="None" />
- <!-- disable warning CA1848: Use the LoggerMessage delegates -->
- <Rule Id="CA1848" Action="None" />
- <!-- disable warning CA2101: Specify marshaling for P/Invoke string arguments -->
- <Rule Id="CA2101" Action="None" />
- <!-- disable warning CA2234: Pass System.Uri objects instead of strings -->
- <Rule Id="CA2234" Action="None" />
- </Rules>
-
- <Rules AnalyzerId="Microsoft.CodeAnalysis.BannedApiAnalyzers" RuleNamespace="Microsoft.Design">
- <!-- error on RS0030: Do not used banned APIs -->
- <Rule Id="RS0030" Action="Error" />
- </Rules>
-
- <Rules AnalyzerId="IDisposableAnalyzers" RuleNamespace="IDisposableAnalyzers.Correctness">
- <!-- disable warning IDISP001: Dispose created -->
- <Rule Id="IDISP001" Action="Info" />
- <!-- TODO: Enable when false positives are fixed -->
- <!-- disable warning IDISP003: Dispose previous before re-assigning -->
- <Rule Id="IDISP003" Action="Info" />
- <!-- disable warning IDISP004: Don't ignore created IDisposable -->
- <Rule Id="IDISP004" Action="Info" />
- <!-- disable warning IDISP007: Don't dispose injected -->
- <Rule Id="IDISP007" Action="Info" />
- <!-- disable warning IDISP008: Don't assign member with injected and created disposables -->
- <Rule Id="IDISP008" Action="Info" />
- </Rules>
-</RuleSet>
diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index 0590ded32..ba402dfe0 100644
--- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- TODO: Remove once we update SkiaSharp > 2.88.5 -->
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index c5aadc890..2dac5598f 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -195,8 +195,10 @@ public class SkiaEncoder : IImageEncoder
return string.Empty;
}
+ // Use FileStream with FileShare.Read instead of having Skia open the file to allow concurrent read access
+ using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
// Any larger than 128x128 is too slow and there's no visually discernible difference
- return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
+ return BlurHashEncoder.Encode(xComp, yComp, fileStream, 128, 128);
}
private bool RequiresSpecialCharacterHack(string path)
diff --git a/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
index 7af77758b..03733d4f8 100644
--- a/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
+++ b/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
@@ -12,7 +12,7 @@ public class SplashscreenBuilder
{
private const int FinalWidth = 1920;
private const int FinalHeight = 1080;
- // generated collage resolution should be higher than the final resolution
+ // generated collage resolution should be greater than the final resolution
private const int WallWidth = FinalWidth * 3;
private const int WallHeight = FinalHeight * 2;
private const int Rows = 6;
diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs
index 5d4732234..fcb315b3a 100644
--- a/src/Jellyfin.Drawing/ImageProcessor.cs
+++ b/src/Jellyfin.Drawing/ImageProcessor.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Mime;
+using System.Reflection.Metadata.Ecma335;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -15,6 +16,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
@@ -66,7 +68,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
var semaphoreCount = config.Configuration.ParallelImageEncodingLimit;
if (semaphoreCount < 1)
{
- semaphoreCount = 2 * Environment.ProcessorCount;
+ semaphoreCount = Environment.ProcessorCount;
}
_parallelEncodingLimit = new(semaphoreCount);
@@ -404,8 +406,27 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
}
/// <inheritdoc />
+ public string GetImageCacheTag(string baseItemPath, DateTime imageDateModified)
+ => (baseItemPath + imageDateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
+
+ /// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
- => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ => GetImageCacheTag(item.Path, image.DateModified);
+
+ /// <inheritdoc />
+ public string GetImageCacheTag(BaseItemDto item, ItemImageInfo image)
+ => GetImageCacheTag(item.Path, image.DateModified);
+
+ /// <inheritdoc />
+ public string? GetImageCacheTag(BaseItemDto item, ChapterInfo chapter)
+ {
+ if (chapter.ImagePath is null)
+ {
+ return null;
+ }
+
+ return GetImageCacheTag(item.Path, chapter.ImageDateModified);
+ }
/// <inheritdoc />
public string? GetImageCacheTag(BaseItem item, ChapterInfo chapter)
@@ -431,8 +452,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
return null;
}
- return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
- .ToString("N", CultureInfo.InvariantCulture);
+ return GetImageCacheTag(user.ProfileImage.Path, user.ProfileImage.LastModified);
}
private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
index 4a02f90f9..5f4b3fe8d 100644
--- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
+++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index f786cc3b4..1613d83bc 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -15,7 +15,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Extensions</PackageId>
- <VersionPrefix>10.10.0</VersionPrefix>
+ <VersionPrefix>10.11.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedCollectionConverter.cs
index ccbc296fd..b1946143d 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedCollectionConverter.cs
@@ -1,15 +1,15 @@
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
- /// Convert comma delimited string to array of type.
+ /// Convert comma delimited string to collection of type.
/// </summary>
/// <typeparam name="T">Type to convert to.</typeparam>
- public sealed class JsonCommaDelimitedArrayConverter<T> : JsonDelimitedArrayConverter<T>
+ public sealed class JsonCommaDelimitedCollectionConverter<T> : JsonDelimitedCollectionConverter<T>
{
/// <summary>
- /// Initializes a new instance of the <see cref="JsonCommaDelimitedArrayConverter{T}"/> class.
+ /// Initializes a new instance of the <see cref="JsonCommaDelimitedCollectionConverter{T}"/> class.
/// </summary>
- public JsonCommaDelimitedArrayConverter() : base()
+ public JsonCommaDelimitedCollectionConverter() : base()
{
}
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedCollectionConverterFactory.cs
index ae9e1f67a..daa79b2b5 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedCollectionConverterFactory.cs
@@ -1,28 +1,31 @@
using System;
+using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
- /// Json Pipe delimited array converter factory.
+ /// Json comma delimited collection converter factory.
/// </summary>
/// <remarks>
/// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow.
/// </remarks>
- public class JsonPipeDelimitedArrayConverterFactory : JsonConverterFactory
+ public class JsonCommaDelimitedCollectionConverterFactory : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
- return true;
+ return typeToConvert.IsArray
+ || (typeToConvert.IsGenericType
+ && (typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyCollection<>)) || typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyList<>))));
}
/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
- return (JsonConverter?)Activator.CreateInstance(typeof(JsonPipeDelimitedArrayConverter<>).MakeGenericType(structType));
+ return (JsonConverter?)Activator.CreateInstance(typeof(JsonCommaDelimitedCollectionConverter<>).MakeGenericType(structType));
}
}
}
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedCollectionConverter.cs
index 936a5a97c..fe85d7f73 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedCollectionConverter.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
-using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -11,14 +10,14 @@ 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 JsonDelimitedCollectionConverter<T> : JsonConverter<IReadOnlyCollection<T>>
{
private readonly TypeConverter _typeConverter;
/// <summary>
- /// Initializes a new instance of the <see cref="JsonDelimitedArrayConverter{T}"/> class.
+ /// Initializes a new instance of the <see cref="JsonDelimitedCollectionConverter{T}"/> class.
/// </summary>
- protected JsonDelimitedArrayConverter()
+ protected JsonDelimitedCollectionConverter()
{
_typeConverter = TypeDescriptor.GetConverter(typeof(T));
}
@@ -29,7 +28,7 @@ namespace Jellyfin.Extensions.Json.Converters
protected virtual char Delimiter { get; }
/// <inheritdoc />
- public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ public override IReadOnlyCollection<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
@@ -53,52 +52,25 @@ namespace Jellyfin.Extensions.Json.Converters
}
catch (FormatException)
{
- // Ignore unconvertable inputs
+ // Ignore unconvertible inputs
}
}
- return typedValues.ToArray();
+ if (typeToConvert.IsArray)
+ {
+ return typedValues.ToArray();
+ }
+
+ return typedValues;
}
return JsonSerializer.Deserialize<T[]>(ref reader, options);
}
/// <inheritdoc />
- public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options)
+ public override void Write(Utf8JsonWriter writer, IReadOnlyCollection<T>? value, JsonSerializerOptions options)
{
- if (value is not null)
- {
- writer.WriteStartArray();
- if (value.Length > 0)
- {
- var toWrite = value.Length - 1;
- foreach (var it in value)
- {
- var wrote = false;
- if (it is not null)
- {
- writer.WriteStringValue(it.ToString());
- wrote = true;
- }
-
- if (toWrite > 0)
- {
- if (wrote)
- {
- writer.WriteStringValue(Delimiter.ToString());
- }
-
- toWrite--;
- }
- }
- }
-
- writer.WriteEndArray();
- }
- else
- {
- writer.WriteNullValue();
- }
+ JsonSerializer.Serialize(writer, value, options);
}
}
}
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedCollectionConverter.cs
index 55720ee4f..57378a360 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedCollectionConverter.cs
@@ -4,12 +4,12 @@ namespace Jellyfin.Extensions.Json.Converters
/// Convert Pipe delimited string to array of type.
/// </summary>
/// <typeparam name="T">Type to convert to.</typeparam>
- public sealed class JsonPipeDelimitedArrayConverter<T> : JsonDelimitedArrayConverter<T>
+ public sealed class JsonPipeDelimitedCollectionConverter<T> : JsonDelimitedCollectionConverter<T>
{
/// <summary>
- /// Initializes a new instance of the <see cref="JsonPipeDelimitedArrayConverter{T}"/> class.
+ /// Initializes a new instance of the <see cref="JsonPipeDelimitedCollectionConverter{T}"/> class.
/// </summary>
- public JsonPipeDelimitedArrayConverter() : base()
+ public JsonPipeDelimitedCollectionConverter() : base()
{
}
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedCollectionConverterFactory.cs
index a95e493db..f487fcaca 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedCollectionConverterFactory.cs
@@ -1,28 +1,31 @@
using System;
+using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
- /// Json comma delimited array converter factory.
+ /// Json Pipe delimited collection converter factory.
/// </summary>
/// <remarks>
/// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow.
/// </remarks>
- public class JsonCommaDelimitedArrayConverterFactory : JsonConverterFactory
+ public class JsonPipeDelimitedCollectionConverterFactory : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
- return true;
+ return typeToConvert.IsArray
+ || (typeToConvert.IsGenericType
+ && (typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyCollection<>)) || typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyList<>))));
}
/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
- return (JsonConverter?)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType));
+ return (JsonConverter?)Activator.CreateInstance(typeof(JsonPipeDelimitedCollectionConverter<>).MakeGenericType(structType));
}
}
}
diff --git a/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs b/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs
index 79c5873d5..71e46764a 100644
--- a/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs
+++ b/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs
@@ -79,7 +79,7 @@ namespace Jellyfin.LiveTv.Channels
// Every so often
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks
+ Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks
}
};
}
diff --git a/src/Jellyfin.LiveTv/Guide/GuideManager.cs b/src/Jellyfin.LiveTv/Guide/GuideManager.cs
index f657422a0..ac59a6d12 100644
--- a/src/Jellyfin.LiveTv/Guide/GuideManager.cs
+++ b/src/Jellyfin.LiveTv/Guide/GuideManager.cs
@@ -40,6 +40,11 @@ public class GuideManager : IGuideManager
private readonly LiveTvDtoService _tvDtoService;
/// <summary>
+ /// Amount of days images are pre-cached from external sources.
+ /// </summary>
+ public const int MaxCacheDays = 2;
+
+ /// <summary>
/// Initializes a new instance of the <see cref="GuideManager"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger{TCategoryName}"/>.</param>
@@ -204,14 +209,14 @@ public class GuideManager : IGuideManager
progress.Report(15);
numComplete = 0;
- var programs = new List<Guid>();
+ var programIds = new List<Guid>();
var channels = new List<Guid>();
var guideDays = GetGuideDays();
- _logger.LogInformation("Refreshing guide with {0} days of guide data", guideDays);
+ _logger.LogInformation("Refreshing guide with {Days} days of guide data", guideDays);
- var maxCacheDate = DateTime.UtcNow.AddDays(2);
+ var maxCacheDate = DateTime.UtcNow.AddDays(MaxCacheDays);
foreach (var currentChannel in list)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -238,11 +243,12 @@ public class GuideManager : IGuideManager
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
var newPrograms = new List<LiveTvProgram>();
- var updatedPrograms = new List<BaseItem>();
+ var updatedPrograms = new List<LiveTvProgram>();
foreach (var program in channelPrograms)
{
var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
+ var id = programItem.Id;
if (isNew)
{
newPrograms.Add(programItem);
@@ -252,7 +258,7 @@ public class GuideManager : IGuideManager
updatedPrograms.Add(programItem);
}
- programs.Add(programItem.Id);
+ programIds.Add(programItem.Id);
isMovie |= program.IsMovie;
isSeries |= program.IsSeries;
@@ -261,12 +267,17 @@ public class GuideManager : IGuideManager
isKids |= program.IsKids;
}
- _logger.LogDebug("Channel {0} has {1} new programs and {2} updated programs", currentChannel.Name, newPrograms.Count, updatedPrograms.Count);
+ _logger.LogDebug(
+ "Channel {Name} has {NewCount} new programs and {UpdatedCount} updated programs",
+ currentChannel.Name,
+ newPrograms.Count,
+ updatedPrograms.Count);
if (newPrograms.Count > 0)
{
- _libraryManager.CreateItems(newPrograms, null, cancellationToken);
- await PrecacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
+ _libraryManager.CreateItems(newPrograms, currentChannel, cancellationToken);
+
+ await PreCacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
}
if (updatedPrograms.Count > 0)
@@ -276,7 +287,8 @@ public class GuideManager : IGuideManager
currentChannel,
ItemUpdateType.MetadataImport,
cancellationToken).ConfigureAwait(false);
- await PrecacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
+
+ await PreCacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
}
currentChannel.IsMovie = isMovie;
@@ -313,7 +325,7 @@ public class GuideManager : IGuideManager
}
progress.Report(100);
- return new Tuple<List<Guid>, List<Guid>>(channels, programs);
+ return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
}
private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
@@ -488,35 +500,27 @@ public class GuideManager : IGuideManager
forceUpdate = true;
}
- var seriesId = info.SeriesId;
-
- if (!item.ParentId.Equals(channel.Id))
+ var channelId = channel.Id;
+ if (!item.ParentId.Equals(channelId))
{
+ item.ParentId = channel.Id;
forceUpdate = true;
}
- item.ParentId = channel.Id;
-
item.Audio = info.Audio;
- item.ChannelId = channel.Id;
- item.CommunityRating ??= info.CommunityRating;
- if ((item.CommunityRating ?? 0).Equals(0))
- {
- item.CommunityRating = null;
- }
-
+ item.ChannelId = channelId;
+ item.CommunityRating = info.CommunityRating;
item.EpisodeTitle = info.EpisodeTitle;
item.ExternalId = info.Id;
- if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal))
+ var seriesId = info.SeriesId;
+ if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.OrdinalIgnoreCase))
{
+ item.ExternalSeriesId = seriesId;
forceUpdate = true;
}
- item.ExternalSeriesId = seriesId;
-
var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle);
-
if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle))
{
item.SeriesName = info.Name;
@@ -564,7 +568,6 @@ public class GuideManager : IGuideManager
}
item.Tags = tags.ToArray();
-
item.Genres = info.Genres.ToArray();
if (info.IsHD ?? false)
@@ -575,41 +578,35 @@ public class GuideManager : IGuideManager
item.IsMovie = info.IsMovie;
item.IsRepeat = info.IsRepeat;
-
if (item.IsSeries != isSeries)
{
+ item.IsSeries = isSeries;
forceUpdate = true;
}
- item.IsSeries = isSeries;
-
item.Name = info.Name;
- item.OfficialRating ??= info.OfficialRating;
- item.Overview ??= info.Overview;
+ item.OfficialRating = info.OfficialRating;
+ item.Overview = info.Overview;
item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
- item.ProviderIds = info.ProviderIds;
-
foreach (var providerId in info.SeriesProviderIds)
{
info.ProviderIds["Series" + providerId.Key] = providerId.Value;
}
+ item.ProviderIds = info.ProviderIds;
if (item.StartDate != info.StartDate)
{
+ item.StartDate = info.StartDate;
forceUpdate = true;
}
- item.StartDate = info.StartDate;
-
if (item.EndDate != info.EndDate)
{
+ item.EndDate = info.EndDate;
forceUpdate = true;
}
- item.EndDate = info.EndDate;
-
item.ProductionYear = info.ProductionYear;
-
if (!isSeries || info.IsRepeat)
{
item.PremiereDate = info.OriginalAirDate;
@@ -618,100 +615,113 @@ public class GuideManager : IGuideManager
item.IndexNumber = info.EpisodeNumber;
item.ParentIndexNumber = info.SeasonNumber;
- if (!item.HasImage(ImageType.Primary))
+ forceUpdate |= UpdateImages(item, info);
+
+ if (isNew)
{
- if (!string.IsNullOrWhiteSpace(info.ImagePath))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = info.ImagePath,
- Type = ImageType.Primary
- },
- 0);
- }
- else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = info.ImageUrl,
- Type = ImageType.Primary
- },
- 0);
- }
+ item.OnMetadataChanged();
+
+ return (item, true, false);
}
- if (!item.HasImage(ImageType.Thumb))
+ var isUpdated = forceUpdate;
+ var etag = info.Etag;
+ if (string.IsNullOrWhiteSpace(etag))
{
- if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = info.ThumbImageUrl,
- Type = ImageType.Thumb
- },
- 0);
- }
+ isUpdated = true;
}
-
- if (!item.HasImage(ImageType.Logo))
+ else if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
{
- if (!string.IsNullOrWhiteSpace(info.LogoImageUrl))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = info.LogoImageUrl,
- Type = ImageType.Logo
- },
- 0);
- }
+ item.SetProviderId(EtagKey, etag);
+ isUpdated = true;
}
- if (!item.HasImage(ImageType.Backdrop))
+ if (isUpdated)
{
- if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = info.BackdropImageUrl,
- Type = ImageType.Backdrop
- },
- 0);
- }
+ item.OnMetadataChanged();
+
+ return (item, false, true);
}
- var isUpdated = false;
- if (isNew)
+ return (item, false, false);
+ }
+
+ private static bool UpdateImages(BaseItem item, ProgramInfo info)
+ {
+ var updated = false;
+
+ // Primary
+ updated |= UpdateImage(ImageType.Primary, item, info);
+
+ // Thumbnail
+ updated |= UpdateImage(ImageType.Thumb, item, info);
+
+ // Logo
+ updated |= UpdateImage(ImageType.Logo, item, info);
+
+ // Backdrop
+ updated |= UpdateImage(ImageType.Backdrop, item, info);
+
+ return updated;
+ }
+
+ private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info)
+ {
+ var image = item.GetImages(imageType).FirstOrDefault();
+ var currentImagePath = image?.Path;
+ var newImagePath = imageType switch
{
- }
- else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
+ ImageType.Primary => info.ImagePath,
+ _ => null
+ };
+ var newImageUrl = imageType switch
{
- isUpdated = true;
+ ImageType.Backdrop => info.BackdropImageUrl,
+ ImageType.Logo => info.LogoImageUrl,
+ ImageType.Primary => info.ImageUrl,
+ ImageType.Thumb => info.ThumbImageUrl,
+ _ => null
+ };
+
+ var sameImage = (currentImagePath?.Equals(newImageUrl, StringComparison.OrdinalIgnoreCase) ?? false)
+ || (currentImagePath?.Equals(newImagePath, StringComparison.OrdinalIgnoreCase) ?? false);
+ if (sameImage)
+ {
+ return false;
}
- else
+
+ if (!string.IsNullOrWhiteSpace(newImagePath))
{
- var etag = info.Etag;
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = newImagePath,
+ Type = imageType
+ },
+ 0);
- if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
- {
- item.SetProviderId(EtagKey, etag);
- isUpdated = true;
- }
+ return true;
}
- if (isNew || isUpdated)
+ if (!string.IsNullOrWhiteSpace(newImageUrl))
{
- item.OnMetadataChanged();
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = newImageUrl,
+ Type = imageType
+ },
+ 0);
+
+ return true;
}
- return (item, isNew, isUpdated);
+ item.RemoveImage(image);
+
+ return false;
}
- private async Task PrecacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
+ private async Task PreCacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
{
await Parallel.ForEachAsync(
programs
@@ -730,6 +740,7 @@ public class GuideManager : IGuideManager
var imageInfo = program.ImageInfos[i];
if (!imageInfo.IsLocalFile)
{
+ _logger.LogDebug("Caching image locally: {Url}", imageInfo.Path);
try
{
program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal(
@@ -741,7 +752,7 @@ public class GuideManager : IGuideManager
}
catch (Exception ex)
{
- _logger.LogWarning(ex, "Unable to precache {Url}", imageInfo.Path);
+ _logger.LogWarning(ex, "Unable to pre-cache {Url}", imageInfo.Path);
}
}
}
diff --git a/src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs b/src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs
index a9fde0850..5164d695f 100644
--- a/src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs
+++ b/src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs
@@ -66,7 +66,7 @@ public class RefreshGuideScheduledTask : IScheduledTask, IConfigurableScheduledT
{
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval,
+ Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromHours(24).Ticks
}
};
diff --git a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
index 0c660637f..c04954207 100644
--- a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
+++ b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
@@ -124,22 +124,7 @@ namespace Jellyfin.LiveTv.IO
private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile)
{
- string videoArgs;
- if (EncodeVideo(mediaSource))
- {
- const int MaxBitrate = 25000000;
- videoArgs = string.Format(
- CultureInfo.InvariantCulture,
- "-codec:v:0 libx264 -force_key_frames \"expr:gte(t,n_forced*5)\" {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -profile:v high -level 41",
- GetOutputSizeParam(),
- MaxBitrate);
- }
- else
- {
- videoArgs = "-codec:v:0 copy";
- }
-
- videoArgs += " -fflags +genpts";
+ string videoArgs = "-codec:v:0 copy -fflags +genpts";
var flags = new List<string>();
if (mediaSource.IgnoreDts)
@@ -205,19 +190,6 @@ namespace Jellyfin.LiveTv.IO
private static string GetAudioArgs(MediaSourceInfo mediaSource)
{
return "-codec:a:0 copy";
-
- // var audioChannels = 2;
- // var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
- // if (audioStream is not null)
- // {
- // audioChannels = audioStream.Channels ?? audioChannels;
- // }
- // return "-codec:a:0 aac -strict experimental -ab 320000";
- }
-
- private static bool EncodeVideo(MediaSourceInfo mediaSource)
- {
- return false;
}
protected string GetOutputSizeParam()
diff --git a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj
index c58889740..f04c02504 100644
--- a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj
+++ b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs
index 3df2d0d2c..39c2bd375 100644
--- a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs
+++ b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs
@@ -230,10 +230,15 @@ public class ListingsManager : IListingsManager
var listingsProviderInfo = config.ListingProviders
.First(info => string.Equals(providerId, info.Id, StringComparison.OrdinalIgnoreCase));
+ var channelMappingExists = listingsProviderInfo.ChannelMappings
+ .Any(pair => string.Equals(pair.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(pair.Value, providerChannelNumber, StringComparison.OrdinalIgnoreCase));
+
listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings
.Where(pair => !string.Equals(pair.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
- if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
+ if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase)
+ && !channelMappingExists)
{
var newItem = new NameValuePair
{
diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
index c7a57859e..d6f15906e 100644
--- a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
@@ -19,6 +19,7 @@ using System.Threading.Tasks;
using AsyncKeyedLock;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
+using Jellyfin.LiveTv.Guide;
using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
@@ -38,7 +39,7 @@ namespace Jellyfin.LiveTv.Listings
private readonly IHttpClientFactory _httpClientFactory;
private readonly AsyncNonKeyedLocker _tokenLock = new(1);
- private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
+ private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private DateTime _lastErrorResponse;
private bool _disposed = false;
@@ -86,7 +87,7 @@ namespace Jellyfin.LiveTv.Listings
{
_logger.LogWarning("SchedulesDirect token is empty, returning empty program list");
- return Enumerable.Empty<ProgramInfo>();
+ return [];
}
var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
@@ -94,7 +95,7 @@ namespace Jellyfin.LiveTv.Listings
_logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
var requestList = new List<RequestScheduleForChannelDto>()
{
- new RequestScheduleForChannelDto()
+ new()
{
StationId = channelId,
Date = dates
@@ -109,7 +110,7 @@ namespace Jellyfin.LiveTv.Listings
var dailySchedules = await Request<IReadOnlyList<DayDto>>(options, true, info, cancellationToken).ConfigureAwait(false);
if (dailySchedules is null)
{
- return Array.Empty<ProgramInfo>();
+ return [];
}
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
@@ -120,17 +121,17 @@ namespace Jellyfin.LiveTv.Listings
var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
- var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken)
- .ConfigureAwait(false);
+ var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
if (programDetails is null)
{
- return Array.Empty<ProgramInfo>();
+ return [];
}
var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
var programIdsWithImages = programDetails
- .Where(p => p.HasImageArtwork).Select(p => p.ProgramId)
+ .Where(p => p.HasImageArtwork)
+ .Select(p => p.ProgramId)
.ToList();
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
@@ -138,17 +139,15 @@ namespace Jellyfin.LiveTv.Listings
var programsInfo = new List<ProgramInfo>();
foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs))
{
- // _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
- // " which corresponds to channel " + channelNumber + " and program id " +
- // schedule.ProgramId + " which says it has images? " +
- // programDict[schedule.ProgramId].hasImageArtwork);
-
if (string.IsNullOrEmpty(schedule.ProgramId))
{
continue;
}
- if (images is not null)
+ // Only add images which will be pre-cached until we can implement dynamic token fetching
+ var endDate = schedule.AirDateTime?.AddSeconds(schedule.Duration);
+ var willBeCached = endDate.HasValue && endDate.Value < DateTime.UtcNow.AddDays(GuideManager.MaxCacheDays);
+ if (willBeCached && images is not null)
{
var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
if (imageIndex > -1)
@@ -456,7 +455,7 @@ namespace Jellyfin.LiveTv.Listings
if (programIds.Count == 0)
{
- return Array.Empty<ShowImagesDto>();
+ return [];
}
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
@@ -483,7 +482,7 @@ namespace Jellyfin.LiveTv.Listings
{
_logger.LogError(ex, "Error getting image info from schedules direct");
- return Array.Empty<ShowImagesDto>();
+ return [];
}
}
diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
index 856b7a89b..79bcbe649 100644
--- a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
@@ -8,7 +8,7 @@ namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
public class LineupDto
{
/// <summary>
- /// Gets or sets the linup.
+ /// Gets or sets the lineup.
/// </summary>
[JsonPropertyName("lineup")]
public string? Lineup { get; set; }
diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
index ea583a1ce..89c4ee5a8 100644
--- a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the provider callsign.
/// </summary>
[JsonPropertyName("providerCallsign")]
- public string? ProvderCallsign { get; set; }
+ public string? ProviderCallsign { get; set; }
/// <summary>
/// Gets or sets the logical channel number.
diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
index cafc8e273..7998a7a92 100644
--- a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
@@ -8,7 +8,7 @@ namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
public class MetadataDto
{
/// <summary>
- /// Gets or sets the linup.
+ /// Gets or sets the lineup.
/// </summary>
[JsonPropertyName("lineup")]
public string? Lineup { get; set; }
diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
index 8c3906f86..7bfc4bc8b 100644
--- a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
@@ -64,7 +64,7 @@ namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
public IReadOnlyList<MetadataProgramsDto> Metadata { get; set; } = Array.Empty<MetadataProgramsDto>();
/// <summary>
- /// Gets or sets the list of content raitings.
+ /// Gets or sets the list of content ratings.
/// </summary>
[JsonPropertyName("contentRating")]
public IReadOnlyList<ContentRatingDto> ContentRating { get; set; } = Array.Empty<ContentRatingDto>();
diff --git a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
index 7dc30f727..7938b7a6e 100644
--- a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
@@ -84,10 +84,11 @@ namespace Jellyfin.LiveTv.Listings
_logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false);
+ var redirectedUrl = response.RequestMessage?.RequestUri?.ToString() ?? info.Path;
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
- return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false);
+ return await UnzipIfNeededAndCopy(redirectedUrl, stream, cacheFile, cancellationToken).ConfigureAwait(false);
}
}
else
@@ -112,7 +113,8 @@ namespace Jellyfin.LiveTv.Listings
await using (fileStream.ConfigureAwait(false))
{
- if (Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gz", StringComparison.OrdinalIgnoreCase))
+ if (Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gz", StringComparison.OrdinalIgnoreCase) ||
+ Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gzip", StringComparison.OrdinalIgnoreCase))
{
try
{
diff --git a/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs
index 9e7323f5b..6a68b8c25 100644
--- a/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs
+++ b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs
@@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.Json;
+using System.Threading;
using Jellyfin.Extensions.Json;
using Microsoft.Extensions.Logging;
@@ -15,7 +16,7 @@ namespace Jellyfin.LiveTv.Timers
where T : class
{
private readonly string _dataPath;
- private readonly object _fileDataLock = new object();
+ private readonly Lock _fileDataLock = new();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private T[]? _items;
diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs
index c8d678e2f..e3afe1513 100644
--- a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs
@@ -93,7 +93,7 @@ namespace Jellyfin.LiveTv.TunerHosts
}
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{
- var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
+ var channel = GetChannelInfo(extInf, tunerHostId, trimmedLine);
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
channel.Path = trimmedLine;
@@ -106,7 +106,7 @@ namespace Jellyfin.LiveTv.TunerHosts
return channels;
}
- private ChannelInfo GetChannelnfo(string extInf, string tunerHostId, string mediaUrl)
+ private ChannelInfo GetChannelInfo(string extInf, string tunerHostId, string mediaUrl)
{
var channel = new ChannelInfo()
{
diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
index ee79802a1..dc581724a 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
+++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
index c79dcee3c..c826d3d9c 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/src/Jellyfin.Networking/Jellyfin.Networking.csproj b/src/Jellyfin.Networking/Jellyfin.Networking.csproj
index 24b3ecaab..1a146549d 100644
--- a/src/Jellyfin.Networking/Jellyfin.Networking.csproj
+++ b/src/Jellyfin.Networking/Jellyfin.Networking.csproj
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@@ -14,7 +14,4 @@
<ProjectReference Include="..\..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
</ItemGroup>
- <ItemGroup>
- <PackageReference Include="Mono.Nat" />
- </ItemGroup>
</Project>
diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs
index 5a13cc417..dd01e9533 100644
--- a/src/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -27,7 +27,7 @@ public class NetworkManager : INetworkManager, IDisposable
/// <summary>
/// Threading lock for network properties.
/// </summary>
- private readonly object _initLock;
+ private readonly Lock _initLock;
private readonly ILogger<NetworkManager> _logger;
@@ -35,7 +35,7 @@ public class NetworkManager : INetworkManager, IDisposable
private readonly IConfiguration _startupConfig;
- private readonly object _networkEventLock;
+ private readonly Lock _networkEventLock;
/// <summary>
/// Holds the published server URLs and the IPs to use them on.
@@ -57,7 +57,7 @@ public class NetworkManager : INetworkManager, IDisposable
/// <summary>
/// Dictionary containing interface addresses and their subnets.
/// </summary>
- private IReadOnlyList<IPData> _interfaces;
+ private List<IPData> _interfaces;
/// <summary>
/// Unfiltered user defined LAN subnets (<see cref="NetworkConfiguration.LocalNetworkSubnets"/>)
@@ -93,7 +93,7 @@ public class NetworkManager : INetworkManager, IDisposable
_interfaces = new List<IPData>();
_macAddresses = new List<PhysicalAddress>();
_publishedServerUrls = new List<PublishedServerUriOverride>();
- _networkEventLock = new object();
+ _networkEventLock = new();
_remoteAddressFilter = new List<IPNetwork>();
_ = bool.TryParse(startupConfig[DetectNetworkChangeKey], out var detectNetworkChange);
@@ -702,7 +702,7 @@ public class NetworkManager : INetworkManager, IDisposable
return false;
}
}
- else if (!_lanSubnets.Any(x => x.Contains(remoteIP)))
+ else if (!IsInLocalNetwork(remoteIP))
{
// Remote not enabled. So everyone should be LAN.
return false;
@@ -973,7 +973,7 @@ public class NetworkManager : INetworkManager, IDisposable
bindPreference = string.Empty;
int? port = null;
- // Only consider subnets including the source IP, prefering specific overrides
+ // Only consider subnets including the source IP, preferring specific overrides
List<PublishedServerUriOverride> validPublishedServerUrls;
if (!isInExternalSubnet)
{
@@ -997,7 +997,9 @@ public class NetworkManager : INetworkManager, IDisposable
// Get interface matching override subnet
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
- if (intf?.Address is not null)
+ if (intf?.Address is not null
+ || (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any))
+ || (data.Data.AddressFamily == AddressFamily.InterNetworkV6 && data.Data.Address.Equals(IPAddress.IPv6Any)))
{
// If matching interface is found, use override
bindPreference = data.OverrideUri;
@@ -1025,6 +1027,7 @@ public class NetworkManager : INetworkManager, IDisposable
}
_logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
+
return true;
}
@@ -1063,6 +1066,7 @@ public class NetworkManager : INetworkManager, IDisposable
// If none exists, this will select the first external interface if there is one.
bindAddress = externalInterfaces
.OrderByDescending(x => x.Subnet.Contains(source))
+ .ThenByDescending(x => x.Subnet.PrefixLength)
.ThenBy(x => x.Index)
.Select(x => x.Address)
.First();
@@ -1080,6 +1084,7 @@ public class NetworkManager : INetworkManager, IDisposable
// If none exists, this will select the first internal interface if there is one.
bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
.OrderByDescending(x => x.Subnet.Contains(source))
+ .ThenByDescending(x => x.Subnet.PrefixLength)
.ThenBy(x => x.Index)
.Select(x => x.Address)
.FirstOrDefault();
diff --git a/src/Jellyfin.Networking/PortForwardingHost.cs b/src/Jellyfin.Networking/PortForwardingHost.cs
deleted file mode 100644
index d01343624..000000000
--- a/src/Jellyfin.Networking/PortForwardingHost.cs
+++ /dev/null
@@ -1,192 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Net;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using Mono.Nat;
-
-namespace Jellyfin.Networking;
-
-/// <summary>
-/// <see cref="IHostedService"/> responsible for UPnP port forwarding.
-/// </summary>
-public sealed class PortForwardingHost : IHostedService, IDisposable
-{
- private readonly IServerApplicationHost _appHost;
- private readonly ILogger<PortForwardingHost> _logger;
- private readonly IServerConfigurationManager _config;
- private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new();
-
- private Timer? _timer;
- private string? _configIdentifier;
- private bool _disposed;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PortForwardingHost"/> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="appHost">The application host.</param>
- /// <param name="config">The configuration manager.</param>
- public PortForwardingHost(
- ILogger<PortForwardingHost> logger,
- IServerApplicationHost appHost,
- IServerConfigurationManager config)
- {
- _logger = logger;
- _appHost = appHost;
- _config = config;
- }
-
- private string GetConfigIdentifier()
- {
- const char Separator = '|';
- var config = _config.GetNetworkConfiguration();
-
- return new StringBuilder(32)
- .Append(config.EnableUPnP).Append(Separator)
- .Append(config.PublicHttpPort).Append(Separator)
- .Append(config.PublicHttpsPort).Append(Separator)
- .Append(_appHost.HttpPort).Append(Separator)
- .Append(_appHost.HttpsPort).Append(Separator)
- .Append(_appHost.ListenWithHttps).Append(Separator)
- .Append(config.EnableRemoteAccess).Append(Separator)
- .ToString();
- }
-
- private void OnConfigurationUpdated(object? sender, EventArgs e)
- {
- var oldConfigIdentifier = _configIdentifier;
- _configIdentifier = GetConfigIdentifier();
-
- if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase))
- {
- Stop();
- Start();
- }
- }
-
- /// <inheritdoc />
- public Task StartAsync(CancellationToken cancellationToken)
- {
- Start();
-
- _config.ConfigurationUpdated += OnConfigurationUpdated;
-
- return Task.CompletedTask;
- }
-
- /// <inheritdoc />
- public Task StopAsync(CancellationToken cancellationToken)
- {
- Stop();
-
- return Task.CompletedTask;
- }
-
- private void Start()
- {
- var config = _config.GetNetworkConfiguration();
- if (!config.EnableUPnP || !config.EnableRemoteAccess)
- {
- return;
- }
-
- _logger.LogInformation("Starting NAT discovery");
-
- NatUtility.DeviceFound += OnNatUtilityDeviceFound;
- NatUtility.StartDiscovery();
-
- _timer?.Dispose();
- _timer = new Timer(_ => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
- }
-
- private void Stop()
- {
- _logger.LogInformation("Stopping NAT discovery");
-
- NatUtility.StopDiscovery();
- NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
-
- _timer?.Dispose();
- _timer = null;
- }
-
- private async void OnNatUtilityDeviceFound(object? sender, DeviceEventArgs e)
- {
- ObjectDisposedException.ThrowIf(_disposed, this);
-
- try
- {
- // On some systems the device discovered event seems to fire repeatedly
- // This check will help ensure we're not trying to port map the same device over and over
- if (!_createdRules.TryAdd(e.Device.DeviceEndpoint, 0))
- {
- return;
- }
-
- await Task.WhenAll(CreatePortMaps(e.Device)).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error creating port forwarding rules");
- }
- }
-
- private IEnumerable<Task> CreatePortMaps(INatDevice device)
- {
- var config = _config.GetNetworkConfiguration();
- yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort);
-
- if (_appHost.ListenWithHttps)
- {
- yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort);
- }
- }
-
- private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort)
- {
- _logger.LogDebug(
- "Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}",
- privatePort,
- publicPort,
- device.DeviceEndpoint);
-
- try
- {
- var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name);
- await device.CreatePortMapAsync(mapping).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(
- ex,
- "Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.",
- privatePort,
- publicPort,
- device.DeviceEndpoint);
- }
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- _config.ConfigurationUpdated -= OnConfigurationUpdated;
-
- _timer?.Dispose();
- _timer = null;
-
- _disposed = true;
- }
-}
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props
index bec3481cb..6b851021f 100644
--- a/tests/Directory.Build.props
+++ b/tests/Directory.Build.props
@@ -4,9 +4,8 @@
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable>
- <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
index 6f5c0ed0c..ffb996703 100644
--- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
@@ -100,6 +100,7 @@ namespace Jellyfin.Api.Tests.Auth
var authorizationInfo = SetupUser();
var authenticateResult = await _sut.AuthenticateAsync();
+ Assert.NotNull(authorizationInfo.User);
Assert.True(authenticateResult.Principal?.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username));
}
@@ -111,6 +112,7 @@ namespace Jellyfin.Api.Tests.Auth
var authorizationInfo = SetupUser(isAdmin);
var authenticateResult = await _sut.AuthenticateAsync();
+ Assert.NotNull(authorizationInfo.User);
var expectedRole = authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
Assert.True(authenticateResult.Principal?.HasClaim(ClaimTypes.Role, expectedRole));
}
@@ -132,7 +134,6 @@ 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(
diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedCollectionModelBinderTests.cs
index e37c9d91f..e6b9acfe1 100644
--- a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs
+++ b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedCollectionModelBinderTests.cs
@@ -12,7 +12,7 @@ using Xunit;
namespace Jellyfin.Api.Tests.ModelBinders
{
- public sealed class CommaDelimitedArrayModelBinderTests
+ public sealed class CommaDelimitedCollectionModelBinderTests
{
[Fact]
public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedStringArrayQuery()
@@ -22,7 +22,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "lol,xd";
var queryParamType = typeof(string[]);
- var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
+ var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@@ -47,7 +47,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "42,0";
var queryParamType = typeof(int[]);
- var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
+ var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@@ -72,7 +72,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "How,Much";
var queryParamType = typeof(TestType[]);
- var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
+ var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@@ -97,7 +97,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "How,,Much";
var queryParamType = typeof(TestType[]);
- var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
+ var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@@ -123,7 +123,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString2 = "Much";
var queryParamType = typeof(TestType[]);
- var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
+ var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
@@ -151,7 +151,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
IReadOnlyList<TestType> queryParamValues = Array.Empty<TestType>();
var queryParamType = typeof(TestType[]);
- var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
+ var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
@@ -179,7 +179,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "🔥,😢";
var queryParamType = typeof(IReadOnlyList<TestType>);
- var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
+ var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@@ -205,7 +205,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString2 = "😱";
var queryParamType = typeof(IReadOnlyList<TestType>);
- var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger<CommaDelimitedArrayModelBinder>());
+ var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger<CommaDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedCollectionModelBinderTests.cs
index 7c05ee036..941f4f12c 100644
--- a/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs
+++ b/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedCollectionModelBinderTests.cs
@@ -12,7 +12,7 @@ using Xunit;
namespace Jellyfin.Api.Tests.ModelBinders
{
- public sealed class PipeDelimitedArrayModelBinderTests
+ public sealed class PipeDelimitedCollectionModelBinderTests
{
[Fact]
public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedStringArrayQuery()
@@ -22,7 +22,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "lol|xd";
var queryParamType = typeof(string[]);
- var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+ var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@@ -47,7 +47,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "42|0";
var queryParamType = typeof(int[]);
- var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+ var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@@ -72,7 +72,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "How|Much";
var queryParamType = typeof(TestType[]);
- var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+ var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@@ -97,7 +97,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "How||Much";
var queryParamType = typeof(TestType[]);
- var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+ var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@@ -123,7 +123,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString2 = "Much";
var queryParamType = typeof(TestType[]);
- var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+ var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
@@ -151,7 +151,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
IReadOnlyList<TestType> queryParamValues = Array.Empty<TestType>();
var queryParamType = typeof(TestType[]);
- var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+ var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
@@ -179,7 +179,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString = "🔥|😢";
var queryParamType = typeof(IReadOnlyList<TestType>);
- var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+ var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
@@ -205,7 +205,7 @@ namespace Jellyfin.Api.Tests.ModelBinders
var queryParamString2 = "😱";
var queryParamType = typeof(IReadOnlyList<TestType>);
- var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
+ var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger<PipeDelimitedCollectionModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs
index 125229ff9..d58a62cc8 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs
@@ -1,5 +1,6 @@
using System.Text.Json;
using FsCheck;
+using FsCheck.Fluent;
using FsCheck.Xunit;
using Jellyfin.Extensions.Json.Converters;
using Xunit;
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedCollectionTests.cs
index 9fc015823..83f917c17 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedCollectionTests.cs
@@ -1,4 +1,7 @@
using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Jellyfin.Extensions.Tests.Json.Models;
@@ -7,7 +10,7 @@ using Xunit;
namespace Jellyfin.Extensions.Tests.Json.Converters
{
- public class JsonCommaDelimitedArrayTests
+ public class JsonCommaDelimitedCollectionTests
{
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
{
@@ -37,6 +40,29 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
}
[Fact]
+ public void Deserialize_EmptyList_Success()
+ {
+ var desiredValue = new GenericBodyListModel<string>
+ {
+ Value = []
+ };
+
+ Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<GenericBodyListModel<string>>(@"{ ""Value"": """" }", _jsonOptions));
+ }
+
+ [Fact]
+ public void Deserialize_EmptyIReadOnlyList_Success()
+ {
+ var desiredValue = new GenericBodyIReadOnlyListModel<string>
+ {
+ Value = []
+ };
+
+ var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": """" }", _jsonOptions);
+ Assert.Equal(desiredValue.Value, value?.Value);
+ }
+
+ [Fact]
public void Deserialize_String_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<string>
@@ -49,6 +75,17 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
}
[Fact]
+ public void Deserialize_StringList_Valid_Success()
+ {
+ var desiredValue = new GenericBodyListModel<string>
+ {
+ Value = ["a", "b", "c"]
+ };
+
+ Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<GenericBodyListModel<string>>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions));
+ }
+
+ [Fact]
public void Deserialize_String_Space_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<string>
@@ -92,7 +129,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
};
- var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", _jsonOptions);
+ var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,TotallyNotAValidCommand,MoveDown"" }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
@@ -131,5 +168,41 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
+
+ [Fact]
+ public void Serialize_GenericCommandType_ReadOnlyArray_Valid_Success()
+ {
+ var valueToSerialize = new GenericBodyIReadOnlyCollectionModel<GeneralCommandType>
+ {
+ Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }.AsReadOnly()
+ };
+
+ string value = JsonSerializer.Serialize<GenericBodyIReadOnlyCollectionModel<GeneralCommandType>>(valueToSerialize, _jsonOptions);
+ Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value);
+ }
+
+ [Fact]
+ public void Serialize_GenericCommandType_ImmutableArrayArray_Valid_Success()
+ {
+ var valueToSerialize = new GenericBodyIReadOnlyCollectionModel<GeneralCommandType>
+ {
+ Value = ImmutableArray.Create(new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown })
+ };
+
+ string value = JsonSerializer.Serialize<GenericBodyIReadOnlyCollectionModel<GeneralCommandType>>(valueToSerialize, _jsonOptions);
+ Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value);
+ }
+
+ [Fact]
+ public void Serialize_GenericCommandType_List_Valid_Success()
+ {
+ var valueToSerialize = new GenericBodyIReadOnlyListModel<GeneralCommandType>
+ {
+ Value = new List<GeneralCommandType> { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ };
+
+ string value = JsonSerializer.Serialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(valueToSerialize, _jsonOptions);
+ Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value);
+ }
}
}
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs
index 9b977b9a5..26989d59b 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Jellyfin.Extensions.Tests.Json.Models;
@@ -87,5 +88,17 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
+
+ [Fact]
+ public void Serialize_GenericCommandType_IReadOnlyList_Valid_Success()
+ {
+ var valueToSerialize = new GenericBodyIReadOnlyListModel<GeneralCommandType>
+ {
+ Value = new List<GeneralCommandType> { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
+ };
+
+ string value = JsonSerializer.Serialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(valueToSerialize, _jsonOptions);
+ Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value);
+ }
}
}
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs
index ef135278f..a698c9c92 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs
@@ -8,13 +8,13 @@ namespace Jellyfin.Extensions.Tests.Json.Models
/// The generic body model.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
- public class GenericBodyArrayModel<T>
+ public sealed class GenericBodyArrayModel<T>
{
/// <summary>
/// Gets or sets the value.
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1819:Properties should not return arrays", MessageId = "Value", Justification = "Imported from ServiceStack")]
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public T[] Value { get; set; } = default!;
}
}
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyCollectionModel.cs b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyCollectionModel.cs
new file mode 100644
index 000000000..14cbc0f50
--- /dev/null
+++ b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyCollectionModel.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using Jellyfin.Extensions.Json.Converters;
+
+namespace Jellyfin.Extensions.Tests.Json.Models
+{
+ /// <summary>
+ /// The generic body <c>IReadOnlyCollection</c> model.
+ /// </summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ public sealed class GenericBodyIReadOnlyCollectionModel<T>
+ {
+ /// <summary>
+ /// Gets or sets the value.
+ /// </summary>
+ [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
+ public IReadOnlyCollection<T> Value { get; set; } = default!;
+ }
+}
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs
index 8e7b5a35b..eaa06a5dd 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs
@@ -8,12 +8,12 @@ namespace Jellyfin.Extensions.Tests.Json.Models
/// The generic body <c>IReadOnlyList</c> model.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
- public class GenericBodyIReadOnlyListModel<T>
+ public sealed class GenericBodyIReadOnlyListModel<T>
{
/// <summary>
/// Gets or sets the value.
/// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
+ [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
public IReadOnlyList<T> Value { get; set; } = default!;
}
}
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyListModel.cs b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyListModel.cs
new file mode 100644
index 000000000..463f9922f
--- /dev/null
+++ b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyListModel.cs
@@ -0,0 +1,22 @@
+#pragma warning disable CA1002 // Do not expose generic lists
+#pragma warning disable CA2227 // Collection properties should be read only
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using Jellyfin.Extensions.Json.Converters;
+
+namespace Jellyfin.Extensions.Tests.Json.Models
+{
+ /// <summary>
+ /// The generic body <c>List</c> model.
+ /// </summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ public sealed class GenericBodyListModel<T>
+ {
+ /// <summary>
+ /// Gets or sets the value.
+ /// </summary>
+ [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))]
+ public List<T> Value { get; set; } = default!;
+ }
+}
diff --git a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
index 69d20bd3f..028f12afa 100644
--- a/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
@@ -6,8 +6,8 @@ namespace Jellyfin.Extensions.Tests
public class StringExtensionsTests
{
[Theory]
- [InlineData("", "")] // Identity edge-case (no diactritics)
- [InlineData("Indiana Jones", "Indiana Jones")] // Identity (no diactritics)
+ [InlineData("", "")] // Identity edge-case (no diacritics)
+ [InlineData("Indiana Jones", "Indiana Jones")] // Identity (no diacritics)
[InlineData("a\ud800b", "ab")] // Invalid UTF-16 char stripping
[InlineData("åäö", "aao")] // Issue #7484
[InlineData("Jön", "Jon")] // Issue #7484
@@ -25,8 +25,8 @@ namespace Jellyfin.Extensions.Tests
}
[Theory]
- [InlineData("", false)] // Identity edge-case (no diactritics)
- [InlineData("Indiana Jones", false)] // Identity (no diactritics)
+ [InlineData("", false)] // Identity edge-case (no diacritics)
+ [InlineData("Indiana Jones", false)] // Identity (no diacritics)
[InlineData("a\ud800b", true)] // Invalid UTF-16 char stripping
[InlineData("åäö", true)] // Issue #7484
[InlineData("Jön", true)] // Issue #7484
diff --git a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj
index cf967b84c..fdcf7d61e 100644
--- a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj
+++ b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
@@ -22,7 +22,7 @@
<PackageReference Include="Xunit.SkippableFact" />
<PackageReference Include="coverlet.collector" />
</ItemGroup>
-
+
<ItemGroup>
<ProjectReference Include="..\..\src\Jellyfin.LiveTv\Jellyfin.LiveTv.csproj" />
</ItemGroup>
diff --git a/tests/Jellyfin.LiveTv.Tests/Listings/ListingsManagerTests.cs b/tests/Jellyfin.LiveTv.Tests/Listings/ListingsManagerTests.cs
new file mode 100644
index 000000000..40934d9c6
--- /dev/null
+++ b/tests/Jellyfin.LiveTv.Tests/Listings/ListingsManagerTests.cs
@@ -0,0 +1,50 @@
+using System;
+using Jellyfin.LiveTv.Configuration;
+using Jellyfin.LiveTv.Listings;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.LiveTv.Tests.Listings;
+
+public class ListingsManagerTests
+{
+ private readonly IConfigurationManager _config;
+ private readonly IListingsProvider[] _listingsProviders;
+ private readonly ILogger<ListingsManager> _logger;
+ private readonly ITaskManager _taskManager;
+ private readonly ITunerHostManager _tunerHostManager;
+
+ public ListingsManagerTests()
+ {
+ _logger = Mock.Of<ILogger<ListingsManager>>();
+ _config = Mock.Of<IConfigurationManager>();
+ _taskManager = Mock.Of<ITaskManager>();
+ _tunerHostManager = Mock.Of<ITunerHostManager>();
+ _listingsProviders = new[] { Mock.Of<IListingsProvider>() };
+ }
+
+ [Fact]
+ public void DeleteListingsProvider_DeletesProvider()
+ {
+ // Arrange
+ var id = "MockId";
+ var manager = new ListingsManager(_logger, _config, _taskManager, _tunerHostManager, _listingsProviders);
+
+ Mock.Get(_config)
+ .Setup(x => x.GetConfiguration(It.IsAny<string>()))
+ .Returns(new LiveTvOptions { ListingProviders = [new ListingsProviderInfo { Id = id }] });
+
+ // Act
+ manager.DeleteListingsProvider(id);
+
+ // Assert
+ Assert.DoesNotContain(
+ _config.GetLiveTvConfiguration().ListingProviders,
+ p => p.Id.Equals(id, StringComparison.Ordinal));
+ }
+}
diff --git a/tests/Jellyfin.LiveTv.Tests/SchedulesDirect/SchedulesDirectDeserializeTests.cs b/tests/Jellyfin.LiveTv.Tests/SchedulesDirect/SchedulesDirectDeserializeTests.cs
index 6975d56d9..59cd42c05 100644
--- a/tests/Jellyfin.LiveTv.Tests/SchedulesDirect/SchedulesDirectDeserializeTests.cs
+++ b/tests/Jellyfin.LiveTv.Tests/SchedulesDirect/SchedulesDirectDeserializeTests.cs
@@ -232,7 +232,7 @@ namespace Jellyfin.LiveTv.Tests.SchedulesDirect
Assert.Equal(2, channelDto!.Map.Count);
Assert.Equal("24326", channelDto.Map[0].StationId);
Assert.Equal("001", channelDto.Map[0].Channel);
- Assert.Equal("BBC ONE South", channelDto.Map[0].ProvderCallsign);
+ Assert.Equal("BBC ONE South", channelDto.Map[0].ProviderCallsign);
Assert.Equal("1", channelDto.Map[0].LogicalChannelNumber);
Assert.Equal("providerCallsign", channelDto.Map[0].MatchType);
}
diff --git a/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs
index 0a4e060df..c710df082 100644
--- a/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs
+++ b/tests/Jellyfin.Model.Tests/Extensions/StringHelperTests.cs
@@ -1,5 +1,6 @@
using System;
using FsCheck;
+using FsCheck.Fluent;
using FsCheck.Xunit;
using MediaBrowser.Model.Extensions;
using Xunit;
diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
index 406381f14..7bfab570b 100644
--- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
@@ -77,6 +77,8 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("Season 3/The Series S3 E9 - The title.avi", 9)]
[InlineData("Season 3/S003 E009.avi", 9)]
[InlineData("Season 3/Season 3 Episode 9.avi", 9)]
+ [InlineData("[VCB-Studio] Re Zero kara Hajimeru Isekai Seikatsu [21][Ma10p_1080p][x265_flac].mkv", 21)]
+ [InlineData("[CASO&Sumisora][Oda_Nobuna_no_Yabou][04][BDRIP][1920x1080][x264_AAC][7620E503].mp4", 4)]
// [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number
// TODO: [InlineData("Season 2/16 12 Some Title.avi", 16)]
diff --git a/tests/Jellyfin.Naming.Tests/TV/TvParserHelpersTest.cs b/tests/Jellyfin.Naming.Tests/TV/TvParserHelpersTest.cs
index 2d4b5b730..5dd004408 100644
--- a/tests/Jellyfin.Naming.Tests/TV/TvParserHelpersTest.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/TvParserHelpersTest.cs
@@ -15,17 +15,17 @@ public class TvParserHelpersTest
[InlineData("Unreleased", SeriesStatus.Unreleased)]
public void SeriesStatusParserTest_Valid(string statusString, SeriesStatus? status)
{
- var successful = TvParserHelpers.TryParseSeriesStatus(statusString, out var parsered);
+ var successful = TvParserHelpers.TryParseSeriesStatus(statusString, out var parsed);
Assert.True(successful);
- Assert.Equal(status, parsered);
+ Assert.Equal(status, parsed);
}
[Theory]
[InlineData("XXX")]
public void SeriesStatusParserTest_InValid(string statusString)
{
- var successful = TvParserHelpers.TryParseSeriesStatus(statusString, out var parsered);
+ var successful = TvParserHelpers.TryParseSeriesStatus(statusString, out var parsed);
Assert.False(successful);
- Assert.Null(parsered);
+ Assert.Null(parsed);
}
}
diff --git a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs
index 01546aa2b..4ebd54786 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs
@@ -1,4 +1,5 @@
using FsCheck;
+using FsCheck.Fluent;
using FsCheck.Xunit;
using MediaBrowser.Common.Net;
using Xunit;
diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
index 3b7c43100..4144300da 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
@@ -238,7 +238,7 @@ namespace Jellyfin.Networking.Tests
// User on external network, internal binding only - so assumption is a proxy forward, return external override.
[InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "external=http://helloworld.com", "http://helloworld.com")]
- // User on external network, no binding - so result is the 1st external which is overriden.
+ // User on external network, no binding - so result is the 1st external which is overridden.
[InlineData("jellyfin.org", "192.168.1.0/24", "", false, "external=http://helloworld.com", "http://helloworld.com")]
// User assumed to be internal, no binding - so result is the 1st matching interface.
diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
index 0c7d2487c..1ec859223 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
@@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Mime;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
@@ -292,6 +293,9 @@ namespace Jellyfin.Providers.Tests.Manager
providerManager.Setup(pm => pm.SaveImage(item, It.IsAny<Stream>(), It.IsAny<string>(), imageType, null, It.IsAny<CancellationToken>()))
.Callback<BaseItem, Stream, string, ImageType, int?, CancellationToken>((callbackItem, _, _, callbackType, _, _) => callbackItem.SetImagePath(callbackType, 0, new FileSystemMetadata()))
.Returns(Task.CompletedTask);
+ providerManager.Setup(pm => pm.SaveImage(item, It.IsAny<string>(), It.IsAny<string>(), imageType, null, null, It.IsAny<CancellationToken>()))
+ .Callback<BaseItem, string, string, ImageType, int?, bool?, CancellationToken>((callbackItem, _, _, callbackType, _, _, _) => callbackItem.SetImagePath(callbackType, 0, new FileSystemMetadata()))
+ .Returns(Task.CompletedTask);
var itemImageProvider = GetItemImageProvider(providerManager.Object, null);
var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { dynamicProvider.Object }, refreshOptions, CancellationToken.None);
@@ -388,7 +392,7 @@ namespace Jellyfin.Providers.Tests.Manager
{
ReasonPhrase = url,
StatusCode = HttpStatusCode.OK,
- Content = new StringContent(Content, Encoding.UTF8, "image/jpeg")
+ Content = new StringContent(Content, Encoding.UTF8, MediaTypeNames.Image.Jpeg)
});
var refreshOptions = fullRefresh
diff --git a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs
index cedcaf9c0..b32ecf6ec 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs
@@ -330,7 +330,7 @@ namespace Jellyfin.Providers.Tests.Manager
MetadataService<Movie, MovieInfo>.MergeBaseItemData(source, target, lockedFields, replaceData, false);
actualValue = target.People;
- return newValue?.Equals(actualValue) ?? actualValue is null;
+ return newValue?.SequenceEqual((IEnumerable<PersonInfo>)actualValue!) ?? actualValue is null;
}
/// <summary>
diff --git a/tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs b/tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs
index eed9eedc7..3062cb7b4 100644
--- a/tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Omdb/JsonOmdbConverterTests.cs
@@ -31,7 +31,7 @@ namespace Jellyfin.Providers.Tests.Omdb
[Theory]
[InlineData("\"N/A\"")]
[InlineData("null")]
- public void Deserialization_To_Nullable_Int_Shoud_Be_Null(string input)
+ public void Deserialization_To_Nullable_Int_Should_Be_Null(string input)
{
var result = JsonSerializer.Deserialize<int?>(input, _options);
Assert.Null(result);
@@ -49,7 +49,7 @@ namespace Jellyfin.Providers.Tests.Omdb
[Theory]
[InlineData("\"N/A\"")]
[InlineData("null")]
- public void Deserialization_To_Nullable_String_Shoud_Be_Null(string input)
+ public void Deserialization_To_Nullable_String_Should_Be_Null(string input)
{
var result = JsonSerializer.Deserialize<string?>(input, _options);
Assert.Null(result);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
index 0d2b488bc..105f5d7af 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs
@@ -3,8 +3,10 @@ using System.Collections.Generic;
using AutoFixture;
using AutoFixture.AutoMoq;
using Emby.Server.Implementations.Data;
+using Jellyfin.Server.Implementations.Item;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Configuration;
using Moq;
@@ -18,7 +20,7 @@ namespace Jellyfin.Server.Implementations.Tests.Data
public const string MetaDataPath = "/meta/data/path";
private readonly IFixture _fixture;
- private readonly SqliteItemRepository _sqliteItemRepository;
+ private readonly BaseItemRepository _sqliteItemRepository;
public SqliteItemRepositoryTests()
{
@@ -40,7 +42,7 @@ namespace Jellyfin.Server.Implementations.Tests.Data
_fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
_fixture.Inject(appHost);
_fixture.Inject(config);
- _sqliteItemRepository = _fixture.Create<SqliteItemRepository>();
+ _sqliteItemRepository = _fixture.Create<BaseItemRepository>();
}
public static TheoryData<string, ItemImageInfo> ItemImageInfoFromValueString_Valid_TestData()
@@ -97,31 +99,6 @@ namespace Jellyfin.Server.Implementations.Tests.Data
return data;
}
- [Theory]
- [MemberData(nameof(ItemImageInfoFromValueString_Valid_TestData))]
- public void ItemImageInfoFromValueString_Valid_Success(string value, ItemImageInfo expected)
- {
- var result = _sqliteItemRepository.ItemImageInfoFromValueString(value);
- Assert.Equal(expected.Path, result.Path);
- Assert.Equal(expected.Type, result.Type);
- Assert.Equal(expected.DateModified, result.DateModified);
- Assert.Equal(expected.Width, result.Width);
- Assert.Equal(expected.Height, result.Height);
- Assert.Equal(expected.BlurHash, result.BlurHash);
- }
-
- [Theory]
- [InlineData("")]
- [InlineData("*")]
- [InlineData("https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0")]
- [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*6374520964785129080*WjQbtJtSO8nhNZ%L_Io#R/oaS<o}-;adXAoIn7j[%hW9s:WGw[nN")] // Invalid modified date
- [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*-637452096478512963*WjQbtJtSO8nhNZ%L_Io#R/oaS<o}-;adXAoIn7j[%hW9s:WGw[nN")] // Negative modified date
- [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*637452096478512963*Invalid*1920*1080*WjQbtJtSO8nhNZ%L_Io#R/oaS6o}-;adXAoIn7j[%hW9s:WGw[nN")] // Invalid type
- public void ItemImageInfoFromValueString_Invalid_Null(string value)
- {
- Assert.Null(_sqliteItemRepository.ItemImageInfoFromValueString(value));
- }
-
public static TheoryData<string, ItemImageInfo[]> DeserializeImages_Valid_TestData()
{
var data = new TheoryData<string, ItemImageInfo[]>();
@@ -202,97 +179,6 @@ namespace Jellyfin.Server.Implementations.Tests.Data
return data;
}
- [Theory]
- [MemberData(nameof(DeserializeImages_Valid_TestData))]
- public void DeserializeImages_Valid_Success(string value, ItemImageInfo[] expected)
- {
- var result = _sqliteItemRepository.DeserializeImages(value);
- Assert.Equal(expected.Length, result.Length);
- for (int i = 0; i < expected.Length; i++)
- {
- Assert.Equal(expected[i].Path, result[i].Path);
- Assert.Equal(expected[i].Type, result[i].Type);
- Assert.Equal(expected[i].DateModified, result[i].DateModified);
- Assert.Equal(expected[i].Width, result[i].Width);
- Assert.Equal(expected[i].Height, result[i].Height);
- Assert.Equal(expected[i].BlurHash, result[i].BlurHash);
- }
- }
-
- [Theory]
- [MemberData(nameof(DeserializeImages_ValidAndInvalid_TestData))]
- public void DeserializeImages_ValidAndInvalid_Success(string value, ItemImageInfo[] expected)
- {
- var result = _sqliteItemRepository.DeserializeImages(value);
- Assert.Equal(expected.Length, result.Length);
- for (int i = 0; i < expected.Length; i++)
- {
- Assert.Equal(expected[i].Path, result[i].Path);
- Assert.Equal(expected[i].Type, result[i].Type);
- Assert.Equal(expected[i].DateModified, result[i].DateModified);
- Assert.Equal(expected[i].Width, result[i].Width);
- Assert.Equal(expected[i].Height, result[i].Height);
- Assert.Equal(expected[i].BlurHash, result[i].BlurHash);
- }
- }
-
- [Theory]
- [MemberData(nameof(DeserializeImages_Valid_TestData))]
- public void SerializeImages_Valid_Success(string expected, ItemImageInfo[] value)
- {
- Assert.Equal(expected, _sqliteItemRepository.SerializeImages(value));
- }
-
- public static TheoryData<string, Dictionary<string, string>> DeserializeProviderIds_Valid_TestData()
- {
- var data = new TheoryData<string, Dictionary<string, string>>();
-
- data.Add(
- "Imdb=tt0119567",
- new Dictionary<string, string>()
- {
- { "Imdb", "tt0119567" },
- });
-
- data.Add(
- "Imdb=tt0119567|Tmdb=330|TmdbCollection=328",
- new Dictionary<string, string>()
- {
- { "Imdb", "tt0119567" },
- { "Tmdb", "330" },
- { "TmdbCollection", "328" },
- });
-
- data.Add(
- "MusicBrainzAlbum=9d363e43-f24f-4b39-bc5a-7ef305c677c7|MusicBrainzReleaseGroup=63eba062-847c-3b73-8b0f-6baf27bba6fa|AudioDbArtist=111352|AudioDbAlbum=2116560|MusicBrainzAlbumArtist=20244d07-534f-4eff-b4d4-930878889970",
- new Dictionary<string, string>()
- {
- { "MusicBrainzAlbum", "9d363e43-f24f-4b39-bc5a-7ef305c677c7" },
- { "MusicBrainzReleaseGroup", "63eba062-847c-3b73-8b0f-6baf27bba6fa" },
- { "AudioDbArtist", "111352" },
- { "AudioDbAlbum", "2116560" },
- { "MusicBrainzAlbumArtist", "20244d07-534f-4eff-b4d4-930878889970" },
- });
-
- return data;
- }
-
- [Theory]
- [MemberData(nameof(DeserializeProviderIds_Valid_TestData))]
- public void DeserializeProviderIds_Valid_Success(string value, Dictionary<string, string> expected)
- {
- var result = new ProviderIdsExtensionsTestsObject();
- SqliteItemRepository.DeserializeProviderIds(value, result);
- Assert.Equal(expected, result.ProviderIds);
- }
-
- [Theory]
- [MemberData(nameof(DeserializeProviderIds_Valid_TestData))]
- public void SerializeProviderIds_Valid_Success(string expected, Dictionary<string, string> values)
- {
- Assert.Equal(expected, SqliteItemRepository.SerializeProviderIds(values));
- }
-
private sealed class ProviderIdsExtensionsTestsObject : IHasProviderIds
{
public Dictionary<string, string> ProviderIds { get; set; } = new Dictionary<string, string>();
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index 0afbf7e63..65f018ee3 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -84,7 +84,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
- Assert.Equal(54, ratings.Count);
+ Assert.Equal(56, ratings.Count);
var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
Assert.NotNull(tvma);
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest.json
index 57367ce88..6aa40c1dd 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest.json
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest.json
@@ -540,7 +540,7 @@
{
"guid": "022a3003-993f-45f1-8565-87d12af2e12a",
"name": "InfuseSync",
- "description": "This plugin will track all media changes while any Infuse clients are offline to decrease sync times when logging back in to your server.",
+ "description": "This plugin will track all media changes while any Infuse clients are offline to decrease sync times when logging back into your server.",
"overview": "Blazing fast indexing for Infuse",
"owner": "Firecore LLC",
"category": "General",
diff --git a/tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs b/tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs
index 1bd51b246..9a4389e7a 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/TypedBaseItem/BaseItemKindTests.cs
@@ -35,7 +35,7 @@ namespace Jellyfin.Server.Implementations.Tests.TypedBaseItem
public void EnumParse_GivenValidBaseItemType_ReturnsEnumValue(Type baseItemDescendantType)
{
var enumValue = Enum.Parse<BaseItemKind>(baseItemDescendantType.Name);
- Assert.True(Enum.IsDefined(typeof(BaseItemKind), enumValue));
+ Assert.True(Enum.IsDefined(enumValue));
}
[Theory]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs
index 295f558fa..665afe111 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs
@@ -10,6 +10,9 @@ namespace Jellyfin.Server.Implementations.Tests.Users
[InlineData("this_is_valid")]
[InlineData("this is also valid")]
[InlineData("0@_-' .")]
+ [InlineData("Aa0@_-' .+")]
+ [InlineData("thisisa+testemail@test.foo")]
+ [InlineData("------@@@--+++----@@--abcdefghijklmn---------@----_-_-___-_ .9foo+")]
public void ThrowIfInvalidUsername_WhenValidUsername_DoesNotThrowArgumentException(string username)
{
var ex = Record.Exception(() => UserManager.ThrowIfInvalidUsername(username));
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs
index 39d449e27..d92dbbd73 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs
@@ -14,7 +14,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
public sealed class DashboardControllerTests : IClassFixture<JellyfinApplicationFactory>
{
private readonly JellyfinApplicationFactory _factory;
- private readonly JsonSerializerOptions _jsonOpions = JsonDefaults.Options;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private static string? _accessToken;
public DashboardControllerTests(JellyfinApplicationFactory factory)
@@ -65,7 +65,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- _ = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOpions);
+ _ = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOptions);
// TODO: check content
}
@@ -81,7 +81,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
- var data = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOpions);
+ var data = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOptions);
Assert.NotNull(data);
Assert.Empty(data);
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs
index 23de2489e..64b9bd8e1 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs
@@ -35,7 +35,7 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact
[Theory]
[InlineData("Users/{0}/Items")]
[InlineData("Users/{0}/Items/Resume")]
- public async Task GetUserItems_NonExistentUserId_NotFound(string format)
+ public async Task GetUserItems_NonexistentUserId_NotFound(string format)
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs
index 06abae14c..6881a9210 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs
@@ -29,7 +29,7 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa
[InlineData("Shows/{0}/Similar")]
[InlineData("Movies/{0}/Similar")]
[InlineData("Trailers/{0}/Similar")]
- public async Task Get_NonExistentItemId_NotFound(string format)
+ public async Task Get_NonexistentItemId_NotFound(string format)
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
@@ -41,7 +41,7 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa
[Theory]
[InlineData("Items/{0}")]
[InlineData("Items?ids={0}")]
- public async Task Delete_NonExistentItemId_Unauthorised(string format)
+ public async Task Delete_NonexistentItemId_Unauthorised(string format)
{
var client = _factory.CreateClient();
@@ -52,7 +52,7 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa
[Theory]
[InlineData("Items/{0}")]
[InlineData("Items?ids={0}")]
- public async Task Delete_NonExistentItemId_NotFound(string format)
+ public async Task Delete_NonexistentItemId_NotFound(string format)
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs
index bf3bfdad4..e7166d424 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs
@@ -45,7 +45,7 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl
}
[Fact]
- [Priority(0)]
+ [Priority(-2)]
public async Task UpdateLibraryOptions_Invalid_NotFound()
{
var client = _factory.CreateClient();
@@ -62,12 +62,23 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl
}
[Fact]
- [Priority(0)]
+ [Priority(-2)]
public async Task UpdateLibraryOptions_Valid_Success()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+ var createBody = new AddVirtualFolderDto()
+ {
+ LibraryOptions = new LibraryOptions()
+ {
+ Enabled = false
+ }
+ };
+
+ using var createResponse = await client.PostAsJsonAsync("Library/VirtualFolders?name=test&refreshLibrary=true", createBody, _jsonOptions);
+ Assert.Equal(HttpStatusCode.NoContent, createResponse.StatusCode);
+
using var response = await client.GetAsync("Library/VirtualFolders");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs
index c02eedb20..3b9ed1778 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs
@@ -16,7 +16,7 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory
}
[Fact]
- public async Task DeleteMarkUnplayedItem_NonExistentUserId_NotFound()
+ public async Task DeleteMarkUnplayedItem_NonexistentUserId_NotFound()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
@@ -26,7 +26,7 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory
}
[Fact]
- public async Task PostMarkPlayedItem_NonExistentUserId_NotFound()
+ public async Task PostMarkPlayedItem_NonexistentUserId_NotFound()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
@@ -36,7 +36,7 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory
}
[Fact]
- public async Task DeleteMarkUnplayedItem_NonExistentItemId_NotFound()
+ public async Task DeleteMarkUnplayedItem_NonexistentItemId_NotFound()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
@@ -48,7 +48,7 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory
}
[Fact]
- public async Task PostMarkPlayedItem_NonExistentItemId_NotFound()
+ public async Task PostMarkPlayedItem_NonexistentItemId_NotFound()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
index 4fcacd2ca..16c63ed49 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
@@ -20,7 +20,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
private const string TestUsername = "testUser01";
private readonly JellyfinApplicationFactory _factory;
- private readonly JsonSerializerOptions _jsonOpions = JsonDefaults.Options;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private static string? _accessToken;
private static Guid _testUserId = Guid.Empty;
@@ -30,10 +30,10 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
}
private Task<HttpResponseMessage> CreateUserByName(HttpClient httpClient, CreateUserByName request)
- => httpClient.PostAsJsonAsync("Users/New", request, _jsonOpions);
+ => httpClient.PostAsJsonAsync("Users/New", request, _jsonOptions);
private Task<HttpResponseMessage> UpdateUserPassword(HttpClient httpClient, Guid userId, UpdateUserPassword request)
- => httpClient.PostAsJsonAsync("Users/" + userId.ToString("N", CultureInfo.InvariantCulture) + "/Password", request, _jsonOpions);
+ => httpClient.PostAsJsonAsync("Users/" + userId.ToString("N", CultureInfo.InvariantCulture) + "/Password", request, _jsonOptions);
[Fact]
[Priority(-1)]
@@ -43,7 +43,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
using var response = await client.GetAsync("Users/Public");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOpions);
+ var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOptions);
// User are hidden by default
Assert.NotNull(users);
Assert.Empty(users);
@@ -58,7 +58,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
using var response = await client.GetAsync("Users");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOpions);
+ var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOptions);
Assert.NotNull(users);
Assert.Single(users);
Assert.False(users![0].HasConfiguredPassword);
@@ -90,7 +90,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
using var response = await CreateUserByName(client, createRequest);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var user = await response.Content.ReadFromJsonAsync<UserDto>(_jsonOpions);
+ var user = await response.Content.ReadFromJsonAsync<UserDto>(_jsonOptions);
Assert.Equal(TestUsername, user!.Name);
Assert.False(user.HasPassword);
Assert.False(user.HasConfiguredPassword);
@@ -151,7 +151,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
- await client.GetStreamAsync("Users"), _jsonOpions);
+ await client.GetStreamAsync("Users"), _jsonOptions);
var user = users!.First(x => x.Id.Equals(_testUserId));
Assert.True(user.HasPassword);
Assert.True(user.HasConfiguredPassword);
@@ -174,7 +174,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
- await client.GetStreamAsync("Users"), _jsonOpions);
+ await client.GetStreamAsync("Users"), _jsonOptions);
var user = users!.First(x => x.Id.Equals(_testUserId));
Assert.False(user.HasPassword);
Assert.False(user.HasConfiguredPassword);
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs
index 130281c6d..8df86111e 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs
@@ -23,7 +23,7 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
}
[Fact]
- public async Task GetRootFolder_NonExistenUserId_NotFound()
+ public async Task GetRootFolder_NonexistentUserId_NotFound()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
@@ -47,7 +47,7 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
[InlineData("Users/{0}/Items/{1}/LocalTrailers")]
[InlineData("Users/{0}/Items/{1}/SpecialFeatures")]
[InlineData("Users/{0}/Items/{1}/Lyrics")]
- public async Task GetItem_NonExistenUserId_NotFound(string format)
+ public async Task GetItem_NonexistentUserId_NotFound(string format)
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
@@ -64,7 +64,7 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
[InlineData("Users/{0}/Items/{1}/LocalTrailers")]
[InlineData("Users/{0}/Items/{1}/SpecialFeatures")]
[InlineData("Users/{0}/Items/{1}/Lyrics")]
- public async Task GetItem_NonExistentItemId_NotFound(string format)
+ public async Task GetItem_NonexistentItemId_NotFound(string format)
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs
index 47bec5d79..1916ced12 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs
@@ -16,7 +16,7 @@ public sealed class VideosControllerTests : IClassFixture<JellyfinApplicationFac
}
[Fact]
- public async Task DeleteAlternateSources_NonExistentItemId_NotFound()
+ public async Task DeleteAlternateSources_NonexistentItemId_NotFound()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
index 12d6e1934..f9126ce9b 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
@@ -85,7 +85,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Contains("Bryan Fuller", writers.Select(x => x.Name));
Assert.Contains("Michael Green", writers.Select(x => x.Name));
- // Direcotrs
+ // Directors
var directors = result.People.Where(x => x.Type == PersonKind.Director).ToArray();
Assert.Single(directors);
Assert.Contains("David Slade", directors.Select(x => x.Name));
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
index 075c70da8..9c2655154 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
@@ -149,7 +149,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal(new DateTime(2019, 8, 6, 9, 1, 18), item.DateCreated);
// userData
- var userData = _userDataManager.GetUserData(_testUser, item);
+ var userData = _userDataManager.GetUserData(_testUser, item)!;
Assert.Equal(2, userData.PlayCount);
Assert.True(userData.Played);
Assert.Equal(new DateTime(2021, 02, 11, 07, 47, 23), userData.LastPlayedDate);
@@ -257,5 +257,23 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
}
+
+ [Fact]
+ public void Parsing_Fields_With_Escaped_Xml_Special_Characters_Success()
+ {
+ var result = new MetadataResult<Video>()
+ {
+ Item = new Movie()
+ };
+
+ _parser.Fetch(result, "Test Data/Lilo & Stitch.nfo", CancellationToken.None);
+ var item = (Movie)result.Item;
+
+ Assert.Equal("Lilo & Stitch", item.Name);
+ Assert.Equal("Lilo & Stitch", item.OriginalTitle);
+ Assert.Equal("Lilo & Stitch Collection", item.CollectionName);
+ Assert.StartsWith(">>", item.Overview, StringComparison.InvariantCulture);
+ Assert.EndsWith("<<", item.Overview, StringComparison.InvariantCulture);
+ }
}
}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Lilo & Stitch.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Lilo & Stitch.nfo
new file mode 100644
index 000000000..1eab687a2
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Lilo & Stitch.nfo
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<movie>
+ <title>Lilo &amp; Stitch</title>
+ <originaltitle>Lilo &amp; Stitch</originaltitle>
+ <set>Lilo &amp; Stitch Collection</set>
+ <plot>&gt;&gt;As Stitch, a runaway genetic experiment from a faraway planet, wreaks havoc on the Hawaiian Islands, he becomes the mischievous adopted alien "puppy" of an independent little girl named Lilo and learns about loyalty, friendship, and ʻohana, the Hawaiian tradition of family.&lt;&lt;</plot>
+</movie>
diff --git a/tests/jellyfin-tests.ruleset b/tests/jellyfin-tests.ruleset
deleted file mode 100644
index 9d133da56..000000000
--- a/tests/jellyfin-tests.ruleset
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RuleSet Name="Rules for Jellyfin.Api.Tests" Description="Code analysis rules for Jellyfin.Api.Tests.csproj" ToolsVersion="14.0">
-
- <!-- Include the solution default RuleSet. The rules in this file will override the defaults. -->
- <Include Path="../jellyfin.ruleset" Action="Default" />
-
- <!-- StyleCop Analyzer Rules -->
- <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
- <!-- SA0001: XML comment analysis is disabled due to project configuration -->
- <Rule Id="SA0001" Action="None" />
- </Rules>
-
- <!-- FxCop Analyzer Rules -->
- <Rules AnalyzerId="Microsoft.CodeAnalysis.FxCopAnalyzers" RuleNamespace="Microsoft.Design">
- <!-- CA1707: Identifiers should not contain underscores -->
- <Rule Id="CA1707" Action="None" />
- <!-- CA2007: Consider calling ConfigureAwait on the awaited task -->
- <Rule Id="CA2007" Action="None" />
- <!-- CA2234: Pass system uri objects instead of strings -->
- <Rule Id="CA2234" Action="Info" />
- </Rules>
-
- <!-- xUnit -->
- <Rules AnalyzerId="xUnit" RuleNamespace="xUnit">
- <!-- Test methods must have a supported return type. -->
- <Rule Id="xUnit1028" Action="None" />
- </Rules>
-</RuleSet>