aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Directory.Packages.props5
-rw-r--r--Emby.Naming/Video/ExtraRuleResolver.cs7
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs5
-rw-r--r--Emby.Naming/Video/VideoResolver.cs15
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs1
-rw-r--r--Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs41
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs21
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj3
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs40
-rw-r--r--Emby.Server.Implementations/Library/PathManager.cs45
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs13
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs14
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json8
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs168
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/0-prefer.csv11
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/0-prefer.json34
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/au.csv17
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/au.json69
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/be.csv11
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/be.json55
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/br.csv14
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/br.json55
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ca.csv18
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ca.json90
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/cl.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/co.csv7
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/co.json55
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/de.csv17
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/de.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/dk.csv7
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/dk.json48
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/es.csv25
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/es.json90
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/fi.csv10
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/fi.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/fr.csv13
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/fr.json69
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/gb.csv23
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/gb.json97
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ie.csv10
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ie.json55
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/jp.csv11
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/jp.json62
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/kz.csv6
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/kz.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/mx.csv6
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/mx.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/nl.csv8
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/nl.json55
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/no.csv10
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/no.json69
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/nz.csv16
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/nz.json69
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ro.csv6
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ro.json48
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ru.csv6
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ru.json48
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/se.csv10
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/se.json55
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/sk.csv6
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/sk.json41
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/uk.csv22
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/uk.json97
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/us.csv52
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/us.json83
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs12
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs88
-rw-r--r--Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs71
-rw-r--r--Emby.Server.Implementations/SystemManager.cs2
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs24
-rw-r--r--Jellyfin.Api/Controllers/BrandingController.cs13
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs25
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs90
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs3
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs4
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs4
-rw-r--r--Jellyfin.Api/Controllers/LocalizationController.cs5
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs4
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs24
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs85
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs6
-rw-r--r--Jellyfin.Data/Enums/VideoRangeType.cs21
-rw-r--r--Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs70
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs136
-rw-r--r--Jellyfin.Server.Implementations/Item/KeyframeRepository.cs64
-rw-r--r--Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs4
-rw-r--r--Jellyfin.Server.Implementations/Item/OrderMapper.cs57
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs6
-rw-r--r--Jellyfin.Server/Filters/AdditionalModelFilter.cs2
-rw-r--r--Jellyfin.Server/Migrations/MigrationRunner.cs9
-rw-r--r--Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs63
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs176
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs563
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs88
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs299
-rw-r--r--Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs3
-rw-r--r--Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs5
-rw-r--r--MediaBrowser.Controller/Dto/DtoOptions.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs55
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs12
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs19
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs16
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs37
-rw-r--r--MediaBrowser.Controller/IO/IPathManager.cs32
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/BitStreamFilterOptionType.cs32
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs253
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs1
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs47
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs13
-rw-r--r--MediaBrowser.Controller/Persistence/IKeyframeRepository.cs29
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs5
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs8
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs418
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs87
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs53
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs30
-rw-r--r--MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs7
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaFrameInfo.cs184
-rw-r--r--MediaBrowser.MediaEncoding/Probing/MediaFrameSideDataInfo.cs16
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs15
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs33
-rw-r--r--MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs20
-rw-r--r--MediaBrowser.Model/Branding/BrandingOptions.cs7
-rw-r--r--MediaBrowser.Model/Branding/BrandingOptionsDto.cs25
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs9
-rw-r--r--MediaBrowser.Model/Dlna/ConditionProcessor.cs13
-rw-r--r--MediaBrowser.Model/Dlna/ProfileConditionValue.cs3
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs12
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs317
-rw-r--r--MediaBrowser.Model/Dto/BaseItemDto.cs2
-rw-r--r--MediaBrowser.Model/Dto/BaseItemPerson.cs2
-rw-r--r--MediaBrowser.Model/Dto/MediaSourceInfo.cs10
-rw-r--r--MediaBrowser.Model/Dto/MetadataEditorInfo.cs70
-rw-r--r--MediaBrowser.Model/Entities/MediaAttachment.cs80
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs38
-rw-r--r--MediaBrowser.Model/Entities/ParentalRating.cs55
-rw-r--r--MediaBrowser.Model/Entities/ParentalRatingEntry.cs22
-rw-r--r--MediaBrowser.Model/Entities/ParentalRatingScore.cs32
-rw-r--r--MediaBrowser.Model/Entities/ParentalRatingSystem.cs28
-rw-r--r--MediaBrowser.Model/Extensions/ContainerHelper.cs4
-rw-r--r--MediaBrowser.Model/Globalization/ILocalizationManager.cs101
-rw-r--r--MediaBrowser.Model/LiveTv/TunerHostInfo.cs3
-rw-r--r--MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs2
-rw-r--r--MediaBrowser.Model/Querying/ItemFields.cs79
-rw-r--r--MediaBrowser.Model/Search/SearchHint.cs2
-rw-r--r--MediaBrowser.Model/Session/TranscodeReason.cs1
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs2
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs1
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs19
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs4
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs4
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/KeyframeData.cs32
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs9
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs5
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs4
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/KeyframeDataConfiguration.cs18
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.Designer.cs1658
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs48
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.Designer.cs1681
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs41
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.Designer.cs1655
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs28
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.Designer.cs1657
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs36
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.Designer.cs1658
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs20
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.Designer.cs1694
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs47
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs265
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs60
-rw-r--r--src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs153
-rw-r--r--src/Jellyfin.Extensions/EnumerableExtensions.cs19
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs2
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Cache/CacheDecorator.cs79
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Extractors/FfProbeKeyframeExtractor.cs2
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Extractors/IKeyframeExtractor.cs4
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Extractors/MatroskaKeyframeExtractor.cs2
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj1
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs11
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs8
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs24
-rw-r--r--tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs4
-rw-r--r--tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs4
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs215
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs16
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs243
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json31
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json31
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-32.json565
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-33.json582
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs62
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs35
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs66
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Playlists/PlaylistManagerTests.cs40
205 files changed, 17519 insertions, 1997 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index ae1a2fd71..0dcce1ea1 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -269,3 +269,4 @@
- [Robert Lützner](https://github.com/rluetzner)
- [Nathan McCrina](https://github.com/nfmccrina)
- [Martin Reuter](https://github.com/reuterma24)
+ - [Michael McElroy](https://github.com/mcmcelro)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 89311142c..c81982aa8 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,6 +9,7 @@
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" />
+ <PackageVersion Include="BitFaster.Caching" Version="2.5.3" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
<PackageVersion Include="BlurHashSharp" Version="1.3.4" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
@@ -79,7 +80,7 @@
<PackageVersion Include="System.Text.Json" Version="9.0.3" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.3" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
- <PackageVersion Include="z440.atl.core" Version="6.19.0" />
+ <PackageVersion Include="z440.atl.core" Version="6.20.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
@@ -87,4 +88,4 @@
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
<PackageVersion Include="xunit" Version="2.9.3" />
</ItemGroup>
-</Project>
+</Project> \ No newline at end of file
diff --git a/Emby.Naming/Video/ExtraRuleResolver.cs b/Emby.Naming/Video/ExtraRuleResolver.cs
index 3219472ef..528906589 100644
--- a/Emby.Naming/Video/ExtraRuleResolver.cs
+++ b/Emby.Naming/Video/ExtraRuleResolver.cs
@@ -18,8 +18,9 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="namingOptions">The naming options.</param>
+ /// <param name="libraryRoot">Top-level folder for the containing library.</param>
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
- public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
+ public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions, string? libraryRoot = "")
{
var result = new ExtraResult();
@@ -69,7 +70,9 @@ namespace Emby.Naming.Video
else if (rule.RuleType == ExtraRuleType.DirectoryName)
{
var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
- if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
+ string fullDirectory = Path.GetDirectoryName(pathSpan).ToString();
+ if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 12bc22a6a..a3134f3f6 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -27,8 +27,9 @@ namespace Emby.Naming.Video
/// <param name="namingOptions">The naming options.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
/// <param name="parseName">Whether to parse the name or use the filename.</param>
+ /// <param name="libraryRoot">Top-level folder for the containing library.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
- public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
+ public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true, string? libraryRoot = "")
{
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
@@ -65,7 +66,7 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
- Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
+ Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName, libraryRoot))
.OfType<VideoFileInfo>()
.ToList()
};
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index db5bfdbf9..afbf6f8fa 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -17,10 +17,11 @@ namespace Emby.Naming.Video
/// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="parseName">Whether to parse the name or use the filename.</param>
+ /// <param name="libraryRoot">Top-level folder for the containing library.</param>
/// <returns>VideoFileInfo.</returns>
- public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
+ public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "")
{
- return Resolve(path, true, namingOptions, parseName);
+ return Resolve(path, true, namingOptions, parseName, libraryRoot);
}
/// <summary>
@@ -28,10 +29,11 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param>
+ /// <param name="libraryRoot">Top-level folder for the containing library.</param>
/// <returns>VideoFileInfo.</returns>
- public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
+ public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions, string? libraryRoot = "")
{
- return Resolve(path, false, namingOptions);
+ return Resolve(path, false, namingOptions, libraryRoot: libraryRoot);
}
/// <summary>
@@ -41,9 +43,10 @@ namespace Emby.Naming.Video
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
+ /// <param name="libraryRoot">Top-level folder for the containing library.</param>
/// <returns>VideoFileInfo.</returns>
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
- public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true)
+ public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "")
{
if (string.IsNullOrEmpty(path))
{
@@ -75,7 +78,7 @@ namespace Emby.Naming.Video
var format3DResult = Format3DParser.Parse(path, namingOptions);
- var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions);
+ var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions, libraryRoot);
var name = Path.GetFileNameWithoutExtension(path);
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 4d959905d..5bb75e2b9 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -505,6 +505,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>();
+ serviceCollection.AddSingleton<IKeyframeRepository, KeyframeRepository>();
serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>();
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
index 63481b1f8..9a80eafe5 100644
--- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
+++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
@@ -1,14 +1,14 @@
#pragma warning disable CS1591
using System;
+using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
-using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Trickplay;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -19,15 +19,18 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
private readonly ILibraryManager _libraryManager;
private readonly ILogger<CleanDatabaseScheduledTask> _logger;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ private readonly IPathManager _pathManager;
public CleanDatabaseScheduledTask(
ILibraryManager libraryManager,
ILogger<CleanDatabaseScheduledTask> logger,
- IDbContextFactory<JellyfinDbContext> dbProvider)
+ IDbContextFactory<JellyfinDbContext> dbProvider,
+ IPathManager pathManager)
{
_libraryManager = libraryManager;
_logger = logger;
_dbProvider = dbProvider;
+ _pathManager = pathManager;
}
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
@@ -56,6 +59,38 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
{
_logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
+ foreach (var mediaSource in item.GetMediaSources(false))
+ {
+ // Delete extracted subtitles
+ try
+ {
+ var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id);
+ if (Directory.Exists(subtitleFolder))
+ {
+ Directory.Delete(subtitleFolder, true);
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.LogWarning("Failed to remove subtitle cache folder for {Item}: {Exception}", item.Id, e.Message);
+ }
+
+ // Delete extracted attachments
+ try
+ {
+ var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
+ if (Directory.Exists(attachmentFolder))
+ {
+ Directory.Delete(attachmentFolder, true);
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.LogWarning("Failed to remove attachment cache folder for {Item}: {Exception}", item.Id, e.Message);
+ }
+ }
+
+ // Delete item
_libraryManager.DeleteItem(item, new DeleteOptions
{
DeleteFileLocation = false
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 0ce967e6a..5b0fc9ef3 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -41,7 +41,6 @@ namespace Emby.Server.Implementations.Dto
private readonly ILogger<DtoService> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IUserDataManager _userDataRepository;
- private readonly IItemRepository _itemRepo;
private readonly IImageProcessor _imageProcessor;
private readonly IProviderManager _providerManager;
@@ -58,7 +57,6 @@ namespace Emby.Server.Implementations.Dto
ILogger<DtoService> logger,
ILibraryManager libraryManager,
IUserDataManager userDataRepository,
- IItemRepository itemRepo,
IImageProcessor imageProcessor,
IProviderManager providerManager,
IRecordingsManager recordingsManager,
@@ -71,7 +69,6 @@ namespace Emby.Server.Implementations.Dto
_logger = logger;
_libraryManager = libraryManager;
_userDataRepository = userDataRepository;
- _itemRepo = itemRepo;
_imageProcessor = imageProcessor;
_providerManager = providerManager;
_recordingsManager = recordingsManager;
@@ -99,11 +96,11 @@ namespace Emby.Server.Implementations.Dto
if (item is LiveTvChannel tvChannel)
{
- (channelTuples ??= new()).Add((dto, tvChannel));
+ (channelTuples ??= []).Add((dto, tvChannel));
}
else if (item is LiveTvProgram)
{
- (programTuples ??= new()).Add((item, dto));
+ (programTuples ??= []).Add((item, dto));
}
if (item is IItemByName byName)
@@ -590,12 +587,12 @@ namespace Emby.Server.Implementations.Dto
if (dto.ImageBlurHashes is not null)
{
// Only add BlurHash for the person's image.
- baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+ baseItemPerson.ImageBlurHashes = [];
foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
{
if (blurHash is not null)
{
- baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
+ baseItemPerson.ImageBlurHashes[imageType] = [];
foreach (var (imageId, blurHashValue) in blurHash)
{
if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
@@ -674,11 +671,11 @@ namespace Emby.Server.Implementations.Dto
if (!string.IsNullOrEmpty(image.BlurHash))
{
- dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
+ dto.ImageBlurHashes ??= [];
if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value))
{
- value = new Dictionary<string, string>();
+ value = [];
dto.ImageBlurHashes[image.Type] = value;
}
@@ -709,7 +706,7 @@ namespace Emby.Server.Implementations.Dto
if (hashes.Count > 0)
{
- dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
+ dto.ImageBlurHashes ??= [];
dto.ImageBlurHashes[imageType] = hashes;
}
@@ -756,7 +753,7 @@ namespace Emby.Server.Implementations.Dto
dto.AspectRatio = hasAspectRatio.AspectRatio;
}
- dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+ dto.ImageBlurHashes = [];
var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
if (backdropLimit > 0)
@@ -772,7 +769,7 @@ namespace Emby.Server.Implementations.Dto
if (options.EnableImages)
{
- dto.ImageTags = new Dictionary<ImageType, string>();
+ dto.ImageTags = [];
// Prevent implicitly captured closure
var currentItem = item;
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 8f89f35ac..d99923b4f 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -22,6 +22,7 @@
</ItemGroup>
<ItemGroup>
+ <PackageReference Include="BitFaster.Caching" />
<PackageReference Include="DiscUtils.Udf" />
<PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
@@ -67,6 +68,6 @@
<EmbeddedResource Include="Localization\iso6392.txt" />
<EmbeddedResource Include="Localization\countries.json" />
<EmbeddedResource Include="Localization\Core\*.json" />
- <EmbeddedResource Include="Localization\Ratings\*.csv" />
+ <EmbeddedResource Include="Localization\Ratings\*.json" />
</ItemGroup>
</Project>
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index eb8e31072..1303bb3cb 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -2,7 +2,6 @@
#pragma warning disable CA5394
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -11,6 +10,7 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using BitFaster.Caching.Lru;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Server.Implementations.Library.Resolvers;
@@ -64,7 +64,6 @@ namespace Emby.Server.Implementations.Library
private const string ShortcutFileExtension = ".mblink";
private readonly ILogger<LibraryManager> _logger;
- private readonly ConcurrentDictionary<Guid, BaseItem> _cache;
private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository;
@@ -81,6 +80,7 @@ namespace Emby.Server.Implementations.Library
private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver;
private readonly IPathManager _pathManager;
+ private readonly FastConcurrentLru<Guid, BaseItem> _cache;
/// <summary>
/// The _root folder sync lock.
@@ -150,7 +150,9 @@ namespace Emby.Server.Implementations.Library
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
- _cache = new ConcurrentDictionary<Guid, BaseItem>();
+
+ _cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
+
_namingOptions = namingOptions;
_peopleRepository = peopleRepository;
_pathManager = pathManager;
@@ -158,7 +160,7 @@ namespace Emby.Server.Implementations.Library
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
- RecordConfigurationValues(configurationManager.Configuration);
+ RecordConfigurationValues(_configurationManager.Configuration);
}
/// <summary>
@@ -306,7 +308,7 @@ namespace Emby.Server.Implementations.Library
}
}
- _cache[item.Id] = item;
+ _cache.AddOrUpdate(item.Id, item);
}
public void DeleteItem(BaseItem item, DeleteOptions options)
@@ -460,14 +462,13 @@ namespace Emby.Server.Implementations.Library
item.SetParent(null);
_itemRepository.DeleteItem(item.Id);
+ _cache.TryRemove(item.Id, out _);
foreach (var child in children)
{
_itemRepository.DeleteItem(child.Id);
_cache.TryRemove(child.Id, out _);
}
- _cache.TryRemove(item.Id, out _);
-
ReportItemRemoved(item, parent);
}
@@ -491,7 +492,24 @@ namespace Emby.Server.Implementations.Library
if (item is Video video)
{
+ // Trickplay
list.Add(_pathManager.GetTrickplayDirectory(video));
+
+ // Subtitles and attachments
+ foreach (var mediaSource in item.GetMediaSources(false))
+ {
+ var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id);
+ if (subtitleFolder is not null)
+ {
+ list.Add(subtitleFolder);
+ }
+
+ var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
+ if (attachmentFolder is not null)
+ {
+ list.Add(attachmentFolder);
+ }
+ }
}
return list;
@@ -1255,7 +1273,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Guid can't be empty", nameof(id));
}
- if (_cache.TryGetValue(id, out BaseItem? item))
+ if (_cache.TryGet(id, out var item))
{
return item;
}
@@ -1272,7 +1290,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public T? GetItemById<T>(Guid id)
- where T : BaseItem
+ where T : BaseItem
{
var item = GetItemById(id);
if (item is T typedItem)
@@ -2708,7 +2726,7 @@ namespace Emby.Server.Implementations.Library
public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{
- var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions);
+ var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
if (ownerVideoInfo is null)
{
yield break;
@@ -2898,7 +2916,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(name));
}
- name = _fileSystem.GetValidFilename(name);
+ name = _fileSystem.GetValidFilename(name.Trim());
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs
index c910abadb..83a6df964 100644
--- a/Emby.Server.Implementations/Library/PathManager.cs
+++ b/Emby.Server.Implementations/Library/PathManager.cs
@@ -1,5 +1,7 @@
+using System;
using System.Globalization;
using System.IO;
+using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
@@ -12,25 +14,60 @@ namespace Emby.Server.Implementations.Library;
public class PathManager : IPathManager
{
private readonly IServerConfigurationManager _config;
+ private readonly IApplicationPaths _appPaths;
/// <summary>
/// Initializes a new instance of the <see cref="PathManager"/> class.
/// </summary>
/// <param name="config">The server configuration manager.</param>
+ /// <param name="appPaths">The application paths.</param>
public PathManager(
- IServerConfigurationManager config)
+ IServerConfigurationManager config,
+ IApplicationPaths appPaths)
{
_config = config;
+ _appPaths = appPaths;
+ }
+
+ private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
+
+ private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
+
+ /// <inheritdoc />
+ public string GetAttachmentPath(string mediaSourceId, string fileName)
+ {
+ return Path.Join(GetAttachmentFolderPath(mediaSourceId), fileName);
+ }
+
+ /// <inheritdoc />
+ public string GetAttachmentFolderPath(string mediaSourceId)
+ {
+ var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
+
+ return Path.Join(AttachmentCachePath, id[..2], id);
+ }
+
+ /// <inheritdoc />
+ public string GetSubtitleFolderPath(string mediaSourceId)
+ {
+ var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
+
+ return Path.Join(SubtitleCachePath, id[..2], id);
+ }
+
+ /// <inheritdoc />
+ public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
+ {
+ return Path.Join(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
}
/// <inheritdoc />
public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false)
{
- var basePath = _config.ApplicationPaths.TrickplayPath;
- var idString = item.Id.ToString("N", CultureInfo.InvariantCulture);
+ var id = item.Id.ToString("D", CultureInfo.InvariantCulture).AsSpan();
return saveWithMedia
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
- : Path.Combine(basePath, idString);
+ : Path.Join(_config.ApplicationPaths.TrickplayPath, id[..2], id);
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
index b4791b945..b9f9f2972 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
@@ -54,9 +54,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
_ => _videoResolvers
};
- public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType)
+ public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "")
{
- var extraResult = GetExtraInfo(path, _namingOptions);
+ var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot);
if (extraResult.ExtraType is null)
{
extraType = null;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 4debe722b..f1aeb1340 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -270,11 +270,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
var videoInfos = files
- .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName))
+ .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName, parent.ContainingFolderPath))
.Where(f => f is not null)
.ToList();
- var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName);
+ var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath);
var result = new MultiItemResolverResult
{
diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
index 0c9edd839..71ce3b601 100644
--- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
@@ -11,7 +11,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library;
@@ -78,15 +77,15 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
CollapseBoxSetItems = false,
Recursive = true,
DtoOptions = new DtoOptions(false),
- ImageTypes = new[] { imageType },
+ ImageTypes = [imageType],
Limit = 30,
// TODO max parental rating configurable
- MaxParentalRating = 10,
- OrderBy = new[]
- {
+ MaxParentalRating = new(10, null),
+ OrderBy =
+ [
(ItemSortBy.Random, SortOrder.Ascending)
- },
- IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series }
+ ],
+ IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series]
});
}
}
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index 8b88b904b..be1d96bf0 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -1,14 +1,13 @@
#pragma warning disable RS0030 // Do not use banned APIs
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
+using BitFaster.Caching.Lru;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
-using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -26,11 +25,9 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public class UserDataManager : IUserDataManager
{
- private readonly ConcurrentDictionary<string, UserItemData> _userData =
- new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
-
private readonly IServerConfigurationManager _config;
private readonly IDbContextFactory<JellyfinDbContext> _repository;
+ private readonly FastConcurrentLru<string, UserItemData> _cache;
/// <summary>
/// Initializes a new instance of the <see cref="UserDataManager"/> class.
@@ -43,6 +40,7 @@ namespace Emby.Server.Implementations.Library
{
_config = config;
_repository = repository;
+ _cache = new FastConcurrentLru<string, UserItemData>(Environment.ProcessorCount, _config.Configuration.CacheSize, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
@@ -81,7 +79,7 @@ namespace Emby.Server.Implementations.Library
var userId = user.InternalId;
var cacheKey = GetCacheKey(userId, item.Id);
- _userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
+ _cache.AddOrUpdate(cacheKey, userData);
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
{
@@ -182,7 +180,7 @@ namespace Emby.Server.Implementations.Library
{
var cacheKey = GetCacheKey(user.InternalId, itemId);
- if (_userData.TryGetValue(cacheKey, out var data))
+ if (_cache.TryGet(cacheKey, out var data))
{
return data;
}
@@ -197,7 +195,7 @@ namespace Emby.Server.Implementations.Library
};
}
- return _userData.GetOrAdd(cacheKey, data);
+ return _cache.GetOrAdd(cacheKey, _ => data);
}
private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index e89ede10b..1dce58923 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -129,5 +129,11 @@
"TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.",
"TaskAudioNormalization": "Odio Normalisering",
"TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon",
- "TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie."
+ "TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie.",
+ "TaskDownloadMissingLyrics": "Laai tekorte lirieke af",
+ "TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies",
+ "TaskExtractMediaSegments": "Media Segment Skandeer",
+ "TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.",
+ "TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging",
+ "TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings."
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 754a01329..9598f9e6c 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -26,20 +25,18 @@ namespace Emby.Server.Implementations.Localization
private const string CulturesPath = "Emby.Server.Implementations.Localization.iso6392.txt";
private const string CountriesPath = "Emby.Server.Implementations.Localization.countries.json";
private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly;
- private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated", "nr" };
+ private static readonly string[] _unratedValues = ["n/a", "unrated", "not rated", "nr"];
private readonly IServerConfigurationManager _configurationManager;
private readonly ILogger<LocalizationManager> _logger;
- private readonly Dictionary<string, Dictionary<string, ParentalRating>> _allParentalRatings =
- new Dictionary<string, Dictionary<string, ParentalRating>>(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary<string, Dictionary<string, ParentalRatingScore?>> _allParentalRatings = new(StringComparer.OrdinalIgnoreCase);
- private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries =
- new ConcurrentDictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries = new(StringComparer.OrdinalIgnoreCase);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
- private List<CultureDto> _cultures = new List<CultureDto>();
+ private List<CultureDto> _cultures = [];
/// <summary>
/// Initializes a new instance of the <see cref="LocalizationManager" /> class.
@@ -68,35 +65,26 @@ namespace Emby.Server.Implementations.Localization
continue;
}
- string countryCode = resource.Substring(RatingsPath.Length, 2);
- var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
-
- var stream = _assembly.GetManifestResourceStream(resource);
- await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
+ using var stream = _assembly.GetManifestResourceStream(resource);
+ if (stream is not null)
{
- using var reader = new StreamReader(stream!);
- await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+ var ratingSystem = await JsonSerializer.DeserializeAsync<ParentalRatingSystem>(stream, _jsonOptions).ConfigureAwait(false)
+ ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'");
+
+ var dict = new Dictionary<string, ParentalRatingScore?>();
+ if (ratingSystem.Ratings is not null)
{
- if (string.IsNullOrWhiteSpace(line))
+ foreach (var ratingEntry in ratingSystem.Ratings)
{
- continue;
+ foreach (var ratingString in ratingEntry.RatingStrings)
+ {
+ dict[ratingString] = ratingEntry.RatingScore;
+ }
}
- string[] parts = line.Split(',');
- if (parts.Length == 2
- && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
- {
- var name = parts[0];
- dict.Add(name, new ParentalRating(name, value));
- }
- else
- {
- _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
- }
+ _allParentalRatings[ratingSystem.CountryCode] = dict;
}
}
-
- _allParentalRatings[countryCode] = dict;
}
await LoadCultures().ConfigureAwait(false);
@@ -111,22 +99,29 @@ namespace Emby.Server.Implementations.Localization
private async Task LoadCultures()
{
- List<CultureDto> list = new List<CultureDto>();
+ List<CultureDto> list = [];
- await using var stream = _assembly.GetManifestResourceStream(CulturesPath)
- ?? throw new InvalidOperationException($"Invalid resource path: '{CulturesPath}'");
- using var reader = new StreamReader(stream);
- await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+ using var stream = _assembly.GetManifestResourceStream(CulturesPath);
+ if (stream is null)
+ {
+ throw new InvalidOperationException($"Invalid resource path: '{CulturesPath}'");
+ }
+ else
{
- if (string.IsNullOrWhiteSpace(line))
+ using var reader = new StreamReader(stream);
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- continue;
- }
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
- var parts = line.Split('|');
+ var parts = line.Split('|');
+ if (parts.Length != 5)
+ {
+ throw new InvalidDataException($"Invalid culture data found at: '{line}'");
+ }
- if (parts.Length == 5)
- {
string name = parts[3];
if (string.IsNullOrWhiteSpace(name))
{
@@ -139,21 +134,21 @@ namespace Emby.Server.Implementations.Localization
continue;
}
- string[] threeletterNames;
+ string[] threeLetterNames;
if (string.IsNullOrWhiteSpace(parts[1]))
{
- threeletterNames = new[] { parts[0] };
+ threeLetterNames = [parts[0]];
}
else
{
- threeletterNames = new[] { parts[0], parts[1] };
+ threeLetterNames = [parts[0], parts[1]];
}
- list.Add(new CultureDto(name, name, twoCharName, threeletterNames));
+ list.Add(new CultureDto(name, name, twoCharName, threeLetterNames));
}
- }
- _cultures = list;
+ _cultures = list;
+ }
}
/// <inheritdoc />
@@ -176,82 +171,80 @@ namespace Emby.Server.Implementations.Localization
}
/// <inheritdoc />
- public IEnumerable<CountryInfo> GetCountries()
+ public IReadOnlyList<CountryInfo> GetCountries()
{
- using StreamReader reader = new StreamReader(
- _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'"));
- return JsonSerializer.Deserialize<IEnumerable<CountryInfo>>(reader.ReadToEnd(), _jsonOptions)
- ?? throw new InvalidOperationException($"Resource contains invalid data: '{CountriesPath}'");
+ using var stream = _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'");
+
+ return JsonSerializer.Deserialize<IReadOnlyList<CountryInfo>>(stream, _jsonOptions) ?? [];
}
/// <inheritdoc />
- public IEnumerable<ParentalRating> GetParentalRatings()
+ public IReadOnlyList<ParentalRating> GetParentalRatings()
{
// Use server default language for ratings
// Fall back to empty list if there are no parental ratings for that language
- var ratings = GetParentalRatingsDictionary()?.Values.ToList()
- ?? new List<ParentalRating>();
+ var ratings = GetParentalRatingsDictionary()?.Select(x => new ParentalRating(x.Key, x.Value)).ToList() ?? [];
// Add common ratings to ensure them being available for selection
// Based on the US rating system due to it being the main source of rating in the metadata providers
// Unrated
- if (!ratings.Any(x => x.Value is null))
+ if (!ratings.Any(x => x is null))
{
- ratings.Add(new ParentalRating("Unrated", null));
+ ratings.Add(new("Unrated", null));
}
// Minimum rating possible
- if (ratings.All(x => x.Value != 0))
+ if (ratings.All(x => x.RatingScore?.Score != 0))
{
- ratings.Add(new ParentalRating("Approved", 0));
+ ratings.Add(new("Approved", new(0, null)));
}
// Matches PG (this has different age restrictions depending on country)
- if (ratings.All(x => x.Value != 10))
+ if (ratings.All(x => x.RatingScore?.Score != 10))
{
- ratings.Add(new ParentalRating("10", 10));
+ ratings.Add(new("10", new(10, null)));
}
// Matches PG-13
- if (ratings.All(x => x.Value != 13))
+ if (ratings.All(x => x.RatingScore?.Score != 13))
{
- ratings.Add(new ParentalRating("13", 13));
+ ratings.Add(new("13", new(13, null)));
}
// Matches TV-14
- if (ratings.All(x => x.Value != 14))
+ if (ratings.All(x => x.RatingScore?.Score != 14))
{
- ratings.Add(new ParentalRating("14", 14));
+ ratings.Add(new("14", new(14, null)));
}
// Catchall if max rating of country is less than 21
// Using 21 instead of 18 to be sure to allow access to all rated content except adult and banned
- if (!ratings.Any(x => x.Value >= 21))
+ if (!ratings.Any(x => x.RatingScore?.Score >= 21))
{
- ratings.Add(new ParentalRating("21", 21));
+ ratings.Add(new ParentalRating("21", new(21, null)));
}
// A lot of countries don't explicitly have a separate rating for adult content
- if (ratings.All(x => x.Value != 1000))
+ if (ratings.All(x => x.RatingScore?.Score != 1000))
{
- ratings.Add(new ParentalRating("XXX", 1000));
+ ratings.Add(new ParentalRating("XXX", new(1000, null)));
}
// A lot of countries don't explicitly have a separate rating for banned content
- if (ratings.All(x => x.Value != 1001))
+ if (ratings.All(x => x.RatingScore?.Score != 1001))
{
- ratings.Add(new ParentalRating("Banned", 1001));
+ ratings.Add(new ParentalRating("Banned", new(1001, null)));
}
- return ratings.OrderBy(r => r.Value);
+ return [.. ratings.OrderBy(r => r.RatingScore?.Score).ThenBy(r => r.RatingScore?.SubScore)];
}
/// <summary>
/// Gets the parental ratings dictionary.
/// </summary>
/// <param name="countryCode">The optional two letter ISO language string.</param>
- /// <returns><see cref="Dictionary{String, ParentalRating}" />.</returns>
- private Dictionary<string, ParentalRating>? GetParentalRatingsDictionary(string? countryCode = null)
+ /// <returns><see cref="Dictionary{String, ParentalRatingScore}" />.</returns>
+ private Dictionary<string, ParentalRatingScore?>? GetParentalRatingsDictionary(string? countryCode = null)
{
// Fallback to server default if no country code is specified.
if (string.IsNullOrEmpty(countryCode))
@@ -268,7 +261,7 @@ namespace Emby.Server.Implementations.Localization
}
/// <inheritdoc />
- public int? GetRatingLevel(string rating, string? countryCode = null)
+ public ParentalRatingScore? GetRatingScore(string rating, string? countryCode = null)
{
ArgumentException.ThrowIfNullOrEmpty(rating);
@@ -278,11 +271,11 @@ namespace Emby.Server.Implementations.Localization
return null;
}
- // Convert integers directly
+ // Convert ints directly
// This may override some of the locale specific age ratings (but those always map to the same age)
if (int.TryParse(rating, out var ratingAge))
{
- return ratingAge;
+ return new(ratingAge, null);
}
// Fairly common for some users to have "Rated R" in their rating field
@@ -295,9 +288,9 @@ namespace Emby.Server.Implementations.Localization
if (!string.IsNullOrEmpty(countryCode))
{
var ratingsDictionary = GetParentalRatingsDictionary(countryCode);
- if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
+ if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value))
{
- return value.Value;
+ return value;
}
}
else
@@ -305,9 +298,9 @@ namespace Emby.Server.Implementations.Localization
// Fall back to server default language for ratings check
// If it has no ratings, use the US ratings
var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us");
- if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value))
+ if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value))
{
- return value.Value;
+ return value;
}
}
@@ -316,7 +309,7 @@ namespace Emby.Server.Implementations.Localization
{
if (dictionary.TryGetValue(rating, out var value))
{
- return value.Value;
+ return value;
}
}
@@ -326,7 +319,7 @@ namespace Emby.Server.Implementations.Localization
var ratingLevelRightPart = rating.AsSpan().RightPart(':');
if (ratingLevelRightPart.Length != 0)
{
- return GetRatingLevel(ratingLevelRightPart.ToString());
+ return GetRatingScore(ratingLevelRightPart.ToString());
}
}
@@ -342,7 +335,7 @@ namespace Emby.Server.Implementations.Localization
if (ratingLevelRightPart.Length != 0)
{
// Check rating system of culture
- return GetRatingLevel(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName);
+ return GetRatingScore(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName);
}
}
@@ -406,7 +399,7 @@ namespace Emby.Server.Implementations.Localization
private async Task CopyInto(IDictionary<string, string> dictionary, string resourcePath)
{
- await using var stream = _assembly.GetManifestResourceStream(resourcePath);
+ using var stream = _assembly.GetManifestResourceStream(resourcePath);
// If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain
if (stream is null)
{
@@ -414,12 +407,7 @@ namespace Emby.Server.Implementations.Localization
return;
}
- var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false);
- if (dict is null)
- {
- throw new InvalidOperationException($"Resource contains invalid data: '{stream}'");
- }
-
+ var dict = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, _jsonOptions).ConfigureAwait(false) ?? throw new InvalidOperationException($"Resource contains invalid data: '{stream}'");
foreach (var key in dict.Keys)
{
dictionary[key] = dict[key];
diff --git a/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv b/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv
deleted file mode 100644
index 36886ba76..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv
+++ /dev/null
@@ -1,11 +0,0 @@
-E,0
-EC,0
-T,7
-M,18
-AO,18
-UR,18
-RP,18
-X,1000
-XX,1000
-XXX,1000
-XXXX,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/0-prefer.json b/Emby.Server.Implementations/Localization/Ratings/0-prefer.json
new file mode 100644
index 000000000..b39015161
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/0-prefer.json
@@ -0,0 +1,34 @@
+{
+ "countryCode": "0-prefer",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["E", "EC"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["T"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["M", "AO", "UR", "RP"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["X", "XX", "XXX", "XXXX"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv
deleted file mode 100644
index 6e12759a4..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/au.csv
+++ /dev/null
@@ -1,17 +0,0 @@
-Exempt,0
-G,0
-7+,7
-PG,15
-M,15
-MA,15
-MA15+,15
-MA 15+,15
-16+,16
-R,18
-R18+,18
-R 18+,18
-18+,18
-X18+,1000
-X 18+,1000
-X,1000
-RC,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/au.json b/Emby.Server.Implementations/Localization/Ratings/au.json
new file mode 100644
index 000000000..a563df899
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/au.json
@@ -0,0 +1,69 @@
+{
+ "countryCode": "au",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["Exempt", "G"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["7+"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["PG"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["M"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 2
+ }
+ },
+ {
+ "ratingStrings": ["MA", "MA 15+", "MA15+"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 3
+ }
+ },
+ {
+ "ratingStrings": ["16+"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["18+", "R", "R18+", "R 18+"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["X", "X18", "X 18"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["RC"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": 0
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/be.csv b/Emby.Server.Implementations/Localization/Ratings/be.csv
deleted file mode 100644
index d171a7132..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/be.csv
+++ /dev/null
@@ -1,11 +0,0 @@
-AL,0
-KT,0
-TOUS,0
-MG6,6
-6,6
-9,9
-KNT,12
-12,12
-14,14
-16,16
-18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/be.json b/Emby.Server.Implementations/Localization/Ratings/be.json
new file mode 100644
index 000000000..18ea2c260
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/be.json
@@ -0,0 +1,55 @@
+{
+ "countryCode": "be",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["AL", "KT", "TOUS"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6", "MG6"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["9"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12", "KNT"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/br.csv b/Emby.Server.Implementations/Localization/Ratings/br.csv
deleted file mode 100644
index f6053c88c..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/br.csv
+++ /dev/null
@@ -1,14 +0,0 @@
-Livre,0
-L,0
-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/br.json b/Emby.Server.Implementations/Localization/Ratings/br.json
new file mode 100644
index 000000000..f455b6643
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/br.json
@@ -0,0 +1,55 @@
+{
+ "countryCode": "br",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["L", "AL", "Livre"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["9"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["10", "A10", "ER"],
+ "ratingScore": {
+ "score": 10,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12", "A12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["14", "A14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16", "A16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18", "A18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.csv b/Emby.Server.Implementations/Localization/Ratings/ca.csv
deleted file mode 100644
index 41dbda134..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/ca.csv
+++ /dev/null
@@ -1,18 +0,0 @@
-E,0
-G,0
-TV-Y,0
-TV-G,0
-TV-Y7,7
-TV-Y7-FV,7
-PG,9
-TV-PG,9
-TV-14,14
-14A,14
-16+,16
-NC-17,17
-R,18
-TV-MA,18
-18A,18
-18+,18
-A,1000
-Prohibited,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.json b/Emby.Server.Implementations/Localization/Ratings/ca.json
new file mode 100644
index 000000000..fa43a8f2b
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/ca.json
@@ -0,0 +1,90 @@
+{
+ "countryCode": "ca",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["E", "G", "TV-Y", "TV-G"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-Y7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-Y7-FV"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["PG", "TV-PG"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["14A"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["16+"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["NC-17"],
+ "ratingScore": {
+ "score": 17,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["18A"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["18+", "TV-MA", "R"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["A"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["Prohibited"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": 0
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/cl.json b/Emby.Server.Implementations/Localization/Ratings/cl.json
new file mode 100644
index 000000000..086619471
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/cl.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "cl",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["TE"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["TE+7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18", "18V", "18S"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/co.csv b/Emby.Server.Implementations/Localization/Ratings/co.csv
deleted file mode 100644
index e1e96c590..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/co.csv
+++ /dev/null
@@ -1,7 +0,0 @@
-T,0
-7,7
-12,12
-15,15
-18,18
-X,1000
-Prohibited,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/co.json b/Emby.Server.Implementations/Localization/Ratings/co.json
new file mode 100644
index 000000000..4eff6dcc5
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/co.json
@@ -0,0 +1,55 @@
+{
+ "countryCode": "co",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["T"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["X"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["Prohibited"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/de.csv b/Emby.Server.Implementations/Localization/Ratings/de.csv
deleted file mode 100644
index f6181575e..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/de.csv
+++ /dev/null
@@ -1,17 +0,0 @@
-Educational,0
-Infoprogramm,0
-FSK-0,0
-FSK 0,0
-0,0
-FSK-6,6
-FSK 6,6
-6,6
-FSK-12,12
-FSK 12,12
-12,12
-FSK-16,16
-FSK 16,16
-16,16
-FSK-18,18
-FSK 18,18
-18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/de.json b/Emby.Server.Implementations/Localization/Ratings/de.json
new file mode 100644
index 000000000..30c34b230
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/de.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "de",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["0", "FSK 0", "FSK-0", "Educational", "Infoprogramm"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6", "FSK 6", "FSK-6"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12", "FSK 12", "FSK-12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16", "FSK 16", "FSK-16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18", "FSK 18", "FSK-18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/dk.csv b/Emby.Server.Implementations/Localization/Ratings/dk.csv
deleted file mode 100644
index 4ef63b2ea..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/dk.csv
+++ /dev/null
@@ -1,7 +0,0 @@
-F,0
-A,0
-7,7
-11,11
-12,12
-15,15
-16,16
diff --git a/Emby.Server.Implementations/Localization/Ratings/dk.json b/Emby.Server.Implementations/Localization/Ratings/dk.json
new file mode 100644
index 000000000..9fcd6d44f
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/dk.json
@@ -0,0 +1,48 @@
+{
+ "countryCode": "dk",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["F", "A"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["11"],
+ "ratingScore": {
+ "score": 11,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv
deleted file mode 100644
index ee5866090..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/es.csv
+++ /dev/null
@@ -1,25 +0,0 @@
-A,0
-A/fig,0
-A/i,0
-A/i/fig,0
-APTA,0
-ERI,0
-TP,0
-0+,0
-6+,6
-7/fig,7
-7/i,7
-7/i/fig,7
-7,7
-9+,9
-10,10
-12,12
-12/fig,12
-13,13
-14,14
-16,16
-16/fig,16
-18,18
-18/fig,18
-X,1000
-Banned,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/es.json b/Emby.Server.Implementations/Localization/Ratings/es.json
new file mode 100644
index 000000000..c19629939
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/es.json
@@ -0,0 +1,90 @@
+{
+ "countryCode": "es",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["0+", "A", "A/i", "A/fig", "A/i/fig", "APTA", "ERI", "TP"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6+"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7", "7/i", "7/fig", "7/i/fig"],
+ "ratingScore": {
+ "score": 11,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["9+"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["10"],
+ "ratingScore": {
+ "score": 10,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12", "12/fig"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["13"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16", "16/fig"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18", "18/fig"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["X"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["Banned"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.csv b/Emby.Server.Implementations/Localization/Ratings/fi.csv
deleted file mode 100644
index 7ff92f259..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/fi.csv
+++ /dev/null
@@ -1,10 +0,0 @@
-S,0
-T,0
-K7,7
-7,7
-K12,12
-12,12
-K16,16
-16,16
-K18,18
-18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.json b/Emby.Server.Implementations/Localization/Ratings/fi.json
new file mode 100644
index 000000000..3152317b5
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/fi.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "fi",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["S", "T"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7", "K7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12", "K12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16", "K16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18", "K18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.csv b/Emby.Server.Implementations/Localization/Ratings/fr.csv
deleted file mode 100644
index 139ea376b..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/fr.csv
+++ /dev/null
@@ -1,13 +0,0 @@
-Public Averti,0
-Tous Publics,0
-TP,0
-U,0
-0+,0
-6+,6
-9+,9
-10,10
-12,12
-14+,14
-16,16
-18,18
-X,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.json b/Emby.Server.Implementations/Localization/Ratings/fr.json
new file mode 100644
index 000000000..e8bafd6b8
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/fr.json
@@ -0,0 +1,69 @@
+{
+ "countryCode": "fr",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["0+", "Public Averti", "Tous Publics", "TP", "U"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6+"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["9+"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["10"],
+ "ratingScore": {
+ "score": 10,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["14+"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["X"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.csv b/Emby.Server.Implementations/Localization/Ratings/gb.csv
deleted file mode 100644
index 858b9a32d..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/gb.csv
+++ /dev/null
@@ -1,23 +0,0 @@
-All,0
-E,0
-G,0
-U,0
-0+,0
-6+,6
-7+,7
-PG,8
-9,9
-12,12
-12+,12
-12A,12
-12PG,12
-Teen,13
-13+,13
-14+,14
-15,15
-16,16
-Caution,18
-18,18
-Mature,1000
-Adult,1000
-R18,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.json b/Emby.Server.Implementations/Localization/Ratings/gb.json
new file mode 100644
index 000000000..7fc88272c
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/gb.json
@@ -0,0 +1,97 @@
+{
+ "countryCode": "gb",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["0+", "All", "E", "G", "U"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["6+"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["7+"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["PG"],
+ "ratingScore": {
+ "score": 8,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["9"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["12A", "12PG"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["12", "12+"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["13+", "Teen"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["14+"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 3
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["18", "Caution"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["Mature", "Adult", "R18"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": 0
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.csv b/Emby.Server.Implementations/Localization/Ratings/ie.csv
deleted file mode 100644
index d3c634fc9..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/ie.csv
+++ /dev/null
@@ -1,10 +0,0 @@
-G,4
-PG,12
-12,12
-12A,12
-12PG,12
-15,15
-15PG,15
-15A,15
-16,16
-18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.json b/Emby.Server.Implementations/Localization/Ratings/ie.json
new file mode 100644
index 000000000..f6cc56ed6
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/ie.json
@@ -0,0 +1,55 @@
+{
+ "countryCode": "ie",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["G"],
+ "ratingScore": {
+ "score": 4,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["12A", "12PG", "PG"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["15A", "15PG"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 3
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 1
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/jp.csv b/Emby.Server.Implementations/Localization/Ratings/jp.csv
deleted file mode 100644
index bfb5fdaae..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/jp.csv
+++ /dev/null
@@ -1,11 +0,0 @@
-A,0
-G,0
-B,12
-PG12,12
-C,15
-15+,15
-R15+,15
-16+,16
-D,17
-Z,18
-18+,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/jp.json b/Emby.Server.Implementations/Localization/Ratings/jp.json
new file mode 100644
index 000000000..efff9e92c
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/jp.json
@@ -0,0 +1,62 @@
+{
+ "countryCode": "jp",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["A", "G"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["PG12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["B"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["15A", "15PG"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["C", "15+", "R15+"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["16+"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["D"],
+ "ratingScore": {
+ "score": 17,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18+", "Z"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.csv b/Emby.Server.Implementations/Localization/Ratings/kz.csv
deleted file mode 100644
index e26b32b67..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/kz.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-K,0
-БА,12
-Б14,14
-E16,16
-E18,18
-HA,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.json b/Emby.Server.Implementations/Localization/Ratings/kz.json
new file mode 100644
index 000000000..0f8f0c68e
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/kz.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "kz",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["K"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["БА"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["Б14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["E16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["E18", "HA"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/mx.csv b/Emby.Server.Implementations/Localization/Ratings/mx.csv
deleted file mode 100644
index 305912f23..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/mx.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-A,0
-AA,0
-B,12
-B-15,15
-C,18
-D,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/mx.json b/Emby.Server.Implementations/Localization/Ratings/mx.json
new file mode 100644
index 000000000..9dc3b89bd
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/mx.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "mx",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["A", "AA"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["B"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["B-15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["C"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["D"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/nl.csv b/Emby.Server.Implementations/Localization/Ratings/nl.csv
deleted file mode 100644
index 44f372b2d..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/nl.csv
+++ /dev/null
@@ -1,8 +0,0 @@
-AL,0
-MG6,6
-6,6
-9,9
-12,12
-14,14
-16,16
-18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/nl.json b/Emby.Server.Implementations/Localization/Ratings/nl.json
new file mode 100644
index 000000000..2e43eb83a
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/nl.json
@@ -0,0 +1,55 @@
+{
+ "countryCode": "nl",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["AL"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6", "MG6"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["9"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/no.csv b/Emby.Server.Implementations/Localization/Ratings/no.csv
deleted file mode 100644
index 6856a2dbb..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/no.csv
+++ /dev/null
@@ -1,10 +0,0 @@
-A,0
-6,6
-7,7
-9,9
-11,11
-12,12
-15,15
-18,18
-C,18
-Not approved,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/no.json b/Emby.Server.Implementations/Localization/Ratings/no.json
new file mode 100644
index 000000000..a5e952316
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/no.json
@@ -0,0 +1,69 @@
+{
+ "countryCode": "no",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["A"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["9"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["11"],
+ "ratingScore": {
+ "score": 11,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["Not approved"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.csv b/Emby.Server.Implementations/Localization/Ratings/nz.csv
deleted file mode 100644
index 633da78fe..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/nz.csv
+++ /dev/null
@@ -1,16 +0,0 @@
-Exempt,0
-G,0
-GY,13
-PG,13
-R13,13
-RP13,13
-R15,15
-M,16
-R16,16
-RP16,16
-GA,18
-R18,18
-RP18,18
-MA,1000
-R,1001
-Objectionable,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.json b/Emby.Server.Implementations/Localization/Ratings/nz.json
new file mode 100644
index 000000000..3c1332271
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/nz.json
@@ -0,0 +1,69 @@
+{
+ "countryCode": "nz",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["Exempt", "G"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["RP13", "PG"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["GY", "R13"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["RP16", "M"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["R16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["RP18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["R18", "GA"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["MA"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["Objectionable", "R"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": 0
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.csv b/Emby.Server.Implementations/Localization/Ratings/ro.csv
deleted file mode 100644
index 44c23e248..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/ro.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-AG,0
-AP-12,12
-N-15,15
-IM-18,18
-IM-18-XXX,1000
-IC,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.json b/Emby.Server.Implementations/Localization/Ratings/ro.json
new file mode 100644
index 000000000..9cf735a54
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/ro.json
@@ -0,0 +1,48 @@
+{
+ "countryCode": "ro",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["AG"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["AP-12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["N-15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["IM-18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["IM-18-XXX"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["IC"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.csv b/Emby.Server.Implementations/Localization/Ratings/ru.csv
deleted file mode 100644
index 8b264070b..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/ru.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-0+,0
-6+,6
-12+,12
-16+,16
-18+,18
-Refused classification,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.json b/Emby.Server.Implementations/Localization/Ratings/ru.json
new file mode 100644
index 000000000..d1b8b13aa
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/ru.json
@@ -0,0 +1,48 @@
+{
+ "countryCode": "ru",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["0+"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["6+"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12+"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["16+"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18+"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["Refused classification"],
+ "ratingScore": {
+ "score": 1001,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/se.csv b/Emby.Server.Implementations/Localization/Ratings/se.csv
deleted file mode 100644
index e129c3561..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/se.csv
+++ /dev/null
@@ -1,10 +0,0 @@
-Alla,0
-Barntillåten,0
-Btl,0
-0+,0
-7,7
-9+,9
-10+,10
-11,11
-14,14
-15,15
diff --git a/Emby.Server.Implementations/Localization/Ratings/se.json b/Emby.Server.Implementations/Localization/Ratings/se.json
new file mode 100644
index 000000000..70084995d
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/se.json
@@ -0,0 +1,55 @@
+{
+ "countryCode": "se",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["0+", "Alla", "Barntillåten", "Btl"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["9+"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["10+"],
+ "ratingScore": {
+ "score": 10,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["11"],
+ "ratingScore": {
+ "score": 11,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/sk.csv b/Emby.Server.Implementations/Localization/Ratings/sk.csv
deleted file mode 100644
index dbafd8efa..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/sk.csv
+++ /dev/null
@@ -1,6 +0,0 @@
-NR,0
-U,0
-7,7
-12,12
-15,15
-18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/sk.json b/Emby.Server.Implementations/Localization/Ratings/sk.json
new file mode 100644
index 000000000..5ec6111ec
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/sk.json
@@ -0,0 +1,41 @@
+{
+ "countryCode": "sk",
+ "supportsSubScores": false,
+ "ratings": [
+ {
+ "ratingStrings": ["U", "NR"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["12"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": null
+ }
+ },
+ {
+ "ratingStrings": ["18"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": null
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/uk.csv b/Emby.Server.Implementations/Localization/Ratings/uk.csv
deleted file mode 100644
index 75b1c2058..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/uk.csv
+++ /dev/null
@@ -1,22 +0,0 @@
-All,0
-E,0
-G,0
-U,0
-0+,0
-6+,6
-7+,7
-PG,8
-9+,9
-12,12
-12+,12
-12A,12
-Teen,13
-13+,13
-14+,14
-15,15
-16,16
-Caution,18
-18,18
-Mature,1000
-Adult,1000
-R18,1000
diff --git a/Emby.Server.Implementations/Localization/Ratings/uk.json b/Emby.Server.Implementations/Localization/Ratings/uk.json
new file mode 100644
index 000000000..7fc88272c
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/uk.json
@@ -0,0 +1,97 @@
+{
+ "countryCode": "gb",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["0+", "All", "E", "G", "U"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["6+"],
+ "ratingScore": {
+ "score": 6,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["7+"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["PG"],
+ "ratingScore": {
+ "score": 8,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["9"],
+ "ratingScore": {
+ "score": 9,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["12A", "12PG"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["12", "12+"],
+ "ratingScore": {
+ "score": 12,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["13+", "Teen"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["14+"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["15"],
+ "ratingScore": {
+ "score": 15,
+ "subScore": 3
+ }
+ },
+ {
+ "ratingStrings": ["16"],
+ "ratingScore": {
+ "score": 16,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["18", "Caution"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["Mature", "Adult", "R18"],
+ "ratingScore": {
+ "score": 1000,
+ "subScore": 0
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Localization/Ratings/us.csv b/Emby.Server.Implementations/Localization/Ratings/us.csv
deleted file mode 100644
index 9aa5c00eb..000000000
--- a/Emby.Server.Implementations/Localization/Ratings/us.csv
+++ /dev/null
@@ -1,52 +0,0 @@
-Approved,0
-G,0
-TV-G,0
-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-14,14
-TV-14-D,14
-TV-14-L,14
-TV-14-S,14
-TV-14-V,14
-TV-14-DL,14
-TV-14-DS,14
-TV-14-DV,14
-TV-14-LS,14
-TV-14-LV,14
-TV-14-SV,14
-TV-14-DLS,14
-TV-14-DLV,14
-TV-14-DSV,14
-TV-14-LSV,14
-TV-14-DLSV,14
-NC-17,17
-R,17
-TV-MA,17
-TV-MA-L,17
-TV-MA-S,17
-TV-MA-V,17
-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/Localization/Ratings/us.json b/Emby.Server.Implementations/Localization/Ratings/us.json
new file mode 100644
index 000000000..08a637312
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/us.json
@@ -0,0 +1,83 @@
+{
+ "countryCode": "us",
+ "supportsSubScores": true,
+ "ratings": [
+ {
+ "ratingStrings": ["Approved", "G", "TV-G", "TV-Y"],
+ "ratingScore": {
+ "score": 0,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-Y7"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-Y7-FV"],
+ "ratingScore": {
+ "score": 7,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["PG", "TV-PG"],
+ "ratingScore": {
+ "score": 10,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-PG-D", "TV-PG-L", "TV-PG-S", "TV-PG-V", "TV-PG-DL", "TV-PG-DS", "TV-PG-DV", "TV-PG-LS", "TV-PG-LV", "TV-PG-SV", "TV-PG-DLS", "TV-PG-DLV", "TV-PG-DSV", "TV-PG-LSV", "TV-PG-DLSV"],
+ "ratingScore": {
+ "score": 10,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["PG-13"],
+ "ratingScore": {
+ "score": 13,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-14"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["TV-14-D", "TV-14-L", "TV-14-S", "TV-14-V", "TV-14-DL", "TV-14-DS", "TV-14-DV", "TV-14-LS", "TV-14-LV", "TV-14-SV", "TV-14-DLS", "TV-14-DLV", "TV-14-DSV", "TV-14-LSV", "TV-14-DLSV"],
+ "ratingScore": {
+ "score": 14,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["R"],
+ "ratingScore": {
+ "score": 17,
+ "subScore": 0
+ }
+ },
+ {
+ "ratingStrings": ["NC-17", "TV-MA", "TV-MA-L", "TV-MA-S", "TV-MA-V", "TV-MA-LS", "TV-MA-LV", "TV-MA-SV", "TV-MA-LSV"],
+ "ratingScore": {
+ "score": 17,
+ "subScore": 1
+ }
+ },
+ {
+ "ratingStrings": ["TV-X", "TV-AO"],
+ "ratingScore": {
+ "score": 18,
+ "subScore": 0
+ }
+ }
+ ]
+}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 7b0a16441..98a43b6c9 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -283,6 +283,16 @@ namespace Emby.Server.Implementations.Playlists
RefreshPriority.High);
}
+ internal static int DetermineAdjustedIndex(int newPriorIndexAllChildren, int newIndex)
+ {
+ if (newIndex == 0)
+ {
+ return newPriorIndexAllChildren > 0 ? newPriorIndexAllChildren - 1 : 0;
+ }
+
+ return newPriorIndexAllChildren + 1;
+ }
+
public async Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId)
{
if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
@@ -305,7 +315,7 @@ namespace Emby.Server.Implementations.Playlists
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 adjustedNewIndex = DetermineAdjustedIndex(newPriorItemIndexOnAllChildren, newIndex);
var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
if (item is null)
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 42f7deca1..924f50286 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -64,6 +64,9 @@ namespace Emby.Server.Implementations.Session
private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
= new(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, string>> _activeLiveStreamSessions
+ = new(StringComparer.OrdinalIgnoreCase);
+
private Timer _idleTimer;
private Timer _inactiveTimer;
@@ -311,7 +314,7 @@ namespace Emby.Server.Implementations.Session
_activeConnections.TryRemove(key, out _);
if (!string.IsNullOrEmpty(session.PlayState?.LiveStreamId))
{
- await _mediaSourceManager.CloseLiveStream(session.PlayState.LiveStreamId).ConfigureAwait(false);
+ await CloseLiveStreamIfNeededAsync(session.PlayState.LiveStreamId, session.Id).ConfigureAwait(false);
}
await OnSessionEnded(session).ConfigureAwait(false);
@@ -319,6 +322,42 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
+ public async Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId)
+ {
+ bool liveStreamNeedsToBeClosed = false;
+
+ if (_activeLiveStreamSessions.TryGetValue(liveStreamId, out var activeSessionMappings))
+ {
+ if (activeSessionMappings.TryRemove(sessionIdOrPlaySessionId, out var correspondingId))
+ {
+ if (!string.IsNullOrEmpty(correspondingId))
+ {
+ activeSessionMappings.TryRemove(correspondingId, out _);
+ }
+
+ liveStreamNeedsToBeClosed = true;
+ }
+
+ if (activeSessionMappings.IsEmpty)
+ {
+ _activeLiveStreamSessions.TryRemove(liveStreamId, out _);
+ }
+ }
+
+ if (liveStreamNeedsToBeClosed)
+ {
+ try
+ {
+ await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error closing live stream");
+ }
+ }
+ }
+
+ /// <inheritdoc />
public async ValueTask ReportSessionEnded(string sessionId)
{
CheckDisposed();
@@ -737,6 +776,11 @@ namespace Emby.Server.Implementations.Session
}
}
+ if (!string.IsNullOrEmpty(info.LiveStreamId))
+ {
+ UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
+ }
+
var eventArgs = new PlaybackStartEventArgs
{
Item = libraryItem,
@@ -794,6 +838,32 @@ namespace Emby.Server.Implementations.Session
return OnPlaybackProgress(info, false);
}
+ private void UpdateLiveStreamActiveSessionMappings(string liveStreamId, string sessionId, string playSessionId)
+ {
+ var activeSessionMappings = _activeLiveStreamSessions.GetOrAdd(liveStreamId, _ => new ConcurrentDictionary<string, string>());
+
+ if (!string.IsNullOrEmpty(playSessionId))
+ {
+ if (!activeSessionMappings.TryGetValue(sessionId, out var currentPlaySessionId) || currentPlaySessionId != playSessionId)
+ {
+ if (!string.IsNullOrEmpty(currentPlaySessionId))
+ {
+ activeSessionMappings.TryRemove(currentPlaySessionId, out _);
+ }
+
+ activeSessionMappings[sessionId] = playSessionId;
+ activeSessionMappings[playSessionId] = sessionId;
+ }
+ }
+ else
+ {
+ if (!activeSessionMappings.TryGetValue(sessionId, out _))
+ {
+ activeSessionMappings[sessionId] = string.Empty;
+ }
+ }
+ }
+
/// <summary>
/// Used to report playback progress for an item.
/// </summary>
@@ -834,6 +904,11 @@ namespace Emby.Server.Implementations.Session
}
}
+ if (!string.IsNullOrEmpty(info.LiveStreamId))
+ {
+ UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
+ }
+
var eventArgs = new PlaybackProgressEventArgs
{
Item = libraryItem,
@@ -1016,14 +1091,7 @@ namespace Emby.Server.Implementations.Session
if (!string.IsNullOrEmpty(info.LiveStreamId))
{
- try
- {
- await _mediaSourceManager.CloseLiveStream(info.LiveStreamId).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error closing live stream");
- }
+ await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
}
var eventArgs = new PlaybackStopEventArgs
@@ -1740,7 +1808,6 @@ namespace Emby.Server.Implementations.Session
fields.Remove(ItemFields.DateLastSaved);
fields.Remove(ItemFields.DisplayPreferencesId);
fields.Remove(ItemFields.Etag);
- fields.Remove(ItemFields.InheritedParentalRatingValue);
fields.Remove(ItemFields.ItemCounts);
fields.Remove(ItemFields.MediaSourceCount);
fields.Remove(ItemFields.MediaStreams);
@@ -2071,6 +2138,7 @@ namespace Emby.Server.Implementations.Session
}
_activeConnections.Clear();
+ _activeLiveStreamSessions.Clear();
}
}
}
diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
index b4ee2c723..789af01cc 100644
--- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
@@ -1,45 +1,54 @@
-#pragma warning disable CS1591
-
using System;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Querying;
-namespace Emby.Server.Implementations.Sorting
+namespace Emby.Server.Implementations.Sorting;
+
+/// <summary>
+/// Class providing comparison for official ratings.
+/// </summary>
+public class OfficialRatingComparer : IBaseItemComparer
{
- public class OfficialRatingComparer : IBaseItemComparer
+ private readonly ILocalizationManager _localizationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OfficialRatingComparer"/> class.
+ /// </summary>
+ /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ public OfficialRatingComparer(ILocalizationManager localizationManager)
{
- private readonly ILocalizationManager _localization;
+ _localizationManager = localizationManager;
+ }
- public OfficialRatingComparer(ILocalizationManager localization)
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public ItemSortBy Type => ItemSortBy.OfficialRating;
+
+ /// <summary>
+ /// Compares the specified x.
+ /// </summary>
+ /// <param name="x">The x.</param>
+ /// <param name="y">The y.</param>
+ /// <returns>System.Int32.</returns>
+ public int Compare(BaseItem? x, BaseItem? y)
+ {
+ ArgumentNullException.ThrowIfNull(x);
+ ArgumentNullException.ThrowIfNull(y);
+ var zeroRating = new ParentalRatingScore(0, 0);
+
+ var ratingX = string.IsNullOrEmpty(x.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(x.OfficialRating) ?? zeroRating;
+ var ratingY = string.IsNullOrEmpty(y.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(y.OfficialRating) ?? zeroRating;
+ var scoreCompare = ratingX.Score.CompareTo(ratingY.Score);
+ if (scoreCompare is 0)
{
- _localization = localization;
+ return (ratingX.SubScore ?? 0).CompareTo(ratingY.SubScore ?? 0);
}
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public ItemSortBy Type => ItemSortBy.OfficialRating;
-
- /// <summary>
- /// Compares the specified x.
- /// </summary>
- /// <param name="x">The x.</param>
- /// <param name="y">The y.</param>
- /// <returns>System.Int32.</returns>
- public int Compare(BaseItem? x, BaseItem? y)
- {
- ArgumentNullException.ThrowIfNull(x);
-
- ArgumentNullException.ThrowIfNull(y);
-
- var levelX = string.IsNullOrEmpty(x.OfficialRating) ? 0 : _localization.GetRatingLevel(x.OfficialRating) ?? 0;
- var levelY = string.IsNullOrEmpty(y.OfficialRating) ? 0 : _localization.GetRatingLevel(y.OfficialRating) ?? 0;
-
- return levelX.CompareTo(levelY);
- }
+ return scoreCompare;
}
}
diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs
index c4552474c..5936df7f1 100644
--- a/Emby.Server.Implementations/SystemManager.cs
+++ b/Emby.Server.Implementations/SystemManager.cs
@@ -53,6 +53,7 @@ public class SystemManager : ISystemManager
HasPendingRestart = _applicationHost.HasPendingRestart,
IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested,
Version = _applicationHost.ApplicationVersionString,
+ ProductName = _applicationHost.Name,
WebSocketPortNumber = _applicationHost.HttpPort,
CompletedInstallations = _installationManager.CompletedInstallations.ToArray(),
Id = _applicationHost.SystemId,
@@ -65,6 +66,7 @@ public class SystemManager : ISystemManager
TranscodingTempPath = _configurationManager.GetTranscodePath(),
ServerName = _applicationHost.FriendlyName,
LocalAddress = _applicationHost.GetSmartApiUrl(request),
+ StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted,
SupportsLibraryMonitor = true,
PackageName = _startupOptions.PackageName,
CastReceiverApplications = _configurationManager.Configuration.CastReceiverApplications
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 272b4034e..e334e1264 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -92,18 +92,18 @@ public class AudioController : BaseJellyfinApiController
[ProducesAudioFile]
public async Task<ActionResult> GetAudioStream(
[FromRoute, Required] Guid itemId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -114,7 +114,7 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] string? level,
+ [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -133,8 +133,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -259,18 +259,18 @@ public class AudioController : BaseJellyfinApiController
[ProducesAudioFile]
public async Task<ActionResult> GetAudioStreamByContainer(
[FromRoute, Required] Guid itemId,
- [FromRoute, Required] string container,
+ [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -281,7 +281,7 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] string? level,
+ [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -300,8 +300,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs
index 3c2c4b4db..1d948ff20 100644
--- a/Jellyfin.Api/Controllers/BrandingController.cs
+++ b/Jellyfin.Api/Controllers/BrandingController.cs
@@ -29,9 +29,18 @@ public class BrandingController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns>
[HttpGet("Configuration")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BrandingOptions> GetBrandingOptions()
+ public ActionResult<BrandingOptionsDto> GetBrandingOptions()
{
- return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
+ var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
+
+ var brandingOptionsDto = new BrandingOptionsDto
+ {
+ LoginDisclaimer = brandingOptions.LoginDisclaimer,
+ CustomCss = brandingOptions.CustomCss,
+ SplashscreenEnabled = brandingOptions.SplashscreenEnabled
+ };
+
+ return brandingOptionsDto;
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index abe8bec2d..8dcaebf6d 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -9,6 +9,7 @@ using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Branding;
using MediaBrowser.Model.Configuration;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -120,6 +121,30 @@ public class ConfigurationController : BaseJellyfinApiController
}
/// <summary>
+ /// Updates branding configuration.
+ /// </summary>
+ /// <param name="configuration">Branding configuration.</param>
+ /// <response code="204">Branding configuration updated.</response>
+ /// <returns>Update status.</returns>
+ [HttpPost("Configuration/Branding")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult UpdateBrandingConfiguration([FromBody, Required] BrandingOptionsDto configuration)
+ {
+ // Get the current branding configuration to preserve SplashscreenLocation
+ var currentBranding = (BrandingOptions)_configurationManager.GetConfiguration("branding");
+
+ // Update only the properties from BrandingOptionsDto
+ currentBranding.LoginDisclaimer = configuration.LoginDisclaimer;
+ currentBranding.CustomCss = configuration.CustomCss;
+ currentBranding.SplashscreenEnabled = configuration.SplashscreenEnabled;
+
+ _configurationManager.SaveConfiguration("branding", currentBranding);
+
+ return NoContent();
+ }
+
+ /// <summary>
/// Updates the path to the media encoder.
/// </summary>
/// <param name="mediaEncoderPath">Media encoder path form body.</param>
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index ca8ab0ef7..4cac8ed67 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -166,18 +166,18 @@ public class DynamicHlsController : BaseJellyfinApiController
[ProducesPlaylistFile]
public async Task<ActionResult> GetLiveHlsStream(
[FromRoute, Required] Guid itemId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -188,7 +188,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] string? level,
+ [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -207,8 +207,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -415,12 +415,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -431,7 +431,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] string? level,
+ [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -452,8 +452,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -591,12 +591,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -608,7 +608,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] string? level,
+ [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -627,8 +627,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -761,12 +761,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -777,7 +777,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] string? level,
+ [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -798,8 +798,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -933,12 +933,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -950,7 +950,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] string? level,
+ [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -969,8 +969,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -1106,7 +1106,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
- [FromRoute, Required] string container,
+ [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromQuery, Required] long runtimeTicks,
[FromQuery, Required] long actualSegmentLengthTicks,
[FromQuery] bool? @static,
@@ -1114,12 +1114,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -1130,7 +1130,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] string? level,
+ [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -1151,8 +1151,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -1291,7 +1291,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
- [FromRoute, Required] string container,
+ [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromQuery, Required] long runtimeTicks,
[FromQuery, Required] long actualSegmentLengthTicks,
[FromQuery] bool? @static,
@@ -1299,12 +1299,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -1316,7 +1316,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] string? level,
+ [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -1335,8 +1335,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -1419,8 +1419,9 @@ public class DynamicHlsController : BaseJellyfinApiController
TranscodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
-
+ var mediaSourceId = state.BaseRequest.MediaSourceId;
var request = new CreateMainPlaylistRequest(
+ mediaSourceId is null ? null : Guid.Parse(mediaSourceId),
state.MediaPath,
state.SegmentLength * 1000,
state.RunTimeTicks ?? 0,
@@ -1675,7 +1676,7 @@ public class DynamicHlsController : BaseJellyfinApiController
}
var audioCodec = _encodingHelper.GetAudioEncoder(state);
- var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+ var bitStreamArgs = _encodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
// opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer
var strictArgs = string.Empty;
@@ -1753,7 +1754,7 @@ public class DynamicHlsController : BaseJellyfinApiController
if (channels.HasValue
&& (channels.Value != 2
- || (state.AudioStream?.Channels != null && !useDownMixAlgorithm)))
+ || (state.AudioStream?.Channels is not null && !useDownMixAlgorithm)))
{
args += " -ac " + channels.Value;
}
@@ -1822,10 +1823,11 @@ public class DynamicHlsController : BaseJellyfinApiController
// 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;
+ var videoIsDoVi = EncodingHelper.IsDovi(state.VideoStream);
if (EncodingHelper.IsCopyCodec(codec)
- && (videoIsDoVi && clientSupportsDoVi))
+ && (videoIsDoVi && clientSupportsDoVi)
+ && !_encodingHelper.IsDoviRemoved(state))
{
if (isActualOutputVideoCodecHevc)
{
@@ -1855,7 +1857,7 @@ public class DynamicHlsController : BaseJellyfinApiController
// If h264_mp4toannexb is ever added, do not use it for live tv.
if (state.VideoStream is not null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{
- string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
+ string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state, MediaStreamType.Video);
if (!string.IsNullOrEmpty(bitStreamArgs))
{
args += " " + bitStreamArgs;
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index e7b7405ca..abda053d3 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -1727,7 +1727,8 @@ public class ImageController : BaseJellyfinApiController
[FromQuery, Range(0, 100)] int quality = 90)
{
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
- if (!brandingOptions.SplashscreenEnabled)
+ var isAdmin = User.IsInRole(Constants.UserRoles.Administrator);
+ if (!brandingOptions.SplashscreenEnabled && !isAdmin)
{
return NotFound();
}
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 803c2f1f7..a49128336 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -448,13 +448,13 @@ public class ItemsController : BaseJellyfinApiController
// Min official rating
if (!string.IsNullOrWhiteSpace(minOfficialRating))
{
- query.MinParentalRating = _localization.GetRatingLevel(minOfficialRating);
+ query.MinParentalRating = _localization.GetRatingScore(minOfficialRating);
}
// Max official rating
if (!string.IsNullOrWhiteSpace(maxOfficialRating))
{
- query.MaxParentalRating = _localization.GetRatingLevel(maxOfficialRating);
+ query.MaxParentalRating = _localization.GetRatingScore(maxOfficialRating);
}
// Artists
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 5461d12fa..10f1789ad 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -1190,7 +1190,9 @@ public class LiveTvController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesVideoFile]
- public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
+ public ActionResult GetLiveStreamFile(
+ [FromRoute, Required] string streamId,
+ [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container)
{
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
if (liveStreamInfo is null)
diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs
index f65d95c41..bbce5a9e1 100644
--- a/Jellyfin.Api/Controllers/LocalizationController.cs
+++ b/Jellyfin.Api/Controllers/LocalizationController.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
-using Jellyfin.Api.Constants;
using MediaBrowser.Common.Api;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
@@ -45,7 +44,7 @@ public class LocalizationController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the list of countries.</returns>
[HttpGet("Countries")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<CountryInfo>> GetCountries()
+ public ActionResult<IReadOnlyList<CountryInfo>> GetCountries()
{
return Ok(_localization.GetCountries());
}
@@ -57,7 +56,7 @@ public class LocalizationController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the list of parental ratings.</returns>
[HttpGet("ParentalRatings")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<ParentalRating>> GetParentalRatings()
+ public ActionResult<IReadOnlyList<ParentalRating>> GetParentalRatings()
{
return Ok(_localization.GetParentalRatings());
}
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index a5b5fde62..fd6334703 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -102,13 +102,13 @@ public class UniversalAudioController : BaseJellyfinApiController
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] Guid? userId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] int? maxAudioChannels,
[FromQuery] int? transcodingAudioChannels,
[FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] long? startTimeTicks,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? transcodingContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? transcodingContainer,
[FromQuery] MediaStreamProtocol? transcodingProtocol,
[FromQuery] int? maxAudioSampleRate,
[FromQuery] int? maxAudioBitDepth,
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 6f18c1603..97f3239bb 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -315,18 +315,18 @@ public class VideosController : BaseJellyfinApiController
[ProducesVideoFile]
public async Task<ActionResult> GetVideoStream(
[FromRoute, Required] Guid itemId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -337,7 +337,7 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] string? level,
+ [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -358,8 +358,8 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -556,18 +556,18 @@ public class VideosController : BaseJellyfinApiController
[ProducesVideoFile]
public Task<ActionResult> GetVideoStreamByContainer(
[FromRoute, Required] Guid itemId,
- [FromRoute, Required] string container,
+ [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -578,7 +578,7 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] string? level,
+ [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -599,8 +599,8 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
+ [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index ebd0288ca..a38ad379c 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -345,13 +345,15 @@ public class DynamicHlsHelper
if (videoRange == VideoRange.HDR)
{
- if (videoRangeType == VideoRangeType.HLG)
+ switch (videoRangeType)
{
- builder.Append(",VIDEO-RANGE=HLG");
- }
- else
- {
- builder.Append(",VIDEO-RANGE=PQ");
+ case VideoRangeType.HLG:
+ case VideoRangeType.DOVIWithHLG:
+ builder.Append(",VIDEO-RANGE=HLG");
+ break;
+ default:
+ builder.Append(",VIDEO-RANGE=PQ");
+ break;
}
}
}
@@ -418,36 +420,67 @@ public class DynamicHlsHelper
/// <param name="state">StreamState of the current stream.</param>
private void AppendPlaylistSupplementalCodecsField(StringBuilder builder, StreamState state)
{
- // Dolby Vision currently cannot exist when transcoding
+ // HDR dynamic metadata currently cannot exist when transcoding
if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
return;
}
- var dvProfile = state.VideoStream.DvProfile;
- var dvLevel = state.VideoStream.DvLevel;
- var dvRangeString = state.VideoStream.VideoRangeType switch
+ if (EncodingHelper.IsDovi(state.VideoStream) && !_encodingHelper.IsDoviRemoved(state))
{
- VideoRangeType.DOVIWithHDR10 => "db1p",
- VideoRangeType.DOVIWithHLG => "db4h",
- _ => string.Empty
- };
+ AppendDvString();
+ }
+ else if (EncodingHelper.IsHdr10Plus(state.VideoStream) && !_encodingHelper.IsHdr10PlusRemoved(state))
+ {
+ AppendHdr10PlusString();
+ }
- if (dvProfile is null || dvLevel is null || string.IsNullOrEmpty(dvRangeString))
+ return;
+
+ void AppendDvString()
{
- return;
+ var dvProfile = state.VideoStream.DvProfile;
+ var dvLevel = state.VideoStream.DvLevel;
+ var dvRangeString = state.VideoStream.VideoRangeType switch
+ {
+ VideoRangeType.DOVIWithHDR10 => "db1p",
+ VideoRangeType.DOVIWithHLG => "db4h",
+ VideoRangeType.DOVIWithHDR10Plus => "db1p", // The HDR10+ metadata would be removed if Dovi metadata is not removed
+ _ => string.Empty // Don't label Dovi with EL and SDR due to compatability issues, ignore invalid configurations
+ };
+
+ if (dvProfile is null || dvLevel is null || string.IsNullOrEmpty(dvRangeString))
+ {
+ return;
+ }
+
+ var dvFourCc = string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav1" : "dvh1";
+ builder.Append(",SUPPLEMENTAL-CODECS=\"")
+ .Append(dvFourCc)
+ .Append('.')
+ .Append(dvProfile.Value.ToString("D2", CultureInfo.InvariantCulture))
+ .Append('.')
+ .Append(dvLevel.Value.ToString("D2", CultureInfo.InvariantCulture))
+ .Append('/')
+ .Append(dvRangeString)
+ .Append('"');
}
- var dvFourCc = string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav1" : "dvh1";
- builder.Append(",SUPPLEMENTAL-CODECS=\"")
- .Append(dvFourCc)
- .Append('.')
- .Append(dvProfile.Value.ToString("D2", CultureInfo.InvariantCulture))
- .Append('.')
- .Append(dvLevel.Value.ToString("D2", CultureInfo.InvariantCulture))
- .Append('/')
- .Append(dvRangeString)
- .Append('"');
+ void AppendHdr10PlusString()
+ {
+ var videoCodecLevel = GetOutputVideoCodecLevel(state);
+ if (string.IsNullOrEmpty(state.ActualOutputVideoCodec) || videoCodecLevel is null)
+ {
+ return;
+ }
+
+ var videoCodecString = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
+ builder.Append(",SUPPLEMENTAL-CODECS=\"")
+ .Append(videoCodecString)
+ .Append('/')
+ .Append("cdm4")
+ .Append('"');
+ }
}
/// <summary>
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 7b493d3fa..63c9c173b 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -290,9 +290,7 @@ public class MediaInfoHelper
mediaSource.SupportsDirectPlay = false;
mediaSource.SupportsDirectStream = false;
- mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-');
- mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
- mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
+ mediaSource.TranscodingUrl = streamInfo.ToUrl(null, claimsPrincipal.GetToken(), "&allowVideoStreamCopy=false&allowAudioStreamCopy=false");
mediaSource.TranscodingContainer = streamInfo.Container;
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding)
@@ -305,7 +303,7 @@ public class MediaInfoHelper
if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream))
{
streamInfo.PlayMethod = PlayMethod.Transcode;
- mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-');
+ mediaSource.TranscodingUrl = streamInfo.ToUrl(null, claimsPrincipal.GetToken(), null);
if (!allowVideoStreamCopy)
{
diff --git a/Jellyfin.Data/Enums/VideoRangeType.cs b/Jellyfin.Data/Enums/VideoRangeType.cs
index 853c2c73d..ce232d73c 100644
--- a/Jellyfin.Data/Enums/VideoRangeType.cs
+++ b/Jellyfin.Data/Enums/VideoRangeType.cs
@@ -46,6 +46,27 @@ public enum VideoRangeType
DOVIWithSDR,
/// <summary>
+ /// Dolby Vision with Enhancment Layer (Profile 7).
+ /// </summary>
+ DOVIWithEL,
+
+ /// <summary>
+ /// Dolby Vision and HDR10+ Metadata coexists.
+ /// </summary>
+ DOVIWithHDR10Plus,
+
+ /// <summary>
+ /// Dolby Vision with Enhancment Layer (Profile 7) and HDR10+ Metadata coexists.
+ /// </summary>
+ DOVIWithELHDR10Plus,
+
+ /// <summary>
+ /// Dolby Vision with invalid configuration. e.g. Profile 8 compat id 6.
+ /// When using this range, the server would assume the video is still HDR10 after removing the Dolby Vision metadata.
+ /// </summary>
+ DOVIInvalid,
+
+ /// <summary>
/// HDR10+ video range type (10bit to 16bit).
/// </summary>
HDR10Plus
diff --git a/Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs
new file mode 100644
index 000000000..d70ac672f
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace Jellyfin.Server.Implementations.Extensions;
+
+/// <summary>
+/// Provides <see cref="Expression"/> extension methods.
+/// </summary>
+public static class ExpressionExtensions
+{
+ /// <summary>
+ /// Combines two predicates into a single predicate using a logical OR operation.
+ /// </summary>
+ /// <typeparam name="T">The predicate parameter type.</typeparam>
+ /// <param name="firstPredicate">The first predicate expression to combine.</param>
+ /// <param name="secondPredicate">The second predicate expression to combine.</param>
+ /// <returns>A new expression representing the OR combination of the input predicates.</returns>
+ public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> firstPredicate, Expression<Func<T, bool>> secondPredicate)
+ {
+ ArgumentNullException.ThrowIfNull(firstPredicate);
+ ArgumentNullException.ThrowIfNull(secondPredicate);
+
+ var invokedExpression = Expression.Invoke(secondPredicate, firstPredicate.Parameters);
+ return Expression.Lambda<Func<T, bool>>(Expression.OrElse(firstPredicate.Body, invokedExpression), firstPredicate.Parameters);
+ }
+
+ /// <summary>
+ /// Combines multiple predicates into a single predicate using a logical OR operation.
+ /// </summary>
+ /// <typeparam name="T">The predicate parameter type.</typeparam>
+ /// <param name="predicates">A collection of predicate expressions to combine.</param>
+ /// <returns>A new expression representing the OR combination of all input predicates.</returns>
+ public static Expression<Func<T, bool>> Or<T>(this IEnumerable<Expression<Func<T, bool>>> predicates)
+ {
+ ArgumentNullException.ThrowIfNull(predicates);
+
+ return predicates.Aggregate((aggregatePredicate, nextPredicate) => aggregatePredicate.Or(nextPredicate));
+ }
+
+ /// <summary>
+ /// Combines two predicates into a single predicate using a logical AND operation.
+ /// </summary>
+ /// <typeparam name="T">The predicate parameter type.</typeparam>
+ /// <param name="firstPredicate">The first predicate expression to combine.</param>
+ /// <param name="secondPredicate">The second predicate expression to combine.</param>
+ /// <returns>A new expression representing the AND combination of the input predicates.</returns>
+ public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> firstPredicate, Expression<Func<T, bool>> secondPredicate)
+ {
+ ArgumentNullException.ThrowIfNull(firstPredicate);
+ ArgumentNullException.ThrowIfNull(secondPredicate);
+
+ var invokedExpression = Expression.Invoke(secondPredicate, firstPredicate.Parameters);
+ return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(firstPredicate.Body, invokedExpression), firstPredicate.Parameters);
+ }
+
+ /// <summary>
+ /// Combines multiple predicates into a single predicate using a logical AND operation.
+ /// </summary>
+ /// <typeparam name="T">The predicate parameter type.</typeparam>
+ /// <param name="predicates">A collection of predicate expressions to combine.</param>
+ /// <returns>A new expression representing the AND combination of all input predicates.</returns>
+ public static Expression<Func<T, bool>> And<T>(this IEnumerable<Expression<Func<T, bool>>> predicates)
+ {
+ ArgumentNullException.ThrowIfNull(predicates);
+
+ return predicates.Aggregate((aggregatePredicate, nextPredicate) => aggregatePredicate.And(nextPredicate));
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 7b5b6b94d..4a6a7f8cd 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -7,9 +7,7 @@
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;
@@ -22,6 +20,7 @@ using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
+using Jellyfin.Server.Implementations.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
@@ -115,6 +114,7 @@ public sealed class BaseItemRepository
context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
+ context.KeyframeData.Where(e => e.ItemId == id).ExecuteDelete();
context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete();
context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
@@ -535,7 +535,7 @@ public sealed class BaseItemRepository
if (!localItemValueCache.TryGetValue(itemValue, out var refValue))
{
refValue = context.ItemValues
- .Where(f => f.CleanValue == GetCleanValue(itemValue.Value) && (int)f.Type == itemValue.MagicNumber)
+ .Where(f => f.Value == itemValue.Value && (int)f.Type == itemValue.MagicNumber)
.Select(e => e.ItemValueId)
.FirstOrDefault();
}
@@ -784,6 +784,7 @@ public sealed class BaseItemRepository
entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
entity.IsInMixedFolder = dto.IsInMixedFolder;
entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
+ entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
entity.CriticRating = dto.CriticRating;
entity.PresentationUniqueKey = dto.PresentationUniqueKey;
entity.OriginalTitle = dto.OriginalTitle;
@@ -1209,45 +1210,6 @@ public sealed class BaseItemRepository
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)
@@ -1302,7 +1264,7 @@ public sealed class BaseItemRepository
var firstOrdering = orderBy.FirstOrDefault();
if (firstOrdering != default)
{
- var expression = MapOrderByField(firstOrdering.OrderBy, filter);
+ var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter);
if (firstOrdering.SortOrder == SortOrder.Ascending)
{
orderedQuery = query.OrderBy(expression);
@@ -1327,7 +1289,7 @@ public sealed class BaseItemRepository
foreach (var item in orderBy.Skip(1))
{
- var expression = MapOrderByField(item.OrderBy, filter);
+ var expression = OrderMapper.MapOrderByField(item.OrderBy, filter);
if (item.SortOrder == SortOrder.Ascending)
{
orderedQuery = orderedQuery!.ThenBy(expression);
@@ -1838,61 +1800,73 @@ public sealed class BaseItemRepository
.Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
}
- if (filter.HasParentalRating ?? false)
+ Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
+ if (filter.MinParentalRating != null)
{
- if (filter.MinParentalRating.HasValue)
+ var min = filter.MinParentalRating;
+ minParentalRatingFilter = e => e.InheritedParentalRatingValue >= min.Score || e.InheritedParentalRatingValue == null;
+ if (min.SubScore != null)
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue >= filter.MinParentalRating.Value);
+ minParentalRatingFilter = minParentalRatingFilter.And(e => e.InheritedParentalRatingValue >= min.SubScore || e.InheritedParentalRatingValue == null);
}
+ }
- if (filter.MaxParentalRating.HasValue)
+ Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
+ if (filter.MaxParentalRating != null)
+ {
+ var max = filter.MaxParentalRating;
+ maxParentalRatingFilter = e => e.InheritedParentalRatingValue <= max.Score || e.InheritedParentalRatingValue == null;
+ if (max.SubScore != null)
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue < filter.MaxParentalRating.Value);
+ maxParentalRatingFilter = maxParentalRatingFilter.And(e => e.InheritedParentalRatingValue <= max.SubScore || e.InheritedParentalRatingValue == null);
}
}
- else if (filter.BlockUnratedItems.Length > 0)
+
+ if (filter.HasParentalRating ?? false)
{
- var unratedItems = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
- if (filter.MinParentalRating.HasValue)
+ if (minParentalRatingFilter != null)
{
- 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);
- }
+ baseQuery = baseQuery.Where(minParentalRatingFilter);
}
- else
+
+ if (maxParentalRatingFilter != null)
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue != null && !unratedItems.Contains(e.UnratedType));
+ baseQuery = baseQuery.Where(maxParentalRatingFilter);
}
}
- else if (filter.MinParentalRating.HasValue)
+ else if (filter.BlockUnratedItems.Length > 0)
{
- if (filter.MaxParentalRating.HasValue)
+ var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
+ Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !unratedItemTypes.Contains(e.UnratedType);
+
+ if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value && e.InheritedParentalRatingValue <= filter.MaxParentalRating.Value);
+ baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)));
+ }
+ else if (minParentalRatingFilter != null)
+ {
+ baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
+ }
+ else if (maxParentalRatingFilter != null)
+ {
+ baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
}
else
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value);
+ baseQuery = baseQuery.Where(unratedItemFilter);
}
}
- else if (filter.MaxParentalRating.HasValue)
+ else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MaxParentalRating.Value);
+ if (minParentalRatingFilter != null)
+ {
+ baseQuery = baseQuery.Where(minParentalRatingFilter);
+ }
+
+ if (maxParentalRatingFilter != null)
+ {
+ baseQuery = baseQuery.Where(maxParentalRatingFilter);
+ }
}
else if (!filter.HasParentalRating ?? false)
{
@@ -2116,7 +2090,7 @@ public sealed class BaseItemRepository
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)));
+ .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.Children!.Any(w => w.ItemId == e.Id)));
}
if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
@@ -2151,7 +2125,7 @@ public sealed class BaseItemRepository
{
baseQuery = baseQuery
.Where(e =>
- e.ParentAncestors!
+ e.Parents!
.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}\"")));
@@ -2160,7 +2134,7 @@ public sealed class BaseItemRepository
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))));
+ .Where(e => e.Parents!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))));
}
}
diff --git a/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs
new file mode 100644
index 000000000..a2267700f
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using MediaBrowser.Controller.Persistence;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.Item;
+
+/// <summary>
+/// Repository for obtaining Keyframe data.
+/// </summary>
+public class KeyframeRepository : IKeyframeRepository
+{
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="KeyframeRepository"/> class.
+ /// </summary>
+ /// <param name="dbProvider">The EFCore db factory.</param>
+ public KeyframeRepository(IDbContextFactory<JellyfinDbContext> dbProvider)
+ {
+ _dbProvider = dbProvider;
+ }
+
+ private static MediaEncoding.Keyframes.KeyframeData Map(KeyframeData entity)
+ {
+ return new MediaEncoding.Keyframes.KeyframeData(
+ entity.TotalDuration,
+ (entity.KeyframeTicks ?? []).ToList());
+ }
+
+ private KeyframeData Map(MediaEncoding.Keyframes.KeyframeData dto, Guid itemId)
+ {
+ return new()
+ {
+ ItemId = itemId,
+ TotalDuration = dto.TotalDuration,
+ KeyframeTicks = dto.KeyframeTicks.ToList()
+ };
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyList<MediaEncoding.Keyframes.KeyframeData> GetKeyframeData(Guid itemId)
+ {
+ using var context = _dbProvider.CreateDbContext();
+
+ return context.KeyframeData.AsNoTracking().Where(e => e.ItemId.Equals(itemId)).Select(e => Map(e)).ToList();
+ }
+
+ /// <inheritdoc />
+ public async Task SaveKeyframeDataAsync(Guid itemId, MediaEncoding.Keyframes.KeyframeData data, CancellationToken cancellationToken)
+ {
+ using var context = _dbProvider.CreateDbContext();
+ using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
+ await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+ await context.KeyframeData.AddAsync(Map(data, itemId), cancellationToken).ConfigureAwait(false);
+ await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+ await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
index 36c3b9e56..1be31db72 100644
--- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
@@ -140,6 +140,7 @@ public class MediaStreamRepository : IMediaStreamRepository
dto.DvBlSignalCompatibilityId = entity.DvBlSignalCompatibilityId;
dto.IsHearingImpaired = entity.IsHearingImpaired.GetValueOrDefault();
dto.Rotation = entity.Rotation;
+ dto.Hdr10PlusPresentFlag = entity.Hdr10PlusPresentFlag;
if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
{
@@ -207,7 +208,8 @@ public class MediaStreamRepository : IMediaStreamRepository
BlPresentFlag = dto.BlPresentFlag,
DvBlSignalCompatibilityId = dto.DvBlSignalCompatibilityId,
IsHearingImpaired = dto.IsHearingImpaired,
- Rotation = dto.Rotation
+ Rotation = dto.Rotation,
+ Hdr10PlusPresentFlag = dto.Hdr10PlusPresentFlag,
};
return entity;
}
diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs
new file mode 100644
index 000000000..03249b927
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using MediaBrowser.Controller.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Implementations.Item;
+
+/// <summary>
+/// Static class for methods which maps types of ordering to their respecting ordering functions.
+/// </summary>
+public static class OrderMapper
+{
+ /// <summary>
+ /// Creates Func to be executed later with a given BaseItemEntity input for sorting items on query.
+ /// </summary>
+ /// <param name="sortBy">Item property to sort by.</param>
+ /// <param name="query">Context Query.</param>
+ /// <returns>Func to be executed later for sorting query.</returns>
+ public static Expression<Func<BaseItemEntity, object?>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query)
+ {
+ 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.Equals(query.User!.Id))!.LastPlayedDate,
+ ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.PlayCount,
+ ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.IsFavorite,
+ ItemSortBy.IsFolder => e => e.IsFolder,
+ ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played,
+ ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(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 ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)),
+ 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
+ };
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 3c39e5503..3dfb14d71 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -342,7 +342,8 @@ namespace Jellyfin.Server.Implementations.Users
},
Policy = new UserPolicy
{
- MaxParentalRating = user.MaxParentalAgeRating,
+ MaxParentalRating = user.MaxParentalRatingScore,
+ MaxParentalSubRating = user.MaxParentalRatingSubScore,
EnableUserPreferenceAccess = user.EnableUserPreferenceAccess,
RemoteClientBitrateLimit = user.RemoteClientBitrateLimit ?? 0,
AuthenticationProviderId = user.AuthenticationProviderId,
@@ -668,7 +669,8 @@ namespace Jellyfin.Server.Implementations.Users
_ => policy.LoginAttemptsBeforeLockout
};
- user.MaxParentalAgeRating = policy.MaxParentalRating;
+ user.MaxParentalRatingScore = policy.MaxParentalRating;
+ user.MaxParentalRatingSubScore = policy.MaxParentalSubRating;
user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
user.AuthenticationProviderId = policy.AuthenticationProviderId;
diff --git a/Jellyfin.Server/Filters/AdditionalModelFilter.cs b/Jellyfin.Server/Filters/AdditionalModelFilter.cs
index bf38f741c..4cd0fc231 100644
--- a/Jellyfin.Server/Filters/AdditionalModelFilter.cs
+++ b/Jellyfin.Server/Filters/AdditionalModelFilter.cs
@@ -25,7 +25,7 @@ namespace Jellyfin.Server.Filters
public class AdditionalModelFilter : IDocumentFilter
{
// Array of options that should not be visible in the api spec.
- private static readonly Type[] _ignoredConfigurations = { typeof(MigrationOptions) };
+ private static readonly Type[] _ignoredConfigurations = { typeof(MigrationOptions), typeof(MediaBrowser.Model.Branding.BrandingOptions) };
private readonly IServerConfigurationManager _serverConfigurationManager;
/// <summary>
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index fd540c9c0..c3a2e1bc4 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -29,7 +29,8 @@ namespace Jellyfin.Server.Migrations
typeof(PreStartupRoutines.CreateNetworkConfiguration),
typeof(PreStartupRoutines.MigrateMusicBrainzTimeout),
typeof(PreStartupRoutines.MigrateNetworkConfiguration),
- typeof(PreStartupRoutines.MigrateEncodingOptions)
+ typeof(PreStartupRoutines.MigrateEncodingOptions),
+ typeof(PreStartupRoutines.RenameEnableGroupingIntoCollections)
};
/// <summary>
@@ -48,13 +49,15 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.RemoveDownloadImagesInAdvance),
typeof(Routines.MigrateAuthenticationDb),
typeof(Routines.FixPlaylistOwner),
- typeof(Routines.MigrateRatingLevels),
typeof(Routines.AddDefaultCastReceivers),
typeof(Routines.UpdateDefaultPluginRepository),
typeof(Routines.FixAudioData),
- typeof(Routines.MoveTrickplayFiles),
typeof(Routines.RemoveDuplicatePlaylistChildren),
typeof(Routines.MigrateLibraryDb),
+ typeof(Routines.MoveExtractedFiles),
+ typeof(Routines.MigrateRatingLevels),
+ typeof(Routines.MoveTrickplayFiles),
+ typeof(Routines.MigrateKeyframeData),
};
/// <summary>
diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs
new file mode 100644
index 000000000..0a37b35a6
--- /dev/null
+++ b/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs
@@ -0,0 +1,63 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Xml.Linq;
+using Emby.Server.Implementations;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.PreStartupRoutines;
+
+/// <inheritdoc />
+public class RenameEnableGroupingIntoCollections : IMigrationRoutine
+{
+ private readonly ServerApplicationPaths _applicationPaths;
+ private readonly ILogger<RenameEnableGroupingIntoCollections> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RenameEnableGroupingIntoCollections"/> class.
+ /// </summary>
+ /// <param name="applicationPaths">An instance of <see cref="ServerApplicationPaths"/>.</param>
+ /// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
+ public RenameEnableGroupingIntoCollections(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory)
+ {
+ _applicationPaths = applicationPaths;
+ _logger = loggerFactory.CreateLogger<RenameEnableGroupingIntoCollections>();
+ }
+
+ /// <inheritdoc />
+ public Guid Id => Guid.Parse("E73B777D-CD5C-4E71-957A-B86B3660B7CF");
+
+ /// <inheritdoc />
+ public string Name => nameof(RenameEnableGroupingIntoCollections);
+
+ /// <inheritdoc />
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc />
+ public void Perform()
+ {
+ string path = Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "system.xml");
+ if (!File.Exists(path))
+ {
+ _logger.LogWarning("Configuration file not found: {Path}", path);
+ return;
+ }
+
+ try
+ {
+ XDocument xmlDocument = XDocument.Load(path);
+ var element = xmlDocument.Descendants("EnableGroupingIntoCollections").FirstOrDefault();
+ if (element is not null)
+ {
+ element.Name = "EnableGroupingMoviesIntoCollections";
+ _logger.LogInformation("The tag <EnableGroupingIntoCollections> was successfully renamed to <EnableGroupingMoviesIntoCollections>.");
+ xmlDocument.Save(path);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "An error occurred while updating the XML file: {Message}", ex.Message);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
new file mode 100644
index 000000000..b8e69db8e
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
@@ -0,0 +1,176 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migration to move extracted files to the new directories.
+/// </summary>
+public class MigrateKeyframeData : IDatabaseMigrationRoutine
+{
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<MoveTrickplayFiles> _logger;
+ private readonly IApplicationPaths _appPaths;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MigrateKeyframeData"/> class.
+ /// </summary>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
+ /// <param name="dbProvider">The EFCore db factory.</param>
+ public MigrateKeyframeData(
+ ILibraryManager libraryManager,
+ ILogger<MoveTrickplayFiles> logger,
+ IApplicationPaths appPaths,
+ IDbContextFactory<JellyfinDbContext> dbProvider)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _appPaths = appPaths;
+ _dbProvider = dbProvider;
+ }
+
+ private string KeyframeCachePath => Path.Combine(_appPaths.DataPath, "keyframes");
+
+ /// <inheritdoc />
+ public Guid Id => new("EA4bCAE1-09A4-428E-9B90-4B4FD2EA1B24");
+
+ /// <inheritdoc />
+ public string Name => "MigrateKeyframeData";
+
+ /// <inheritdoc />
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc />
+ public void Perform()
+ {
+ const int Limit = 100;
+ int itemCount = 0, offset = 0, previousCount;
+
+ var sw = Stopwatch.StartNew();
+ var itemsQuery = new InternalItemsQuery
+ {
+ MediaTypes = [MediaType.Video],
+ SourceTypes = [SourceType.Library],
+ IsVirtualItem = false,
+ IsFolder = false
+ };
+
+ using var context = _dbProvider.CreateDbContext();
+ context.KeyframeData.ExecuteDelete();
+ using var transaction = context.Database.BeginTransaction();
+ List<KeyframeData> keyframes = [];
+
+ do
+ {
+ var result = _libraryManager.GetItemsResult(itemsQuery);
+ _logger.LogInformation("Importing keyframes for {Count} items", result.TotalRecordCount);
+
+ var items = result.Items;
+ previousCount = items.Count;
+ offset += Limit;
+ foreach (var item in items)
+ {
+ if (TryGetKeyframeData(item, out var data))
+ {
+ keyframes.Add(data);
+ }
+
+ if (++itemCount % 10_000 == 0)
+ {
+ context.KeyframeData.AddRange(keyframes);
+ keyframes.Clear();
+ _logger.LogInformation("Imported keyframes for {Count} items in {Time}", itemCount, sw.Elapsed);
+ }
+ }
+ } while (previousCount == Limit);
+
+ context.KeyframeData.AddRange(keyframes);
+ context.SaveChanges();
+ transaction.Commit();
+
+ _logger.LogInformation("Imported keyframes for {Count} items in {Time}", itemCount, sw.Elapsed);
+
+ if (Directory.Exists(KeyframeCachePath))
+ {
+ Directory.Delete(KeyframeCachePath, true);
+ }
+ }
+
+ private bool TryGetKeyframeData(BaseItem item, [NotNullWhen(true)] out KeyframeData? data)
+ {
+ data = null;
+ var path = item.Path;
+ if (!string.IsNullOrEmpty(path))
+ {
+ var cachePath = GetCachePath(KeyframeCachePath, path);
+ if (TryReadFromCache(cachePath, out var keyframeData))
+ {
+ data = new()
+ {
+ ItemId = item.Id,
+ KeyframeTicks = keyframeData.KeyframeTicks.ToList(),
+ TotalDuration = keyframeData.TotalDuration
+ };
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private string? GetCachePath(string keyframeCachePath, string filePath)
+ {
+ DateTime? lastWriteTimeUtc;
+ try
+ {
+ lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath);
+ }
+ catch (IOException e)
+ {
+ _logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
+
+ return null;
+ }
+
+ ReadOnlySpan<char> filename = (filePath + "_" + lastWriteTimeUtc.Value.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5() + ".json";
+ var prefix = filename[..1];
+
+ return Path.Join(keyframeCachePath, prefix, filename);
+ }
+
+ private static bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult)
+ {
+ if (File.Exists(cachePath))
+ {
+ var bytes = File.ReadAllBytes(cachePath);
+ cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
+
+ return cachedResult is not null;
+ }
+
+ cachedResult = null;
+
+ return false;
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
index 427f04f9d..3fc9bea84 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
@@ -73,273 +73,352 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
var dataPath = _paths.DataPath;
var libraryDbPath = Path.Combine(dataPath, DbFilename);
- using var connection = new SqliteConnection($"Filename={libraryDbPath}");
- var migrationTotalTime = TimeSpan.Zero;
+ using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
- var stopwatch = new Stopwatch();
- stopwatch.Start();
+ var fullOperationTimer = new Stopwatch();
+ fullOperationTimer.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, SortName, CleanName, UnratedType FROM TypedBaseItems
- """;
- dbContext.BaseItems.ExecuteDelete();
+ using (var operation = GetPreparedDbContext("Cleanup database"))
+ {
+ operation.JellyfinDbContext.AttachmentStreamInfos.ExecuteDelete();
+ operation.JellyfinDbContext.BaseItems.ExecuteDelete();
+ operation.JellyfinDbContext.ItemValues.ExecuteDelete();
+ operation.JellyfinDbContext.UserData.ExecuteDelete();
+ operation.JellyfinDbContext.MediaStreamInfos.ExecuteDelete();
+ operation.JellyfinDbContext.Peoples.ExecuteDelete();
+ operation.JellyfinDbContext.PeopleBaseItemMap.ExecuteDelete();
+ operation.JellyfinDbContext.Chapters.ExecuteDelete();
+ operation.JellyfinDbContext.AncestorIds.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)
+ connection.Open();
+
+ var baseItemIds = new HashSet<Guid>();
+ using (var operation = GetPreparedDbContext("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, SortName, CleanName, UnratedType FROM TypedBaseItems
+ """;
+ using (new TrackedMigrationStep("Loading TypedBaseItems", _logger))
+ {
+ foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery))
+ {
+ var baseItem = GetItem(dto);
+ operation.JellyfinDbContext.BaseItems.Add(baseItem.BaseItem);
+ baseItemIds.Add(baseItem.BaseItem.Id);
+ foreach (var dataKey in baseItem.LegacyUserDataKey)
+ {
+ legacyBaseItemWithUserKeys[dataKey] = baseItem.BaseItem;
+ }
+ }
+ }
+
+ using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entries", _logger))
{
- legacyBaseItemWithUserKeys[dataKey] = baseItem.BaseItem;
+ operation.JellyfinDbContext.SaveChanges();
}
}
- _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();
+ using (var operation = GetPreparedDbContext("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)
+ """;
- _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), (Database.Implementations.Entities.ItemValue ItemValue, List<Guid> ItemIds)>();
+ using (new TrackedMigrationStep("loading ItemValues", _logger))
+ {
+ 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, []);
+ }
- // EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
- var localItems = new Dictionary<(int Type, string CleanValue), (Database.Implementations.Entities.ItemValue ItemValue, List<Guid> ItemIds)>();
+ existing.ItemIds.Add(itemId);
+ }
- 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, []);
+ foreach (var item in localItems)
+ {
+ operation.JellyfinDbContext.ItemValues.Add(item.Value.ItemValue);
+ operation.JellyfinDbContext.ItemValuesMap.AddRange(item.Value.ItemIds.Distinct().Select(f => new ItemValueMap()
+ {
+ Item = null!,
+ ItemValue = null!,
+ ItemId = f,
+ ItemValueId = item.Value.ItemValue.ItemValueId
+ }));
+ }
}
- existing.ItemIds.Add(itemId);
+ using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues entries", _logger))
+ {
+ operation.JellyfinDbContext.SaveChanges();
+ }
}
- foreach (var item in localItems)
+ using (var operation = GetPreparedDbContext("moving UserData"))
{
- dbContext.ItemValues.Add(item.Value.ItemValue);
- dbContext.ItemValuesMap.AddRange(item.Value.ItemIds.Distinct().Select(f => new ItemValueMap()
+ 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)
+ """);
+
+ using (new TrackedMigrationStep("loading UserData", _logger))
{
- Item = null!,
- ItemValue = null!,
- ItemId = f,
- ItemValueId = item.Value.ItemValue.ItemValueId
- }));
- }
+ var users = operation.JellyfinDbContext.Users.AsNoTracking().ToImmutableArray();
+ var userIdBlacklist = new HashSet<int>();
- _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();
+ foreach (var entity in queryResult)
+ {
+ var userData = GetUserData(users, entity, userIdBlacklist);
+ if (userData is null)
+ {
+ var userDataId = entity.GetString(0);
+ var internalUserId = entity.GetInt32(1);
- _logger.LogInformation("Start moving UserData.");
- var queryResult = connection.Query("""
- SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex FROM UserDatas
+ if (!userIdBlacklist.Contains(internalUserId))
+ {
+ _logger.LogError("Was not able to migrate user data with key {0} because its id {InternalId} does not match any existing user.", userDataId, internalUserId);
+ userIdBlacklist.Add(internalUserId);
+ }
- WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
- """);
+ continue;
+ }
- dbContext.UserData.ExecuteDelete();
+ 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;
+ }
- var users = dbContext.Users.AsNoTracking().ToImmutableArray();
+ userData.ItemId = refItem.Id;
+ operation.JellyfinDbContext.UserData.Add(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;
+ users.Clear();
}
- if (!legacyBaseItemWithUserKeys.TryGetValue(userData.CustomDataKey!, out var refItem))
+ legacyBaseItemWithUserKeys.Clear();
+
+ using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries", _logger))
{
- _logger.LogError("Was not able to migrate user data with key {0} because it does not reference a valid BaseItem.", entity.GetString(0));
- continue;
+ operation.JellyfinDbContext.SaveChanges();
}
-
- userData.ItemId = refItem.Id;
- dbContext.UserData.Add(userData);
}
- users.Clear();
- legacyBaseItemWithUserKeys.Clear();
- _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))
+ using (var operation = GetPreparedDbContext("moving MediaStreamInfos"))
{
- dbContext.MediaStreamInfos.Add(GetMediaStream(dto));
- }
-
- _logger.LogInformation("Try saving {0} MediaStreamInfos entries.", dbContext.MediaStreamInfos.Local.Count);
- dbContext.SaveChanges();
+ 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)
+ """;
- 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)>();
- var baseItemIds = dbContext.BaseItems.Select(b => b.Id).ToHashSet();
+ using (new TrackedMigrationStep("loading MediaStreamInfos", _logger))
+ {
+ foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
+ {
+ operation.JellyfinDbContext.MediaStreamInfos.Add(GetMediaStream(dto));
+ }
+ }
- foreach (SqliteDataReader reader in connection.Query(personsQuery))
- {
- var itemId = reader.GetGuid(0);
- if (!baseItemIds.Contains(itemId))
+ using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStreamInfos entries", _logger))
{
- _logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1));
- continue;
+ operation.JellyfinDbContext.SaveChanges();
}
+ }
+
+ using (var operation = GetPreparedDbContext("moving AttachmentStreamInfos"))
+ {
+ const string mediaAttachmentQuery =
+ """
+ SELECT ItemId, AttachmentIndex, Codec, CodecTag, Comment, filename, MIMEType
+ FROM mediaattachments
+ WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = mediaattachments.ItemId)
+ """;
- var entity = GetPerson(reader);
- if (!peopleCache.TryGetValue(entity.Name, out var personCache))
+ using (new TrackedMigrationStep("loading AttachmentStreamInfos", _logger))
{
- peopleCache[entity.Name] = personCache = (entity, []);
+ foreach (SqliteDataReader dto in connection.Query(mediaAttachmentQuery))
+ {
+ operation.JellyfinDbContext.AttachmentStreamInfos.Add(GetMediaAttachment(dto));
+ }
}
- if (reader.TryGetString(2, out var role))
+ using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AttachmentStreamInfos.Local.Count} AttachmentStreamInfos entries", _logger))
{
+ operation.JellyfinDbContext.SaveChanges();
}
+ }
+
+ using (var operation = GetPreparedDbContext("moving People"))
+ {
+ const string personsQuery =
+ """
+ SELECT ItemId, Name, Role, PersonType, SortOrder FROM People
+ WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = People.ItemId)
+ """;
- int? sortOrder = reader.IsDBNull(4) ? null : reader.GetInt32(4);
+ var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
- personCache.Items.Add(new PeopleBaseItemMap()
+ using (new TrackedMigrationStep("loading People", _logger))
{
- Item = null!,
- ItemId = itemId,
- People = null!,
- PeopleId = personCache.Person.Id,
- ListOrder = sortOrder,
- SortOrder = sortOrder,
- Role = role
- });
- }
+ foreach (SqliteDataReader reader in connection.Query(personsQuery))
+ {
+ var itemId = reader.GetGuid(0);
+ if (!baseItemIds.Contains(itemId))
+ {
+ _logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1));
+ continue;
+ }
- baseItemIds.Clear();
+ var entity = GetPerson(reader);
+ if (!peopleCache.TryGetValue(entity.Name, out var personCache))
+ {
+ peopleCache[entity.Name] = personCache = (entity, []);
+ }
- foreach (var item in peopleCache)
- {
- dbContext.Peoples.Add(item.Value.Person);
- dbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId)));
- }
+ if (reader.TryGetString(2, out var role))
+ {
+ }
- peopleCache.Clear();
+ int? sortOrder = reader.IsDBNull(4) ? null : reader.GetInt32(4);
- _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();
+ personCache.Items.Add(new PeopleBaseItemMap()
+ {
+ Item = null!,
+ ItemId = itemId,
+ People = null!,
+ PeopleId = personCache.Person.Id,
+ ListOrder = sortOrder,
+ SortOrder = sortOrder,
+ Role = role
+ });
+ }
- _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();
+ baseItemIds.Clear();
- foreach (SqliteDataReader dto in connection.Query(chapterQuery))
- {
- var chapter = GetChapter(dto);
- dbContext.Chapters.Add(chapter);
- }
+ foreach (var item in peopleCache)
+ {
+ operation.JellyfinDbContext.Peoples.Add(item.Value.Person);
+ operation.JellyfinDbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId)));
+ }
- _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();
+ peopleCache.Clear();
+ }
- _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();
+ using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries and {operation.JellyfinDbContext.PeopleBaseItemMap.Local.Count} maps", _logger))
+ {
+ operation.JellyfinDbContext.SaveChanges();
+ }
+ }
- foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
+ using (var operation = GetPreparedDbContext("moving Chapters"))
{
- var ancestorId = GetAncestorId(dto);
- dbContext.AncestorIds.Add(ancestorId);
+ const string chapterQuery =
+ """
+ SELECT ItemId,StartPositionTicks,Name,ImagePath,ImageDateModified,ChapterIndex from Chapters2
+ WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId)
+ """;
+
+ using (new TrackedMigrationStep("loading Chapters", _logger))
+ {
+ foreach (SqliteDataReader dto in connection.Query(chapterQuery))
+ {
+ var chapter = GetChapter(dto);
+ operation.JellyfinDbContext.Chapters.Add(chapter);
+ }
+ }
+
+ using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries", _logger))
+ {
+ operation.JellyfinDbContext.SaveChanges();
+ }
}
- _logger.LogInformation("Try saving {0} AncestorIds entries.", dbContext.Chapters.Local.Count);
+ using (var operation = GetPreparedDbContext("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)
+ """;
+
+ using (new TrackedMigrationStep("loading AncestorIds", _logger))
+ {
+ foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
+ {
+ var ancestorId = GetAncestorId(dto);
+ operation.JellyfinDbContext.AncestorIds.Add(ancestorId);
+ }
+ }
- dbContext.SaveChanges();
- migrationTotalTime += stopwatch.Elapsed;
- _logger.LogInformation("Saving AncestorIds took {0}.", stopwatch.Elapsed);
- stopwatch.Restart();
+ using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId entries", _logger))
+ {
+ operation.JellyfinDbContext.SaveChanges();
+ }
+ }
connection.Close();
+
_logger.LogInformation("Migration of the Library.db done.");
- _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
+ _logger.LogInformation("Migrating Library db took {0}.", fullOperationTimer.Elapsed);
SqliteConnection.ClearAllPools();
+ _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
File.Move(libraryDbPath, libraryDbPath + ".old", true);
- _logger.LogInformation("Migrating Library db took {0}.", migrationTotalTime);
-
_jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult();
}
- private UserData? GetUserData(ImmutableArray<User> users, SqliteDataReader dto)
+ private DatabaseMigrationStep GetPreparedDbContext(string operationName)
+ {
+ var dbContext = _provider.CreateDbContext();
+ dbContext.ChangeTracker.AutoDetectChangesEnabled = false;
+ dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
+ return new DatabaseMigrationStep(dbContext, operationName, _logger);
+ }
+
+ private UserData? GetUserData(ImmutableArray<User> users, SqliteDataReader dto, HashSet<int> userIdBlacklist)
{
var internalUserId = dto.GetInt32(1);
var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
if (user is null)
{
+ if (userIdBlacklist.Contains(internalUserId))
+ {
+ return null;
+ }
+
_logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId, users.Length);
return null;
}
@@ -654,6 +733,48 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
return item;
}
+ /// <summary>
+ /// Gets the attachment.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <returns>MediaAttachment.</returns>
+ private AttachmentStreamInfo GetMediaAttachment(SqliteDataReader reader)
+ {
+ var item = new AttachmentStreamInfo
+ {
+ Index = reader.GetInt32(1),
+ Item = null!,
+ ItemId = reader.GetGuid(0),
+ };
+
+ 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 (BaseItemEntity BaseItem, string[] LegacyUserDataKey) GetItem(SqliteDataReader reader)
{
var entity = new BaseItemEntity()
@@ -1214,4 +1335,58 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
return image;
}
+
+ private class TrackedMigrationStep : IDisposable
+ {
+ private readonly string _operationName;
+ private readonly ILogger _logger;
+ private readonly Stopwatch _operationTimer;
+ private bool _disposed;
+
+ public TrackedMigrationStep(string operationName, ILogger logger)
+ {
+ _operationName = operationName;
+ _logger = logger;
+ _operationTimer = Stopwatch.StartNew();
+ logger.LogInformation("Start {OperationName}", operationName);
+ }
+
+ public bool Disposed
+ {
+ get => _disposed;
+ set => _disposed = value;
+ }
+
+ public virtual void Dispose()
+ {
+ if (Disposed)
+ {
+ return;
+ }
+
+ Disposed = true;
+ _logger.LogInformation("{OperationName} took '{Time}'", _operationName, _operationTimer.Elapsed);
+ }
+ }
+
+ private sealed class DatabaseMigrationStep : TrackedMigrationStep
+ {
+ public DatabaseMigrationStep(JellyfinDbContext jellyfinDbContext, string operationName, ILogger logger) : base(operationName, logger)
+ {
+ JellyfinDbContext = jellyfinDbContext;
+ }
+
+ public JellyfinDbContext JellyfinDbContext { get; }
+
+ public override void Dispose()
+ {
+ if (Disposed)
+ {
+ return;
+ }
+
+ JellyfinDbContext.Dispose();
+ base.Dispose();
+ }
+ }
}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
index 9c2184029..c38beb723 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
@@ -1,36 +1,33 @@
using System;
-using System.Globalization;
-using System.IO;
-using Emby.Server.Implementations.Data;
-using MediaBrowser.Controller;
+using System.Linq;
+using Jellyfin.Database.Implementations;
using MediaBrowser.Model.Globalization;
-using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines
{
/// <summary>
- /// Migrate rating levels to new rating level system.
+ /// Migrate rating levels.
/// </summary>
- internal class MigrateRatingLevels : IMigrationRoutine
+ internal class MigrateRatingLevels : IDatabaseMigrationRoutine
{
- private const string DbFilename = "library.db";
private readonly ILogger<MigrateRatingLevels> _logger;
- private readonly IServerApplicationPaths _applicationPaths;
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
private readonly ILocalizationManager _localizationManager;
public MigrateRatingLevels(
- IServerApplicationPaths applicationPaths,
+ IDbContextFactory<JellyfinDbContext> provider,
ILoggerFactory loggerFactory,
ILocalizationManager localizationManager)
{
- _applicationPaths = applicationPaths;
+ _provider = provider;
_localizationManager = localizationManager;
_logger = loggerFactory.CreateLogger<MigrateRatingLevels>();
}
/// <inheritdoc/>
- public Guid Id => Guid.Parse("{73DAB92A-178B-48CD-B05B-FE18733ACDC8}");
+ public Guid Id => Guid.Parse("{98724538-EB11-40E3-931A-252C55BDDE7A}");
/// <inheritdoc/>
public string Name => "MigrateRatingLevels";
@@ -41,54 +38,37 @@ namespace Jellyfin.Server.Migrations.Routines
/// <inheritdoc/>
public void Perform()
{
- var dbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
-
- // Back up the database before modifying any entries
- for (int i = 1; ; i++)
+ _logger.LogInformation("Recalculating parental rating levels based on rating string.");
+ using var context = _provider.CreateDbContext();
+ using var transaction = context.Database.BeginTransaction();
+ var ratings = context.BaseItems.AsNoTracking().Select(e => e.OfficialRating).Distinct();
+ foreach (var rating in ratings)
{
- var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
- if (!File.Exists(bakPath))
+ if (string.IsNullOrEmpty(rating))
{
- try
- {
- File.Copy(dbPath, bakPath);
- _logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
- break;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
- throw;
- }
+ int? value = null;
+ context.BaseItems
+ .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty)
+ .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingValue, value));
+ context.BaseItems
+ .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty)
+ .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingSubValue, value));
}
- }
-
- // Migrate parental rating strings to new levels
- _logger.LogInformation("Recalculating parental rating levels based on rating string.");
- using var connection = new SqliteConnection($"Filename={dbPath}");
- connection.Open();
- using (var transaction = connection.BeginTransaction())
- {
- var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems");
- foreach (var entry in queryResult)
+ else
{
- if (!entry.TryGetString(0, out var ratingString) || string.IsNullOrEmpty(ratingString))
- {
- connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';");
- }
- else
- {
- var ratingValue = _localizationManager.GetRatingLevel(ratingString)?.ToString(CultureInfo.InvariantCulture) ?? "NULL";
-
- using var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;");
- statement.TryBind("@Value", ratingValue);
- statement.TryBind("@Rating", ratingString);
- statement.ExecuteNonQuery();
- }
+ var ratingValue = _localizationManager.GetRatingScore(rating);
+ var score = ratingValue?.Score;
+ var subScore = ratingValue?.SubScore;
+ context.BaseItems
+ .Where(e => e.OfficialRating == rating)
+ .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingValue, score));
+ context.BaseItems
+ .Where(e => e.OfficialRating == rating)
+ .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingSubValue, subScore));
}
-
- transaction.Commit();
}
+
+ transaction.Commit();
}
}
}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
index c40560660..1b5fab7a8 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
@@ -112,7 +112,8 @@ namespace Jellyfin.Server.Migrations.Routines
{
Id = entry.GetGuid(1),
InternalId = entry.GetInt64(0),
- MaxParentalAgeRating = policy.MaxParentalRating,
+ MaxParentalRatingScore = policy.MaxParentalRating,
+ MaxParentalRatingSubScore = null,
EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,
InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount,
diff --git a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs
new file mode 100644
index 000000000..f63c5fd40
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs
@@ -0,0 +1,299 @@
+#pragma warning disable CA5351 // Do Not Use Broken Cryptographic Algorithms
+
+using System;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migration to move extracted files to the new directories.
+/// </summary>
+public class MoveExtractedFiles : IDatabaseMigrationRoutine
+{
+ private readonly IApplicationPaths _appPaths;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<MoveExtractedFiles> _logger;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IPathManager _pathManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MoveExtractedFiles"/> class.
+ /// </summary>
+ /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+ /// <param name="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
+ public MoveExtractedFiles(
+ IApplicationPaths appPaths,
+ ILibraryManager libraryManager,
+ ILogger<MoveExtractedFiles> logger,
+ IMediaSourceManager mediaSourceManager,
+ IPathManager pathManager)
+ {
+ _appPaths = appPaths;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _mediaSourceManager = mediaSourceManager;
+ _pathManager = pathManager;
+ }
+
+ private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
+
+ private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
+
+ /// <inheritdoc />
+ public Guid Id => new("9063b0Ef-CFF1-4EDC-9A13-74093681A89B");
+
+ /// <inheritdoc />
+ public string Name => "MoveExtractedFiles";
+
+ /// <inheritdoc />
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc />
+ public void Perform()
+ {
+ const int Limit = 500;
+ int itemCount = 0, offset = 0;
+
+ var sw = Stopwatch.StartNew();
+ var itemsQuery = new InternalItemsQuery
+ {
+ MediaTypes = [MediaType.Video],
+ SourceTypes = [SourceType.Library],
+ IsVirtualItem = false,
+ IsFolder = false,
+ Limit = Limit,
+ StartIndex = offset,
+ EnableTotalRecordCount = true,
+ };
+
+ var records = _libraryManager.GetItemsResult(itemsQuery).TotalRecordCount;
+ _logger.LogInformation("Checking {Count} items for movable extracted files.", records);
+
+ // Make sure directories exist
+ Directory.CreateDirectory(SubtitleCachePath);
+ Directory.CreateDirectory(AttachmentCachePath);
+
+ itemsQuery.EnableTotalRecordCount = false;
+ do
+ {
+ itemsQuery.StartIndex = offset;
+ var result = _libraryManager.GetItemsResult(itemsQuery);
+
+ var items = result.Items;
+ foreach (var item in items)
+ {
+ if (MoveSubtitleAndAttachmentFiles(item))
+ {
+ itemCount++;
+ }
+ }
+
+ offset += Limit;
+ if (offset % 5_000 == 0)
+ {
+ _logger.LogInformation("Checked extracted files for {Count} items in {Time}.", offset, sw.Elapsed);
+ }
+ } while (offset < records);
+
+ _logger.LogInformation("Checked {Checked} items - Moved files for {Items} items in {Time}.", records, itemCount, sw.Elapsed);
+
+ // Get all subdirectories with 1 character names (those are the legacy directories)
+ var subdirectories = Directory.GetDirectories(SubtitleCachePath, "*", SearchOption.AllDirectories).Where(s => s.Length == SubtitleCachePath.Length + 2).ToList();
+ subdirectories.AddRange(Directory.GetDirectories(AttachmentCachePath, "*", SearchOption.AllDirectories).Where(s => s.Length == AttachmentCachePath.Length + 2));
+
+ // Remove all legacy subdirectories
+ foreach (var subdir in subdirectories)
+ {
+ Directory.Delete(subdir, true);
+ }
+
+ // Remove old cache path
+ var attachmentCachePath = Path.Join(_appPaths.CachePath, "attachments");
+ if (Directory.Exists(attachmentCachePath))
+ {
+ Directory.Delete(attachmentCachePath, true);
+ }
+
+ _logger.LogInformation("Cleaned up left over subtitles and attachments.");
+ }
+
+ private bool MoveSubtitleAndAttachmentFiles(BaseItem item)
+ {
+ var mediaStreams = item.GetMediaStreams().Where(s => s.Type == MediaStreamType.Subtitle && !s.IsExternal);
+ var itemIdString = item.Id.ToString("N", CultureInfo.InvariantCulture);
+ var modified = false;
+ foreach (var mediaStream in mediaStreams)
+ {
+ if (mediaStream.Codec is null)
+ {
+ continue;
+ }
+
+ var mediaStreamIndex = mediaStream.Index;
+ var extension = GetSubtitleExtension(mediaStream.Codec);
+ var oldSubtitleCachePath = GetOldSubtitleCachePath(item.Path, mediaStream.Index, extension);
+ if (string.IsNullOrEmpty(oldSubtitleCachePath) || !File.Exists(oldSubtitleCachePath))
+ {
+ continue;
+ }
+
+ var newSubtitleCachePath = _pathManager.GetSubtitlePath(itemIdString, mediaStreamIndex, extension);
+ if (File.Exists(newSubtitleCachePath))
+ {
+ File.Delete(oldSubtitleCachePath);
+ }
+ else
+ {
+ var newDirectory = Path.GetDirectoryName(newSubtitleCachePath);
+ if (newDirectory is not null)
+ {
+ Directory.CreateDirectory(newDirectory);
+ File.Move(oldSubtitleCachePath, newSubtitleCachePath, false);
+ _logger.LogDebug("Moved subtitle {Index} for {Item} from {Source} to {Destination}", mediaStreamIndex, item.Id, oldSubtitleCachePath, newSubtitleCachePath);
+
+ modified = true;
+ }
+ }
+ }
+
+ var attachments = _mediaSourceManager.GetMediaAttachments(item.Id).Where(a => !string.Equals(a.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)).ToList();
+ var shouldExtractOneByOne = attachments.Any(a => !string.IsNullOrEmpty(a.FileName)
+ && (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
+ foreach (var attachment in attachments)
+ {
+ var attachmentIndex = attachment.Index;
+ var oldAttachmentPath = GetOldAttachmentDataPath(item.Path, attachmentIndex);
+ if (string.IsNullOrEmpty(oldAttachmentPath) || !File.Exists(oldAttachmentPath))
+ {
+ oldAttachmentPath = GetOldAttachmentCachePath(itemIdString, attachment, shouldExtractOneByOne);
+ if (string.IsNullOrEmpty(oldAttachmentPath) || !File.Exists(oldAttachmentPath))
+ {
+ continue;
+ }
+ }
+
+ var newAttachmentPath = _pathManager.GetAttachmentPath(itemIdString, attachment.FileName ?? attachmentIndex.ToString(CultureInfo.InvariantCulture));
+ if (File.Exists(newAttachmentPath))
+ {
+ File.Delete(oldAttachmentPath);
+ }
+ else
+ {
+ var newDirectory = Path.GetDirectoryName(newAttachmentPath);
+ if (newDirectory is not null)
+ {
+ Directory.CreateDirectory(newDirectory);
+ File.Move(oldAttachmentPath, newAttachmentPath, false);
+ _logger.LogDebug("Moved attachment {Index} for {Item} from {Source} to {Destination}", attachmentIndex, item.Id, oldAttachmentPath, newAttachmentPath);
+
+ modified = true;
+ }
+ }
+ }
+
+ return modified;
+ }
+
+ private string? GetOldAttachmentDataPath(string? mediaPath, int attachmentStreamIndex)
+ {
+ if (mediaPath is null)
+ {
+ return null;
+ }
+
+ string filename;
+ var protocol = _mediaSourceManager.GetPathProtocol(mediaPath);
+ if (protocol == MediaProtocol.File)
+ {
+ DateTime? date;
+ try
+ {
+ date = File.GetLastWriteTimeUtc(mediaPath);
+ }
+ catch (IOException e)
+ {
+ _logger.LogDebug("Skipping attachment at index {Index} for {Path}: {Exception}", attachmentStreamIndex, mediaPath, e.Message);
+
+ return null;
+ }
+
+ filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Value.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
+ }
+
+ return Path.Join(_appPaths.DataPath, "attachments", filename[..1], filename);
+ }
+
+ private string? GetOldAttachmentCachePath(string mediaSourceId, MediaAttachment attachment, bool shouldExtractOneByOne)
+ {
+ var attachmentFolderPath = Path.Join(_appPaths.CachePath, "attachments", mediaSourceId);
+ if (shouldExtractOneByOne)
+ {
+ return Path.Join(attachmentFolderPath, attachment.Index.ToString(CultureInfo.InvariantCulture));
+ }
+
+ if (string.IsNullOrEmpty(attachment.FileName))
+ {
+ return null;
+ }
+
+ return Path.Join(attachmentFolderPath, attachment.FileName);
+ }
+
+ private string? GetOldSubtitleCachePath(string path, int streamIndex, string outputSubtitleExtension)
+ {
+ DateTime? date;
+ try
+ {
+ date = File.GetLastWriteTimeUtc(path);
+ }
+ catch (IOException e)
+ {
+ _logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message);
+
+ return null;
+ }
+
+ var ticksParam = string.Empty;
+ ReadOnlySpan<char> filename = new Guid(MD5.HashData(Encoding.Unicode.GetBytes(path + "_" + streamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Value.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam))) + outputSubtitleExtension;
+
+ return Path.Join(SubtitleCachePath, filename[..1], filename);
+ }
+
+ private static string GetSubtitleExtension(string codec)
+ {
+ if (codec.ToLower(CultureInfo.InvariantCulture).Equals("ass", StringComparison.OrdinalIgnoreCase)
+ || codec.ToLower(CultureInfo.InvariantCulture).Equals("ssa", StringComparison.OrdinalIgnoreCase))
+ {
+ return "." + codec;
+ }
+ else if (codec.Contains("pgs", StringComparison.OrdinalIgnoreCase))
+ {
+ return ".sup";
+ }
+ else
+ {
+ return ".srt";
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
index f4ebac377..eeb11e14c 100644
--- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
+++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
@@ -4,7 +4,6 @@ using System.Globalization;
using System.IO;
using System.Linq;
using Jellyfin.Data.Enums;
-using MediaBrowser.Common;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Trickplay;
@@ -16,7 +15,7 @@ namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Migration to move trickplay files to the new directory.
/// </summary>
-public class MoveTrickplayFiles : IMigrationRoutine
+public class MoveTrickplayFiles : IDatabaseMigrationRoutine
{
private readonly ITrickplayManager _trickplayManager;
private readonly IFileSystem _fileSystem;
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
index f84bccc25..e183a1d63 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
+++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
@@ -1,12 +1,10 @@
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;
@@ -15,16 +13,13 @@ namespace Jellyfin.Server.Migrations.Routines;
/// </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;
}
diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs
index cb638cf90..a71cdbd62 100644
--- a/MediaBrowser.Controller/Dto/DtoOptions.cs
+++ b/MediaBrowser.Controller/Dto/DtoOptions.cs
@@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Dto
EnableUserData = true;
AddCurrentProgram = true;
- Fields = allFields ? AllItemFields : Array.Empty<ItemFields>();
+ Fields = allFields ? AllItemFields : [];
ImageTypes = AllImageTypes;
}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 1dd289631..d48426672 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -23,6 +23,7 @@ using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
@@ -580,6 +581,9 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public int? InheritedParentalRatingValue { get; set; }
+ [JsonIgnore]
+ public int? InheritedParentalRatingSubValue { get; set; }
+
/// <summary>
/// Gets or sets the critic rating.
/// </summary>
@@ -1539,7 +1543,8 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- var maxAllowedRating = user.MaxParentalAgeRating;
+ var maxAllowedRating = user.MaxParentalRatingScore;
+ var maxAllowedSubRating = user.MaxParentalRatingSubScore;
var rating = CustomRatingForComparison;
if (string.IsNullOrEmpty(rating))
@@ -1553,10 +1558,10 @@ namespace MediaBrowser.Controller.Entities
return !GetBlockUnratedValue(user);
}
- var value = LocalizationManager.GetRatingLevel(rating);
+ var ratingScore = LocalizationManager.GetRatingScore(rating);
// Could not determine rating level
- if (!value.HasValue)
+ if (ratingScore is null)
{
var isAllowed = !GetBlockUnratedValue(user);
@@ -1568,10 +1573,15 @@ namespace MediaBrowser.Controller.Entities
return isAllowed;
}
- return !maxAllowedRating.HasValue || value.Value <= maxAllowedRating.Value;
+ if (maxAllowedSubRating is not null)
+ {
+ return (ratingScore.SubScore ?? 0) <= maxAllowedSubRating && ratingScore.Score <= maxAllowedRating.Value;
+ }
+
+ return !maxAllowedRating.HasValue || ratingScore.Score <= maxAllowedRating.Value;
}
- public int? GetInheritedParentalRatingValue()
+ public ParentalRatingScore GetParentalRatingScore()
{
var rating = CustomRatingForComparison;
@@ -1585,7 +1595,7 @@ namespace MediaBrowser.Controller.Entities
return null;
}
- return LocalizationManager.GetRatingLevel(rating);
+ return LocalizationManager.GetRatingScore(rating);
}
public List<string> GetInheritedTags()
@@ -1683,7 +1693,7 @@ namespace MediaBrowser.Controller.Entities
public virtual string GetClientTypeName()
{
- if (IsFolder && SourceType == SourceType.Channel && this is not Channel)
+ if (IsFolder && SourceType == SourceType.Channel && this is not Channel && this is not Season && this is not Series)
{
return "ChannelFolderItem";
}
@@ -2517,11 +2527,29 @@ namespace MediaBrowser.Controller.Entities
var item = this;
- var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? null;
- if (inheritedParentalRatingValue != item.InheritedParentalRatingValue)
+ var rating = item.GetParentalRatingScore();
+ if (rating is not null)
{
- item.InheritedParentalRatingValue = inheritedParentalRatingValue;
- updateType |= ItemUpdateType.MetadataImport;
+ if (rating.Score != item.InheritedParentalRatingValue)
+ {
+ item.InheritedParentalRatingValue = rating.Score;
+ updateType |= ItemUpdateType.MetadataImport;
+ }
+
+ if (rating.SubScore != item.InheritedParentalRatingSubValue)
+ {
+ item.InheritedParentalRatingSubValue = rating.SubScore;
+ updateType |= ItemUpdateType.MetadataImport;
+ }
+ }
+ else
+ {
+ if (item.InheritedParentalRatingValue is not null)
+ {
+ item.InheritedParentalRatingValue = null;
+ item.InheritedParentalRatingSubValue = null;
+ updateType |= ItemUpdateType.MetadataImport;
+ }
}
return updateType;
@@ -2541,8 +2569,9 @@ namespace MediaBrowser.Controller.Entities
.Select(i => i.OfficialRating)
.Where(i => !string.IsNullOrEmpty(i))
.Distinct(StringComparer.OrdinalIgnoreCase)
- .Select(rating => (rating, LocalizationManager.GetRatingLevel(rating)))
- .OrderBy(i => i.Item2 ?? 1000)
+ .Select(rating => (rating, LocalizationManager.GetRatingScore(rating)))
+ .OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score)
+ .ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore)
.Select(i => i.rating);
OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index dd85a6ec0..4da22854b 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -1064,11 +1064,6 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- if (queryParent is Series)
- {
- return false;
- }
-
if (queryParent is Season)
{
return false;
@@ -1088,12 +1083,15 @@ namespace MediaBrowser.Controller.Entities
if (!param.HasValue)
{
- if (user is not null && !configurationManager.Configuration.EnableGroupingIntoCollections)
+ if (user is not null && query.IncludeItemTypes.Any(type =>
+ (type == BaseItemKind.Movie && !configurationManager.Configuration.EnableGroupingMoviesIntoCollections) ||
+ (type == BaseItemKind.Series && !configurationManager.Configuration.EnableGroupingShowsIntoCollections)))
{
return false;
}
- if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Movie))
+ if (query.IncludeItemTypes.Length == 0
+ || query.IncludeItemTypes.Any(type => type == BaseItemKind.Movie || type == BaseItemKind.Series))
{
param = true;
}
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 5ce5fd4fa..9a83dba45 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -232,9 +232,9 @@ namespace MediaBrowser.Controller.Entities
public int? IndexNumber { get; set; }
- public int? MinParentalRating { get; set; }
+ public ParentalRatingScore? MinParentalRating { get; set; }
- public int? MaxParentalRating { get; set; }
+ public ParentalRatingScore? MaxParentalRating { get; set; }
public bool? HasDeadParentId { get; set; }
@@ -360,16 +360,17 @@ namespace MediaBrowser.Controller.Entities
public void SetUser(User user)
{
- MaxParentalRating = user.MaxParentalAgeRating;
-
- if (MaxParentalRating.HasValue)
+ var maxRating = user.MaxParentalRatingScore;
+ if (maxRating.HasValue)
{
- string other = UnratedItem.Other.ToString();
- BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
- .Where(i => i != other)
- .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
+ MaxParentalRating = new(maxRating.Value, user.MaxParentalRatingSubScore);
}
+ var other = UnratedItem.Other.ToString();
+ BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
+ .Where(i => i != other)
+ .Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
+
ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags);
IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags);
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index 1293528fb..408161b03 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
+using System.Threading;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
@@ -152,6 +153,21 @@ namespace MediaBrowser.Controller.Entities.TV
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
+ if (SourceType == SourceType.Channel)
+ {
+ try
+ {
+ query.Parent = this;
+ query.ChannelIds = new[] { ChannelId };
+ return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult();
+ }
+ catch
+ {
+ // Already logged at lower levels
+ return new QueryResult<BaseItem>();
+ }
+ }
+
if (query.User is null)
{
return base.GetItemsInternal(query);
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 5dad15851..b4ad05921 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Entities.TV
/// <summary>
/// Class Series.
/// </summary>
- public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer
+ public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer, ISupportsBoxSetGrouping
{
public Series()
{
@@ -226,6 +226,21 @@ namespace MediaBrowser.Controller.Entities.TV
{
var user = query.User;
+ if (SourceType == SourceType.Channel)
+ {
+ try
+ {
+ query.Parent = this;
+ query.ChannelIds = [ChannelId];
+ return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult();
+ }
+ catch
+ {
+ // Already logged at lower levels
+ return new QueryResult<BaseItem>();
+ }
+ }
+
if (query.Recursive)
{
var seriesKey = GetUniqueSeriesKey(this);
@@ -372,7 +387,25 @@ namespace MediaBrowser.Controller.Entities.TV
query.IsMissing = false;
}
- var allItems = LibraryManager.GetItemList(query);
+ IReadOnlyList<BaseItem> allItems;
+ if (SourceType == SourceType.Channel)
+ {
+ try
+ {
+ query.Parent = parentSeason;
+ query.ChannelIds = [ChannelId];
+ allItems = [.. ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult().Items];
+ }
+ catch
+ {
+ // Already logged at lower levels
+ return [];
+ }
+ }
+ else
+ {
+ allItems = LibraryManager.GetItemList(query);
+ }
return GetSeasonEpisodes(parentSeason, user, allItems, options, shouldIncludeMissingEpisodes);
}
diff --git a/MediaBrowser.Controller/IO/IPathManager.cs b/MediaBrowser.Controller/IO/IPathManager.cs
index 036889810..7c20164a6 100644
--- a/MediaBrowser.Controller/IO/IPathManager.cs
+++ b/MediaBrowser.Controller/IO/IPathManager.cs
@@ -1,4 +1,5 @@
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
namespace MediaBrowser.Controller.IO;
@@ -14,4 +15,35 @@ public interface IPathManager
/// <param name="saveWithMedia">Whether or not the tile should be saved next to the media file.</param>
/// <returns>The absolute path.</returns>
public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false);
+
+ /// <summary>
+ /// Gets the path to the subtitle file.
+ /// </summary>
+ /// <param name="mediaSourceId">The media source id.</param>
+ /// <param name="streamIndex">The stream index.</param>
+ /// <param name="extension">The subtitle file extension.</param>
+ /// <returns>The absolute path.</returns>
+ public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension);
+
+ /// <summary>
+ /// Gets the path to the subtitle file.
+ /// </summary>
+ /// <param name="mediaSourceId">The media source id.</param>
+ /// <returns>The absolute path.</returns>
+ public string GetSubtitleFolderPath(string mediaSourceId);
+
+ /// <summary>
+ /// Gets the path to the attachment file.
+ /// </summary>
+ /// <param name="mediaSourceId">The media source id.</param>
+ /// <param name="fileName">The attachmentFileName index.</param>
+ /// <returns>The absolute path.</returns>
+ public string GetAttachmentPath(string mediaSourceId, string fileName);
+
+ /// <summary>
+ /// Gets the path to the attachment folder.
+ /// </summary>
+ /// <param name="mediaSourceId">The media source id.</param>
+ /// <returns>The absolute path.</returns>
+ public string GetAttachmentFolderPath(string mediaSourceId);
}
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index ba4a2a59c..3353ad63f 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -18,6 +18,7 @@
</PropertyGroup>
<ItemGroup>
+ <PackageReference Include="BitFaster.Caching" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="System.Threading.Tasks.Dataflow" />
@@ -27,6 +28,7 @@
<ProjectReference Include="../Emby.Naming/Emby.Naming.csproj" />
<ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
<ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
+ <ProjectReference Include="../src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj" />
</ItemGroup>
<ItemGroup>
diff --git a/MediaBrowser.Controller/MediaEncoding/BitStreamFilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/BitStreamFilterOptionType.cs
new file mode 100644
index 000000000..41d21e440
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/BitStreamFilterOptionType.cs
@@ -0,0 +1,32 @@
+namespace MediaBrowser.Controller.MediaEncoding;
+
+/// <summary>
+/// Enum BitStreamFilterOptionType.
+/// </summary>
+public enum BitStreamFilterOptionType
+{
+ /// <summary>
+ /// hevc_metadata bsf with remove_dovi option.
+ /// </summary>
+ HevcMetadataRemoveDovi = 0,
+
+ /// <summary>
+ /// hevc_metadata bsf with remove_hdr10plus option.
+ /// </summary>
+ HevcMetadataRemoveHdr10Plus = 1,
+
+ /// <summary>
+ /// av1_metadata bsf with remove_dovi option.
+ /// </summary>
+ Av1MetadataRemoveDovi = 2,
+
+ /// <summary>
+ /// av1_metadata bsf with remove_hdr10plus option.
+ /// </summary>
+ Av1MetadataRemoveHdr10Plus = 3,
+
+ /// <summary>
+ /// dovi_rpu bsf with strip option.
+ /// </summary>
+ DoviRpuStrip = 4,
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index cf76f336c..aed7820a6 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -19,6 +19,7 @@ using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
@@ -37,7 +38,13 @@ namespace MediaBrowser.Controller.MediaEncoding
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
/// This should matches all common valid codecs.
/// </summary>
- public const string ValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
+ public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
+
+ /// <summary>
+ /// The level validation regex.
+ /// This regular expression matches strings representing a double.
+ /// </summary>
+ public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?";
private const string _defaultMjpegEncoder = "mjpeg";
@@ -55,6 +62,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly ISubtitleEncoder _subtitleEncoder;
private readonly IConfiguration _config;
private readonly IConfigurationManager _configurationManager;
+ private readonly IPathManager _pathManager;
// i915 hang was fixed by linux 6.2 (3f882f2)
private readonly Version _minKerneli915Hang = new Version(5, 18);
@@ -77,7 +85,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1);
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
- private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled);
+ private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled);
private static readonly string[] _videoProfilesH264 =
[
@@ -153,13 +161,22 @@ namespace MediaBrowser.Controller.MediaEncoding
IMediaEncoder mediaEncoder,
ISubtitleEncoder subtitleEncoder,
IConfiguration config,
- IConfigurationManager configurationManager)
+ IConfigurationManager configurationManager,
+ IPathManager pathManager)
{
_appPaths = appPaths;
_mediaEncoder = mediaEncoder;
_subtitleEncoder = subtitleEncoder;
_config = config;
_configurationManager = configurationManager;
+ _pathManager = pathManager;
+ }
+
+ private enum DynamicHdrMetadataRemovalPlan
+ {
+ None,
+ RemoveDovi,
+ RemoveHdr10Plus,
}
[GeneratedRegex(@"\s+")]
@@ -342,11 +359,8 @@ namespace MediaBrowser.Controller.MediaEncoding
return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder || isVideoToolBoxDecoder;
}
- return state.VideoStream.VideoRange == VideoRange.HDR
- && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
- || state.VideoStream.VideoRangeType == VideoRangeType.HLG
- || state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10
- || state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG);
+ // GPU tonemapping supports all HDR RangeTypes
+ return state.VideoStream.VideoRange == VideoRange.HDR;
}
private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@@ -381,8 +395,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
return state.VideoStream.VideoRange == VideoRange.HDR
- && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
- || state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10);
+ && IsDoviWithHdr10Bl(state.VideoStream);
}
private bool IsVideoToolboxTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@@ -397,7 +410,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// Certain DV profile 5 video works in Safari with direct playing, but the VideoToolBox does not produce correct mapping results with transcoding.
// All other HDR formats working.
return state.VideoStream.VideoRange == VideoRange.HDR
- && state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.HDR10Plus or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG;
+ && (IsDoviWithHdr10Bl(state.VideoStream)
+ || state.VideoStream.VideoRangeType is VideoRangeType.HLG);
}
private bool IsVideoStreamHevcRext(EncodingJobInfo state)
@@ -452,7 +466,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetMjpegEncoder(state, encodingOptions);
}
- if (_validationRegex.IsMatch(codec))
+ if (_containerValidationRegex.IsMatch(codec))
{
return codec.ToLowerInvariant();
}
@@ -493,7 +507,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public static string GetInputFormat(string container)
{
- if (string.IsNullOrEmpty(container) || !_validationRegex.IsMatch(container))
+ if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container))
{
return null;
}
@@ -711,7 +725,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var codec = state.OutputAudioCodec;
- if (!_validationRegex.IsMatch(codec))
+ if (!_containerValidationRegex.IsMatch(codec))
{
codec = "aac";
}
@@ -862,9 +876,9 @@ namespace MediaBrowser.Controller.MediaEncoding
&& _mediaEncoder.EncoderVersion >= _minFFmpegVaapiDeviceVendorId;
// Priority: 'renderNodePath' > 'vendorId' > 'kernelDriver'
- var driverOpts = string.IsNullOrEmpty(renderNodePath)
- ? (haveVendorId ? $",vendor_id={vendorId}" : (string.IsNullOrEmpty(kernelDriver) ? string.Empty : $",kernel_driver={kernelDriver}"))
- : renderNodePath;
+ var driverOpts = File.Exists(renderNodePath)
+ ? renderNodePath
+ : (haveVendorId ? $",vendor_id={vendorId}" : (string.IsNullOrEmpty(kernelDriver) ? string.Empty : $",kernel_driver={kernelDriver}"));
// 'driver' behaves similarly to env LIBVA_DRIVER_NAME
driverOpts += string.IsNullOrEmpty(driver) ? string.Empty : ",driver=" + driver;
@@ -1301,6 +1315,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|| codec.Contains("hevc", StringComparison.OrdinalIgnoreCase);
}
+ public static bool IsAv1(MediaStream stream)
+ {
+ var codec = stream.Codec ?? string.Empty;
+
+ return codec.Contains("av1", StringComparison.OrdinalIgnoreCase);
+ }
+
public static bool IsAAC(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
@@ -1308,8 +1329,125 @@ namespace MediaBrowser.Controller.MediaEncoding
return codec.Contains("aac", StringComparison.OrdinalIgnoreCase);
}
- public static string GetBitStreamArgs(MediaStream stream)
+ public static bool IsDoviWithHdr10Bl(MediaStream stream)
+ {
+ var rangeType = stream?.VideoRangeType;
+
+ return rangeType is VideoRangeType.DOVIWithHDR10
+ or VideoRangeType.DOVIWithEL
+ or VideoRangeType.DOVIWithHDR10Plus
+ or VideoRangeType.DOVIWithELHDR10Plus
+ or VideoRangeType.DOVIInvalid;
+ }
+
+ public static bool IsDovi(MediaStream stream)
+ {
+ var rangeType = stream?.VideoRangeType;
+
+ return IsDoviWithHdr10Bl(stream)
+ || (rangeType is VideoRangeType.DOVI
+ or VideoRangeType.DOVIWithHLG
+ or VideoRangeType.DOVIWithSDR);
+ }
+
+ public static bool IsHdr10Plus(MediaStream stream)
+ {
+ var rangeType = stream?.VideoRangeType;
+
+ return rangeType is VideoRangeType.HDR10Plus
+ or VideoRangeType.DOVIWithHDR10Plus
+ or VideoRangeType.DOVIWithELHDR10Plus;
+ }
+
+ /// <summary>
+ /// Check if dynamic HDR metadata should be removed during stream copy.
+ /// Please note this check assumes the range check has already been done
+ /// and trivial fallbacks like HDR10+ to HDR10, DOVIWithHDR10 to HDR10 is already checked.
+ /// </summary>
+ private static DynamicHdrMetadataRemovalPlan ShouldRemoveDynamicHdrMetadata(EncodingJobInfo state)
+ {
+ var videoStream = state.VideoStream;
+ if (videoStream.VideoRange is not VideoRange.HDR)
+ {
+ return DynamicHdrMetadataRemovalPlan.None;
+ }
+
+ var requestedRangeTypes = state.GetRequestedRangeTypes(state.VideoStream.Codec);
+ if (requestedRangeTypes.Length == 0)
+ {
+ return DynamicHdrMetadataRemovalPlan.None;
+ }
+
+ var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase);
+ var requestHasDOVI = requestedRangeTypes.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
+ var requestHasDOVIwithEL = requestedRangeTypes.Contains(VideoRangeType.DOVIWithEL.ToString(), StringComparison.OrdinalIgnoreCase);
+ var requestHasDOVIwithELHDR10plus = requestedRangeTypes.Contains(VideoRangeType.DOVIWithELHDR10Plus.ToString(), StringComparison.OrdinalIgnoreCase);
+
+ var shouldRemoveHdr10Plus = false;
+ // Case 1: Client supports HDR10, does not support DOVI with EL but EL presets
+ var shouldRemoveDovi = (!requestHasDOVIwithEL && requestHasHDR10) && videoStream.VideoRangeType == VideoRangeType.DOVIWithEL;
+
+ // Case 2: Client supports DOVI, does not support broken DOVI config
+ // Client does not report DOVI support should be allowed to copy bad data for remuxing as HDR10 players would not crash
+ shouldRemoveDovi = shouldRemoveDovi || (requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVIInvalid);
+
+ // Special case: we have a video with both EL and HDR10+
+ // If the client supports EL but not in the case of coexistence with HDR10+, remove HDR10+ for compatibility reasons.
+ // Otherwise, remove DOVI if the client is not a DOVI player
+ if (videoStream.VideoRangeType == VideoRangeType.DOVIWithELHDR10Plus)
+ {
+ shouldRemoveHdr10Plus = requestHasDOVIwithEL && !requestHasDOVIwithELHDR10plus;
+ shouldRemoveDovi = shouldRemoveDovi || !shouldRemoveHdr10Plus;
+ }
+
+ if (shouldRemoveDovi)
+ {
+ return DynamicHdrMetadataRemovalPlan.RemoveDovi;
+ }
+
+ // If the client is a Dolby Vision Player, remove the HDR10+ metadata to avoid playback issues
+ shouldRemoveHdr10Plus = shouldRemoveHdr10Plus || (requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10Plus);
+ return shouldRemoveHdr10Plus ? DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus : DynamicHdrMetadataRemovalPlan.None;
+ }
+
+ private bool CanEncoderRemoveDynamicHdrMetadata(DynamicHdrMetadataRemovalPlan plan, MediaStream videoStream)
+ {
+ return plan switch
+ {
+ DynamicHdrMetadataRemovalPlan.RemoveDovi => _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.DoviRpuStrip)
+ || (IsH265(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.HevcMetadataRemoveDovi))
+ || (IsAv1(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.Av1MetadataRemoveDovi)),
+ DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus => (IsH265(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.HevcMetadataRemoveHdr10Plus))
+ || (IsAv1(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.Av1MetadataRemoveHdr10Plus)),
+ _ => true,
+ };
+ }
+
+ public bool IsDoviRemoved(EncodingJobInfo state)
+ {
+ return state?.VideoStream is not null && ShouldRemoveDynamicHdrMetadata(state) == DynamicHdrMetadataRemovalPlan.RemoveDovi
+ && CanEncoderRemoveDynamicHdrMetadata(DynamicHdrMetadataRemovalPlan.RemoveDovi, state.VideoStream);
+ }
+
+ public bool IsHdr10PlusRemoved(EncodingJobInfo state)
+ {
+ return state?.VideoStream is not null && ShouldRemoveDynamicHdrMetadata(state) == DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus
+ && CanEncoderRemoveDynamicHdrMetadata(DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus, state.VideoStream);
+ }
+
+ public string GetBitStreamArgs(EncodingJobInfo state, MediaStreamType streamType)
{
+ if (state is null)
+ {
+ return null;
+ }
+
+ var stream = streamType switch
+ {
+ MediaStreamType.Audio => state.AudioStream,
+ MediaStreamType.Video => state.VideoStream,
+ _ => state.VideoStream
+ };
// TODO This is auto inserted into the mpegts mux so it might not be needed.
// https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb
if (IsH264(stream))
@@ -1317,21 +1455,57 @@ namespace MediaBrowser.Controller.MediaEncoding
return "-bsf:v h264_mp4toannexb";
}
+ if (IsAAC(stream))
+ {
+ // Convert adts header(mpegts) to asc header(mp4).
+ return "-bsf:a aac_adtstoasc";
+ }
+
if (IsH265(stream))
{
- return "-bsf:v hevc_mp4toannexb";
+ var filter = "-bsf:v hevc_mp4toannexb";
+
+ // The following checks are not complete because the copy would be rejected
+ // if the encoder cannot remove required metadata.
+ // And if bsf is used, we must already be using copy codec.
+ switch (ShouldRemoveDynamicHdrMetadata(state))
+ {
+ default:
+ case DynamicHdrMetadataRemovalPlan.None:
+ break;
+ case DynamicHdrMetadataRemovalPlan.RemoveDovi:
+ filter += _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.HevcMetadataRemoveDovi)
+ ? ",hevc_metadata=remove_dovi=1"
+ : ",dovi_rpu=strip=1";
+ break;
+ case DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus:
+ filter += ",hevc_metadata=remove_hdr10plus=1";
+ break;
+ }
+
+ return filter;
}
- if (IsAAC(stream))
+ if (IsAv1(stream))
{
- // Convert adts header(mpegts) to asc header(mp4).
- return "-bsf:a aac_adtstoasc";
+ switch (ShouldRemoveDynamicHdrMetadata(state))
+ {
+ default:
+ case DynamicHdrMetadataRemovalPlan.None:
+ return null;
+ case DynamicHdrMetadataRemovalPlan.RemoveDovi:
+ return _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.Av1MetadataRemoveDovi)
+ ? "-bsf:v av1_metadata=remove_dovi=1"
+ : "-bsf:v dovi_rpu=strip=1";
+ case DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus:
+ return "-bsf:v av1_metadata=remove_hdr10plus=1";
+ }
}
return null;
}
- public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
+ public string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
{
var bitStreamArgs = string.Empty;
var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.');
@@ -1342,7 +1516,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
{
- bitStreamArgs = GetBitStreamArgs(state.AudioStream);
+ bitStreamArgs = GetBitStreamArgs(state, MediaStreamType.Audio);
bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs;
}
@@ -1621,7 +1795,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var alphaParam = enableAlpha ? ":alpha=1" : string.Empty;
var sub2videoParam = enableSub2video ? ":sub2video=1" : string.Empty;
- var fontPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
+ var fontPath = _pathManager.GetAttachmentFolderPath(state.MediaSource.Id);
var fontParam = string.Format(
CultureInfo.InvariantCulture,
":fontsdir='{0}'",
@@ -2169,7 +2343,6 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// DOVIWithHDR10 should be compatible with HDR10 supporting players. Same goes with HLG and of course SDR. So allow copy of those formats
-
var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasHLG = requestedRangeTypes.Contains(VideoRangeType.HLG.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase);
@@ -2177,9 +2350,17 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase)
&& !((requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10)
|| (requestHasHLG && videoStream.VideoRangeType == VideoRangeType.DOVIWithHLG)
- || (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)))
- {
- return false;
+ || (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)
+ || (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus)))
+ {
+ // Check complicated cases where we need to remove dynamic metadata
+ // Conservatively refuse to copy if the encoder can't remove dynamic metadata,
+ // but a removal is required for compatability reasons.
+ var dynamicHdrMetadataRemovalPlan = ShouldRemoveDynamicHdrMetadata(state);
+ if (!CanEncoderRemoveDynamicHdrMetadata(dynamicHdrMetadataRemovalPlan, videoStream))
+ {
+ return false;
+ }
}
}
@@ -5621,7 +5802,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var doDeintH2645 = doDeintH264 || doDeintHevc;
var doOclTonemap = IsHwTonemapAvailable(state, options);
- var hasSubs = state.SubtitleStream != null && ShouldEncodeSubtitle(state);
+ var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state);
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
var hasAssSubs = hasSubs
@@ -6621,6 +6802,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|| string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|| string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
+ var isAv1SupportedSwFormatsVt = is8_10bitSwFormatsVt || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
// The related patches make videotoolbox hardware surface working is only available in jellyfin-ffmpeg 7.0.1 at the moment.
bool useHwSurface = (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface) && IsVideoToolboxFullSupported();
@@ -6654,6 +6836,13 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface);
}
+
+ if (string.Equals("av1", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
+ && isAv1SupportedSwFormatsVt
+ && _mediaEncoder.IsVideoToolboxAv1DecodeAvailable)
+ {
+ return GetHwaccelType(state, options, "av1", bitDepth, useHwSurface);
+ }
}
return null;
@@ -6982,7 +7171,7 @@ namespace MediaBrowser.Controller.MediaEncoding
state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
- if (state.ReadInputAtNativeFramerate
+ if ((state.ReadInputAtNativeFramerate && !state.IsSegmentedLiveStream)
|| (mediaSource.Protocol == MediaProtocol.File
&& string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase)))
{
@@ -7236,7 +7425,7 @@ namespace MediaBrowser.Controller.MediaEncoding
&& string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{
- string bitStreamArgs = GetBitStreamArgs(state.VideoStream);
+ string bitStreamArgs = GetBitStreamArgs(state, MediaStreamType.Video);
if (!string.IsNullOrEmpty(bitStreamArgs))
{
args += " " + bitStreamArgs;
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index 7586ac902..8d6211051 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Globalization;
using System.Linq;
using Jellyfin.Data.Enums;
diff --git a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
index 09840d2ee..d8d136472 100644
--- a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs
@@ -9,26 +9,33 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-namespace MediaBrowser.Controller.MediaEncoding
-{
- public interface IAttachmentExtractor
- {
- Task<(MediaAttachment Attachment, Stream Stream)> GetAttachment(
- BaseItem item,
- string mediaSourceId,
- int attachmentStreamIndex,
- CancellationToken cancellationToken);
+namespace MediaBrowser.Controller.MediaEncoding;
- Task ExtractAllAttachments(
- string inputFile,
- MediaSourceInfo mediaSource,
- string outputPath,
- CancellationToken cancellationToken);
+public interface IAttachmentExtractor
+{
+ /// <summary>
+ /// Gets the path to the attachment file.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/>.</param>
+ /// <param name="mediaSourceId">The media source id.</param>
+ /// <param name="attachmentStreamIndex">The attachment index.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The async task.</returns>
+ Task<(MediaAttachment Attachment, Stream Stream)> GetAttachment(
+ BaseItem item,
+ string mediaSourceId,
+ int attachmentStreamIndex,
+ CancellationToken cancellationToken);
- Task ExtractAllAttachmentsExternal(
- string inputArgument,
- string id,
- string outputPath,
- CancellationToken cancellationToken);
- }
+ /// <summary>
+ /// Gets the path to the attachment file.
+ /// </summary>
+ /// <param name="inputFile">The input file path.</param>
+ /// <param name="mediaSource">The <see cref="MediaSourceInfo" /> source id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The async task.</returns>
+ Task ExtractAllAttachments(
+ string inputFile,
+ MediaSourceInfo mediaSource,
+ CancellationToken cancellationToken);
}
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index c767b4a51..de6353c4c 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -76,6 +76,12 @@ namespace MediaBrowser.Controller.MediaEncoding
bool IsVaapiDeviceSupportVulkanDrmInterop { get; }
/// <summary>
+ /// Gets a value indicating whether av1 decoding is available via VideoToolbox.
+ /// </summary>
+ /// <value><c>true</c> if the av1 is available via VideoToolbox, <c>false</c> otherwise.</value>
+ bool IsVideoToolboxAv1DecodeAvailable { get; }
+
+ /// <summary>
/// Whether given encoder codec is supported.
/// </summary>
/// <param name="encoder">The encoder.</param>
@@ -111,6 +117,13 @@ namespace MediaBrowser.Controller.MediaEncoding
bool SupportsFilterWithOption(FilterOptionType option);
/// <summary>
+ /// Whether the bitstream filter is supported with the given option.
+ /// </summary>
+ /// <param name="option">The option.</param>
+ /// <returns><c>true</c> if the bitstream filter is supported, <c>false</c> otherwise.</returns>
+ bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option);
+
+ /// <summary>
/// Extracts the audio image.
/// </summary>
/// <param name="path">The path.</param>
diff --git a/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs b/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs
new file mode 100644
index 000000000..4930434a7
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.MediaEncoding.Keyframes;
+
+namespace MediaBrowser.Controller.Persistence;
+
+/// <summary>
+/// Provides methods for accessing keyframe data.
+/// </summary>
+public interface IKeyframeRepository
+{
+ /// <summary>
+ /// Gets the keyframe data.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <returns>The keyframe data.</returns>
+ IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId);
+
+ /// <summary>
+ /// Saves the keyframe data.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="data">The keyframe data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The task object representing the asynchronous operation.</returns>
+ Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
index 474f09dc5..a1edfa3c9 100644
--- a/MediaBrowser.Controller/Providers/DirectoryService.cs
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -10,14 +10,15 @@ namespace MediaBrowser.Controller.Providers
{
public class DirectoryService : IDirectoryService
{
- private readonly IFileSystem _fileSystem;
-
+ // TODO make static and switch to FastConcurrentLru.
private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, List<string>> _filePathCache = new(StringComparer.Ordinal);
+ private readonly IFileSystem _fileSystem;
+
public DirectoryService(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 21131e6b5..47bcfdb6e 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -342,5 +342,13 @@ namespace MediaBrowser.Controller.Session
Task RevokeUserTokens(Guid userId, string currentAccessToken);
Task CloseIfNeededAsync(SessionInfo session);
+
+ /// <summary>
+ /// Used to close the livestream if needed.
+ /// </summary>
+ /// <param name="liveStreamId">The livestream id.</param>
+ /// <param name="sessionIdOrPlaySessionId">The session id or playsession id.</param>
+ /// <returns>Task.</returns>
+ Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId);
}
}
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 431fc0b17..89291c73b 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -1,7 +1,4 @@
-#pragma warning disable CS1591
-
using System;
-using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
@@ -9,28 +6,27 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Attachments
{
+ /// <inheritdoc cref="IAttachmentExtractor"/>
public sealed class AttachmentExtractor : IAttachmentExtractor, IDisposable
{
private readonly ILogger<AttachmentExtractor> _logger;
- private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
private readonly IMediaEncoder _mediaEncoder;
private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IPathManager _pathManager;
private readonly AsyncKeyedLocker<string> _semaphoreLocks = new(o =>
{
@@ -38,18 +34,26 @@ namespace MediaBrowser.MediaEncoding.Attachments
o.PoolInitialFill = 1;
});
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AttachmentExtractor"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger{AttachmentExtractor}"/>.</param>
+ /// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
+ /// <param name="mediaEncoder">The <see cref="IMediaEncoder"/>.</param>
+ /// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/>.</param>
+ /// <param name="pathManager">The <see cref="IPathManager"/>.</param>
public AttachmentExtractor(
ILogger<AttachmentExtractor> logger,
- IApplicationPaths appPaths,
IFileSystem fileSystem,
IMediaEncoder mediaEncoder,
- IMediaSourceManager mediaSourceManager)
+ IMediaSourceManager mediaSourceManager,
+ IPathManager pathManager)
{
_logger = logger;
- _appPaths = appPaths;
_fileSystem = fileSystem;
_mediaEncoder = mediaEncoder;
_mediaSourceManager = mediaSourceManager;
+ _pathManager = pathManager;
}
/// <inheritdoc />
@@ -77,350 +81,183 @@ namespace MediaBrowser.MediaEncoding.Attachments
throw new ResourceNotFoundException($"MediaSource {mediaSourceId} has no attachment with stream index {attachmentStreamIndex}");
}
+ if (string.Equals(mediaAttachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ResourceNotFoundException($"Attachment with stream index {attachmentStreamIndex} can't be extracted for MediaSource {mediaSourceId}");
+ }
+
var attachmentStream = await GetAttachmentStream(mediaSource, mediaAttachment, cancellationToken)
.ConfigureAwait(false);
return (mediaAttachment, attachmentStream);
}
+ /// <inheritdoc />
public async Task ExtractAllAttachments(
string inputFile,
MediaSourceInfo mediaSource,
- string outputPath,
CancellationToken cancellationToken)
{
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => !string.IsNullOrEmpty(a.FileName)
&& (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
- if (shouldExtractOneByOne)
- {
- var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
- foreach (var i in attachmentIndexes)
- {
- var newName = Path.Join(outputPath, i.ToString(CultureInfo.InvariantCulture));
- await ExtractAttachment(inputFile, mediaSource, i, newName, cancellationToken).ConfigureAwait(false);
- }
- }
- else
+ if (shouldExtractOneByOne && !inputFile.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
{
- using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
+ foreach (var attachment in mediaSource.MediaAttachments)
{
- if (!Directory.Exists(outputPath))
+ if (!string.Equals(attachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
{
- await ExtractAllAttachmentsInternal(
- _mediaEncoder.GetInputArgument(inputFile, mediaSource),
- outputPath,
- false,
- cancellationToken).ConfigureAwait(false);
+ await ExtractAttachment(inputFile, mediaSource, attachment, cancellationToken).ConfigureAwait(false);
}
}
}
- }
-
- public async Task ExtractAllAttachmentsExternal(
- string inputArgument,
- string id,
- string outputPath,
- CancellationToken cancellationToken)
- {
- using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
+ else
{
- if (!File.Exists(Path.Join(outputPath, id)))
- {
- await ExtractAllAttachmentsInternal(
- inputArgument,
- outputPath,
- true,
- cancellationToken).ConfigureAwait(false);
-
- if (Directory.Exists(outputPath))
- {
- File.Create(Path.Join(outputPath, id));
- }
- }
+ await ExtractAllAttachmentsInternal(
+ inputFile,
+ mediaSource,
+ false,
+ cancellationToken).ConfigureAwait(false);
}
}
private async Task ExtractAllAttachmentsInternal(
- string inputPath,
- string outputPath,
+ string inputFile,
+ MediaSourceInfo mediaSource,
bool isExternal,
CancellationToken cancellationToken)
{
- ArgumentException.ThrowIfNullOrEmpty(inputPath);
- ArgumentException.ThrowIfNullOrEmpty(outputPath);
-
- Directory.CreateDirectory(outputPath);
-
- var processArgs = string.Format(
- CultureInfo.InvariantCulture,
- "-dump_attachment:t \"\" -y {0} -i {1} -t 0 -f null null",
- inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty,
- inputPath);
-
- int exitCode;
-
- using (var process = new Process
- {
- StartInfo = new ProcessStartInfo
- {
- Arguments = processArgs,
- FileName = _mediaEncoder.EncoderPath,
- UseShellExecute = false,
- CreateNoWindow = true,
- WindowStyle = ProcessWindowStyle.Hidden,
- WorkingDirectory = outputPath,
- ErrorDialog = false
- },
- EnableRaisingEvents = true
- })
- {
- _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
-
- process.Start();
+ var inputPath = _mediaEncoder.GetInputArgument(inputFile, mediaSource);
- try
- {
- await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
- exitCode = process.ExitCode;
- }
- catch (OperationCanceledException)
- {
- process.Kill(true);
- exitCode = -1;
- }
- }
-
- var failed = false;
+ ArgumentException.ThrowIfNullOrEmpty(inputPath);
- if (exitCode != 0)
+ var outputFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
+ using (await _semaphoreLocks.LockAsync(outputFolder, cancellationToken).ConfigureAwait(false))
{
- if (isExternal && exitCode == 1)
+ if (!Directory.Exists(outputFolder))
{
- // ffmpeg returns exitCode 1 because there is no video or audio stream
- // this can be ignored
+ Directory.CreateDirectory(outputFolder);
}
else
{
- failed = true;
-
- _logger.LogWarning("Deleting extracted attachments {Path} due to failure: {ExitCode}", outputPath, exitCode);
- try
+ var fileNames = Directory.GetFiles(outputFolder, "*", SearchOption.TopDirectoryOnly).Select(f => Path.GetFileName(f));
+ var missingFiles = mediaSource.MediaAttachments.Where(a => !fileNames.Contains(a.FileName) && !string.Equals(a.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase));
+ if (!missingFiles.Any())
{
- Directory.Delete(outputPath);
- }
- catch (IOException ex)
- {
- _logger.LogError(ex, "Error deleting extracted attachments {Path}", outputPath);
+ // Skip extraction if all files already exist
+ return;
}
}
- }
- else if (!Directory.Exists(outputPath))
- {
- failed = true;
- }
-
- if (failed)
- {
- _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
-
- throw new InvalidOperationException(
- string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath));
- }
-
- _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
- }
- private async Task<Stream> GetAttachmentStream(
- MediaSourceInfo mediaSource,
- MediaAttachment mediaAttachment,
- CancellationToken cancellationToken)
- {
- var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource, mediaAttachment, cancellationToken).ConfigureAwait(false);
- return AsyncFile.OpenRead(attachmentPath);
- }
-
- private async Task<string> GetReadableFile(
- string mediaPath,
- string inputFile,
- MediaSourceInfo mediaSource,
- MediaAttachment mediaAttachment,
- CancellationToken cancellationToken)
- {
- await CacheAllAttachments(mediaPath, inputFile, mediaSource, cancellationToken).ConfigureAwait(false);
-
- var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, mediaAttachment.Index);
- await ExtractAttachment(inputFile, mediaSource, mediaAttachment.Index, outputPath, cancellationToken)
- .ConfigureAwait(false);
-
- return outputPath;
- }
+ var processArgs = string.Format(
+ CultureInfo.InvariantCulture,
+ "-dump_attachment:t \"\" -y {0} -i {1} -t 0 -f null null",
+ inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty,
+ inputPath);
- private async Task CacheAllAttachments(
- string mediaPath,
- string inputFile,
- MediaSourceInfo mediaSource,
- CancellationToken cancellationToken)
- {
- var outputFileLocks = new List<IDisposable>();
- var extractableAttachmentIds = new List<int>();
+ int exitCode;
- try
- {
- foreach (var attachment in mediaSource.MediaAttachments)
+ using (var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ Arguments = processArgs,
+ FileName = _mediaEncoder.EncoderPath,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ WorkingDirectory = outputFolder,
+ ErrorDialog = false
+ },
+ EnableRaisingEvents = true
+ })
{
- var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, attachment.Index);
+ _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
- var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false);
+ process.Start();
- if (File.Exists(outputPath))
+ try
{
- releaser.Dispose();
- continue;
+ await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
+ exitCode = process.ExitCode;
}
-
- outputFileLocks.Add(releaser);
- extractableAttachmentIds.Add(attachment.Index);
- }
-
- if (extractableAttachmentIds.Count > 0)
- {
- await CacheAllAttachmentsInternal(mediaPath, _mediaEncoder.GetInputArgument(inputFile, mediaSource), mediaSource, extractableAttachmentIds, cancellationToken).ConfigureAwait(false);
- }
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "Unable to cache media attachments for File:{File}", mediaPath);
- }
- finally
- {
- outputFileLocks.ForEach(x => x.Dispose());
- }
- }
-
- private async Task CacheAllAttachmentsInternal(
- string mediaPath,
- string inputFile,
- MediaSourceInfo mediaSource,
- List<int> extractableAttachmentIds,
- CancellationToken cancellationToken)
- {
- var outputPaths = new List<string>();
- var processArgs = string.Empty;
-
- foreach (var attachmentId in extractableAttachmentIds)
- {
- var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, attachmentId);
-
- Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid."));
-
- outputPaths.Add(outputPath);
- processArgs += string.Format(
- CultureInfo.InvariantCulture,
- " -dump_attachment:{0} \"{1}\"",
- attachmentId,
- EncodingUtils.NormalizePath(outputPath));
- }
-
- processArgs += string.Format(
- CultureInfo.InvariantCulture,
- " -i {0} -t 0 -f null null",
- inputFile);
-
- int exitCode;
-
- using (var process = new Process
- {
- StartInfo = new ProcessStartInfo
+ catch (OperationCanceledException)
{
- Arguments = processArgs,
- FileName = _mediaEncoder.EncoderPath,
- UseShellExecute = false,
- CreateNoWindow = true,
- WindowStyle = ProcessWindowStyle.Hidden,
- ErrorDialog = false
- },
- EnableRaisingEvents = true
- })
- {
- _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
-
- process.Start();
-
- try
- {
- await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
- exitCode = process.ExitCode;
- }
- catch (OperationCanceledException)
- {
- process.Kill(true);
- exitCode = -1;
+ process.Kill(true);
+ exitCode = -1;
+ }
}
- }
- var failed = false;
+ var failed = false;
- if (exitCode == -1)
- {
- failed = true;
-
- foreach (var outputPath in outputPaths)
+ if (exitCode != 0)
{
- try
+ if (isExternal && exitCode == 1)
{
- _logger.LogWarning("Deleting extracted media attachment due to failure: {Path}", outputPath);
- _fileSystem.DeleteFile(outputPath);
- }
- catch (FileNotFoundException)
- {
- // ffmpeg failed, so it is normal that one or more expected output files do not exist.
- // There is no need to log anything for the user here.
+ // ffmpeg returns exitCode 1 because there is no video or audio stream
+ // this can be ignored
}
- catch (IOException ex)
+ else
{
- _logger.LogError(ex, "Error deleting extracted media attachment {Path}", outputPath);
+ failed = true;
+
+ _logger.LogWarning("Deleting extracted attachments {Path} due to failure: {ExitCode}", outputFolder, exitCode);
+ try
+ {
+ Directory.Delete(outputFolder);
+ }
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error deleting extracted attachments {Path}", outputFolder);
+ }
}
}
- }
- else
- {
- foreach (var outputPath in outputPaths)
+ else if (!Directory.Exists(outputFolder))
{
- if (!File.Exists(outputPath))
- {
- _logger.LogError("ffmpeg media attachment extraction failed for {InputPath} to {OutputPath}", inputFile, outputPath);
- failed = true;
- continue;
- }
+ failed = true;
+ }
- _logger.LogInformation("ffmpeg media attachment extraction completed for {InputPath} to {OutputPath}", inputFile, outputPath);
+ if (failed)
+ {
+ _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputFolder);
+
+ throw new InvalidOperationException(
+ string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputFolder));
}
- }
- if (failed)
- {
- throw new FfmpegException(
- string.Format(CultureInfo.InvariantCulture, "ffmpeg media attachment extraction failed for {0}", inputFile));
+ _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputFolder);
}
}
- private async Task ExtractAttachment(
+ private async Task<Stream> GetAttachmentStream(
+ MediaSourceInfo mediaSource,
+ MediaAttachment mediaAttachment,
+ CancellationToken cancellationToken)
+ {
+ var attachmentPath = await ExtractAttachment(mediaSource.Path, mediaSource, mediaAttachment, cancellationToken)
+ .ConfigureAwait(false);
+ return AsyncFile.OpenRead(attachmentPath);
+ }
+
+ private async Task<string> ExtractAttachment(
string inputFile,
MediaSourceInfo mediaSource,
- int attachmentStreamIndex,
- string outputPath,
+ MediaAttachment mediaAttachment,
CancellationToken cancellationToken)
{
- using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
+ var attachmentFolderPath = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
+ using (await _semaphoreLocks.LockAsync(attachmentFolderPath, cancellationToken).ConfigureAwait(false))
{
- if (!File.Exists(outputPath))
+ var attachmentPath = _pathManager.GetAttachmentPath(mediaSource.Id, mediaAttachment.FileName ?? mediaAttachment.Index.ToString(CultureInfo.InvariantCulture));
+ if (!File.Exists(attachmentPath))
{
await ExtractAttachmentInternal(
_mediaEncoder.GetInputArgument(inputFile, mediaSource),
- attachmentStreamIndex,
- outputPath,
+ mediaAttachment.Index,
+ attachmentPath,
cancellationToken).ConfigureAwait(false);
}
+
+ return attachmentPath;
}
}
@@ -510,23 +347,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
_logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
}
- private string GetAttachmentCachePath(string mediaPath, MediaSourceInfo mediaSource, int attachmentStreamIndex)
- {
- string filename;
- if (mediaSource.Protocol == MediaProtocol.File)
- {
- var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
- filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
- }
- else
- {
- filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
- }
-
- var prefix = filename.AsSpan(0, 1);
- return Path.Join(_appPaths.DataPath, "attachments", prefix, filename);
- }
-
/// <inheritdoc />
public void Dispose()
{
diff --git a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs
new file mode 100644
index 000000000..a8ff58b09
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs
@@ -0,0 +1,87 @@
+#pragma warning disable CA1031
+
+using System;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.MediaEncoding.Encoder;
+
+/// <summary>
+/// Helper class for Apple platform specific operations.
+/// </summary>
+[SupportedOSPlatform("macos")]
+public static class ApplePlatformHelper
+{
+ private static readonly string[] _av1DecodeBlacklistedCpuClass = ["M1", "M2"];
+
+ private static string GetSysctlValue(ReadOnlySpan<byte> name)
+ {
+ IntPtr length = IntPtr.Zero;
+ // Get length of the value
+ int osStatus = SysctlByName(name, IntPtr.Zero, ref length, IntPtr.Zero, 0);
+
+ if (osStatus != 0)
+ {
+ throw new NotSupportedException($"Failed to get sysctl value for {System.Text.Encoding.UTF8.GetString(name)} with error {osStatus}");
+ }
+
+ IntPtr buffer = Marshal.AllocHGlobal(length.ToInt32());
+ try
+ {
+ osStatus = SysctlByName(name, buffer, ref length, IntPtr.Zero, 0);
+ if (osStatus != 0)
+ {
+ throw new NotSupportedException($"Failed to get sysctl value for {System.Text.Encoding.UTF8.GetString(name)} with error {osStatus}");
+ }
+
+ return Marshal.PtrToStringAnsi(buffer) ?? string.Empty;
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(buffer);
+ }
+ }
+
+ private static int SysctlByName(ReadOnlySpan<byte> name, IntPtr oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen)
+ {
+ return NativeMethods.SysctlByName(name.ToArray(), oldp, ref oldlenp, newp, newlen);
+ }
+
+ /// <summary>
+ /// Check if the current system has hardware acceleration for AV1 decoding.
+ /// </summary>
+ /// <param name="logger">The logger used for error logging.</param>
+ /// <returns>Boolean indicates the hwaccel support.</returns>
+ public static bool HasAv1HardwareAccel(ILogger logger)
+ {
+ if (!RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64))
+ {
+ return false;
+ }
+
+ try
+ {
+ string cpuBrandString = GetSysctlValue("machdep.cpu.brand_string"u8);
+ return !_av1DecodeBlacklistedCpuClass.Any(blacklistedCpuClass => cpuBrandString.Contains(blacklistedCpuClass, StringComparison.OrdinalIgnoreCase));
+ }
+ catch (NotSupportedException e)
+ {
+ logger.LogError("Error getting CPU brand string: {Message}", e.Message);
+ }
+ catch (Exception e)
+ {
+ logger.LogError("Unknown error occured: {Exception}", e);
+ }
+
+ return false;
+ }
+
+ private static class NativeMethods
+ {
+ [DllImport("libc", EntryPoint = "sysctlbyname", SetLastError = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ internal static extern int SysctlByName(byte[] name, IntPtr oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen);
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
index 23d9ca7ef..d28cd70ef 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
@@ -5,7 +5,9 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
+using System.Runtime.Versioning;
using System.Text.RegularExpressions;
+using MediaBrowser.Controller.MediaEncoding;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Encoder
@@ -159,6 +161,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ 6, new string[] { "transpose_opencl", "rotate by half-turn" } }
};
+ private static readonly Dictionary<BitStreamFilterOptionType, (string, string)> _bsfOptionsDict = new Dictionary<BitStreamFilterOptionType, (string, string)>
+ {
+ { BitStreamFilterOptionType.HevcMetadataRemoveDovi, ("hevc_metadata", "remove_dovi") },
+ { BitStreamFilterOptionType.HevcMetadataRemoveHdr10Plus, ("hevc_metadata", "remove_hdr10plus") },
+ { BitStreamFilterOptionType.Av1MetadataRemoveDovi, ("av1_metadata", "remove_dovi") },
+ { BitStreamFilterOptionType.Av1MetadataRemoveHdr10Plus, ("av1_metadata", "remove_hdr10plus") },
+ { BitStreamFilterOptionType.DoviRpuStrip, ("dovi_rpu", "strip") }
+ };
+
// These are the library versions that corresponds to our minimum ffmpeg version 4.4 according to the version table below
// Refers to the versions in https://ffmpeg.org/download.html
private static readonly Dictionary<string, Version> _ffmpegMinimumLibraryVersions = new Dictionary<string, Version>
@@ -285,6 +296,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
public IDictionary<int, bool> GetFiltersWithOption() => GetFFmpegFiltersWithOption();
+ public IDictionary<BitStreamFilterOptionType, bool> GetBitStreamFiltersWithOption() => _bsfOptionsDict
+ .ToDictionary(item => item.Key, item => CheckBitStreamFilterWithOption(item.Value.Item1, item.Value.Item2));
+
public Version? GetFFmpegVersion()
{
string output;
@@ -437,6 +451,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
+ [SupportedOSPlatform("macos")]
+ public bool CheckIsVideoToolboxAv1DecodeAvailable()
+ {
+ return ApplePlatformHelper.HasAv1HardwareAccel(_logger);
+ }
+
private IEnumerable<string> GetHwaccelTypes()
{
string? output = null;
@@ -488,6 +508,34 @@ namespace MediaBrowser.MediaEncoding.Encoder
return false;
}
+ public bool CheckBitStreamFilterWithOption(string filter, string option)
+ {
+ if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option))
+ {
+ return false;
+ }
+
+ string output;
+ try
+ {
+ output = GetProcessOutput(_encoderPath, "-h bsf=" + filter, false, null);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error detecting the given bit stream filter");
+ return false;
+ }
+
+ if (output.Contains("Bit stream filter " + filter, StringComparison.Ordinal))
+ {
+ return output.Contains(option, StringComparison.Ordinal);
+ }
+
+ _logger.LogWarning("Bit stream filter: {Name} with option {Option} is not available", filter, option);
+
+ return false;
+ }
+
public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion)
{
if (string.IsNullOrEmpty(keyDesc))
@@ -516,6 +564,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
return !string.IsNullOrEmpty(flag) && GetProcessExitCode(_encoderPath, $"-loglevel quiet -hwaccel_flags +{flag} -hide_banner -f lavfi -i nullsrc=s=1x1:d=100 -f null -");
}
+ public bool CheckSupportedProberOption(string option, string proberPath)
+ {
+ return !string.IsNullOrEmpty(option) && GetProcessExitCode(proberPath, $"-loglevel quiet -f lavfi -i nullsrc=s=1x1:d=1 -{option}");
+ }
+
private IEnumerable<string> GetCodecs(Codec codec)
{
string codecstr = codec == Codec.Encoder ? "encoders" : "decoders";
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 14cf869f9..9a759ba41 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -73,9 +73,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
private List<string> _hwaccels = new List<string>();
private List<string> _filters = new List<string>();
private IDictionary<int, bool> _filtersWithOption = new Dictionary<int, bool>();
+ private IDictionary<BitStreamFilterOptionType, bool> _bitStreamFiltersWithOption = new Dictionary<BitStreamFilterOptionType, bool>();
private bool _isPkeyPauseSupported = false;
private bool _isLowPriorityHwDecodeSupported = false;
+ private bool _proberSupportsFirstVideoFrame = false;
private bool _isVaapiDeviceAmd = false;
private bool _isVaapiDeviceInteliHD = false;
@@ -83,6 +85,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
private bool _isVaapiDeviceSupportVulkanDrmModifier = false;
private bool _isVaapiDeviceSupportVulkanDrmInterop = false;
+ private bool _isVideoToolboxAv1DecodeAvailable = false;
+
private static string[] _vulkanImageDrmFmtModifierExts =
{
"VK_EXT_image_drm_format_modifier",
@@ -159,6 +163,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <inheritdoc />
public bool IsVaapiDeviceSupportVulkanDrmInterop => _isVaapiDeviceSupportVulkanDrmInterop;
+ public bool IsVideoToolboxAv1DecodeAvailable => _isVideoToolboxAv1DecodeAvailable;
+
[GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")]
private static partial Regex FfprobePathRegex();
@@ -218,6 +224,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
SetAvailableEncoders(validator.GetEncoders());
SetAvailableFilters(validator.GetFilters());
SetAvailableFiltersWithOption(validator.GetFiltersWithOption());
+ SetAvailableBitStreamFiltersWithOption(validator.GetBitStreamFiltersWithOption());
SetAvailableHwaccels(validator.GetHwaccels());
SetMediaEncoderVersion(validator);
@@ -225,6 +232,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding", _ffmpegVersion);
_isLowPriorityHwDecodeSupported = validator.CheckSupportedHwaccelFlag("low_priority");
+ _proberSupportsFirstVideoFrame = validator.CheckSupportedProberOption("only_first_vframe", _ffprobePath);
// Check the Vaapi device vendor
if (OperatingSystem.IsLinux()
@@ -261,6 +269,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM interop", options.VaapiDevice);
}
}
+
+ // Check if VideoToolbox supports AV1 decode
+ if (OperatingSystem.IsMacOS() && SupportsHwaccel("videotoolbox"))
+ {
+ _isVideoToolboxAv1DecodeAvailable = validator.CheckIsVideoToolboxAv1DecodeAvailable();
+ }
}
_logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty);
@@ -332,6 +346,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
_filtersWithOption = dict;
}
+ public void SetAvailableBitStreamFiltersWithOption(IDictionary<BitStreamFilterOptionType, bool> dict)
+ {
+ _bitStreamFiltersWithOption = dict;
+ }
+
public void SetMediaEncoderVersion(EncoderValidator validator)
{
_ffmpegVersion = validator.GetFFmpegVersion();
@@ -372,6 +391,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
return false;
}
+ public bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option)
+ {
+ return _bitStreamFiltersWithOption.TryGetValue(option, out var val) && val;
+ }
+
public bool CanEncodeToAudioCodec(string codec)
{
if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
@@ -491,6 +515,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
var args = extractChapters
? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
: "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
+
+ if (_proberSupportsFirstVideoFrame)
+ {
+ args += " -show_frames -only_first_vframe";
+ }
+
args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim();
var process = new Process
diff --git a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs
index d4d153b08..53eea64db 100644
--- a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs
+++ b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs
@@ -30,5 +30,12 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <value>The chapters.</value>
[JsonPropertyName("chapters")]
public IReadOnlyList<MediaChapter> Chapters { get; set; }
+
+ /// <summary>
+ /// Gets or sets the frames.
+ /// </summary>
+ /// <value>The streams.</value>
+ [JsonPropertyName("frames")]
+ public IReadOnlyList<MediaFrameInfo> Frames { get; set; }
}
}
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaFrameInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaFrameInfo.cs
new file mode 100644
index 000000000..bed4368ed
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Probing/MediaFrameInfo.cs
@@ -0,0 +1,184 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.MediaEncoding.Probing;
+
+/// <summary>
+/// Class MediaFrameInfo.
+/// </summary>
+public class MediaFrameInfo
+{
+ /// <summary>
+ /// Gets or sets the media type.
+ /// </summary>
+ [JsonPropertyName("media_type")]
+ public string? MediaType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the StreamIndex.
+ /// </summary>
+ [JsonPropertyName("stream_index")]
+ public int? StreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the KeyFrame.
+ /// </summary>
+ [JsonPropertyName("key_frame")]
+ public int? KeyFrame { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Pts.
+ /// </summary>
+ [JsonPropertyName("pts")]
+ public long? Pts { get; set; }
+
+ /// <summary>
+ /// Gets or sets the PtsTime.
+ /// </summary>
+ [JsonPropertyName("pts_time")]
+ public string? PtsTime { get; set; }
+
+ /// <summary>
+ /// Gets or sets the BestEffortTimestamp.
+ /// </summary>
+ [JsonPropertyName("best_effort_timestamp")]
+ public long BestEffortTimestamp { get; set; }
+
+ /// <summary>
+ /// Gets or sets the BestEffortTimestampTime.
+ /// </summary>
+ [JsonPropertyName("best_effort_timestamp_time")]
+ public string? BestEffortTimestampTime { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Duration.
+ /// </summary>
+ [JsonPropertyName("duration")]
+ public int Duration { get; set; }
+
+ /// <summary>
+ /// Gets or sets the DurationTime.
+ /// </summary>
+ [JsonPropertyName("duration_time")]
+ public string? DurationTime { get; set; }
+
+ /// <summary>
+ /// Gets or sets the PktPos.
+ /// </summary>
+ [JsonPropertyName("pkt_pos")]
+ public string? PktPos { get; set; }
+
+ /// <summary>
+ /// Gets or sets the PktSize.
+ /// </summary>
+ [JsonPropertyName("pkt_size")]
+ public string? PktSize { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Width.
+ /// </summary>
+ [JsonPropertyName("width")]
+ public int? Width { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Height.
+ /// </summary>
+ [JsonPropertyName("height")]
+ public int? Height { get; set; }
+
+ /// <summary>
+ /// Gets or sets the CropTop.
+ /// </summary>
+ [JsonPropertyName("crop_top")]
+ public int? CropTop { get; set; }
+
+ /// <summary>
+ /// Gets or sets the CropBottom.
+ /// </summary>
+ [JsonPropertyName("crop_bottom")]
+ public int? CropBottom { get; set; }
+
+ /// <summary>
+ /// Gets or sets the CropLeft.
+ /// </summary>
+ [JsonPropertyName("crop_left")]
+ public int? CropLeft { get; set; }
+
+ /// <summary>
+ /// Gets or sets the CropRight.
+ /// </summary>
+ [JsonPropertyName("crop_right")]
+ public int? CropRight { get; set; }
+
+ /// <summary>
+ /// Gets or sets the PixFmt.
+ /// </summary>
+ [JsonPropertyName("pix_fmt")]
+ public string? PixFmt { get; set; }
+
+ /// <summary>
+ /// Gets or sets the SampleAspectRatio.
+ /// </summary>
+ [JsonPropertyName("sample_aspect_ratio")]
+ public string? SampleAspectRatio { get; set; }
+
+ /// <summary>
+ /// Gets or sets the PictType.
+ /// </summary>
+ [JsonPropertyName("pict_type")]
+ public string? PictType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the InterlacedFrame.
+ /// </summary>
+ [JsonPropertyName("interlaced_frame")]
+ public int? InterlacedFrame { get; set; }
+
+ /// <summary>
+ /// Gets or sets the TopFieldFirst.
+ /// </summary>
+ [JsonPropertyName("top_field_first")]
+ public int? TopFieldFirst { get; set; }
+
+ /// <summary>
+ /// Gets or sets the RepeatPict.
+ /// </summary>
+ [JsonPropertyName("repeat_pict")]
+ public int? RepeatPict { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ColorRange.
+ /// </summary>
+ [JsonPropertyName("color_range")]
+ public string? ColorRange { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ColorSpace.
+ /// </summary>
+ [JsonPropertyName("color_space")]
+ public string? ColorSpace { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ColorPrimaries.
+ /// </summary>
+ [JsonPropertyName("color_primaries")]
+ public string? ColorPrimaries { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ColorTransfer.
+ /// </summary>
+ [JsonPropertyName("color_transfer")]
+ public string? ColorTransfer { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ChromaLocation.
+ /// </summary>
+ [JsonPropertyName("chroma_location")]
+ public string? ChromaLocation { get; set; }
+
+ /// <summary>
+ /// Gets or sets the SideDataList.
+ /// </summary>
+ [JsonPropertyName("side_data_list")]
+ public IReadOnlyList<MediaFrameSideDataInfo>? SideDataList { get; set; }
+}
diff --git a/MediaBrowser.MediaEncoding/Probing/MediaFrameSideDataInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaFrameSideDataInfo.cs
new file mode 100644
index 000000000..3f7dd9a69
--- /dev/null
+++ b/MediaBrowser.MediaEncoding/Probing/MediaFrameSideDataInfo.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.MediaEncoding.Probing;
+
+/// <summary>
+/// Class MediaFrameSideDataInfo.
+/// Currently only records the SideDataType for HDR10+ detection.
+/// </summary>
+public class MediaFrameSideDataInfo
+{
+ /// <summary>
+ /// Gets or sets the SideDataType.
+ /// </summary>
+ [JsonPropertyName("side_data_type")]
+ public string? SideDataType { get; set; }
+}
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 6b0fd9a14..a98dbe597 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -105,8 +105,9 @@ namespace MediaBrowser.MediaEncoding.Probing
SetSize(data, info);
var internalStreams = data.Streams ?? Array.Empty<MediaStreamInfo>();
+ var internalFrames = data.Frames ?? Array.Empty<MediaFrameInfo>();
- info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format))
+ info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format, internalFrames))
.Where(i => i is not null)
// Drop subtitle streams if we don't know the codec because it will just cause failures if we don't know how to handle them
.Where(i => i.Type != MediaStreamType.Subtitle || !string.IsNullOrWhiteSpace(i.Codec))
@@ -685,8 +686,9 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <param name="isAudio">if set to <c>true</c> [is info].</param>
/// <param name="streamInfo">The stream info.</param>
/// <param name="formatInfo">The format info.</param>
+ /// <param name="frameInfoList">The frame info.</param>
/// <returns>MediaStream.</returns>
- private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo)
+ private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo, IReadOnlyList<MediaFrameInfo> frameInfoList)
{
// These are mp4 chapters
if (string.Equals(streamInfo.CodecName, "mov_text", StringComparison.OrdinalIgnoreCase))
@@ -904,6 +906,15 @@ namespace MediaBrowser.MediaEncoding.Probing
}
}
}
+
+ var frameInfo = frameInfoList?.FirstOrDefault(i => i.StreamIndex == stream.Index);
+ if (frameInfo?.SideDataList != null)
+ {
+ if (frameInfo.SideDataList.Any(data => string.Equals(data.SideDataType, "HDR Dynamic Metadata SMPTE2094-40 (HDR10+)", StringComparison.OrdinalIgnoreCase)))
+ {
+ stream.Hdr10PlusPresentFlag = true;
+ }
+ }
}
else if (streamInfo.CodecType == CodecType.Data)
{
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index a731d4785..777e33587 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -13,10 +13,10 @@ using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
using MediaBrowser.Common;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
@@ -31,12 +31,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
public sealed class SubtitleEncoder : ISubtitleEncoder, IDisposable
{
private readonly ILogger<SubtitleEncoder> _logger;
- private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
private readonly IMediaEncoder _mediaEncoder;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly ISubtitleParser _subtitleParser;
+ private readonly IPathManager _pathManager;
/// <summary>
/// The _semaphoreLocks.
@@ -49,24 +49,22 @@ namespace MediaBrowser.MediaEncoding.Subtitles
public SubtitleEncoder(
ILogger<SubtitleEncoder> logger,
- IApplicationPaths appPaths,
IFileSystem fileSystem,
IMediaEncoder mediaEncoder,
IHttpClientFactory httpClientFactory,
IMediaSourceManager mediaSourceManager,
- ISubtitleParser subtitleParser)
+ ISubtitleParser subtitleParser,
+ IPathManager pathManager)
{
_logger = logger;
- _appPaths = appPaths;
_fileSystem = fileSystem;
_mediaEncoder = mediaEncoder;
_httpClientFactory = httpClientFactory;
_mediaSourceManager = mediaSourceManager;
_subtitleParser = subtitleParser;
+ _pathManager = pathManager;
}
- private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
-
private MemoryStream ConvertSubtitles(
Stream stream,
string inputFormat,
@@ -830,26 +828,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
private string GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension)
{
- if (mediaSource.Protocol == MediaProtocol.File)
- {
- var ticksParam = string.Empty;
-
- var date = _fileSystem.GetLastWriteTimeUtc(mediaSource.Path);
-
- ReadOnlySpan<char> filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
-
- var prefix = filename.Slice(0, 1);
-
- return Path.Join(SubtitleCachePath, prefix, filename);
- }
- else
- {
- ReadOnlySpan<char> filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
-
- var prefix = filename.Slice(0, 1);
-
- return Path.Join(SubtitleCachePath, prefix, filename);
- }
+ return _pathManager.GetSubtitlePath(mediaSource.Id, subtitleStreamIndex, outputSubtitleExtension);
}
/// <inheritdoc />
diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
index 85bb862c7..0cda803d6 100644
--- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
+++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
@@ -242,14 +242,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
{
- try
- {
- await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
- }
+ await _sessionManager.CloseLiveStreamIfNeededAsync(job.LiveStreamId, job.PlaySessionId).ConfigureAwait(false);
}
}
@@ -405,24 +398,19 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
// If subtitles get burned in fonts may need to be extracted from the media file
if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
{
- var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
{
var concatPath = Path.Join(_appPaths.CachePath, "concat", state.MediaSource.Id + ".concat");
- await _attachmentExtractor.ExtractAllAttachments(concatPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
+ await _attachmentExtractor.ExtractAllAttachments(concatPath, state.MediaSource, cancellationTokenSource.Token).ConfigureAwait(false);
}
else
{
- await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
+ await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, cancellationTokenSource.Token).ConfigureAwait(false);
}
if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase))
{
- string subtitlePath = state.SubtitleStream.Path;
- string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
- string subtitleId = subtitlePath.GetMD5().ToString("N", CultureInfo.InvariantCulture);
-
- await _attachmentExtractor.ExtractAllAttachmentsExternal(subtitlePathArgument, subtitleId, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
+ await _attachmentExtractor.ExtractAllAttachments(state.SubtitleStream.Path, state.MediaSource, cancellationTokenSource.Token).ConfigureAwait(false);
}
}
diff --git a/MediaBrowser.Model/Branding/BrandingOptions.cs b/MediaBrowser.Model/Branding/BrandingOptions.cs
index c6580598b..5ec6b0dd4 100644
--- a/MediaBrowser.Model/Branding/BrandingOptions.cs
+++ b/MediaBrowser.Model/Branding/BrandingOptions.cs
@@ -1,5 +1,3 @@
-using System.Text.Json.Serialization;
-
namespace MediaBrowser.Model.Branding;
/// <summary>
@@ -27,10 +25,5 @@ public class BrandingOptions
/// <summary>
/// Gets or sets the splashscreen location on disk.
/// </summary>
- /// <remarks>
- /// Not served via the API.
- /// Only used to save the custom uploaded user splashscreen in the configuration file.
- /// </remarks>
- [JsonIgnore]
public string? SplashscreenLocation { get; set; }
}
diff --git a/MediaBrowser.Model/Branding/BrandingOptionsDto.cs b/MediaBrowser.Model/Branding/BrandingOptionsDto.cs
new file mode 100644
index 000000000..c0d8cb31c
--- /dev/null
+++ b/MediaBrowser.Model/Branding/BrandingOptionsDto.cs
@@ -0,0 +1,25 @@
+namespace MediaBrowser.Model.Branding;
+
+/// <summary>
+/// The branding options DTO for API use.
+/// This DTO excludes SplashscreenLocation to prevent it from being updated via API.
+/// </summary>
+public class BrandingOptionsDto
+{
+ /// <summary>
+ /// Gets or sets the login disclaimer.
+ /// </summary>
+ /// <value>The login disclaimer.</value>
+ public string? LoginDisclaimer { get; set; }
+
+ /// <summary>
+ /// Gets or sets the custom CSS.
+ /// </summary>
+ /// <value>The custom CSS.</value>
+ public string? CustomCss { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable the splashscreen.
+ /// </summary>
+ public bool SplashscreenEnabled { get; set; } = false;
+}
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 693bf90e7..a58c01c96 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -178,6 +178,11 @@ public class ServerConfiguration : BaseApplicationConfiguration
public int LibraryUpdateDuration { get; set; } = 30;
/// <summary>
+ /// Gets or sets the maximum amount of items to cache.
+ /// </summary>
+ public int CacheSize { get; set; } = Environment.ProcessorCount * 100;
+
+ /// <summary>
/// Gets or sets the image saving convention.
/// </summary>
/// <value>The image saving convention.</value>
@@ -199,7 +204,9 @@ public class ServerConfiguration : BaseApplicationConfiguration
public bool EnableFolderView { get; set; } = false;
- public bool EnableGroupingIntoCollections { get; set; } = false;
+ public bool EnableGroupingMoviesIntoCollections { get; set; } = false;
+
+ public bool EnableGroupingShowsIntoCollections { get; set; } = false;
public bool DisplaySpecialsWithinSeasons { get; set; } = true;
diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
index 1b046f54e..1b61bfe15 100644
--- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs
+++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
@@ -28,6 +28,7 @@ namespace MediaBrowser.Model.Dlna
/// <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="numStreams">The number of streams.</param>
/// <param name="numVideoStreams">The number of video streams.</param>
/// <param name="numAudioStreams">The number of audio streams.</param>
/// <param name="videoCodecTag">The video codec tag.</param>
@@ -48,6 +49,7 @@ namespace MediaBrowser.Model.Dlna
bool? isAnamorphic,
bool? isInterlaced,
int? refFrames,
+ int numStreams,
int? numVideoStreams,
int? numAudioStreams,
string? videoCodecTag,
@@ -83,6 +85,8 @@ namespace MediaBrowser.Model.Dlna
return IsConditionSatisfied(condition, width);
case ProfileConditionValue.RefFrames:
return IsConditionSatisfied(condition, refFrames);
+ case ProfileConditionValue.NumStreams:
+ return IsConditionSatisfied(condition, numStreams);
case ProfileConditionValue.NumAudioStreams:
return IsConditionSatisfied(condition, numAudioStreams);
case ProfileConditionValue.NumVideoStreams:
@@ -341,6 +345,15 @@ namespace MediaBrowser.Model.Dlna
return !condition.IsRequired;
}
+ // Special case: HDR10 also satisfies if the video is HDR10Plus
+ if (currentValue.Value == VideoRangeType.HDR10Plus)
+ {
+ if (IsConditionSatisfied(condition, VideoRangeType.HDR10))
+ {
+ return true;
+ }
+ }
+
var conditionType = condition.Condition;
if (conditionType == ProfileConditionType.EqualsAny)
{
diff --git a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs
index a32433e18..b66a15840 100644
--- a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs
+++ b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs
@@ -27,6 +27,7 @@ namespace MediaBrowser.Model.Dlna
IsInterlaced = 21,
AudioSampleRate = 22,
AudioBitDepth = 23,
- VideoRangeType = 24
+ VideoRangeType = 24,
+ NumStreams = 25
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index e48411e7a..806900e9a 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -338,6 +338,9 @@ namespace MediaBrowser.Model.Dlna
case ProfileConditionValue.IsSecondaryAudio:
return TranscodeReason.SecondaryAudioNotSupported;
+ case ProfileConditionValue.NumStreams:
+ return TranscodeReason.StreamCountExceedsLimit;
+
case ProfileConditionValue.NumAudioStreams:
// TODO
return 0;
@@ -797,7 +800,7 @@ namespace MediaBrowser.Model.Dlna
options.SubtitleStreamIndex,
playlistItem.PlayMethod,
playlistItem.TranscodeReasons,
- playlistItem.ToUrl("media:", "<token>"));
+ playlistItem.ToUrl("media:", "<token>", null));
item.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile);
return playlistItem;
@@ -1019,6 +1022,7 @@ namespace MediaBrowser.Model.Dlna
int? packetLength = videoStream?.PacketLength;
int? refFrames = videoStream?.RefFrames;
+ int numStreams = item.MediaStreams.Count;
int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
@@ -1027,7 +1031,7 @@ namespace MediaBrowser.Model.Dlna
var appliedVideoConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.Video &&
i.ContainsAnyCodec(playlistItem.VideoCodecs, container, useSubContainer) &&
- i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)))
+ i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numStreams, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)))
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
.Reverse();
foreach (var condition in appliedVideoConditions)
@@ -1850,6 +1854,7 @@ namespace MediaBrowser.Model.Dlna
case ProfileConditionValue.AudioProfile:
case ProfileConditionValue.Has64BitOffsets:
case ProfileConditionValue.PacketLength:
+ case ProfileConditionValue.NumStreams:
case ProfileConditionValue.NumAudioStreams:
case ProfileConditionValue.NumVideoStreams:
case ProfileConditionValue.IsSecondaryAudio:
@@ -2258,10 +2263,11 @@ namespace MediaBrowser.Model.Dlna
int? packetLength = videoStream?.PacketLength;
int? refFrames = videoStream?.RefFrames;
+ int numStreams = mediaSource.MediaStreams.Count;
int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio);
int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video);
- return conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc));
+ return conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numStreams, numVideoStreams, numAudioStreams, videoCodecTag, isAvc));
}
/// <summary>
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index e44152213..13acd15a3 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -1,7 +1,13 @@
+#pragma warning disable CA1819 // Properties should not return arrays
+
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Globalization;
+using System.Linq;
+using System.Text;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -871,202 +877,271 @@ public class StreamInfo
/// </summary>
/// <param name="baseUrl">The base Url.</param>
/// <param name="accessToken">The access Token.</param>
+ /// <param name="query">Optional extra query.</param>
/// <returns>A querystring representation of this object.</returns>
- public string ToUrl(string baseUrl, string? accessToken)
+ public string ToUrl(string? baseUrl, string? accessToken, string? query)
{
- ArgumentException.ThrowIfNullOrEmpty(baseUrl);
+ var sb = new StringBuilder();
+ if (!string.IsNullOrEmpty(baseUrl))
+ {
+ sb.Append(baseUrl.TrimEnd('/'));
+ }
- List<string> list = [];
- foreach (NameValuePair pair in BuildParams(this, accessToken))
+ if (MediaType == DlnaProfileType.Audio)
{
- if (string.IsNullOrEmpty(pair.Value))
- {
- continue;
- }
+ sb.Append("/audio/");
+ }
+ else
+ {
+ sb.Append("/videos/");
+ }
- // Try to keep the url clean by omitting defaults
- if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase)
- && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
+ sb.Append(ItemId);
- if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase)
- && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
+ if (SubProtocol == MediaStreamProtocol.hls)
+ {
+ sb.Append("/master.m3u8?");
+ }
+ else
+ {
+ sb.Append("/stream");
- if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase)
- && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(Container))
{
- continue;
+ sb.Append('.');
+ sb.Append(Container);
}
- var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal);
+ sb.Append('?');
+ }
- list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
+ if (!string.IsNullOrEmpty(DeviceProfileId))
+ {
+ sb.Append("&DeviceProfileId=");
+ sb.Append(DeviceProfileId);
}
- string queryString = string.Join('&', list);
+ if (!string.IsNullOrEmpty(DeviceId))
+ {
+ sb.Append("&DeviceId=");
+ sb.Append(DeviceId);
+ }
- return GetUrl(baseUrl, queryString);
- }
+ if (!string.IsNullOrEmpty(MediaSourceId))
+ {
+ sb.Append("&MediaSourceId=");
+ sb.Append(MediaSourceId);
+ }
- private string GetUrl(string baseUrl, string queryString)
- {
- ArgumentException.ThrowIfNullOrEmpty(baseUrl);
+ // default true so don't store.
+ if (IsDirectStream)
+ {
+ sb.Append("&Static=true");
+ }
- string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
+ if (VideoCodecs.Count != 0)
+ {
+ sb.Append("&VideoCodec=");
+ sb.AppendJoin(',', VideoCodecs);
+ }
- baseUrl = baseUrl.TrimEnd('/');
+ if (AudioCodecs.Count != 0)
+ {
+ sb.Append("&AudioCodec=");
+ sb.AppendJoin(',', AudioCodecs);
+ }
- if (MediaType == DlnaProfileType.Audio)
+ if (AudioStreamIndex.HasValue)
{
- if (SubProtocol == MediaStreamProtocol.hls)
- {
- return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
- }
+ sb.Append("&AudioStreamIndex=");
+ sb.Append(AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture));
+ }
- return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
+ if (SubtitleStreamIndex.HasValue && (AlwaysBurnInSubtitleWhenTranscoding || SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) && SubtitleStreamIndex != -1)
+ {
+ sb.Append("&SubtitleStreamIndex=");
+ sb.Append(SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture));
}
- if (SubProtocol == MediaStreamProtocol.hls)
+ if (VideoBitrate.HasValue)
{
- return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+ sb.Append("&VideoBitrate=");
+ sb.Append(VideoBitrate.Value.ToString(CultureInfo.InvariantCulture));
}
- return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
- }
+ if (AudioBitrate.HasValue)
+ {
+ sb.Append("&AudioBitrate=");
+ sb.Append(AudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
+ }
- private static List<NameValuePair> BuildParams(StreamInfo item, string? accessToken)
- {
- List<NameValuePair> list = [];
+ if (AudioSampleRate.HasValue)
+ {
+ sb.Append("&AudioSampleRate=");
+ sb.Append(AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
+ }
- string audioCodecs = item.AudioCodecs.Count == 0 ?
- string.Empty :
- string.Join(',', item.AudioCodecs);
+ if (MaxFramerate.HasValue)
+ {
+ sb.Append("&MaxFramerate=");
+ sb.Append(MaxFramerate.Value.ToString(CultureInfo.InvariantCulture));
+ }
- string videoCodecs = item.VideoCodecs.Count == 0 ?
- string.Empty :
- string.Join(',', item.VideoCodecs);
+ if (MaxWidth.HasValue)
+ {
+ sb.Append("&MaxWidth=");
+ sb.Append(MaxWidth.Value.ToString(CultureInfo.InvariantCulture));
+ }
- list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
- list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
- list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
- list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
- list.Add(new NameValuePair("VideoCodec", videoCodecs));
- list.Add(new NameValuePair("AudioCodec", audioCodecs));
- list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && (item.AlwaysBurnInSubtitleWhenTranscoding || item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ if (MaxHeight.HasValue)
+ {
+ sb.Append("&MaxHeight=");
+ sb.Append(MaxHeight.Value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ if (SubProtocol == MediaStreamProtocol.hls)
+ {
+ if (!string.IsNullOrEmpty(Container))
+ {
+ sb.Append("&SegmentContainer=");
+ sb.Append(Container);
+ }
- list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
- list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ if (SegmentLength.HasValue)
+ {
+ sb.Append("&SegmentLength=");
+ sb.Append(SegmentLength.Value.ToString(CultureInfo.InvariantCulture));
+ }
- long startPositionTicks = item.StartPositionTicks;
+ if (MinSegments.HasValue)
+ {
+ sb.Append("&MinSegments=");
+ sb.Append(MinSegments.Value.ToString(CultureInfo.InvariantCulture));
+ }
- if (item.SubProtocol == MediaStreamProtocol.hls)
- {
- list.Add(new NameValuePair("StartTimeTicks", string.Empty));
+ sb.Append("&BreakOnNonKeyFrames=");
+ sb.Append(BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture));
}
else
{
- list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture)));
+ if (StartPositionTicks != 0)
+ {
+ sb.Append("&StartTimeTicks=");
+ sb.Append(StartPositionTicks.ToString(CultureInfo.InvariantCulture));
+ }
}
- list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
- list.Add(new NameValuePair("ApiKey", accessToken ?? string.Empty));
+ if (!string.IsNullOrEmpty(PlaySessionId))
+ {
+ sb.Append("&PlaySessionId=");
+ sb.Append(PlaySessionId);
+ }
- string? liveStreamId = item.MediaSource?.LiveStreamId;
- list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
+ if (!string.IsNullOrEmpty(accessToken))
+ {
+ sb.Append("&ApiKey=");
+ sb.Append(accessToken);
+ }
- list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
+ var liveStreamId = MediaSource?.LiveStreamId;
+ if (!string.IsNullOrEmpty(liveStreamId))
+ {
+ sb.Append("&LiveStreamId=");
+ sb.Append(liveStreamId);
+ }
- if (!item.IsDirectStream)
+ if (!IsDirectStream)
{
- if (item.RequireNonAnamorphic)
+ if (RequireNonAnamorphic)
{
- list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ sb.Append("&RequireNonAnamorphic=");
+ sb.Append(RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture));
}
- list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ if (TranscodingMaxAudioChannels.HasValue)
+ {
+ sb.Append("&TranscodingMaxAudioChannels=");
+ sb.Append(TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
+ }
- if (item.EnableSubtitlesInManifest)
+ if (EnableSubtitlesInManifest)
{
- list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ sb.Append("&EnableSubtitlesInManifest=");
+ sb.Append(EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture));
}
- if (item.EnableMpegtsM2TsMode)
+ if (EnableMpegtsM2TsMode)
{
- list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ sb.Append("&EnableMpegtsM2TsMode=");
+ sb.Append(EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture));
}
- if (item.EstimateContentLength)
+ if (EstimateContentLength)
{
- list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ sb.Append("&EstimateContentLength=");
+ sb.Append(EstimateContentLength.ToString(CultureInfo.InvariantCulture));
}
- if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto)
+ if (TranscodeSeekInfo != TranscodeSeekInfo.Auto)
{
- list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant()));
+ sb.Append("&TranscodeSeekInfo=");
+ sb.Append(TranscodeSeekInfo.ToString());
}
- if (item.CopyTimestamps)
+ if (CopyTimestamps)
{
- list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ sb.Append("&CopyTimestamps=");
+ sb.Append(CopyTimestamps.ToString(CultureInfo.InvariantCulture));
}
- list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ sb.Append("&RequireAvc=");
+ sb.Append(RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
- list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ sb.Append("&EnableAudioVbrEncoding=");
+ sb.Append(EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
}
- list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty));
-
- string subtitleCodecs = item.SubtitleCodecs.Count == 0 ?
- string.Empty :
- string.Join(",", item.SubtitleCodecs);
-
- list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
-
- if (item.SubProtocol == MediaStreamProtocol.hls)
+ var etag = MediaSource?.ETag;
+ if (!string.IsNullOrEmpty(etag))
{
- list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
-
- if (item.SegmentLength.HasValue)
- {
- list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
- }
+ sb.Append("&Tag=");
+ sb.Append(etag);
+ }
- if (item.MinSegments.HasValue)
- {
- list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
- }
+ if (SubtitleStreamIndex.HasValue && SubtitleDeliveryMethod != SubtitleDeliveryMethod.External)
+ {
+ sb.Append("&SubtitleMethod=");
+ sb.Append(SubtitleDeliveryMethod);
+ }
- list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)));
+ if (SubtitleStreamIndex.HasValue && SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed && SubtitleCodecs.Count != 0)
+ {
+ sb.Append("&SubtitleCodec=");
+ sb.AppendJoin(',', SubtitleCodecs);
}
- foreach (var pair in item.StreamOptions)
+ foreach (var pair in StreamOptions)
{
- if (string.IsNullOrEmpty(pair.Value))
- {
- continue;
- }
+ // Strip spaces to avoid having to encode h264 profile names
+ sb.Append('&');
+ sb.Append(pair.Key);
+ sb.Append('=');
+ sb.Append(pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal));
+ }
- // strip spaces to avoid having to encode h264 profile names
- list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal)));
+ var transcodeReasonsValues = TranscodeReasons.GetUniqueFlags().ToArray();
+ if (!IsDirectStream && transcodeReasonsValues.Length > 0)
+ {
+ sb.Append("&TranscodeReasons=");
+ sb.AppendJoin(',', transcodeReasonsValues);
}
- if (!item.IsDirectStream)
+ if (!string.IsNullOrEmpty(query))
{
- list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString()));
+ sb.Append(query);
}
- return list;
+ return sb.ToString();
}
/// <summary>
diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs
index 7bfd8ca29..b38763fbf 100644
--- a/MediaBrowser.Model/Dto/BaseItemDto.cs
+++ b/MediaBrowser.Model/Dto/BaseItemDto.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Model.Drawing;
@@ -586,6 +587,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the type of the media.
/// </summary>
/// <value>The type of the media.</value>
+ [DefaultValue(MediaType.Unknown)]
public MediaType MediaType { get; set; }
/// <summary>
diff --git a/MediaBrowser.Model/Dto/BaseItemPerson.cs b/MediaBrowser.Model/Dto/BaseItemPerson.cs
index d3bcf492d..80e2cfb08 100644
--- a/MediaBrowser.Model/Dto/BaseItemPerson.cs
+++ b/MediaBrowser.Model/Dto/BaseItemPerson.cs
@@ -1,6 +1,7 @@
#nullable disable
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
@@ -34,6 +35,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the type.
/// </summary>
/// <value>The type.</value>
+ [DefaultValue(PersonKind.Unknown)]
public PersonKind Type { get; set; }
/// <summary>
diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
index eff2e09da..66de18cfe 100644
--- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs
+++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
@@ -1,12 +1,10 @@
#nullable disable
#pragma warning disable CS1591
-using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
-using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Session;
@@ -17,10 +15,10 @@ namespace MediaBrowser.Model.Dto
{
public MediaSourceInfo()
{
- Formats = Array.Empty<string>();
- MediaStreams = Array.Empty<MediaStream>();
- MediaAttachments = Array.Empty<MediaAttachment>();
- RequiredHttpHeaders = new Dictionary<string, string>();
+ Formats = [];
+ MediaStreams = [];
+ MediaAttachments = [];
+ RequiredHttpHeaders = [];
SupportsTranscoding = true;
SupportsDirectStream = true;
SupportsDirectPlay = true;
diff --git a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs
index a3035bf61..2f3a5d117 100644
--- a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs
+++ b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs
@@ -1,35 +1,55 @@
-#pragma warning disable CS1591
-
-using System;
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Providers;
-namespace MediaBrowser.Model.Dto
+namespace MediaBrowser.Model.Dto;
+
+/// <summary>
+/// A class representing metadata editor information.
+/// </summary>
+public class MetadataEditorInfo
{
- public class MetadataEditorInfo
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MetadataEditorInfo"/> class.
+ /// </summary>
+ public MetadataEditorInfo()
{
- public MetadataEditorInfo()
- {
- ParentalRatingOptions = Array.Empty<ParentalRating>();
- Countries = Array.Empty<CountryInfo>();
- Cultures = Array.Empty<CultureDto>();
- ExternalIdInfos = Array.Empty<ExternalIdInfo>();
- ContentTypeOptions = Array.Empty<NameValuePair>();
- }
-
- public IReadOnlyList<ParentalRating> ParentalRatingOptions { get; set; }
-
- public IReadOnlyList<CountryInfo> Countries { get; set; }
-
- public IReadOnlyList<CultureDto> Cultures { get; set; }
-
- public IReadOnlyList<ExternalIdInfo> ExternalIdInfos { get; set; }
-
- public CollectionType? ContentType { get; set; }
-
- public IReadOnlyList<NameValuePair> ContentTypeOptions { get; set; }
+ ParentalRatingOptions = [];
+ Countries = [];
+ Cultures = [];
+ ExternalIdInfos = [];
+ ContentTypeOptions = [];
}
+
+ /// <summary>
+ /// Gets or sets the parental rating options.
+ /// </summary>
+ public IReadOnlyList<ParentalRating> ParentalRatingOptions { get; set; }
+
+ /// <summary>
+ /// Gets or sets the countries.
+ /// </summary>
+ public IReadOnlyList<CountryInfo> Countries { get; set; }
+
+ /// <summary>
+ /// Gets or sets the cultures.
+ /// </summary>
+ public IReadOnlyList<CultureDto> Cultures { get; set; }
+
+ /// <summary>
+ /// Gets or sets the external id infos.
+ /// </summary>
+ public IReadOnlyList<ExternalIdInfo> ExternalIdInfos { get; set; }
+
+ /// <summary>
+ /// Gets or sets the content type.
+ /// </summary>
+ public CollectionType? ContentType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the content type options.
+ /// </summary>
+ public IReadOnlyList<NameValuePair> ContentTypeOptions { get; set; }
}
diff --git a/MediaBrowser.Model/Entities/MediaAttachment.cs b/MediaBrowser.Model/Entities/MediaAttachment.cs
index 34e3eabc9..f8f7ad0f9 100644
--- a/MediaBrowser.Model/Entities/MediaAttachment.cs
+++ b/MediaBrowser.Model/Entities/MediaAttachment.cs
@@ -1,51 +1,49 @@
-#nullable disable
-namespace MediaBrowser.Model.Entities
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// Class MediaAttachment.
+/// </summary>
+public class MediaAttachment
{
/// <summary>
- /// Class MediaAttachment.
+ /// Gets or sets the codec.
/// </summary>
- public class MediaAttachment
- {
- /// <summary>
- /// Gets or sets the codec.
- /// </summary>
- /// <value>The codec.</value>
- public string Codec { get; set; }
+ /// <value>The codec.</value>
+ public string? Codec { get; set; }
- /// <summary>
- /// Gets or sets the codec tag.
- /// </summary>
- /// <value>The codec tag.</value>
- public string CodecTag { get; set; }
+ /// <summary>
+ /// Gets or sets the codec tag.
+ /// </summary>
+ /// <value>The codec tag.</value>
+ public string? CodecTag { get; set; }
- /// <summary>
- /// Gets or sets the comment.
- /// </summary>
- /// <value>The comment.</value>
- public string Comment { get; set; }
+ /// <summary>
+ /// Gets or sets the comment.
+ /// </summary>
+ /// <value>The comment.</value>
+ public string? Comment { get; set; }
- /// <summary>
- /// Gets or sets the index.
- /// </summary>
- /// <value>The index.</value>
- public int Index { get; set; }
+ /// <summary>
+ /// Gets or sets the index.
+ /// </summary>
+ /// <value>The index.</value>
+ public int Index { get; set; }
- /// <summary>
- /// Gets or sets the filename.
- /// </summary>
- /// <value>The filename.</value>
- public string FileName { get; set; }
+ /// <summary>
+ /// Gets or sets the filename.
+ /// </summary>
+ /// <value>The filename.</value>
+ public string? FileName { get; set; }
- /// <summary>
- /// Gets or sets the MIME type.
- /// </summary>
- /// <value>The MIME type.</value>
- public string MimeType { get; set; }
+ /// <summary>
+ /// Gets or sets the MIME type.
+ /// </summary>
+ /// <value>The MIME type.</value>
+ public string? MimeType { get; set; }
- /// <summary>
- /// Gets or sets the delivery URL.
- /// </summary>
- /// <value>The delivery URL.</value>
- public string DeliveryUrl { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the delivery URL.
+ /// </summary>
+ /// <value>The delivery URL.</value>
+ public string? DeliveryUrl { get; set; }
}
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index 400768ef3..95b5b43f8 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -153,10 +153,13 @@ namespace MediaBrowser.Model.Entities
/// <value>The title.</value>
public string Title { get; set; }
+ public bool? Hdr10PlusPresentFlag { get; set; }
+
/// <summary>
/// Gets the video range.
/// </summary>
/// <value>The video range.</value>
+ [DefaultValue(VideoRange.Unknown)]
public VideoRange VideoRange
{
get
@@ -171,6 +174,7 @@ namespace MediaBrowser.Model.Entities
/// Gets the video range type.
/// </summary>
/// <value>The video range type.</value>
+ [DefaultValue(VideoRangeType.Unknown)]
public VideoRangeType VideoRangeType
{
get
@@ -778,8 +782,8 @@ namespace MediaBrowser.Model.Entities
var blPresentFlag = BlPresentFlag == 1;
var dvBlCompatId = DvBlSignalCompatibilityId;
- var isDoViProfile = dvProfile == 5 || dvProfile == 7 || dvProfile == 8 || dvProfile == 10;
- var isDoViFlag = rpuPresentFlag && blPresentFlag && (dvBlCompatId == 0 || dvBlCompatId == 1 || dvBlCompatId == 4 || dvBlCompatId == 2 || dvBlCompatId == 6);
+ var isDoViProfile = dvProfile is 5 or 7 or 8 or 10;
+ var isDoViFlag = rpuPresentFlag && blPresentFlag && dvBlCompatId is 0 or 1 or 4 or 2 or 6;
if ((isDoViProfile && isDoViFlag)
|| string.Equals(codecTag, "dovi", StringComparison.OrdinalIgnoreCase)
@@ -787,7 +791,7 @@ namespace MediaBrowser.Model.Entities
|| string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase))
{
- return dvProfile switch
+ var dvRangeSet = dvProfile switch
{
5 => (VideoRange.HDR, VideoRangeType.DOVI),
8 => dvBlCompatId switch
@@ -795,32 +799,40 @@ namespace MediaBrowser.Model.Entities
1 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10),
4 => (VideoRange.HDR, VideoRangeType.DOVIWithHLG),
2 => (VideoRange.SDR, VideoRangeType.DOVIWithSDR),
- // While not in Dolby Spec, Profile 8 CCid 6 media are possible to create, and since CCid 6 stems from Bluray (Profile 7 originally) an HDR10 base layer is guaranteed to exist.
- 6 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10),
- // There is no other case to handle here as per Dolby Spec. Default case included for completeness and linting purposes
- _ => (VideoRange.SDR, VideoRangeType.SDR)
+ // Out of Dolby Spec files should be marked as invalid
+ _ => (VideoRange.HDR, VideoRangeType.DOVIInvalid)
},
- 7 => (VideoRange.HDR, VideoRangeType.HDR10),
+ 7 => (VideoRange.HDR, VideoRangeType.DOVIWithEL),
10 => dvBlCompatId switch
{
0 => (VideoRange.HDR, VideoRangeType.DOVI),
1 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10),
2 => (VideoRange.SDR, VideoRangeType.DOVIWithSDR),
4 => (VideoRange.HDR, VideoRangeType.DOVIWithHLG),
- // While not in Dolby Spec, Profile 8 CCid 6 media are possible to create, and since CCid 6 stems from Bluray (Profile 7 originally) an HDR10 base layer is guaranteed to exist.
- 6 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10),
- // There is no other case to handle here as per Dolby Spec. Default case included for completeness and linting purposes
- _ => (VideoRange.SDR, VideoRangeType.SDR)
+ // Out of Dolby Spec files should be marked as invalid
+ _ => (VideoRange.HDR, VideoRangeType.DOVIInvalid)
},
_ => (VideoRange.SDR, VideoRangeType.SDR)
};
+
+ if (Hdr10PlusPresentFlag == true)
+ {
+ return dvRangeSet.Item2 switch
+ {
+ VideoRangeType.DOVIWithHDR10 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10Plus),
+ VideoRangeType.DOVIWithEL => (VideoRange.HDR, VideoRangeType.DOVIWithELHDR10Plus),
+ _ => dvRangeSet
+ };
+ }
+
+ return dvRangeSet;
}
var colorTransfer = ColorTransfer;
if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase))
{
- return (VideoRange.HDR, VideoRangeType.HDR10);
+ return Hdr10PlusPresentFlag == true ? (VideoRange.HDR, VideoRangeType.HDR10Plus) : (VideoRange.HDR, VideoRangeType.HDR10);
}
else if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
{
diff --git a/MediaBrowser.Model/Entities/ParentalRating.cs b/MediaBrowser.Model/Entities/ParentalRating.cs
index c92640818..4f1198902 100644
--- a/MediaBrowser.Model/Entities/ParentalRating.cs
+++ b/MediaBrowser.Model/Entities/ParentalRating.cs
@@ -1,33 +1,40 @@
-#nullable disable
-#pragma warning disable CS1591
+namespace MediaBrowser.Model.Entities;
-namespace MediaBrowser.Model.Entities
+/// <summary>
+/// Class ParentalRating.
+/// </summary>
+public class ParentalRating
{
/// <summary>
- /// Class ParentalRating.
+ /// Initializes a new instance of the <see cref="ParentalRating"/> class.
/// </summary>
- public class ParentalRating
+ /// <param name="name">The name.</param>
+ /// <param name="score">The score.</param>
+ public ParentalRating(string name, ParentalRatingScore? score)
{
- public ParentalRating()
- {
- }
+ Name = name;
+ Value = score?.Score;
+ RatingScore = score;
+ }
- public ParentalRating(string name, int? value)
- {
- Name = name;
- Value = value;
- }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the value.
+ /// </summary>
+ /// <value>The value.</value>
+ /// <remarks>
+ /// Deprecated.
+ /// </remarks>
+ public int? Value { get; set; }
- /// <summary>
- /// Gets or sets the value.
- /// </summary>
- /// <value>The value.</value>
- public int? Value { get; set; }
- }
+ /// <summary>
+ /// Gets or sets the rating score.
+ /// </summary>
+ /// <value>The rating score.</value>
+ public ParentalRatingScore? RatingScore { get; set; }
}
diff --git a/MediaBrowser.Model/Entities/ParentalRatingEntry.cs b/MediaBrowser.Model/Entities/ParentalRatingEntry.cs
new file mode 100644
index 000000000..69be74ac0
--- /dev/null
+++ b/MediaBrowser.Model/Entities/ParentalRatingEntry.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// A class representing an parental rating entry.
+/// </summary>
+public class ParentalRatingEntry
+{
+ /// <summary>
+ /// Gets or sets the rating strings.
+ /// </summary>
+ [JsonPropertyName("ratingStrings")]
+ public required IReadOnlyList<string> RatingStrings { get; set; }
+
+ /// <summary>
+ /// Gets or sets the score.
+ /// </summary>
+ [JsonPropertyName("ratingScore")]
+ public required ParentalRatingScore RatingScore { get; set; }
+}
diff --git a/MediaBrowser.Model/Entities/ParentalRatingScore.cs b/MediaBrowser.Model/Entities/ParentalRatingScore.cs
new file mode 100644
index 000000000..b9bb99685
--- /dev/null
+++ b/MediaBrowser.Model/Entities/ParentalRatingScore.cs
@@ -0,0 +1,32 @@
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// A class representing an parental rating score.
+/// </summary>
+public class ParentalRatingScore
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ParentalRatingScore"/> class.
+ /// </summary>
+ /// <param name="score">The score.</param>
+ /// <param name="subScore">The sub score.</param>
+ public ParentalRatingScore(int score, int? subScore)
+ {
+ Score = score;
+ SubScore = subScore;
+ }
+
+ /// <summary>
+ /// Gets or sets the score.
+ /// </summary>
+ [JsonPropertyName("score")]
+ public int Score { get; set; }
+
+ /// <summary>
+ /// Gets or sets the sub score.
+ /// </summary>
+ [JsonPropertyName("subScore")]
+ public int? SubScore { get; set; }
+}
diff --git a/MediaBrowser.Model/Entities/ParentalRatingSystem.cs b/MediaBrowser.Model/Entities/ParentalRatingSystem.cs
new file mode 100644
index 000000000..b452f2901
--- /dev/null
+++ b/MediaBrowser.Model/Entities/ParentalRatingSystem.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Model.Entities;
+
+/// <summary>
+/// A class representing a parental rating system.
+/// </summary>
+public class ParentalRatingSystem
+{
+ /// <summary>
+ /// Gets or sets the country code.
+ /// </summary>
+ [JsonPropertyName("countryCode")]
+ public required string CountryCode { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether sub scores are supported.
+ /// </summary>
+ [JsonPropertyName("supportsSubScores")]
+ public bool SupportsSubScores { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ratings.
+ /// </summary>
+ [JsonPropertyName("ratings")]
+ public IReadOnlyList<ParentalRatingEntry>? Ratings { get; set; }
+}
diff --git a/MediaBrowser.Model/Extensions/ContainerHelper.cs b/MediaBrowser.Model/Extensions/ContainerHelper.cs
index 39e5358ba..848cc2f62 100644
--- a/MediaBrowser.Model/Extensions/ContainerHelper.cs
+++ b/MediaBrowser.Model/Extensions/ContainerHelper.cs
@@ -21,7 +21,7 @@ public static class ContainerHelper
public static bool ContainsContainer(string? profileContainers, string? inputContainer)
{
var isNegativeList = false;
- if (profileContainers != null && profileContainers.StartsWith('-'))
+ if (profileContainers is not null && profileContainers.StartsWith('-'))
{
isNegativeList = true;
profileContainers = profileContainers[1..];
@@ -42,7 +42,7 @@ public static class ContainerHelper
public static bool ContainsContainer(string? profileContainers, ReadOnlySpan<char> inputContainer)
{
var isNegativeList = false;
- if (profileContainers != null && profileContainers.StartsWith('-'))
+ if (profileContainers is not null && profileContainers.StartsWith('-'))
{
isNegativeList = true;
profileContainers = profileContainers[1..];
diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
index 20deaa505..d9df95325 100644
--- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs
+++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
@@ -1,65 +1,64 @@
using System.Collections.Generic;
using MediaBrowser.Model.Entities;
-namespace MediaBrowser.Model.Globalization
+namespace MediaBrowser.Model.Globalization;
+
+/// <summary>
+/// Interface ILocalizationManager.
+/// </summary>
+public interface ILocalizationManager
{
/// <summary>
- /// Interface ILocalizationManager.
+ /// Gets the cultures.
/// </summary>
- public interface ILocalizationManager
- {
- /// <summary>
- /// Gets the cultures.
- /// </summary>
- /// <returns><see cref="IEnumerable{CultureDto}" />.</returns>
- IEnumerable<CultureDto> GetCultures();
+ /// <returns><see cref="IEnumerable{CultureDto}" />.</returns>
+ IEnumerable<CultureDto> GetCultures();
- /// <summary>
- /// Gets the countries.
- /// </summary>
- /// <returns><see cref="IEnumerable{CountryInfo}" />.</returns>
- IEnumerable<CountryInfo> GetCountries();
+ /// <summary>
+ /// Gets the countries.
+ /// </summary>
+ /// <returns><see cref="IReadOnlyList{CountryInfo}" />.</returns>
+ IReadOnlyList<CountryInfo> GetCountries();
- /// <summary>
- /// Gets the parental ratings.
- /// </summary>
- /// <returns><see cref="IEnumerable{ParentalRating}" />.</returns>
- IEnumerable<ParentalRating> GetParentalRatings();
+ /// <summary>
+ /// Gets the parental ratings.
+ /// </summary>
+ /// <returns><see cref="IReadOnlyList{ParentalRating}" />.</returns>
+ IReadOnlyList<ParentalRating> GetParentalRatings();
- /// <summary>
- /// Gets the rating level.
- /// </summary>
- /// <param name="rating">The rating.</param>
- /// <param name="countryCode">The optional two letter ISO language string.</param>
- /// <returns><see cref="int" /> or <c>null</c>.</returns>
- int? GetRatingLevel(string rating, string? countryCode = null);
+ /// <summary>
+ /// Gets the rating level.
+ /// </summary>
+ /// <param name="rating">The rating.</param>
+ /// <param name="countryCode">The optional two letter ISO language string.</param>
+ /// <returns><see cref="ParentalRatingScore" /> or <c>null</c>.</returns>
+ ParentalRatingScore? GetRatingScore(string rating, string? countryCode = null);
- /// <summary>
- /// Gets the localized string.
- /// </summary>
- /// <param name="phrase">The phrase.</param>
- /// <param name="culture">The culture.</param>
- /// <returns><see cref="string" />.</returns>
- string GetLocalizedString(string phrase, string culture);
+ /// <summary>
+ /// Gets the localized string.
+ /// </summary>
+ /// <param name="phrase">The phrase.</param>
+ /// <param name="culture">The culture.</param>
+ /// <returns><see cref="string" />.</returns>
+ string GetLocalizedString(string phrase, string culture);
- /// <summary>
- /// Gets the localized string.
- /// </summary>
- /// <param name="phrase">The phrase.</param>
- /// <returns>System.String.</returns>
- string GetLocalizedString(string phrase);
+ /// <summary>
+ /// Gets the localized string.
+ /// </summary>
+ /// <param name="phrase">The phrase.</param>
+ /// <returns>System.String.</returns>
+ string GetLocalizedString(string phrase);
- /// <summary>
- /// Gets the localization options.
- /// </summary>
- /// <returns><see cref="IEnumerable{LocalizationOption}" />.</returns>
- IEnumerable<LocalizationOption> GetLocalizationOptions();
+ /// <summary>
+ /// Gets the localization options.
+ /// </summary>
+ /// <returns><see cref="IEnumerable{LocalizationOption}" />.</returns>
+ IEnumerable<LocalizationOption> GetLocalizationOptions();
- /// <summary>
- /// Returns the correct <see cref="CultureDto" /> for the given language.
- /// </summary>
- /// <param name="language">The language.</param>
- /// <returns>The correct <see cref="CultureDto" /> for the given language.</returns>
- CultureDto? FindLanguageInfo(string language);
- }
+ /// <summary>
+ /// Returns the correct <see cref="CultureDto" /> for the given language.
+ /// </summary>
+ /// <param name="language">The language.</param>
+ /// <returns>The correct <see cref="CultureDto" /> for the given language.</returns>
+ CultureDto? FindLanguageInfo(string language);
}
diff --git a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs
index a355387b1..b70333bce 100644
--- a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs
+++ b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs
@@ -9,6 +9,7 @@ namespace MediaBrowser.Model.LiveTv
{
AllowHWTranscoding = true;
IgnoreDts = true;
+ ReadAtNativeFramerate = false;
AllowStreamSharing = true;
AllowFmp4TranscodingContainer = false;
FallbackMaxStreamingBitrate = 30000000;
@@ -43,5 +44,7 @@ namespace MediaBrowser.Model.LiveTv
public string UserAgent { get; set; }
public bool IgnoreDts { get; set; }
+
+ public bool ReadAtNativeFramerate { get; set; }
}
}
diff --git a/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs b/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs
index 6e5c7885c..d9129c395 100644
--- a/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs
+++ b/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel;
using Jellyfin.Database.Implementations.Enums;
namespace MediaBrowser.Model.MediaSegments;
@@ -21,6 +22,7 @@ public class MediaSegmentDto
/// <summary>
/// Gets or sets the type of content this segment defines.
/// </summary>
+ [DefaultValue(MediaSegmentType.Unknown)]
public MediaSegmentType Type { get; set; }
/// <summary>
diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs
index 49d7c0bcb..ffecd392f 100644
--- a/MediaBrowser.Model/Querying/ItemFields.cs
+++ b/MediaBrowser.Model/Querying/ItemFields.cs
@@ -1,7 +1,3 @@
-#pragma warning disable CS1591
-
-using System;
-
namespace MediaBrowser.Model.Querying
{
/// <summary>
@@ -39,6 +35,9 @@ namespace MediaBrowser.Model.Querying
/// </summary>
Trickplay,
+ /// <summary>
+ /// The child count.
+ /// </summary>
ChildCount,
/// <summary>
@@ -82,11 +81,6 @@ namespace MediaBrowser.Model.Querying
Genres,
/// <summary>
- /// The home page URL.
- /// </summary>
- HomePageUrl,
-
- /// <summary>
/// The item counts.
/// </summary>
ItemCounts,
@@ -101,6 +95,9 @@ namespace MediaBrowser.Model.Querying
/// </summary>
MediaSources,
+ /// <summary>
+ /// The original title.
+ /// </summary>
OriginalTitle,
/// <summary>
@@ -123,6 +120,9 @@ namespace MediaBrowser.Model.Querying
/// </summary>
People,
+ /// <summary>
+ /// Value indicating whether playback access is granted.
+ /// </summary>
PlayAccess,
/// <summary>
@@ -140,6 +140,9 @@ namespace MediaBrowser.Model.Querying
/// </summary>
PrimaryImageAspectRatio,
+ /// <summary>
+ /// The recursive item count.
+ /// </summary>
RecursiveItemCount,
/// <summary>
@@ -148,14 +151,6 @@ namespace MediaBrowser.Model.Querying
Settings,
/// <summary>
- /// The screenshot image tags.
- /// </summary>
- [Obsolete("Screenshot image type is no longer used.")]
- ScreenshotImageTags,
-
- SeriesPrimaryImage,
-
- /// <summary>
/// The series studio.
/// </summary>
SeriesStudio,
@@ -201,26 +196,58 @@ namespace MediaBrowser.Model.Querying
SeasonUserData,
/// <summary>
- /// The service name.
+ /// The last time metadata was refreshed.
/// </summary>
- ServiceName,
- ThemeSongIds,
- ThemeVideoIds,
- ExternalEtag,
- PresentationUniqueKey,
- InheritedParentalRatingValue,
- ExternalSeriesId,
- SeriesPresentationUniqueKey,
DateLastRefreshed,
+
+ /// <summary>
+ /// The last time metadata was saved.
+ /// </summary>
DateLastSaved,
+
+ /// <summary>
+ /// The refresh state.
+ /// </summary>
RefreshState,
+
+ /// <summary>
+ /// The channel image.
+ /// </summary>
ChannelImage,
+
+ /// <summary>
+ /// Value indicating whether media source display is enabled.
+ /// </summary>
EnableMediaSourceDisplay,
+
+ /// <summary>
+ /// The width.
+ /// </summary>
Width,
+
+ /// <summary>
+ /// The height.
+ /// </summary>
Height,
+
+ /// <summary>
+ /// The external Ids.
+ /// </summary>
ExtraIds,
+
+ /// <summary>
+ /// The local trailer count.
+ /// </summary>
LocalTrailerCount,
+
+ /// <summary>
+ /// Value indicating whether the item is HD.
+ /// </summary>
IsHD,
+
+ /// <summary>
+ /// The special feature count.
+ /// </summary>
SpecialFeatureCount
}
}
diff --git a/MediaBrowser.Model/Search/SearchHint.cs b/MediaBrowser.Model/Search/SearchHint.cs
index 2e2979fcf..a18a813cc 100644
--- a/MediaBrowser.Model/Search/SearchHint.cs
+++ b/MediaBrowser.Model/Search/SearchHint.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Search
@@ -115,6 +116,7 @@ namespace MediaBrowser.Model.Search
/// Gets or sets the type of the media.
/// </summary>
/// <value>The type of the media.</value>
+ [DefaultValue(MediaType.Unknown)]
public MediaType MediaType { get; set; }
/// <summary>
diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs
index 39c5ac8fa..902bab9a6 100644
--- a/MediaBrowser.Model/Session/TranscodeReason.cs
+++ b/MediaBrowser.Model/Session/TranscodeReason.cs
@@ -14,6 +14,7 @@ namespace MediaBrowser.Model.Session
SubtitleCodecNotSupported = 1 << 3,
AudioIsExternal = 1 << 4,
SecondaryAudioNotSupported = 1 << 5,
+ StreamCountExceedsLimit = 1 << 26,
// Video Constraints
VideoProfileNotSupported = 1 << 6,
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index 3d430e101..2c393ca86 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -111,6 +111,8 @@ namespace MediaBrowser.Model.Users
/// <value>The max parental rating.</value>
public int? MaxParentalRating { get; set; }
+ public int? MaxParentalSubRating { get; set; }
+
public string[] BlockedTags { get; set; }
public string[] AllowedTags { get; set; }
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index e8994693d..45f66f85f 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -193,6 +193,7 @@ namespace MediaBrowser.Providers.Manager
if (hasRefreshedMetadata && hasRefreshedImages)
{
item.DateLastRefreshed = DateTime.UtcNow;
+ updateType |= item.OnMetadataChanged();
}
updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs
index bec800c03..27e3f93a3 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs
@@ -33,17 +33,18 @@ public class TmdbExternalUrlProvider : IExternalUrlProvider
if (season.Series.TryGetProviderId(MetadataProvider.Tmdb, out var seriesExternalId))
{
var orderString = season.Series.DisplayOrder;
- if (string.IsNullOrEmpty(orderString))
+ var seasonNumber = season.IndexNumber;
+ if (string.IsNullOrEmpty(orderString) && seasonNumber is not null)
{
// Default order is airdate
- yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{season.IndexNumber}";
+ yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{seasonNumber}";
}
if (Enum.TryParse<TvGroupType>(season.Series.DisplayOrder, out var order))
{
- if (order.Equals(TvGroupType.OriginalAirDate))
+ if (order.Equals(TvGroupType.OriginalAirDate) && seasonNumber is not null)
{
- yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{season.IndexNumber}";
+ yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{seasonNumber}";
}
}
}
@@ -53,17 +54,19 @@ public class TmdbExternalUrlProvider : IExternalUrlProvider
if (episode.Series.TryGetProviderId(MetadataProvider.Imdb, out seriesExternalId))
{
var orderString = episode.Series.DisplayOrder;
- if (string.IsNullOrEmpty(orderString))
+ var seasonNumber = episode.Season?.IndexNumber;
+ var episodeNumber = episode.IndexNumber;
+ if (string.IsNullOrEmpty(orderString) && seasonNumber is not null && episodeNumber is not null)
{
// Default order is airdate
- yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{episode.Season.IndexNumber}/episode/{episode.IndexNumber}";
+ yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{seasonNumber}/episode/{episodeNumber}";
}
if (Enum.TryParse<TvGroupType>(orderString, out var order))
{
- if (order.Equals(TvGroupType.OriginalAirDate))
+ if (order.Equals(TvGroupType.OriginalAirDate) && seasonNumber is not null && episodeNumber is not null)
{
- yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{episode.Season.IndexNumber}/episode/{episode.IndexNumber}";
+ yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{seasonNumber}/episode/{episodeNumber}";
}
}
}
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 284415dce..42d59d348 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -55,7 +55,7 @@ namespace MediaBrowser.Providers.TV
foreach (var season in seasons)
{
- var hasUpdate = refreshOptions != null && season.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata);
+ var hasUpdate = refreshOptions is not null && season.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata);
if (hasUpdate)
{
await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
index 2a1a14834..19b1bbe7b 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
@@ -95,7 +95,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
originalTitle.Append(" / ").Append(additionalEpisode.Item.OriginalTitle);
}
- if (additionalEpisode.Item.IndexNumber != null)
+ if (additionalEpisode.Item.IndexNumber is not null)
{
item.Item.IndexNumberEnd = Math.Max((int)additionalEpisode.Item.IndexNumber, item.Item.IndexNumberEnd ?? (int)additionalEpisode.Item.IndexNumber);
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index 4c8a54cc9..0217bded1 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -1020,12 +1020,12 @@ namespace MediaBrowser.XbmcMetadata.Savers
protected static string SortNameOrName(BaseItem item)
{
- if (item == null)
+ if (item is null)
{
return string.Empty;
}
- if (item.SortName != null)
+ if (item.SortName is not null)
{
string trimmed = item.SortName.Trim();
if (trimmed.Length > 0)
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs
index aab3082b3..2f27d9389 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs
@@ -25,7 +25,7 @@ public class AttachmentStreamInfo
/// <summary>
/// Gets or Sets the codec of the attachment.
/// </summary>
- public required string Codec { get; set; }
+ public string? Codec { get; set; }
/// <summary>
/// Gets or Sets the codec tag of the attachment.
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs
index fc9695a09..a09a96317 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs
@@ -84,6 +84,8 @@ public class BaseItemEntity
public int? InheritedParentalRatingValue { get; set; }
+ public int? InheritedParentalRatingSubValue { get; set; }
+
public string? UnratedType { get; set; }
public float? CriticRating { get; set; }
@@ -162,7 +164,7 @@ public class BaseItemEntity
public ICollection<BaseItemProvider>? Provider { get; set; }
- public ICollection<AncestorId>? ParentAncestors { get; set; }
+ public ICollection<AncestorId>? Parents { get; set; }
public ICollection<AncestorId>? Children { get; set; }
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/KeyframeData.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/KeyframeData.cs
new file mode 100644
index 000000000..c34110c4f
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/KeyframeData.cs
@@ -0,0 +1,32 @@
+#pragma warning disable CA2227 // Collection properties should be read only
+
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Keyframe information for a specific file.
+/// </summary>
+public class KeyframeData
+{
+ /// <summary>
+ /// Gets or Sets the ItemId.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the total duration of the stream in ticks.
+ /// </summary>
+ public long TotalDuration { get; set; }
+
+ /// <summary>
+ /// Gets or sets the keyframes in ticks.
+ /// </summary>
+ public ICollection<long>? KeyframeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item reference.
+ /// </summary>
+ public BaseItemEntity? Item { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs
index 207317376..b80b764ba 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs
@@ -99,4 +99,6 @@ public class MediaStreamInfo
public int? Rotation { get; set; }
public string? KeyFrames { get; set; }
+
+ public bool? Hdr10PlusPresentFlag { get; set; }
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
index 31538b5bf..4da7074ec 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
@@ -249,9 +249,14 @@ namespace Jellyfin.Database.Implementations.Entities
public bool EnableUserPreferenceAccess { get; set; }
/// <summary>
- /// Gets or sets the maximum parental age rating.
+ /// Gets or sets the maximum parental rating score.
/// </summary>
- public int? MaxParentalAgeRating { get; set; }
+ public int? MaxParentalRatingScore { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum parental rating sub score.
+ /// </summary>
+ public int? MaxParentalRatingSubScore { get; set; }
/// <summary>
/// Gets or sets the remote client bitrate limit.
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
index 9db70263d..35ad461ec 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
@@ -157,6 +157,11 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
/// </summary>
public DbSet<BaseItemTrailerType> BaseItemTrailerTypes => Set<BaseItemTrailerType>();
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<KeyframeData> KeyframeData => Set<KeyframeData>();
+
/*public DbSet<Artwork> Artwork => Set<Artwork>();
public DbSet<Book> Books => Set<Book>();
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs
index 1cb4a1eb1..67269153d 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs
@@ -14,7 +14,7 @@ public class AncestorIdConfiguration : IEntityTypeConfiguration<AncestorId>
{
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);
+ builder.HasOne(e => e.ParentItem).WithMany(e => e.Children).HasForeignKey(f => f.ParentItemId);
+ builder.HasOne(e => e.Item).WithMany(e => e.Parents).HasForeignKey(f => f.ItemId);
}
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs
index 37816faec..4a76113bf 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs
@@ -24,7 +24,7 @@ public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
builder.HasMany(e => e.MediaStreams);
builder.HasMany(e => e.Chapters);
builder.HasMany(e => e.Provider);
- builder.HasMany(e => e.ParentAncestors);
+ builder.HasMany(e => e.Parents);
builder.HasMany(e => e.Children);
builder.HasMany(e => e.LockedFields);
builder.HasMany(e => e.TrailerTypes);
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs
index c8e003eaa..97ebc2e01 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs
@@ -13,6 +13,7 @@ public class ItemValuesConfiguration : IEntityTypeConfiguration<ItemValue>
public void Configure(EntityTypeBuilder<ItemValue> builder)
{
builder.HasKey(e => e.ItemValueId);
- builder.HasIndex(e => new { e.Type, e.CleanValue }).IsUnique();
+ builder.HasIndex(e => new { e.Type, e.CleanValue });
+ builder.HasIndex(e => new { e.Type, e.Value }).IsUnique();
}
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/KeyframeDataConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/KeyframeDataConfiguration.cs
new file mode 100644
index 000000000..3f5d458ca
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/KeyframeDataConfiguration.cs
@@ -0,0 +1,18 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// KeyframeData Configuration.
+/// </summary>
+public class KeyframeDataConfiguration : IEntityTypeConfiguration<KeyframeData>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<KeyframeData> builder)
+ {
+ builder.HasKey(e => e.ItemId);
+ builder.HasOne(e => e.Item).WithMany().HasForeignKey(e => e.ItemId);
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.Designer.cs
new file mode 100644
index 000000000..d6befbe5e
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.Designer.cs
@@ -0,0 +1,1658 @@
+// <auto-generated />
+using System;
+using Jellyfin.Database.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("20250326065026_AddInheritedParentalRatingSubValue")]
+ partial class AddInheritedParentalRatingSubValue
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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?>("InheritedParentalRatingSubValue")
+ .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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
+ .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");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Children")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("ParentAncestors")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs
new file mode 100644
index 000000000..71f56a149
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs
@@ -0,0 +1,48 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddInheritedParentalRatingSubValue : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.RenameColumn(
+ name: "MaxParentalAgeRating",
+ table: "Users",
+ newName: "MaxParentalRatingScore");
+
+ migrationBuilder.AddColumn<int>(
+ name: "MaxParentalRatingSubScore",
+ table: "Users",
+ type: "INTEGER",
+ nullable: true);
+
+ migrationBuilder.AddColumn<int>(
+ name: "InheritedParentalRatingSubValue",
+ table: "BaseItems",
+ type: "INTEGER",
+ nullable: true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "MaxParentalRatingSubScore",
+ table: "Users");
+
+ migrationBuilder.DropColumn(
+ name: "InheritedParentalRatingValue",
+ table: "BaseItems");
+
+ migrationBuilder.RenameColumn(
+ name: "MaxParentalRatingScore",
+ table: "Users",
+ newName: "MaxParentalAgeRating");
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.Designer.cs
new file mode 100644
index 000000000..434ea820a
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.Designer.cs
@@ -0,0 +1,1681 @@
+// <auto-generated />
+using System;
+using Jellyfin.Database.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("20250327101120_AddKeyframeData")]
+ partial class AddKeyframeData
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Children")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("ParentAncestors")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs
new file mode 100644
index 000000000..c17b35b40
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs
@@ -0,0 +1,41 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddKeyframeData : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "KeyframeData",
+ columns: table => new
+ {
+ ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+ TotalDuration = table.Column<long>(type: "INTEGER", nullable: false),
+ KeyframeTicks = table.Column<string>(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_KeyframeData", x => x.ItemId);
+ table.ForeignKey(
+ name: "FK_KeyframeData_BaseItems_ItemId",
+ column: x => x.ItemId,
+ principalTable: "BaseItems",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "KeyframeData");
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.Designer.cs
new file mode 100644
index 000000000..bad01778d
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.Designer.cs
@@ -0,0 +1,1655 @@
+// <auto-generated />
+using System;
+using Jellyfin.Database.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("20250327171413_AddHdr10PlusFlag")]
+ partial class AddHdr10PlusFlag
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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<bool?>("Hdr10PlusPresentFlag")
+ .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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Children")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("ParentAncestors")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs
new file mode 100644
index 000000000..5766cd382
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddHdr10PlusFlag : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<bool>(
+ name: "Hdr10PlusPresentFlag",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Hdr10PlusPresentFlag",
+ table: "MediaStreamInfos");
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.Designer.cs
new file mode 100644
index 000000000..d668eea92
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.Designer.cs
@@ -0,0 +1,1657 @@
+// <auto-generated />
+using System;
+using Jellyfin.Database.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("20250331182844_FixAttachmentMigration")]
+ partial class FixAttachmentMigration
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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?>("InheritedParentalRatingSubValue")
+ .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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
+ .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");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Children")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("ParentAncestors")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs
new file mode 100644
index 000000000..f921856a2
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs
@@ -0,0 +1,36 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class FixAttachmentMigration : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn<string>(
+ name: "Codec",
+ table: "AttachmentStreamInfos",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "TEXT");
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterColumn<string>(
+ name: "Codec",
+ table: "AttachmentStreamInfos",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: string.Empty,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldNullable: true);
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.Designer.cs
new file mode 100644
index 000000000..d7672b137
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.Designer.cs
@@ -0,0 +1,1658 @@
+// <auto-generated />
+using System;
+using Jellyfin.Database.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("20250401142247_FixAncestors")]
+ partial class FixAncestors
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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?>("InheritedParentalRatingSubValue")
+ .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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
+ .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");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Parents")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("Children")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("Parents");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs
new file mode 100644
index 000000000..e1220bfcf
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs
@@ -0,0 +1,20 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class FixAncestors : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.Designer.cs
new file mode 100644
index 000000000..4ba3352ed
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.Designer.cs
@@ -0,0 +1,1694 @@
+// <auto-generated />
+using System;
+using Jellyfin.Database.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("20250405075612_FixItemValuesIndices")]
+ partial class FixItemValuesIndices
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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?>("DateCreatedFilesystem")
+ .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?>("InheritedParentalRatingSubValue")
+ .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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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.HasIndex("Type", "Value")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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<bool?>("Hdr10PlusPresentFlag")
+ .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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
+ .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");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Parents")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("Children")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("Parents");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs
new file mode 100644
index 000000000..aa667bafd
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs
@@ -0,0 +1,47 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class FixItemValuesIndices : 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" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ItemValues_Type_Value",
+ table: "ItemValues",
+ columns: new[] { "Type", "Value" },
+ unique: true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_ItemValues_Type_CleanValue",
+ table: "ItemValues");
+
+ migrationBuilder.DropIndex(
+ name: "IX_ItemValues_Type_Value",
+ table: "ItemValues");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ItemValues_Type_CleanValue",
+ table: "ItemValues",
+ columns: new[] { "Type", "CleanValue" },
+ unique: true);
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
index 5d8ddde08..dcdc5dd3e 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,9 +15,9 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "9.0.2");
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
- modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -40,9 +40,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId");
b.ToTable("AccessSchedules");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -88,9 +90,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("DateCreated");
b.ToTable("ActivityLogs");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
{
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
@@ -103,9 +107,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("ParentItemId");
b.ToTable("AncestorIds");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
{
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
@@ -114,7 +120,6 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasColumnType("INTEGER");
b.Property<string>("Codec")
- .IsRequired()
.HasColumnType("TEXT");
b.Property<string>("CodecTag")
@@ -132,9 +137,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("ItemId", "Index");
b.ToTable("AttachmentStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
@@ -218,6 +225,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int?>("IndexNumber")
.HasColumnType("INTEGER");
+ b.Property<int?>("InheritedParentalRatingSubValue")
+ .HasColumnType("INTEGER");
+
b.Property<int?>("InheritedParentalRatingValue")
.HasColumnType("INTEGER");
@@ -380,9 +390,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
b.ToTable("BaseItems");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
@@ -415,9 +427,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("ItemId");
b.ToTable("BaseItemImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER");
@@ -430,9 +444,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("ItemId");
b.ToTable("BaseItemMetadataFields");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
{
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
@@ -449,9 +465,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("ProviderId", "ProviderValue", "ItemId");
b.ToTable("BaseItemProviders");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER");
@@ -464,9 +482,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("ItemId");
b.ToTable("BaseItemTrailerTypes");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
{
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
@@ -489,9 +509,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("ItemId", "ChapterIndex");
b.ToTable("Chapters");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -521,9 +543,11 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsUnique();
b.ToTable("CustomItemDisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -578,9 +602,11 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsUnique();
b.ToTable("DisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -600,9 +626,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("DisplayPreferencesId");
b.ToTable("HomeSection");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -625,9 +653,11 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsUnique();
b.ToTable("ImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -669,9 +699,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId");
b.ToTable("ItemDisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
{
b.Property<Guid>("ItemValueId")
.ValueGeneratedOnAdd()
@@ -690,13 +722,17 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("ItemValueId");
- b.HasIndex("Type", "CleanValue")
+ b.HasIndex("Type", "CleanValue");
+
+ b.HasIndex("Type", "Value")
.IsUnique();
b.ToTable("ItemValues");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
{
b.Property<Guid>("ItemValueId")
.HasColumnType("TEXT");
@@ -709,9 +745,29 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("ItemId");
b.ToTable("ItemValuesMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
@@ -736,9 +792,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
b.ToTable("MediaSegments");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
{
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
@@ -806,6 +864,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int?>("ElPresentFlag")
.HasColumnType("INTEGER");
+ b.Property<bool?>("Hdr10PlusPresentFlag")
+ .HasColumnType("INTEGER");
+
b.Property<int?>("Height")
.HasColumnType("INTEGER");
@@ -889,9 +950,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("StreamIndex", "StreamType", "Language");
b.ToTable("MediaStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
@@ -909,9 +972,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("Name");
b.ToTable("Peoples");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
{
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
@@ -937,9 +1002,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("ItemId", "SortOrder");
b.ToTable("PeopleBaseItemMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -968,9 +1035,11 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Permissions");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1001,9 +1070,11 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Preferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1030,9 +1101,11 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsUnique();
b.ToTable("ApiKeys");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1088,9 +1161,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId", "DeviceId");
b.ToTable("Devices");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1109,9 +1184,11 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsUnique();
b.ToTable("DeviceOptions");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b =>
{
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
@@ -1140,9 +1217,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("ItemId", "Width");
b.ToTable("TrickplayInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
@@ -1200,7 +1279,10 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int>("MaxActiveSessions")
.HasColumnType("INTEGER");
- b.Property<int?>("MaxParentalAgeRating")
+ b.Property<int?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
.HasColumnType("INTEGER");
b.Property<bool>("MustUpdatePassword")
@@ -1252,9 +1334,11 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsUnique();
b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
{
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
@@ -1305,27 +1389,29 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("ItemId", "UserId", "Played");
b.ToTable("UserData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
{
- b.HasOne("Jellyfin.Data.Entities.User", null)
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
.WithMany("AccessSchedules")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
- modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
{
- b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
- .WithMany("Children")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Parents")
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
- b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
- .WithMany("ParentAncestors")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("Children")
.HasForeignKey("ParentItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1335,9 +1421,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("ParentItem");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
{
- b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
.WithMany()
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
@@ -1346,9 +1432,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("Item");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
{
- b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
.WithMany("Images")
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
@@ -1357,9 +1443,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("Item");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
{
- b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
.WithMany("LockedFields")
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
@@ -1368,9 +1454,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("Item");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
{
- b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
.WithMany("Provider")
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
@@ -1379,9 +1465,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("Item");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
{
- b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
.WithMany("TrailerTypes")
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
@@ -1390,9 +1476,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("Item");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
{
- b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
.WithMany("Chapters")
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
@@ -1401,50 +1487,50 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("Item");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
{
- b.HasOne("Jellyfin.Data.Entities.User", null)
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
.WithMany("DisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
- modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
{
- b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
.WithMany("HomeSections")
.HasForeignKey("DisplayPreferencesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
- modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
{
- b.HasOne("Jellyfin.Data.Entities.User", null)
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
.WithOne("ProfileImage")
- .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
.OnDelete(DeleteBehavior.Cascade);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
{
- b.HasOne("Jellyfin.Data.Entities.User", null)
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
.WithMany("ItemDisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
- modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
{
- b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
.WithMany("ItemValues")
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
- b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
.WithMany("BaseItemsMap")
.HasForeignKey("ItemValueId")
.OnDelete(DeleteBehavior.Cascade)
@@ -1455,9 +1541,20 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("ItemValue");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
{
- b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
.WithMany("MediaStreams")
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
@@ -1466,15 +1563,15 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("Item");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
{
- b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
.WithMany("Peoples")
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
- b.HasOne("Jellyfin.Data.Entities.People", "People")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
.WithMany("BaseItems")
.HasForeignKey("PeopleId")
.OnDelete(DeleteBehavior.Cascade)
@@ -1485,25 +1582,25 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("People");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
{
- b.HasOne("Jellyfin.Data.Entities.User", null)
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
.WithMany("Permissions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
{
- b.HasOne("Jellyfin.Data.Entities.User", null)
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
.WithMany("Preferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
- modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
{
- b.HasOne("Jellyfin.Data.Entities.User", "User")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@@ -1512,15 +1609,15 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("User");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
{
- b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
.WithMany("UserData")
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
- b.HasOne("Jellyfin.Data.Entities.User", "User")
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@@ -1531,7 +1628,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("User");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
{
b.Navigation("Chapters");
@@ -1545,7 +1642,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("MediaStreams");
- b.Navigation("ParentAncestors");
+ b.Navigation("Parents");
b.Navigation("Peoples");
@@ -1556,22 +1653,22 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("UserData");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
{
b.Navigation("HomeSections");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
{
b.Navigation("BaseItemsMap");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
{
b.Navigation("BaseItems");
});
- modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b =>
{
b.Navigation("AccessSchedules");
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 2dac5598f..73c8c3966 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
+using System.Linq;
using BlurHashSharp.SkiaSharp;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
@@ -24,6 +25,7 @@ public class SkiaEncoder : IImageEncoder
private readonly ILogger<SkiaEncoder> _logger;
private readonly IApplicationPaths _appPaths;
private static readonly SKImageFilter _imageFilter;
+ private static readonly SKTypeface[] _typefaces;
#pragma warning disable CA1810
static SkiaEncoder()
@@ -46,6 +48,21 @@ public class SkiaEncoder : IImageEncoder
kernelOffset,
SKShaderTileMode.Clamp,
true);
+
+ // Initialize the list of typefaces
+ // We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point
+ // But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻‍♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F)
+ _typefaces =
+ [
+ SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '鸡'), // CJK Simplified Chinese
+ SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '雞'), // CJK Traditional Chinese
+ SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ノ'), // CJK Japanese
+ SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '각'), // CJK Korean
+ SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 128169), // Emojis, 128169 is the 💩emoji
+ SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ז'), // Hebrew
+ SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ي'), // Arabic
+ SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) // Default font
+ ];
}
/// <summary>
@@ -98,6 +115,11 @@ public class SkiaEncoder : IImageEncoder
=> new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png, ImageFormat.Svg };
/// <summary>
+ /// Gets the default typeface to use.
+ /// </summary>
+ public static SKTypeface DefaultTypeFace => _typefaces.Last();
+
+ /// <summary>
/// Check if the native lib is available.
/// </summary>
/// <returns>True if the native lib is available, otherwise false.</returns>
@@ -535,20 +557,14 @@ public class SkiaEncoder : IImageEncoder
canvas.Clear(SKColor.Parse(options.BackgroundColor));
}
+ using var paint = new SKPaint();
// Add blur if option is present
- if (blur > 0)
- {
- // create image from resized bitmap to apply blur
- using var paint = new SKPaint();
- using var filter = SKImageFilter.CreateBlur(blur, blur);
- paint.ImageFilter = filter;
- canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
- }
- else
- {
- // draw resized bitmap onto canvas
- canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
- }
+ using var filter = blur > 0 ? SKImageFilter.CreateBlur(blur, blur) : null;
+ paint.FilterQuality = SKFilterQuality.High;
+ paint.ImageFilter = filter;
+
+ // create image from resized bitmap to apply blur
+ canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
// If foreground layer present then draw
if (hasForegroundColor)
@@ -705,4 +721,22 @@ public class SkiaEncoder : IImageEncoder
_logger.LogError(ex, "Error drawing indicator overlay");
}
}
+
+ /// <summary>
+ /// Return the typeface that contains the glyph for the given character.
+ /// </summary>
+ /// <param name="c">The text character.</param>
+ /// <returns>The typeface contains the character.</returns>
+ public static SKTypeface? GetFontForCharacter(string c)
+ {
+ foreach (var typeface in _typefaces)
+ {
+ if (typeface.ContainsGlyphs(c))
+ {
+ return typeface;
+ }
+ }
+
+ return null;
+ }
}
diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index 4aff26c16..03e202e5a 100644
--- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using SkiaSharp;
@@ -23,9 +24,6 @@ public partial class StripCollageBuilder
_skiaEncoder = skiaEncoder;
}
- [GeneratedRegex(@"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]")]
- private static partial Regex NonCjkPatternRegex();
-
[GeneratedRegex(@"\p{IsArabic}|\p{IsArmenian}|\p{IsHebrew}|\p{IsSyriac}|\p{IsThaana}")]
private static partial Regex IsRtlTextRegex();
@@ -111,26 +109,22 @@ public partial class StripCollageBuilder
// resize to the same aspect as the original
var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width);
- using var residedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace));
+ using var resizedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace));
+ using var paint = new SKPaint();
+ paint.FilterQuality = SKFilterQuality.High;
// draw the backdrop
- canvas.DrawImage(residedBackdrop, 0, 0);
+ canvas.DrawImage(resizedBackdrop, 0, 0, paint);
// draw shadow rectangle
using var paintColor = new SKPaint
{
Color = SKColors.Black.WithAlpha(0x78),
- Style = SKPaintStyle.Fill
+ Style = SKPaintStyle.Fill,
+ FilterQuality = SKFilterQuality.High
};
canvas.DrawRect(0, 0, width, height, paintColor);
- var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
-
- // use the system fallback to find a typeface for the given CJK character
- var filteredName = NonCjkPatternRegex().Replace(libraryName ?? string.Empty, string.Empty);
- if (!string.IsNullOrEmpty(filteredName))
- {
- typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]);
- }
+ var typeFace = SkiaEncoder.DefaultTypeFace;
// draw library name
using var textPaint = new SKPaint
@@ -138,9 +132,10 @@ public partial class StripCollageBuilder
Color = SKColors.White,
Style = SKPaintStyle.Fill,
TextSize = 112,
- TextAlign = SKTextAlign.Center,
+ TextAlign = SKTextAlign.Left,
Typeface = typeFace,
- IsAntialias = true
+ IsAntialias = true,
+ FilterQuality = SKFilterQuality.High
};
// scale down text to 90% of the width if text is larger than 95% of the width
@@ -155,13 +150,23 @@ public partial class StripCollageBuilder
return bitmap;
}
+ var realWidth = DrawText(null, 0, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint);
+ if (realWidth > width * 0.95)
+ {
+ textPaint.TextSize = 0.9f * width * textPaint.TextSize / realWidth;
+ realWidth = DrawText(null, 0, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint);
+ }
+
+ var padding = (width - realWidth) / 2;
+
if (IsRtlTextRegex().IsMatch(libraryName))
{
- canvas.DrawShapedText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint);
+ textPaint.TextAlign = SKTextAlign.Right;
+ DrawText(canvas, width - padding, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint, true);
}
else
{
- canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint);
+ DrawText(canvas, padding, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint);
}
return bitmap;
@@ -187,17 +192,125 @@ public partial class StripCollageBuilder
continue;
}
- // Scale image. The FromBitmap creates a copy
+ // Scale image
var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
using var resizeImage = SkiaEncoder.ResizeImage(currentBitmap, imageInfo);
+ using var paint = new SKPaint();
+ paint.FilterQuality = SKFilterQuality.High;
// draw this image into the strip at the next position
var xPos = x * cellWidth;
var yPos = y * cellHeight;
- canvas.DrawImage(resizeImage, xPos, yPos);
+ canvas.DrawImage(resizeImage, xPos, yPos, paint);
}
}
return bitmap;
}
+
+ /// <summary>
+ /// Draw shaped text with given SKPaint.
+ /// </summary>
+ /// <param name="canvas">If not null, draw text to this canvas, otherwise only measure the text width.</param>
+ /// <param name="x">x position of the canvas to draw text.</param>
+ /// <param name="y">y position of the canvas to draw text.</param>
+ /// <param name="text">The text to draw.</param>
+ /// <param name="textPaint">The SKPaint to style the text.</param>
+ /// <returns>The width of the text.</returns>
+ private static float MeasureAndDrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint)
+ {
+ var width = textPaint.MeasureText(text);
+ canvas?.DrawShapedText(text, x, y, textPaint);
+ return width;
+ }
+
+ /// <summary>
+ /// Draw shaped text with given SKPaint, search defined type faces to render as many texts as possible.
+ /// </summary>
+ /// <param name="canvas">If not null, draw text to this canvas, otherwise only measure the text width.</param>
+ /// <param name="x">x position of the canvas to draw text.</param>
+ /// <param name="y">y position of the canvas to draw text.</param>
+ /// <param name="text">The text to draw.</param>
+ /// <param name="textPaint">The SKPaint to style the text.</param>
+ /// <param name="isRtl">If true, render from right to left.</param>
+ /// <returns>The width of the text.</returns>
+ private static float DrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint, bool isRtl = false)
+ {
+ float width = 0;
+
+ if (textPaint.ContainsGlyphs(text))
+ {
+ // Current font can render all characters in text
+ return MeasureAndDrawText(canvas, x, y, text, textPaint);
+ }
+
+ // Iterate over all text elements using TextElementEnumerator
+ // We cannot use foreach here because a human-readable character (grapheme cluster) can be multiple code points
+ // We cannot render character by character because glyphs do not always have same width
+ // And the result will look very unnatural due to the width difference and missing natural spacing
+ var start = 0;
+ var enumerator = StringInfo.GetTextElementEnumerator(text);
+ while (enumerator.MoveNext())
+ {
+ bool notAtEnd;
+ var textElement = enumerator.GetTextElement();
+ if (textPaint.ContainsGlyphs(textElement))
+ {
+ continue;
+ }
+
+ // If we get here, we have a text element which cannot be rendered with current font
+ // Draw previous characters which can be rendered with current font
+ if (start != enumerator.ElementIndex)
+ {
+ var regularText = text.Substring(start, enumerator.ElementIndex - start);
+ width += MeasureAndDrawText(canvas, MoveX(x, width), y, regularText, textPaint);
+ start = enumerator.ElementIndex;
+ }
+
+ // Search for next point where current font can render the character there
+ while ((notAtEnd = enumerator.MoveNext()) && !textPaint.ContainsGlyphs(enumerator.GetTextElement()))
+ {
+ // Do nothing, just move enumerator to the point where current font can render the character
+ }
+
+ // Now we have a substring that should pick another font
+ // The enumerator may or may not be already at the end of the string
+ var subtext = notAtEnd
+ ? text.Substring(start, enumerator.ElementIndex - start)
+ : text[start..];
+
+ var fallback = SkiaEncoder.GetFontForCharacter(textElement);
+
+ if (fallback is not null)
+ {
+ using var fallbackTextPaint = new SKPaint();
+ fallbackTextPaint.Color = textPaint.Color;
+ fallbackTextPaint.Style = textPaint.Style;
+ fallbackTextPaint.TextSize = textPaint.TextSize;
+ fallbackTextPaint.TextAlign = textPaint.TextAlign;
+ fallbackTextPaint.Typeface = fallback;
+ fallbackTextPaint.IsAntialias = textPaint.IsAntialias;
+
+ // Do the search recursively to select all possible fonts
+ width += DrawText(canvas, MoveX(x, width), y, subtext, fallbackTextPaint, isRtl);
+ }
+ else
+ {
+ // Used up all fonts and no fonts can be found, just use current font
+ width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint);
+ }
+
+ start = notAtEnd ? enumerator.ElementIndex : text.Length;
+ }
+
+ // Render the remaining text that current fonts can render
+ if (start < text.Length)
+ {
+ width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint);
+ }
+
+ return width;
+ float MoveX(float currentX, float dWidth) => isRtl ? currentX - dWidth : currentX + dWidth;
+ }
}
diff --git a/src/Jellyfin.Extensions/EnumerableExtensions.cs b/src/Jellyfin.Extensions/EnumerableExtensions.cs
index fd46358a4..3eb9da01f 100644
--- a/src/Jellyfin.Extensions/EnumerableExtensions.cs
+++ b/src/Jellyfin.Extensions/EnumerableExtensions.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
namespace Jellyfin.Extensions;
@@ -55,4 +56,22 @@ public static class EnumerableExtensions
{
yield return item;
}
+
+ /// <summary>
+ /// Gets an IEnumerable consisting of all flags of an enum.
+ /// </summary>
+ /// <param name="flags">The flags enum.</param>
+ /// <typeparam name="T">The type of item.</typeparam>
+ /// <returns>The IEnumerable{Enum}.</returns>
+ public static IEnumerable<T> GetUniqueFlags<T>(this T flags)
+ where T : Enum
+ {
+ foreach (Enum value in Enum.GetValues(flags.GetType()))
+ {
+ if (flags.HasFlag(value))
+ {
+ yield return (T)value;
+ }
+ }
+ }
}
diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
index be81171a0..fb606be0e 100644
--- a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
@@ -190,7 +190,7 @@ namespace Jellyfin.LiveTv.TunerHosts
RequiresClosing = true,
RequiresLooping = info.EnableStreamLooping,
- ReadAtNativeFramerate = false,
+ ReadAtNativeFramerate = info.ReadAtNativeFramerate,
Id = channel.Path.GetMD5().ToString("N", CultureInfo.InvariantCulture),
IsInfiniteStream = true,
diff --git a/src/Jellyfin.MediaEncoding.Hls/Cache/CacheDecorator.cs b/src/Jellyfin.MediaEncoding.Hls/Cache/CacheDecorator.cs
index 127f4079c..8ca0e869a 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Cache/CacheDecorator.cs
+++ b/src/Jellyfin.MediaEncoding.Hls/Cache/CacheDecorator.cs
@@ -1,13 +1,12 @@
+#pragma warning disable CA1826 // Do not use Enumerable methods on indexable collections
+
using System;
using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
-using System.IO;
-using System.Text.Json;
-using Jellyfin.Extensions.Json;
+using System.Linq;
+using System.Threading;
using Jellyfin.MediaEncoding.Hls.Extractors;
using Jellyfin.MediaEncoding.Keyframes;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
namespace Jellyfin.MediaEncoding.Hls.Cache;
@@ -15,82 +14,48 @@ namespace Jellyfin.MediaEncoding.Hls.Cache;
/// <inheritdoc />
public class CacheDecorator : IKeyframeExtractor
{
+ private readonly IKeyframeRepository _keyframeRepository;
private readonly IKeyframeExtractor _keyframeExtractor;
private readonly ILogger<CacheDecorator> _logger;
private readonly string _keyframeExtractorName;
- private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
- private readonly string _keyframeCachePath;
/// <summary>
/// Initializes a new instance of the <see cref="CacheDecorator"/> class.
/// </summary>
- /// <param name="applicationPaths">An instance of the <see cref="IApplicationPaths"/> interface.</param>
+ /// <param name="keyframeRepository">An instance of the <see cref="IKeyframeRepository"/> interface.</param>
/// <param name="keyframeExtractor">An instance of the <see cref="IKeyframeExtractor"/> interface.</param>
/// <param name="logger">An instance of the <see cref="ILogger{CacheDecorator}"/> interface.</param>
- public CacheDecorator(IApplicationPaths applicationPaths, IKeyframeExtractor keyframeExtractor, ILogger<CacheDecorator> logger)
+ public CacheDecorator(IKeyframeRepository keyframeRepository, IKeyframeExtractor keyframeExtractor, ILogger<CacheDecorator> logger)
{
- ArgumentNullException.ThrowIfNull(applicationPaths);
+ ArgumentNullException.ThrowIfNull(keyframeRepository);
ArgumentNullException.ThrowIfNull(keyframeExtractor);
+ _keyframeRepository = keyframeRepository;
_keyframeExtractor = keyframeExtractor;
_logger = logger;
_keyframeExtractorName = keyframeExtractor.GetType().Name;
- // TODO make the dir configurable
- _keyframeCachePath = Path.Combine(applicationPaths.DataPath, "keyframes");
}
/// <inheritdoc />
public bool IsMetadataBased => _keyframeExtractor.IsMetadataBased;
/// <inheritdoc />
- public bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
+ public bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
{
- keyframeData = null;
- var cachePath = GetCachePath(_keyframeCachePath, filePath);
- if (TryReadFromCache(cachePath, out var cachedResult))
+ keyframeData = _keyframeRepository.GetKeyframeData(itemId).FirstOrDefault();
+ if (keyframeData is null)
{
- keyframeData = cachedResult;
- return true;
+ if (!_keyframeExtractor.TryExtractKeyframes(itemId, filePath, out var result))
+ {
+ _logger.LogDebug("Failed to extract keyframes using {ExtractorName}", _keyframeExtractorName);
+ return false;
+ }
+
+ _logger.LogDebug("Successfully extracted keyframes using {ExtractorName}", _keyframeExtractorName);
+ keyframeData = result;
+ _keyframeRepository.SaveKeyframeDataAsync(itemId, keyframeData, CancellationToken.None).GetAwaiter().GetResult();
}
- if (!_keyframeExtractor.TryExtractKeyframes(filePath, out var result))
- {
- _logger.LogDebug("Failed to extract keyframes using {ExtractorName}", _keyframeExtractorName);
- return false;
- }
-
- _logger.LogDebug("Successfully extracted keyframes using {ExtractorName}", _keyframeExtractorName);
- keyframeData = result;
- SaveToCache(cachePath, keyframeData);
return true;
}
-
- private static void SaveToCache(string cachePath, KeyframeData keyframeData)
- {
- var json = JsonSerializer.Serialize(keyframeData, _jsonOptions);
- Directory.CreateDirectory(Path.GetDirectoryName(cachePath) ?? throw new ArgumentException($"Provided path ({cachePath}) is not valid.", nameof(cachePath)));
- File.WriteAllText(cachePath, json);
- }
-
- private static string GetCachePath(string keyframeCachePath, string filePath)
- {
- var lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath);
- ReadOnlySpan<char> filename = (filePath + "_" + lastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5() + ".json";
- var prefix = filename[..1];
-
- return Path.Join(keyframeCachePath, prefix, filename);
- }
-
- private static bool TryReadFromCache(string cachePath, [NotNullWhen(true)] out KeyframeData? cachedResult)
- {
- if (File.Exists(cachePath))
- {
- var bytes = File.ReadAllBytes(cachePath);
- cachedResult = JsonSerializer.Deserialize<KeyframeData>(bytes, _jsonOptions);
- return cachedResult is not null;
- }
-
- cachedResult = null;
- return false;
- }
}
diff --git a/src/Jellyfin.MediaEncoding.Hls/Extractors/FfProbeKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Hls/Extractors/FfProbeKeyframeExtractor.cs
index a8daeeb78..a69746fe0 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Extractors/FfProbeKeyframeExtractor.cs
+++ b/src/Jellyfin.MediaEncoding.Hls/Extractors/FfProbeKeyframeExtractor.cs
@@ -34,7 +34,7 @@ public class FfProbeKeyframeExtractor : IKeyframeExtractor
public bool IsMetadataBased => false;
/// <inheritdoc />
- public bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
+ public bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
{
if (!_namingOptions.VideoFileExtensions.Contains(Path.GetExtension(filePath.AsSpan()), StringComparison.OrdinalIgnoreCase))
{
diff --git a/src/Jellyfin.MediaEncoding.Hls/Extractors/IKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Hls/Extractors/IKeyframeExtractor.cs
index 083e93de1..84bccbc72 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Extractors/IKeyframeExtractor.cs
+++ b/src/Jellyfin.MediaEncoding.Hls/Extractors/IKeyframeExtractor.cs
@@ -1,3 +1,4 @@
+using System;
using System.Diagnostics.CodeAnalysis;
using Jellyfin.MediaEncoding.Keyframes;
@@ -16,8 +17,9 @@ public interface IKeyframeExtractor
/// <summary>
/// Attempt to extract keyframes.
/// </summary>
+ /// <param name="itemId">The item id.</param>
/// <param name="filePath">The path to the file.</param>
/// <param name="keyframeData">The keyframes.</param>
/// <returns>A value indicating whether the keyframe extraction was successful.</returns>
- bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData);
+ bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData);
}
diff --git a/src/Jellyfin.MediaEncoding.Hls/Extractors/MatroskaKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Hls/Extractors/MatroskaKeyframeExtractor.cs
index 1100f8cd5..c7758e919 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Extractors/MatroskaKeyframeExtractor.cs
+++ b/src/Jellyfin.MediaEncoding.Hls/Extractors/MatroskaKeyframeExtractor.cs
@@ -24,7 +24,7 @@ public class MatroskaKeyframeExtractor : IKeyframeExtractor
public bool IsMetadataBased => true;
/// <inheritdoc />
- public bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
+ public bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
{
if (!filePath.AsSpan().EndsWith(".mkv", StringComparison.OrdinalIgnoreCase))
{
diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
index dc581724a..80b5aa84e 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
+++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
@@ -7,6 +7,7 @@
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
+ <ProjectReference Include="../../MediaBrowser.Model/MediaBrowser.Model.csproj" />
<ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" />
<ProjectReference Include="../Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj" />
</ItemGroup>
diff --git a/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs b/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs
index 21d9bb658..f5af50062 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs
+++ b/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs
@@ -1,3 +1,5 @@
+using System;
+
namespace Jellyfin.MediaEncoding.Hls.Playlist;
/// <summary>
@@ -8,6 +10,7 @@ public class CreateMainPlaylistRequest
/// <summary>
/// Initializes a new instance of the <see cref="CreateMainPlaylistRequest"/> class.
/// </summary>
+ /// <param name="mediaSourceId">The media source id.</param>
/// <param name="filePath">The absolute file path to the file.</param>
/// <param name="desiredSegmentLengthMs">The desired segment length in milliseconds.</param>
/// <param name="totalRuntimeTicks">The total duration of the file in ticks.</param>
@@ -15,8 +18,9 @@ public class CreateMainPlaylistRequest
/// <param name="endpointPrefix">The URI prefix for the relative URL in the playlist.</param>
/// <param name="queryString">The desired query string to append (must start with ?).</param>
/// <param name="isRemuxingVideo">Whether the video is being remuxed.</param>
- public CreateMainPlaylistRequest(string filePath, int desiredSegmentLengthMs, long totalRuntimeTicks, string segmentContainer, string endpointPrefix, string queryString, bool isRemuxingVideo)
+ public CreateMainPlaylistRequest(Guid? mediaSourceId, string filePath, int desiredSegmentLengthMs, long totalRuntimeTicks, string segmentContainer, string endpointPrefix, string queryString, bool isRemuxingVideo)
{
+ MediaSourceId = mediaSourceId;
FilePath = filePath;
DesiredSegmentLengthMs = desiredSegmentLengthMs;
TotalRuntimeTicks = totalRuntimeTicks;
@@ -27,6 +31,11 @@ public class CreateMainPlaylistRequest
}
/// <summary>
+ /// Gets the media source id.
+ /// </summary>
+ public Guid? MediaSourceId { get; }
+
+ /// <summary>
/// Gets the file path.
/// </summary>
public string FilePath { get; }
diff --git a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs
index 1846ba26b..fb5027e5b 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs
+++ b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs
@@ -35,7 +35,9 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator
{
IReadOnlyList<double> segments;
// For video transcodes it is sufficient with equal length segments as ffmpeg will create new keyframes
- if (request.IsRemuxingVideo && TryExtractKeyframes(request.FilePath, out var keyframeData))
+ if (request.IsRemuxingVideo
+ && request.MediaSourceId is not null
+ && TryExtractKeyframes(request.MediaSourceId.Value, request.FilePath, out var keyframeData))
{
segments = ComputeSegments(keyframeData, request.DesiredSegmentLengthMs);
}
@@ -104,7 +106,7 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator
return builder.ToString();
}
- private bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
+ private bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
{
keyframeData = null;
if (!IsExtractionAllowedForFile(filePath, _serverConfigurationManager.GetEncodingOptions().AllowOnDemandMetadataBasedKeyframeExtractionForExtensions))
@@ -116,7 +118,7 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator
for (var i = 0; i < len; i++)
{
var extractor = _extractors[i];
- if (!extractor.TryExtractKeyframes(filePath, out var result))
+ if (!extractor.TryExtractKeyframes(itemId, filePath, out var result))
{
continue;
}
diff --git a/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs b/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs
index caf6a2aae..d63ee6777 100644
--- a/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs
+++ b/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs
@@ -9,7 +9,6 @@ using Jellyfin.MediaEncoding.Hls.Extractors;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
@@ -23,7 +22,7 @@ public class KeyframeExtractionScheduledTask : IScheduledTask
private readonly ILocalizationManager _localizationManager;
private readonly ILibraryManager _libraryManager;
private readonly IKeyframeExtractor[] _keyframeExtractors;
- private static readonly BaseItemKind[] _itemTypes = { BaseItemKind.Episode, BaseItemKind.Movie };
+ private static readonly BaseItemKind[] _itemTypes = [BaseItemKind.Episode, BaseItemKind.Movie];
/// <summary>
/// Initializes a new instance of the <see cref="KeyframeExtractionScheduledTask"/> class.
@@ -55,11 +54,11 @@ public class KeyframeExtractionScheduledTask : IScheduledTask
{
var query = new InternalItemsQuery
{
- MediaTypes = new[] { MediaType.Video },
+ MediaTypes = [MediaType.Video],
IsVirtualItem = false,
IncludeItemTypes = _itemTypes,
DtoOptions = new DtoOptions(true),
- SourceTypes = new[] { SourceType.Library },
+ SourceTypes = [SourceType.Library],
Recursive = true,
Limit = Pagesize
};
@@ -74,19 +73,16 @@ public class KeyframeExtractionScheduledTask : IScheduledTask
query.StartIndex = startIndex;
var videos = _libraryManager.GetItemList(query);
- var currentPageCount = videos.Count;
- // TODO parallelize with Parallel.ForEach?
- for (var i = 0; i < currentPageCount; i++)
+ foreach (var video in videos)
{
- var video = videos[i];
// Only local files supported
- if (video.IsFileProtocol && File.Exists(video.Path))
+ var path = video.Path;
+ if (File.Exists(path))
{
- for (var j = 0; j < _keyframeExtractors.Length; j++)
+ foreach (var extractor in _keyframeExtractors)
{
- var extractor = _keyframeExtractors[j];
- // The cache decorator will make sure to save them in the data dir
- if (extractor.TryExtractKeyframes(video.Path, out _))
+ // The cache decorator will make sure to save the keyframes
+ if (extractor.TryExtractKeyframes(video.Id, path, out _))
{
break;
}
@@ -107,5 +103,5 @@ public class KeyframeExtractionScheduledTask : IScheduledTask
}
/// <inheritdoc />
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => Enumerable.Empty<TaskTriggerInfo>();
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => [];
}
diff --git a/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs
index a74dab5f2..e95df1635 100644
--- a/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs
@@ -87,7 +87,7 @@ public class UserControllerTests
Assert.Contains(
Validate(userPolicy), v =>
v.MemberNames.Contains("PasswordResetProviderId") &&
- v.ErrorMessage != null &&
+ v.ErrorMessage is not null &&
v.ErrorMessage.Contains("required", StringComparison.CurrentCultureIgnoreCase));
}
@@ -105,7 +105,7 @@ public class UserControllerTests
Assert.Contains(Validate(userPolicy), v =>
v.MemberNames.Contains("AuthenticationProviderId") &&
- v.ErrorMessage != null &&
+ v.ErrorMessage is not null &&
v.ErrorMessage.Contains("required", StringComparison.CurrentCultureIgnoreCase));
}
diff --git a/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs b/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs
index 07b53bf74..1f59908a8 100644
--- a/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs
+++ b/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs
@@ -181,8 +181,8 @@ namespace Jellyfin.Controller.Tests
fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == path))).Returns(newFileSystemMetadata);
var secondResult = directoryService.GetFile(path);
- Assert.Equal(cachedFileSystemMetadata, result);
- Assert.Equal(cachedFileSystemMetadata, secondResult);
+ Assert.Equivalent(cachedFileSystemMetadata, result);
+ Assert.Equivalent(cachedFileSystemMetadata, secondResult);
}
[Fact]
diff --git a/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs b/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs
new file mode 100644
index 000000000..e32baef55
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs
@@ -0,0 +1,215 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+
+namespace Jellyfin.Model.Tests.Dlna;
+
+public class LegacyStreamInfo : StreamInfo
+{
+ public LegacyStreamInfo(Guid itemId, DlnaProfileType mediaType)
+ {
+ ItemId = itemId;
+ MediaType = mediaType;
+ }
+
+ /// <summary>
+ /// The 10.6 ToUrl code from StreamInfo.cs with which to compare new version.
+ /// </summary>
+ /// <param name="baseUrl">The base url to use.</param>
+ /// <param name="accessToken">The Access token.</param>
+ /// <returns>A url.</returns>
+ public string ToUrl_Original(string baseUrl, string? accessToken)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(baseUrl);
+
+ var list = new List<string>();
+ foreach (NameValuePair pair in BuildParams(this, accessToken))
+ {
+ if (string.IsNullOrEmpty(pair.Value))
+ {
+ continue;
+ }
+
+ // Try to keep the url clean by omitting defaults
+ if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal);
+
+ list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
+ }
+
+ string queryString = string.Join('&', list);
+
+ return GetUrl(baseUrl, queryString);
+ }
+
+ private string GetUrl(string baseUrl, string queryString)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(baseUrl);
+
+ string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
+
+ baseUrl = baseUrl.TrimEnd('/');
+
+ if (MediaType == DlnaProfileType.Audio)
+ {
+ if (SubProtocol == MediaStreamProtocol.hls)
+ {
+ return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+ }
+
+ return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
+ }
+
+ if (SubProtocol == MediaStreamProtocol.hls)
+ {
+ return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+ }
+
+ return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
+ }
+
+ private static List<NameValuePair> BuildParams(StreamInfo item, string? accessToken)
+ {
+ List<NameValuePair> list = [];
+
+ string audioCodecs = item.AudioCodecs.Count == 0 ?
+ string.Empty :
+ string.Join(',', item.AudioCodecs);
+
+ string videoCodecs = item.VideoCodecs.Count == 0 ?
+ string.Empty :
+ string.Join(',', item.VideoCodecs);
+
+ list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
+ list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
+ list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
+ list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ list.Add(new NameValuePair("VideoCodec", videoCodecs));
+ list.Add(new NameValuePair("AudioCodec", audioCodecs));
+ list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && (item.AlwaysBurnInSubtitleWhenTranscoding || item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+ list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+ long startPositionTicks = item.StartPositionTicks;
+
+ if (item.SubProtocol == MediaStreamProtocol.hls)
+ {
+ list.Add(new NameValuePair("StartTimeTicks", string.Empty));
+ list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
+
+ if (item.SegmentLength.HasValue)
+ {
+ list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
+ }
+
+ if (item.MinSegments.HasValue)
+ {
+ list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
+ }
+
+ list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)));
+ }
+ else
+ {
+ list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture)));
+ }
+
+ list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
+ list.Add(new NameValuePair("ApiKey", accessToken ?? string.Empty));
+
+ string? liveStreamId = item.MediaSource?.LiveStreamId;
+ list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
+
+ if (!item.IsDirectStream)
+ {
+ if (item.RequireNonAnamorphic)
+ {
+ list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
+
+ list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+ if (item.EnableSubtitlesInManifest)
+ {
+ list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
+
+ if (item.EnableMpegtsM2TsMode)
+ {
+ list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
+
+ if (item.EstimateContentLength)
+ {
+ list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
+
+ if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto)
+ {
+ list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant()));
+ }
+
+ if (item.CopyTimestamps)
+ {
+ list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
+
+ list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+
+ list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
+ }
+
+ list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty));
+
+ string subtitleCodecs = item.SubtitleCodecs.Count == 0 ? string.Empty : string.Join(",", item.SubtitleCodecs);
+ list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
+ list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
+
+ foreach (var pair in item.StreamOptions)
+ {
+ if (string.IsNullOrEmpty(pair.Value))
+ {
+ continue;
+ }
+
+ // strip spaces to avoid having to encode h264 profile names
+ list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal)));
+ }
+
+ var transcodeReasonsValues = item.TranscodeReasons.GetUniqueFlags().ToArray();
+ if (!item.IsDirectStream && transcodeReasonsValues.Length > 0)
+ {
+ list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString()));
+ }
+
+ return list;
+ }
+}
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
index bd2143f25..2c1080ffe 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
@@ -39,6 +39,8 @@ namespace Jellyfin.Model.Tests
[InlineData("Chrome", "mkv-dvhe.05-eac3-28000k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")]
[InlineData("Chrome", "mkv-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")]
[InlineData("Chrome", "mp4-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")]
+ [InlineData("Chrome", "numstreams-32", PlayMethod.DirectPlay)]
+ [InlineData("Chrome", "numstreams-33", PlayMethod.DirectPlay)]
// Firefox
[InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450
@@ -180,6 +182,12 @@ namespace Jellyfin.Model.Tests
[InlineData("Tizen3-stereo", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
+ [InlineData("Tizen3-stereo", "mkv-dvhe.08-eac3-15200k", PlayMethod.DirectPlay)]
+ [InlineData("Tizen3-stereo", "mp4-dvh1.05-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioChannelsNotSupported, "Transcode")]
+ [InlineData("Tizen3-stereo", "mp4-dvhe.08-eac3-15200k", PlayMethod.DirectPlay)]
+ [InlineData("Tizen3-stereo", "mkv-dvhe.05-eac3-28000k", PlayMethod.Transcode, TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioChannelsNotSupported, "Transcode")]
+ [InlineData("Tizen3-stereo", "numstreams-32", PlayMethod.DirectPlay)]
+ [InlineData("Tizen3-stereo", "numstreams-33", PlayMethod.Transcode, TranscodeReason.StreamCountExceedsLimit, "Remux")]
// Tizen 4 4K 5.1
[InlineData("Tizen4-4K-5.1", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)]
@@ -191,6 +199,12 @@ namespace Jellyfin.Model.Tests
[InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
+ [InlineData("Tizen4-4K-5.1", "mkv-dvhe.08-eac3-15200k", PlayMethod.DirectPlay)]
+ [InlineData("Tizen4-4K-5.1", "mp4-dvh1.05-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported, "Transcode")]
+ [InlineData("Tizen4-4K-5.1", "mp4-dvhe.08-eac3-15200k", PlayMethod.DirectPlay)]
+ [InlineData("Tizen4-4K-5.1", "mkv-dvhe.05-eac3-28000k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported, "Transcode")]
+ [InlineData("Tizen4-4K-5.1", "numstreams-32", PlayMethod.DirectPlay)]
+ [InlineData("Tizen4-4K-5.1", "numstreams-33", PlayMethod.Transcode, TranscodeReason.StreamCountExceedsLimit, "Remux")]
// WebOS 23
[InlineData("WebOS-23", "mkv-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported, "Remux")]
[InlineData("WebOS-23", "mp4-dvh1.05-eac3-15200k", PlayMethod.DirectPlay)]
@@ -588,7 +602,7 @@ namespace Jellyfin.Model.Tests
private static (string Path, NameValueCollection Query, string Filename, string Extension) ParseUri(StreamInfo val)
{
- var href = val.ToUrl("media:", "ACCESSTOKEN").Split("?", 2);
+ var href = val.ToUrl("media:", "ACCESSTOKEN", null).Split("?", 2);
var path = href[0];
var queryString = href.ElementAtOrDefault(1);
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs
new file mode 100644
index 000000000..8dea46806
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs
@@ -0,0 +1,243 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using MediaBrowser.Model.Dlna;
+using Xunit;
+
+namespace Jellyfin.Model.Tests.Dlna;
+
+public class StreamInfoTests
+{
+ private const string BaseUrl = "/test/";
+ private const int RandomSeed = 298347823;
+
+ /// <summary>
+ /// Returns a random float.
+ /// </summary>
+ /// <param name="random">The <see cref="Random"/> instance.</param>
+ /// <returns>A random <see cref="float"/>.</returns>
+ private static float RandomFloat(Random random)
+ {
+ var buffer = new byte[4];
+ random.NextBytes(buffer);
+ return BitConverter.ToSingle(buffer, 0);
+ }
+
+ /// <summary>
+ /// Creates a random array.
+ /// </summary>
+ /// <param name="random">The <see cref="Random"/> instance.</param>
+ /// <param name="elementType">The element <see cref="Type"/> of the array.</param>
+ /// <returns>An <see cref="Array"/> of <see cref="Type"/>.</returns>
+ private static object? RandomArray(Random random, Type? elementType)
+ {
+ if (elementType is null)
+ {
+ return null;
+ }
+
+ if (elementType == typeof(string))
+ {
+ return RandomStringArray(random);
+ }
+
+ if (elementType == typeof(int))
+ {
+ return RandomIntArray(random);
+ }
+
+ if (elementType.IsEnum)
+ {
+ var values = Enum.GetValues(elementType);
+ return RandomIntArray(random, 0, values.Length - 1);
+ }
+
+ throw new ArgumentException("Unsupported array type " + elementType.ToString());
+ }
+
+ /// <summary>
+ /// Creates a random length string.
+ /// </summary>
+ /// <param name="random">The <see cref="Random"/> instance.</param>
+ /// <param name="minLength">The minimum length of the string.</param>
+ /// <param name="maxLength">The maximum length of the string.</param>
+ /// <returns>The string.</returns>
+ private static string RandomString(Random random, int minLength = 0, int maxLength = 256)
+ {
+ var len = random.Next(minLength, maxLength);
+ var sb = new StringBuilder(len);
+
+ while (len > 0)
+ {
+ sb.Append((char)random.Next(65, 97));
+ len--;
+ }
+
+ return sb.ToString();
+ }
+
+ /// <summary>
+ /// Creates a random long.
+ /// </summary>
+ /// <param name="random">The <see cref="Random"/> instance.</param>
+ /// <param name="min">Min value.</param>
+ /// <param name="max">Max value.</param>
+ /// <returns>A random <see cref="long"/> between <paramref name="min"/> and <paramref name="max"/>.</returns>
+ private static long RandomLong(Random random, long min = -9223372036854775808, long max = 9223372036854775807)
+ {
+ long result = random.Next((int)(min >> 32), (int)(max >> 32));
+ result <<= 32;
+ result |= (long)random.Next((int)(min >> 32) << 32, (int)(max >> 32) << 32);
+ return result;
+ }
+
+ /// <summary>
+ /// Creates a random string array containing between <paramref name="minLength"/> and <paramref name="maxLength"/>.
+ /// </summary>
+ /// <param name="random">The <see cref="Random"/> instance.</param>
+ /// <param name="minLength">The minimum number of elements.</param>
+ /// <param name="maxLength">The maximum number of elements.</param>
+ /// <returns>A random <see cref="string[]"/> instance.</returns>
+ private static string[] RandomStringArray(Random random, int minLength = 0, int maxLength = 9)
+ {
+ var len = random.Next(minLength, maxLength);
+ var arr = new List<string>(len);
+ while (len > 0)
+ {
+ arr.Add(RandomString(random, 1, 30));
+ len--;
+ }
+
+ return arr.ToArray();
+ }
+
+ /// <summary>
+ /// Creates a random int array containing between <paramref name="minLength"/> and <paramref name="maxLength"/>.
+ /// </summary>
+ /// <param name="random">The <see cref="Random"/> instance.</param>
+ /// <param name="minLength">The minimum number of elements.</param>
+ /// <param name="maxLength">The maximum number of elements.</param>
+ /// <returns>A random <see cref="int[]"/> instance.</returns>
+ private static int[] RandomIntArray(Random random, int minLength = 0, int maxLength = 9)
+ {
+ var len = random.Next(minLength, maxLength);
+ var arr = new List<int>(len);
+ while (len > 0)
+ {
+ arr.Add(random.Next());
+ len--;
+ }
+
+ return arr.ToArray();
+ }
+
+ /// <summary>
+ /// Fills most properties with random data.
+ /// </summary>
+ /// <param name="destination">The instance to fill with data.</param>
+ private static void FillAllProperties<T>(T destination)
+ {
+ var random = new Random(RandomSeed);
+ var objectType = destination!.GetType();
+ foreach (var property in objectType.GetProperties())
+ {
+ if (!(property.CanRead && property.CanWrite))
+ {
+ continue;
+ }
+
+ var type = property.PropertyType;
+ // If nullable, then set it to null, 25% of the time.
+ if (Nullable.GetUnderlyingType(type) is not null)
+ {
+ if (random.Next(0, 4) == 0)
+ {
+ // Set it to null.
+ property.SetValue(destination, null);
+ continue;
+ }
+ }
+
+ if (type == typeof(Guid))
+ {
+ property.SetValue(destination, Guid.NewGuid());
+ continue;
+ }
+
+ if (type.IsEnum)
+ {
+ Array values = Enum.GetValues(property.PropertyType);
+ property.SetValue(destination, values.GetValue(random.Next(0, values.Length - 1)));
+ continue;
+ }
+
+ if (type == typeof(long))
+ {
+ property.SetValue(destination, RandomLong(random));
+ continue;
+ }
+
+ if (type == typeof(string))
+ {
+ property.SetValue(destination, RandomString(random));
+ continue;
+ }
+
+ if (type == typeof(bool))
+ {
+ property.SetValue(destination, random.Next(0, 1) == 1);
+ continue;
+ }
+
+ if (type == typeof(float))
+ {
+ property.SetValue(destination, RandomFloat(random));
+ continue;
+ }
+
+ if (type.IsArray)
+ {
+ property.SetValue(destination, RandomArray(random, type.GetElementType()));
+ continue;
+ }
+ }
+ }
+
+ [InlineData(DlnaProfileType.Audio)]
+ [InlineData(DlnaProfileType.Video)]
+ [InlineData(DlnaProfileType.Photo)]
+ [Theory]
+ public void Test_Blank_Url_Method(DlnaProfileType type)
+ {
+ var streamInfo = new LegacyStreamInfo(Guid.Empty, type)
+ {
+ DeviceProfile = new DeviceProfile()
+ };
+
+ string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123");
+
+ // New version will return and & after the ? due to optional parameters.
+ string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase);
+
+ Assert.Equal(legacyUrl, newUrl, ignoreCase: true);
+ }
+
+ [Fact]
+ public void Fuzzy_Comparison()
+ {
+ var streamInfo = new LegacyStreamInfo(Guid.Empty, DlnaProfileType.Video)
+ {
+ DeviceProfile = new DeviceProfile()
+ };
+ for (int i = 0; i < 100000; i++)
+ {
+ FillAllProperties(streamInfo);
+ string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123");
+
+ // New version will return and & after the ? due to optional parameters.
+ string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase);
+
+ Assert.Equal(legacyUrl, newUrl, ignoreCase: true);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json
index 2e3e6e6de..9d43d2166 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json
@@ -439,7 +439,14 @@
{
"Condition": "EqualsAny",
"Property": "VideoProfile",
- "Value": "high|main|baseline|constrained baseline|high 10",
+ "Value": "high|main|baseline|constrained baseline",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR",
"IsRequired": false,
"$type": "ProfileCondition"
},
@@ -479,6 +486,13 @@
"$type": "ProfileCondition"
},
{
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR|DOVIWithSDR|HDR10|DOVIWithHDR10|HLG|DOVIWithHLG",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
"Condition": "LessThanEqual",
"Property": "VideoLevel",
"Value": "183",
@@ -510,6 +524,21 @@
"$type": "CodecProfile"
}
],
+ "ContainerProfiles": [
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "LessThanEqual",
+ "Property": "NumStreams",
+ "Value": "32",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "$type": "ContainerProfile"
+ }
+ ],
"ResponseProfiles": [
{
"Container": "m4v",
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json
index 156230471..3859ef994 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json
@@ -439,7 +439,14 @@
{
"Condition": "EqualsAny",
"Property": "VideoProfile",
- "Value": "high|main|baseline|constrained baseline|high 10",
+ "Value": "high|main|baseline|constrained baseline",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR",
"IsRequired": false,
"$type": "ProfileCondition"
},
@@ -472,6 +479,13 @@
"$type": "ProfileCondition"
},
{
+ "Condition": "EqualsAny",
+ "Property": "VideoRangeType",
+ "Value": "SDR|DOVIWithSDR|HDR10|DOVIWithHDR10|HLG|DOVIWithHLG",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
"Condition": "LessThanEqual",
"Property": "VideoLevel",
"Value": "183",
@@ -483,6 +497,21 @@
"$type": "CodecProfile"
}
],
+ "ContainerProfiles": [
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "LessThanEqual",
+ "Property": "NumStreams",
+ "Value": "32",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "$type": "ContainerProfile"
+ }
+ ],
"ResponseProfiles": [
{
"Container": "m4v",
diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-32.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-32.json
new file mode 100644
index 000000000..6d01f8153
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-32.json
@@ -0,0 +1,565 @@
+{
+ "Id": "a766d122b58e45d9492d17af77748bf5",
+ "Path": "/Media/MyVideo-720p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 835317696,
+ "Name": "MyVideo-720p",
+ "ETag": "579a34c6d5dfb21d81539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "h264",
+ "CodecTag": "avc1",
+ "Language": "eng",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "720p H264 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "High",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": 41
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Profile": "LC",
+ "Index": 1,
+ "Score": 203
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 2,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 3,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 4,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 5,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 6,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 7,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 8,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 9,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 10,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 11,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 12,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 13,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 14,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 15,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 16,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 17,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 18,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 19,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 20,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 21,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 22,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 23,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 24,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 25,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 26,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 27,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 28,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 29,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 30,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 31,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ }
+ ],
+ "Bitrate": 2590008,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-33.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-33.json
new file mode 100644
index 000000000..ac24500fe
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-33.json
@@ -0,0 +1,582 @@
+{
+ "Id": "a766d122b58e45d9492d17af77748bf5",
+ "Path": "/Media/MyVideo-720p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 835317696,
+ "Name": "MyVideo-720p",
+ "ETag": "579a34c6d5dfb21d81539a51220b6a23",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "h264",
+ "CodecTag": "avc1",
+ "Language": "eng",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "720p H264 SDR",
+ "NalLengthSize": "0",
+ "BitRate": 2032876,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 720,
+ "Width": 1280,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "High",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": 41
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 164741,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Profile": "LC",
+ "Index": 1,
+ "Score": 203
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 2,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 3,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 4,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 5,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 6,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 7,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 8,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 9,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 10,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 11,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 12,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 13,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 14,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 15,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 16,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 17,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 18,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 19,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 20,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 21,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 22,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 23,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 24,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 25,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 26,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 27,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 28,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 29,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 30,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 31,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ },
+ {
+ "Codec": "srt",
+ "Language": "eng",
+ "TimeBase": "1/1000000",
+ "localizedUndefined": "Undefined",
+ "localizedDefault": "Default",
+ "localizedForced": "Forced",
+ "DisplayTitle": "En - Default",
+ "BitRate": 92,
+ "IsDefault": true,
+ "Type": 2,
+ "Index": 32,
+ "Score": 6421,
+ "IsExternal": true,
+ "IsTextSubtitleStream": true,
+ "SupportsExternalStream": true
+ }
+ ],
+ "Bitrate": 2590008,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}
diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
index 2c33ab492..51eb99f49 100644
--- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs
@@ -2,6 +2,7 @@ using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Model.Entities;
using Xunit;
+
using MediaType = Emby.Naming.Common.MediaType;
namespace Jellyfin.Naming.Tests.Video
@@ -20,6 +21,9 @@ namespace Jellyfin.Naming.Tests.Video
{
Test("trailer.mp4", ExtraType.Trailer);
Test("300-trailer.mp4", ExtraType.Trailer);
+ Test("300.trailer.mp4", ExtraType.Trailer);
+ Test("300_trailer.mp4", ExtraType.Trailer);
+ Test("300 trailer.mp4", ExtraType.Trailer);
Test("theme.mp3", ExtraType.ThemeSong);
}
@@ -43,6 +47,19 @@ namespace Jellyfin.Naming.Tests.Video
Test("300-deletedscene.mp4", ExtraType.DeletedScene);
Test("300-interview.mp4", ExtraType.Interview);
Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes);
+ Test("300-featurette.mp4", ExtraType.Featurette);
+ Test("300-short.mp4", ExtraType.Short);
+ Test("300-extra.mp4", ExtraType.Unknown);
+ Test("300-other.mp4", ExtraType.Unknown);
+ }
+
+ [Theory]
+ [InlineData(ExtraType.ThemeSong, "theme-music")]
+ public void TestDirectoriesAudioExtras(ExtraType type, string dirName)
+ {
+ Test(dirName + "/300.mp3", type);
+ Test("300/" + dirName + "/something.mp3", type);
+ Test("/data/something/Movies/300/" + dirName + "/whoknows.mp3", type);
}
[Theory]
@@ -52,11 +69,14 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData(ExtraType.Scene, "scenes")]
[InlineData(ExtraType.Sample, "samples")]
[InlineData(ExtraType.Short, "shorts")]
+ [InlineData(ExtraType.Trailer, "trailers")]
[InlineData(ExtraType.Featurette, "featurettes")]
[InlineData(ExtraType.Clip, "clips")]
[InlineData(ExtraType.ThemeVideo, "backdrops")]
+ [InlineData(ExtraType.Unknown, "extra")]
[InlineData(ExtraType.Unknown, "extras")]
- public void TestDirectories(ExtraType type, string dirName)
+ [InlineData(ExtraType.Unknown, "other")]
+ public void TestDirectoriesVideoExtras(ExtraType type, string dirName)
{
Test(dirName + "/300.mp4", type);
Test("300/" + dirName + "/something.mkv", type);
@@ -75,10 +95,44 @@ namespace Jellyfin.Naming.Tests.Video
Test("/data/something/Movies/" + dirName + "/" + dirName + ".mp4", null);
}
+ [Theory]
+ [InlineData(ExtraType.ThemeSong, "theme-music")]
+ public void TestTopLevelDirectoriesWithAudioExtraNames(ExtraType typicalType, string dirName)
+ {
+ string libraryRoot = "/data/something/" + dirName;
+ TestWithLibraryRoot(libraryRoot + "/300.mp3", libraryRoot, null);
+ TestWithLibraryRoot(libraryRoot + "/300/" + dirName + "/something.mp3", libraryRoot, typicalType);
+ }
+
+ [Theory]
+ [InlineData(ExtraType.Trailer, "trailers")]
+ [InlineData(ExtraType.ThemeVideo, "backdrops")]
+ [InlineData(ExtraType.BehindTheScenes, "behind the scenes")]
+ [InlineData(ExtraType.DeletedScene, "deleted scenes")]
+ [InlineData(ExtraType.Interview, "interviews")]
+ [InlineData(ExtraType.Scene, "scenes")]
+ [InlineData(ExtraType.Sample, "samples")]
+ [InlineData(ExtraType.Short, "shorts")]
+ [InlineData(ExtraType.Featurette, "featurettes")]
+ [InlineData(ExtraType.Unknown, "extras")]
+ [InlineData(ExtraType.Unknown, "extra")]
+ [InlineData(ExtraType.Unknown, "other")]
+ [InlineData(ExtraType.Clip, "clips")]
+ public void TestTopLevelDirectoriesWithVideoExtraNames(ExtraType typicalType, string dirName)
+ {
+ string libraryRoot = "/data/something/" + dirName;
+ TestWithLibraryRoot(libraryRoot + "/300.mp4", libraryRoot, null);
+ TestWithLibraryRoot(libraryRoot + "/300/" + dirName + "/something.mkv", libraryRoot, typicalType);
+ }
+
[Fact]
public void TestSample()
{
+ Test("sample.mp4", ExtraType.Sample);
Test("300-sample.mp4", ExtraType.Sample);
+ Test("300.sample.mp4", ExtraType.Sample);
+ Test("300_sample.mp4", ExtraType.Sample);
+ Test("300 sample.mp4", ExtraType.Sample);
}
private void Test(string input, ExtraType? expectedType)
@@ -88,6 +142,12 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Equal(expectedType, extraType);
}
+ private void TestWithLibraryRoot(string input, string libraryRoot, ExtraType? expectedType)
+ {
+ var extraType = ExtraRuleResolver.GetExtraInfo(input, _videoOptions, libraryRoot).ExtraType;
+ Assert.Equal(expectedType, extraType);
+ }
+
[Fact]
public void TestExtraInfo_InvalidRuleType()
{
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs
new file mode 100644
index 000000000..caf2b06b7
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs
@@ -0,0 +1,35 @@
+using System;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Server.Implementations.Item;
+using MediaBrowser.Controller.Entities;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Item;
+
+public class OrderMapperTests
+{
+ [Fact]
+ public void ShouldReturnMappedOrderForSortingByPremierDate()
+ {
+ var orderFunc = OrderMapper.MapOrderByField(ItemSortBy.PremiereDate, new InternalItemsQuery()).Compile();
+
+ var expectedDate = new DateTime(1, 2, 3);
+ var expectedProductionYearDate = new DateTime(4, 1, 1);
+
+ var entityWithOnlyProductionYear = new BaseItemEntity { Id = Guid.NewGuid(), Type = "Test", ProductionYear = expectedProductionYearDate.Year };
+ var entityWithOnlyPremierDate = new BaseItemEntity { Id = Guid.NewGuid(), Type = "Test", PremiereDate = expectedDate };
+ var entityWithBothPremierDateAndProductionYear = new BaseItemEntity { Id = Guid.NewGuid(), Type = "Test", PremiereDate = expectedDate, ProductionYear = expectedProductionYearDate.Year };
+ var entityWithoutEitherPremierDateOrProductionYear = new BaseItemEntity { Id = Guid.NewGuid(), Type = "Test" };
+
+ var resultWithOnlyProductionYear = orderFunc(entityWithOnlyProductionYear);
+ var resultWithOnlyPremierDate = orderFunc(entityWithOnlyPremierDate);
+ var resultWithBothPremierDateAndProductionYear = orderFunc(entityWithBothPremierDateAndProductionYear);
+ var resultWithoutEitherPremierDateOrProductionYear = orderFunc(entityWithoutEitherPremierDateOrProductionYear);
+
+ Assert.Equal(resultWithOnlyProductionYear, expectedProductionYearDate);
+ Assert.Equal(resultWithOnlyPremierDate, expectedDate);
+ Assert.Equal(resultWithBothPremierDateAndProductionYear, expectedDate);
+ Assert.Null(resultWithoutEitherPremierDateOrProductionYear);
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index 5babc9117..026da4992 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -88,7 +88,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
Assert.NotNull(tvma);
- Assert.Equal(17, tvma!.Value);
+ Assert.Equal(17, tvma!.RatingScore!.Score);
}
[Fact]
@@ -105,47 +105,49 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal));
Assert.NotNull(fsk);
- Assert.Equal(12, fsk!.Value);
+ Assert.Equal(12, fsk!.RatingScore!.Score);
}
[Theory]
- [InlineData("CA-R", "CA", 18)]
- [InlineData("FSK-16", "DE", 16)]
- [InlineData("FSK-18", "DE", 18)]
- [InlineData("FSK-18", "US", 18)]
- [InlineData("TV-MA", "US", 17)]
- [InlineData("XXX", "asdf", 1000)]
- [InlineData("Germany: FSK-18", "DE", 18)]
- [InlineData("Rated : R", "US", 17)]
- [InlineData("Rated: R", "US", 17)]
- [InlineData("Rated R", "US", 17)]
- [InlineData(" PG-13 ", "US", 13)]
- public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel)
+ [InlineData("CA-R", "CA", 18, 1)]
+ [InlineData("FSK-16", "DE", 16, null)]
+ [InlineData("FSK-18", "DE", 18, null)]
+ [InlineData("FSK-18", "US", 18, null)]
+ [InlineData("TV-MA", "US", 17, 1)]
+ [InlineData("XXX", "asdf", 1000, null)]
+ [InlineData("Germany: FSK-18", "DE", 18, null)]
+ [InlineData("Rated : R", "US", 17, 0)]
+ [InlineData("Rated: R", "US", 17, 0)]
+ [InlineData("Rated R", "US", 17, 0)]
+ [InlineData(" PG-13 ", "US", 13, 0)]
+ public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int? expectedScore, int? expectedSubScore)
{
var localizationManager = Setup(new ServerConfiguration()
{
MetadataCountryCode = countryCode
});
await localizationManager.LoadAll();
- var level = localizationManager.GetRatingLevel(value);
- Assert.NotNull(level);
- Assert.Equal(expectedLevel, level!);
+ var score = localizationManager.GetRatingScore(value);
+ Assert.NotNull(score);
+ Assert.Equal(expectedScore, score.Score);
+ Assert.Equal(expectedSubScore, score.SubScore);
}
[Theory]
- [InlineData("0", 0)]
- [InlineData("1", 1)]
- [InlineData("6", 6)]
- [InlineData("12", 12)]
- [InlineData("42", 42)]
- [InlineData("9999", 9999)]
- public async Task GetRatingLevel_GivenValidAge_Success(string value, int expectedLevel)
+ [InlineData("0", 0, null)]
+ [InlineData("1", 1, null)]
+ [InlineData("6", 6, null)]
+ [InlineData("12", 12, null)]
+ [InlineData("42", 42, null)]
+ [InlineData("9999", 9999, null)]
+ public async Task GetRatingLevel_GivenValidAge_Success(string value, int? expectedScore, int? expectedSubScore)
{
var localizationManager = Setup(new ServerConfiguration { MetadataCountryCode = "nl" });
await localizationManager.LoadAll();
- var level = localizationManager.GetRatingLevel(value);
- Assert.NotNull(level);
- Assert.Equal(expectedLevel, level);
+ var score = localizationManager.GetRatingScore(value);
+ Assert.NotNull(score);
+ Assert.Equal(expectedScore, score.Score);
+ Assert.Equal(expectedSubScore, score.SubScore);
}
[Fact]
@@ -156,10 +158,10 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
UICulture = "de-DE"
});
await localizationManager.LoadAll();
- Assert.Null(localizationManager.GetRatingLevel("NR"));
- Assert.Null(localizationManager.GetRatingLevel("unrated"));
- Assert.Null(localizationManager.GetRatingLevel("Not Rated"));
- Assert.Null(localizationManager.GetRatingLevel("n/a"));
+ Assert.Null(localizationManager.GetRatingScore("NR"));
+ Assert.Null(localizationManager.GetRatingScore("unrated"));
+ Assert.Null(localizationManager.GetRatingScore("Not Rated"));
+ Assert.Null(localizationManager.GetRatingScore("n/a"));
}
[Theory]
@@ -173,7 +175,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
});
await localizationManager.LoadAll();
- Assert.Null(localizationManager.GetRatingLevel(value));
+ Assert.Null(localizationManager.GetRatingScore(value));
}
[Theory]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Playlists/PlaylistManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Playlists/PlaylistManagerTests.cs
new file mode 100644
index 000000000..cc8ca720e
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Playlists/PlaylistManagerTests.cs
@@ -0,0 +1,40 @@
+using Emby.Server.Implementations.Playlists;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Playlists;
+
+public class PlaylistManagerTests
+{
+ [Fact]
+ public void DetermineAdjustedIndexMoveToFirstPositionNoPriorInAllList()
+ {
+ var priorIndexAllChildren = 0;
+ var newIndex = 0;
+
+ var adjustedIndex = PlaylistManager.DetermineAdjustedIndex(priorIndexAllChildren, newIndex);
+
+ Assert.Equal(0, adjustedIndex);
+ }
+
+ [Fact]
+ public void DetermineAdjustedIndexPriorInMiddleOfAllList()
+ {
+ var priorIndexAllChildren = 2;
+ var newIndex = 0;
+
+ var adjustedIndex = PlaylistManager.DetermineAdjustedIndex(priorIndexAllChildren, newIndex);
+
+ Assert.Equal(1, adjustedIndex);
+ }
+
+ [Fact]
+ public void DetermineAdjustedIndexMoveMiddleOfPlaylist()
+ {
+ var priorIndexAllChildren = 2;
+ var newIndex = 1;
+
+ var adjustedIndex = PlaylistManager.DetermineAdjustedIndex(priorIndexAllChildren, newIndex);
+
+ Assert.Equal(3, adjustedIndex);
+ }
+}