From d0b4b2ddb31a54f0705303ab8461be1125d66eab Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 7 Sep 2024 19:07:34 +0000 Subject: [PATCH 001/457] Migrated UserData from library sqlite db to jellyfin.db --- .../ApplicationHost.cs | 2 - .../Data/SqliteUserDataRepository.cs | 369 --------- .../Library/UserDataManager.cs | 91 +- .../TV/TVSeriesManager.cs | 7 +- .../Controllers/UserLibraryController.cs | 16 +- Jellyfin.Data/Entities/UserData.cs | 73 ++ .../JellyfinDbContext.cs | 5 + ...20240907123425_UserDataInJfLib.Designer.cs | 775 ++++++++++++++++++ .../20240907123425_UserDataInJfLib.cs | 79 ++ .../Migrations/JellyfinDbModelSnapshot.cs | 67 +- .../UserDataConfiguration.cs | 23 + .../Migrations/Routines/MigrateUserData.cs | 89 ++ .../Library/IUserDataManager.cs | 2 +- .../Persistence/IUserDataRepository.cs | 55 -- .../Parsers/BaseNfoParser.cs | 21 +- .../Savers/BaseNfoSaver.cs | 59 +- .../Parsers/MovieNfoParserTests.cs | 2 +- 17 files changed, 1244 insertions(+), 491 deletions(-) delete mode 100644 Emby.Server.Implementations/Data/SqliteUserDataRepository.cs create mode 100644 Jellyfin.Data/Entities/UserData.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20240907123425_UserDataInJfLib.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20240907123425_UserDataInJfLib.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs create mode 100644 Jellyfin.Server/Migrations/Routines/MigrateUserData.cs delete mode 100644 MediaBrowser.Controller/Persistence/IUserDataRepository.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 5292003f09..bdf013b5d6 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -492,7 +492,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -580,7 +579,6 @@ namespace Emby.Server.Implementations } ((SqliteItemRepository)Resolve()).Initialize(); - ((SqliteUserDataRepository)Resolve()).Initialize(); var localizationManager = (LocalizationManager)Resolve(); await localizationManager.LoadAll().ConfigureAwait(false); diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs deleted file mode 100644 index bfdcc08f42..0000000000 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ /dev/null @@ -1,369 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using Jellyfin.Data.Entities; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Persistence; -using Microsoft.Data.Sqlite; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Data -{ - public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository - { - private readonly IUserManager _userManager; - - public SqliteUserDataRepository( - ILogger logger, - IServerConfigurationManager config, - IUserManager userManager) - : base(logger) - { - _userManager = userManager; - - DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db"); - } - - /// - /// Opens the connection to the database. - /// - public override void Initialize() - { - base.Initialize(); - - using (var connection = GetConnection()) - { - var userDatasTableExists = TableExists(connection, "UserDatas"); - var userDataTableExists = TableExists(connection, "userdata"); - - var users = userDatasTableExists ? null : _userManager.Users; - using var transaction = connection.BeginTransaction(); - connection.Execute(string.Join( - ';', - "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)", - "drop index if exists idx_userdata", - "drop index if exists idx_userdata1", - "drop index if exists idx_userdata2", - "drop index if exists userdataindex1", - "drop index if exists userdataindex", - "drop index if exists userdataindex3", - "drop index if exists userdataindex4", - "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)", - "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)", - "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)", - "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)", - "create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)")); - - if (!userDataTableExists) - { - transaction.Commit(); - return; - } - - var existingColumnNames = GetColumnNames(connection, "userdata"); - - AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames); - AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames); - AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames); - - if (userDatasTableExists) - { - return; - } - - ImportUserIds(connection, users); - - connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null"); - - transaction.Commit(); - } - } - - private void ImportUserIds(ManagedConnection db, IEnumerable users) - { - var userIdsWithUserData = GetAllUserIdsWithUserData(db); - - using (var statement = db.PrepareStatement("update userdata set InternalUserId=@InternalUserId where UserId=@UserId")) - { - foreach (var user in users) - { - if (!userIdsWithUserData.Contains(user.Id)) - { - continue; - } - - statement.TryBind("@UserId", user.Id); - statement.TryBind("@InternalUserId", user.InternalId); - - statement.ExecuteNonQuery(); - } - } - } - - private List GetAllUserIdsWithUserData(ManagedConnection db) - { - var list = new List(); - - using (var statement = PrepareStatement(db, "select DISTINCT UserId from UserData where UserId not null")) - { - foreach (var row in statement.ExecuteQuery()) - { - try - { - list.Add(row.GetGuid(0)); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error while getting user"); - } - } - } - - return list; - } - - /// - public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(userData); - - if (userId <= 0) - { - throw new ArgumentNullException(nameof(userId)); - } - - ArgumentException.ThrowIfNullOrEmpty(key); - - PersistUserData(userId, key, userData, cancellationToken); - } - - /// - public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(userData); - - if (userId <= 0) - { - throw new ArgumentNullException(nameof(userId)); - } - - PersistAllUserData(userId, userData, cancellationToken); - } - - /// - /// Persists the user data. - /// - /// The user id. - /// The key. - /// The user data. - /// The cancellation token. - public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - using (var connection = GetConnection()) - using (var transaction = connection.BeginTransaction()) - { - SaveUserData(connection, internalUserId, key, userData); - transaction.Commit(); - } - } - - private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData) - { - using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)")) - { - statement.TryBind("@userId", internalUserId); - statement.TryBind("@key", key); - - if (userData.Rating.HasValue) - { - statement.TryBind("@rating", userData.Rating.Value); - } - else - { - statement.TryBindNull("@rating"); - } - - statement.TryBind("@played", userData.Played); - statement.TryBind("@playCount", userData.PlayCount); - statement.TryBind("@isFavorite", userData.IsFavorite); - statement.TryBind("@playbackPositionTicks", userData.PlaybackPositionTicks); - - if (userData.LastPlayedDate.HasValue) - { - statement.TryBind("@lastPlayedDate", userData.LastPlayedDate.Value.ToDateTimeParamValue()); - } - else - { - statement.TryBindNull("@lastPlayedDate"); - } - - if (userData.AudioStreamIndex.HasValue) - { - statement.TryBind("@AudioStreamIndex", userData.AudioStreamIndex.Value); - } - else - { - statement.TryBindNull("@AudioStreamIndex"); - } - - if (userData.SubtitleStreamIndex.HasValue) - { - statement.TryBind("@SubtitleStreamIndex", userData.SubtitleStreamIndex.Value); - } - else - { - statement.TryBindNull("@SubtitleStreamIndex"); - } - - statement.ExecuteNonQuery(); - } - } - - /// - /// Persist all user data for the specified user. - /// - private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - using (var connection = GetConnection()) - using (var transaction = connection.BeginTransaction()) - { - foreach (var userItemData in userDataList) - { - SaveUserData(connection, internalUserId, userItemData.Key, userItemData); - } - - transaction.Commit(); - } - } - - /// - /// Gets the user data. - /// - /// The user id. - /// The key. - /// Task{UserItemData}. - /// - /// userId - /// or - /// key. - /// - public UserItemData GetUserData(long userId, string key) - { - if (userId <= 0) - { - throw new ArgumentNullException(nameof(userId)); - } - - ArgumentException.ThrowIfNullOrEmpty(key); - - using (var connection = GetConnection(true)) - { - using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId")) - { - statement.TryBind("@UserId", userId); - statement.TryBind("@Key", key); - - foreach (var row in statement.ExecuteQuery()) - { - return ReadRow(row); - } - } - - return null; - } - } - - public UserItemData GetUserData(long userId, List keys) - { - ArgumentNullException.ThrowIfNull(keys); - - if (keys.Count == 0) - { - return null; - } - - return GetUserData(userId, keys[0]); - } - - /// - /// Return all user-data associated with the given user. - /// - /// The internal user id. - /// The list of user item data. - public List GetAllUserData(long userId) - { - if (userId <= 0) - { - throw new ArgumentNullException(nameof(userId)); - } - - var list = new List(); - - using (var connection = GetConnection()) - { - using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId")) - { - statement.TryBind("@UserId", userId); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(ReadRow(row)); - } - } - } - - return list; - } - - /// - /// Read a row from the specified reader into the provided userData object. - /// - /// The list of result set values. - /// The user item data. - private UserItemData ReadRow(SqliteDataReader reader) - { - var userData = new UserItemData - { - Key = reader.GetString(0) - }; - - if (reader.TryGetDouble(2, out var rating)) - { - userData.Rating = rating; - } - - userData.Played = reader.GetBoolean(3); - userData.PlayCount = reader.GetInt32(4); - userData.IsFavorite = reader.GetBoolean(5); - userData.PlaybackPositionTicks = reader.GetInt64(6); - - if (reader.TryReadDateTime(7, out var lastPlayedDate)) - { - userData.LastPlayedDate = lastPlayedDate; - } - - if (reader.TryGetInt32(8, out var audioStreamIndex)) - { - userData.AudioStreamIndex = audioStreamIndex; - } - - if (reader.TryGetInt32(9, out var subtitleStreamIndex)) - { - userData.SubtitleStreamIndex = subtitleStreamIndex; - } - - return userData; - } - } -} diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 62d22b23ff..c8c14c187a 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -3,15 +3,17 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.Linq; using System.Threading; using Jellyfin.Data.Entities; +using Jellyfin.Server.Implementations; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using Microsoft.EntityFrameworkCore; using AudioBook = MediaBrowser.Controller.Entities.AudioBook; using Book = MediaBrowser.Controller.Entities.Book; @@ -26,22 +28,18 @@ namespace Emby.Server.Implementations.Library new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); private readonly IServerConfigurationManager _config; - private readonly IUserManager _userManager; - private readonly IUserDataRepository _repository; + private readonly IDbContextFactory _repository; /// /// Initializes a new instance of the class. /// /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. + /// Instance of the interface. public UserDataManager( IServerConfigurationManager config, - IUserManager userManager, - IUserDataRepository repository) + IDbContextFactory repository) { _config = config; - _userManager = userManager; _repository = repository; } @@ -61,11 +59,16 @@ namespace Emby.Server.Implementations.Library var userId = user.InternalId; + using var repository = _repository.CreateDbContext(); + foreach (var key in keys) { - _repository.SaveUserData(userId, key, userData, cancellationToken); + userData.Key = key; + repository.UserData.Add(Map(userData, user.Id)); } + repository.SaveChanges(); + var cacheKey = GetCacheKey(userId, item.Id); _userData.AddOrUpdate(cacheKey, userData, (_, _) => userData); @@ -87,7 +90,7 @@ namespace Emby.Server.Implementations.Library ArgumentNullException.ThrowIfNull(reason); ArgumentNullException.ThrowIfNull(userDataDto); - var userData = GetUserData(user, item); + var userData = GetUserData(user, item) ?? throw new InvalidOperationException("Did not expect UserData to be null."); if (userDataDto.PlaybackPositionTicks.HasValue) { @@ -127,22 +130,68 @@ namespace Emby.Server.Implementations.Library SaveUserData(user, item, userData, reason, CancellationToken.None); } - private UserItemData GetUserData(User user, Guid itemId, List keys) + private UserData Map(UserItemData dto, Guid userId) { - var userId = user.InternalId; - - var cacheKey = GetCacheKey(userId, itemId); - - return _userData.GetOrAdd(cacheKey, _ => GetUserDataInternal(userId, keys)); + return new UserData() + { + Key = dto.Key, + AudioStreamIndex = dto.AudioStreamIndex, + IsFavorite = dto.IsFavorite, + LastPlayedDate = dto.LastPlayedDate, + Likes = dto.Likes, + PlaybackPositionTicks = dto.PlaybackPositionTicks, + PlayCount = dto.PlayCount, + Played = dto.Played, + Rating = dto.Rating, + UserId = userId, + SubtitleStreamIndex = dto.SubtitleStreamIndex, + }; } - private UserItemData GetUserDataInternal(long internalUserId, List keys) + private UserItemData Map(UserData dto) { - var userData = _repository.GetUserData(internalUserId, keys); + return new UserItemData() + { + Key = dto.Key, + AudioStreamIndex = dto.AudioStreamIndex, + IsFavorite = dto.IsFavorite, + LastPlayedDate = dto.LastPlayedDate, + Likes = dto.Likes, + PlaybackPositionTicks = dto.PlaybackPositionTicks, + PlayCount = dto.PlayCount, + Played = dto.Played, + Rating = dto.Rating, + SubtitleStreamIndex = dto.SubtitleStreamIndex, + }; + } + + private UserItemData? GetUserData(User user, Guid itemId, List keys) + { + var cacheKey = GetCacheKey(user.InternalId, itemId); + var data = GetUserDataInternal(user.Id, keys); + + if (data is null) + { + return null; + } + + return _userData.GetOrAdd(cacheKey, data); + } + + private UserItemData? GetUserDataInternal(Guid userId, List keys) + { + using var context = _repository.CreateDbContext(); + var key = keys.FirstOrDefault(); + if (key is null) + { + return null; + } + + var userData = context.UserData.AsNoTracking().FirstOrDefault(e => e.Key == key && e.UserId.Equals(userId)); if (userData is not null) { - return userData; + return Map(userData); } if (keys.Count > 0) @@ -166,7 +215,7 @@ namespace Emby.Server.Implementations.Library } /// - public UserItemData GetUserData(User user, BaseItem item) + public UserItemData? GetUserData(User user, BaseItem item) { return GetUserData(user, item.Id, item.GetUserDataKeys()); } @@ -178,7 +227,7 @@ namespace Emby.Server.Implementations.Library /// public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options) { - var userData = GetUserData(user, item); + var userData = GetUserData(user, item) ?? throw new InvalidOperationException("Did not expect UserData to be null."); var dto = GetUserItemDataDto(userData); item.FillUserDataDtoValues(dto, userData, itemDto, user, options); diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index d11b03a2e2..2a03c30798 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -262,7 +262,7 @@ namespace Emby.Server.Implementations.TV { var userData = _userDataManager.GetUserData(user, nextEpisode); - if (userData.PlaybackPositionTicks > 0) + if (userData?.PlaybackPositionTicks > 0) { return null; } @@ -275,6 +275,11 @@ namespace Emby.Server.Implementations.TV { var userData = _userDataManager.GetUserData(user, lastWatchedEpisode); + if (userData is null) + { + return (DateTime.MinValue, GetEpisode); + } + var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1); return (lastWatchedDate, GetEpisode); diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index e7bf717274..b34daba7f3 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -662,10 +662,13 @@ public class UserLibraryController : BaseJellyfinApiController // Get the user data for this item var data = _userDataRepository.GetUserData(user, item); - // Set favorite status - data.IsFavorite = isFavorite; + if (data is not null) + { + // Set favorite status + data.IsFavorite = isFavorite; - _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None); + _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None); + } return _userDataRepository.GetUserDataDto(item, user); } @@ -681,9 +684,12 @@ public class UserLibraryController : BaseJellyfinApiController // Get the user data for this item var data = _userDataRepository.GetUserData(user, item); - data.Likes = likes; + if (data is not null) + { + data.Likes = likes; - _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None); + _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None); + } return _userDataRepository.GetUserDataDto(item, user); } diff --git a/Jellyfin.Data/Entities/UserData.cs b/Jellyfin.Data/Entities/UserData.cs new file mode 100644 index 0000000000..b9aea664aa --- /dev/null +++ b/Jellyfin.Data/Entities/UserData.cs @@ -0,0 +1,73 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public class UserData +{ + /// + /// Gets or sets the key. + /// + /// The key. + public required string Key { get; set; } + + /// + /// Gets or sets the users 0-10 rating. + /// + /// The rating. + public double? Rating { get; set; } + + /// + /// Gets or sets the playback position ticks. + /// + /// The playback position ticks. + public long PlaybackPositionTicks { get; set; } + + /// + /// Gets or sets the play count. + /// + /// The play count. + public int PlayCount { get; set; } + + /// + /// Gets or sets a value indicating whether this instance is favorite. + /// + /// true if this instance is favorite; otherwise, false. + public bool IsFavorite { get; set; } + + /// + /// Gets or sets the last played date. + /// + /// The last played date. + public DateTime? LastPlayedDate { get; set; } + + /// + /// Gets or sets a value indicating whether this is played. + /// + /// true if played; otherwise, false. + public bool Played { get; set; } + + /// + /// Gets or sets the index of the audio stream. + /// + /// The index of the audio stream. + public int? AudioStreamIndex { get; set; } + + /// + /// Gets or sets the index of the subtitle stream. + /// + /// The index of the subtitle stream. + public int? SubtitleStreamIndex { get; set; } + + /// + /// Gets or sets a value indicating whether the item is liked or not. + /// This should never be serialized. + /// + /// null if [likes] contains no value, true if [likes]; otherwise, false. + public bool? Likes { get; set; } + + public Guid UserId { get; set; } + + public User? User { get; set; } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index 150bc8bb4e..8e2c21fbc8 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -88,6 +88,11 @@ public class JellyfinDbContext : DbContext /// public DbSet MediaSegments => Set(); + /// + /// Gets the containing the user data. + /// + public DbSet UserData => Set(); + /*public DbSet Artwork => Set(); public DbSet Books => Set(); diff --git a/Jellyfin.Server.Implementations/Migrations/20240907123425_UserDataInJfLib.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20240907123425_UserDataInJfLib.Designer.cs new file mode 100644 index 0000000000..609faa1e60 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20240907123425_UserDataInJfLib.Designer.cs @@ -0,0 +1,775 @@ +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20240907123425_UserDataInJfLib")] + partial class UserDataInJfLib + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasIndex("UserId"); + + b.HasIndex("Key", "UserId") + .IsUnique(); + + b.HasIndex("Key", "UserId", "IsFavorite"); + + b.HasIndex("Key", "UserId", "LastPlayedDate"); + + b.HasIndex("Key", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("Key", "UserId", "Played"); + + b.ToTable("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20240907123425_UserDataInJfLib.cs b/Jellyfin.Server.Implementations/Migrations/20240907123425_UserDataInJfLib.cs new file mode 100644 index 0000000000..cb9a01f5b8 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20240907123425_UserDataInJfLib.cs @@ -0,0 +1,79 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class UserDataInJfLib : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserData", + columns: table => new + { + Key = table.Column(type: "TEXT", nullable: false), + Rating = table.Column(type: "REAL", nullable: true), + PlaybackPositionTicks = table.Column(type: "INTEGER", nullable: false), + PlayCount = table.Column(type: "INTEGER", nullable: false), + IsFavorite = table.Column(type: "INTEGER", nullable: false), + LastPlayedDate = table.Column(type: "TEXT", nullable: true), + Played = table.Column(type: "INTEGER", nullable: false), + AudioStreamIndex = table.Column(type: "INTEGER", nullable: true), + SubtitleStreamIndex = table.Column(type: "INTEGER", nullable: true), + Likes = table.Column(type: "INTEGER", nullable: true), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.ForeignKey( + name: "FK_UserData_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_UserData_Key_UserId", + table: "UserData", + columns: new[] { "Key", "UserId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UserData_Key_UserId_IsFavorite", + table: "UserData", + columns: new[] { "Key", "UserId", "IsFavorite" }); + + migrationBuilder.CreateIndex( + name: "IX_UserData_Key_UserId_LastPlayedDate", + table: "UserData", + columns: new[] { "Key", "UserId", "LastPlayedDate" }); + + migrationBuilder.CreateIndex( + name: "IX_UserData_Key_UserId_PlaybackPositionTicks", + table: "UserData", + columns: new[] { "Key", "UserId", "PlaybackPositionTicks" }); + + migrationBuilder.CreateIndex( + name: "IX_UserData_Key_UserId_Played", + table: "UserData", + columns: new[] { "Key", "UserId", "Played" }); + + migrationBuilder.CreateIndex( + name: "IX_UserData_UserId", + table: "UserData", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserData"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index d70f7956a8..399e2a08ac 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using Jellyfin.Server.Implementations; using Microsoft.EntityFrameworkCore; @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -612,6 +612,58 @@ namespace Jellyfin.Server.Implementations.Migrations b.ToTable("Users"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasIndex("UserId"); + + b.HasIndex("Key", "UserId") + .IsUnique(); + + b.HasIndex("Key", "UserId", "IsFavorite"); + + b.HasIndex("Key", "UserId", "LastPlayedDate"); + + b.HasIndex("Key", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("Key", "UserId", "Played"); + + b.ToTable("UserData"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { b.HasOne("Jellyfin.Data.Entities.User", null) @@ -683,6 +735,17 @@ namespace Jellyfin.Server.Implementations.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => { b.Navigation("HomeSections"); diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs new file mode 100644 index 0000000000..8e64844378 --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs @@ -0,0 +1,23 @@ +using System; +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + +/// +/// FluentAPI configuration for the UserData entity. +/// +public class UserDataConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasNoKey(); + builder.HasIndex(d => new { d.Key, d.UserId }).IsUnique(); + builder.HasIndex(d => new { d.Key, d.UserId, d.Played }); + builder.HasIndex(d => new { d.Key, d.UserId, d.PlaybackPositionTicks }); + builder.HasIndex(d => new { d.Key, d.UserId, d.IsFavorite }); + builder.HasIndex(d => new { d.Key, d.UserId, d.LastPlayedDate }); + } +} diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserData.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserData.cs new file mode 100644 index 0000000000..224534d436 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserData.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using Emby.Server.Implementations.Data; +using Jellyfin.Data.Entities; +using Jellyfin.Server.Implementations; +using MediaBrowser.Controller; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines; + +/// +/// The migration routine for migrating the userdata database to EF Core. +/// +public class MigrateUserData : IMigrationRoutine +{ + private const string DbFilename = "library.db"; + + private readonly ILogger _logger; + private readonly IServerApplicationPaths _paths; + private readonly IDbContextFactory _provider; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The database provider. + /// The server application paths. + public MigrateUserData( + ILogger logger, + IDbContextFactory provider, + IServerApplicationPaths paths) + { + _logger = logger; + _provider = provider; + _paths = paths; + } + + /// + public Guid Id => Guid.Parse("5bcb4197-e7c0-45aa-9902-963bceab5798"); + + /// + public string Name => "MigrateUserData"; + + /// + public bool PerformOnNewInstall => false; + + /// + public void Perform() + { + _logger.LogInformation("Migrating the userdata from library.db may take a while, do not stop Jellyfin."); + + var dataPath = _paths.DataPath; + using var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"); + + connection.Open(); + using var dbContext = _provider.CreateDbContext(); + + var queryResult = connection.Query("SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex FROM UserDatas"); + + dbContext.UserData.ExecuteDelete(); + + var users = dbContext.Users.AsNoTracking().ToImmutableArray(); + + foreach (SqliteDataReader dto in queryResult) + { + var entity = new UserData() + { + Key = dto.GetString(0), + UserId = users.ElementAt(dto.GetInt32(1)).Id, + Rating = dto.IsDBNull(2) ? null : dto.GetDouble(2), + Played = dto.GetBoolean(3), + PlayCount = dto.GetInt32(4), + IsFavorite = dto.GetBoolean(5), + PlaybackPositionTicks = dto.GetInt64(6), + LastPlayedDate = dto.IsDBNull(7) ? null : dto.GetDateTime(7), + AudioStreamIndex = dto.IsDBNull(8) ? null : dto.GetInt32(8), + SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9), + }; + + dbContext.UserData.Add(entity); + } + + dbContext.SaveChanges(); + } +} diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs index f36fd393f7..b43c62708f 100644 --- a/MediaBrowser.Controller/Library/IUserDataManager.cs +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -44,7 +44,7 @@ namespace MediaBrowser.Controller.Library /// User to use. /// Item to use. /// User data. - UserItemData GetUserData(User user, BaseItem item); + UserItemData? GetUserData(User user, BaseItem item); /// /// Gets the user data dto. diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs deleted file mode 100644 index f2fb2826a0..0000000000 --- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs +++ /dev/null @@ -1,55 +0,0 @@ -#nullable disable - -using System; -using System.Collections.Generic; -using System.Threading; -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Persistence -{ - /// - /// Provides an interface to implement a UserData repository. - /// - public interface IUserDataRepository : IDisposable - { - /// - /// Saves the user data. - /// - /// The user id. - /// The key. - /// The user data. - /// The cancellation token. - void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken); - - /// - /// Gets the user data. - /// - /// The user id. - /// The key. - /// The user data. - UserItemData GetUserData(long userId, string key); - - /// - /// Gets the user data. - /// - /// The user id. - /// The keys. - /// The user data. - UserItemData GetUserData(long userId, List keys); - - /// - /// Return all user data associated with the given user. - /// - /// The user id. - /// The list of user item data. - List GetAllUserData(long userId); - - /// - /// Save all user data associated with the given user. - /// - /// The user id. - /// The user item data. - /// The cancellation token. - void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken); - } -} diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index a8800431e1..3ad8e1f69b 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -312,8 +312,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (user is not null) { userData = _userDataManager.GetUserData(user, item); - userData.Played = played; - _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); + if (userData is not null) + { + userData.Played = played; + _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); + } } } @@ -326,8 +329,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (user is not null) { userData = _userDataManager.GetUserData(user, item); - userData.PlayCount = count; - _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); + if (userData is not null) + { + userData.PlayCount = count; + _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); + } } } @@ -340,8 +346,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (user is not null) { userData = _userDataManager.GetUserData(user, item); - userData.LastPlayedDate = lastPlayed; - _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); + if (userData is not null) + { + userData.LastPlayedDate = lastPlayed; + _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None); + } } } diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index a547779de4..a3c200447b 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -869,43 +869,46 @@ namespace MediaBrowser.XbmcMetadata.Savers var userdata = userDataRepo.GetUserData(user, item); - writer.WriteElementString( + if (userdata is not null) + { + writer.WriteElementString( "isuserfavorite", userdata.IsFavorite.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); - if (userdata.Rating.HasValue) - { - writer.WriteElementString( - "userrating", - userdata.Rating.Value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); - } - - if (!item.IsFolder) - { - writer.WriteElementString( - "playcount", - userdata.PlayCount.ToString(CultureInfo.InvariantCulture)); - writer.WriteElementString( - "watched", - userdata.Played.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); - - if (userdata.LastPlayedDate.HasValue) + if (userdata.Rating.HasValue) { writer.WriteElementString( - "lastplayed", - userdata.LastPlayedDate.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture).ToLowerInvariant()); + "userrating", + userdata.Rating.Value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); } - writer.WriteStartElement("resume"); + if (!item.IsFolder) + { + writer.WriteElementString( + "playcount", + userdata.PlayCount.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString( + "watched", + userdata.Played.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); - var runTimeTicks = item.RunTimeTicks ?? 0; + if (userdata.LastPlayedDate.HasValue) + { + writer.WriteElementString( + "lastplayed", + userdata.LastPlayedDate.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture).ToLowerInvariant()); + } - writer.WriteElementString( - "position", - TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture)); - writer.WriteElementString( - "total", - TimeSpan.FromTicks(runTimeTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture)); + writer.WriteStartElement("resume"); + + var runTimeTicks = item.RunTimeTicks ?? 0; + + writer.WriteElementString( + "position", + TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString( + "total", + TimeSpan.FromTicks(runTimeTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture)); + } } writer.WriteEndElement(); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs index 5bc4abd06d..20a8f6152f 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs @@ -149,7 +149,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers Assert.Equal(new DateTime(2019, 8, 6, 9, 1, 18), item.DateCreated); // userData - var userData = _userDataManager.GetUserData(_testUser, item); + var userData = _userDataManager.GetUserData(_testUser, item)!; Assert.Equal(2, userData.PlayCount); Assert.True(userData.Played); Assert.Equal(new DateTime(2021, 02, 11, 07, 47, 23), userData.LastPlayedDate); From ee1bdf4e222125ed7382165fd7e09599ca4bd4aa Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 8 Sep 2024 16:56:14 +0000 Subject: [PATCH 002/457] WIP move baseitem to jellyfin.db --- .../Data/SqliteItemRepository.cs | 1646 ----------------- Jellyfin.Data/Entities/AncestorId.cs | 19 + .../Entities/AttachmentStreamInfo.cs | 21 + Jellyfin.Data/Entities/BaseItem.cs | 164 ++ Jellyfin.Data/Entities/Chapter.cs | 22 + Jellyfin.Data/Entities/ItemValue.cs | 16 + Jellyfin.Data/Entities/MediaStreamInfo.cs | 101 + Jellyfin.Data/Entities/People.cs | 16 + .../Item/BaseItemManager.cs | 753 ++++++++ .../Item/ChapterManager.cs | 51 + .../JellyfinDbContext.cs | 35 + .../AncestorIdConfiguration.cs | 20 + .../BaseItemConfiguration.cs | 42 + .../ChapterConfiguration.cs | 20 + .../ItemValuesConfiguration.cs | 20 + .../ModelConfiguration/PeopleConfiguration.cs | 20 + 16 files changed, 1320 insertions(+), 1646 deletions(-) create mode 100644 Jellyfin.Data/Entities/AncestorId.cs create mode 100644 Jellyfin.Data/Entities/AttachmentStreamInfo.cs create mode 100644 Jellyfin.Data/Entities/BaseItem.cs create mode 100644 Jellyfin.Data/Entities/Chapter.cs create mode 100644 Jellyfin.Data/Entities/ItemValue.cs create mode 100644 Jellyfin.Data/Entities/MediaStreamInfo.cs create mode 100644 Jellyfin.Data/Entities/People.cs create mode 100644 Jellyfin.Server.Implementations/Item/BaseItemManager.cs create mode 100644 Jellyfin.Server.Implementations/Item/ChapterManager.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 60f5ee47ac..c7a8421c66 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -63,130 +63,6 @@ namespace Emby.Server.Implementations.Data private readonly ItemFields[] _allItemFields = Enum.GetValues(); - private static readonly string[] _retrieveItemColumns = - { - "type", - "data", - "StartDate", - "EndDate", - "ChannelId", - "IsMovie", - "IsSeries", - "EpisodeTitle", - "IsRepeat", - "CommunityRating", - "CustomRating", - "IndexNumber", - "IsLocked", - "PreferredMetadataLanguage", - "PreferredMetadataCountryCode", - "Width", - "Height", - "DateLastRefreshed", - "Name", - "Path", - "PremiereDate", - "Overview", - "ParentIndexNumber", - "ProductionYear", - "OfficialRating", - "ForcedSortName", - "RunTimeTicks", - "Size", - "DateCreated", - "DateModified", - "guid", - "Genres", - "ParentId", - "Audio", - "ExternalServiceId", - "IsInMixedFolder", - "DateLastSaved", - "LockedFields", - "Studios", - "Tags", - "TrailerTypes", - "OriginalTitle", - "PrimaryVersionId", - "DateLastMediaAdded", - "Album", - "LUFS", - "NormalizationGain", - "CriticRating", - "IsVirtualItem", - "SeriesName", - "SeasonName", - "SeasonId", - "SeriesId", - "PresentationUniqueKey", - "InheritedParentalRatingValue", - "ExternalSeriesId", - "Tagline", - "ProviderIds", - "Images", - "ProductionLocations", - "ExtraIds", - "TotalBitrate", - "ExtraType", - "Artists", - "AlbumArtists", - "ExternalId", - "SeriesPresentationUniqueKey", - "ShowId", - "OwnerId" - }; - - private static readonly string _retrieveItemColumnsSelectQuery = $"select {string.Join(',', _retrieveItemColumns)} from TypedBaseItems where guid = @guid"; - - private static readonly string[] _mediaStreamSaveColumns = - { - "ItemId", - "StreamIndex", - "StreamType", - "Codec", - "Language", - "ChannelLayout", - "Profile", - "AspectRatio", - "Path", - "IsInterlaced", - "BitRate", - "Channels", - "SampleRate", - "IsDefault", - "IsForced", - "IsExternal", - "Height", - "Width", - "AverageFrameRate", - "RealFrameRate", - "Level", - "PixelFormat", - "BitDepth", - "IsAnamorphic", - "RefFrames", - "CodecTag", - "Comment", - "NalLengthSize", - "IsAvc", - "Title", - "TimeBase", - "CodecTimeBase", - "ColorPrimaries", - "ColorSpace", - "ColorTransfer", - "DvVersionMajor", - "DvVersionMinor", - "DvProfile", - "DvLevel", - "RpuPresentFlag", - "ElPresentFlag", - "BlPresentFlag", - "DvBlSignalCompatibilityId", - "IsHearingImpaired", - "Rotation" - }; - private static readonly string _mediaStreamSaveColumnsInsertQuery = $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values "; @@ -336,948 +212,6 @@ namespace Emby.Server.Implementations.Data /// protected override TempStoreMode TempStore => TempStoreMode.Memory; - /// - /// Opens the connection to the database. - /// - public override void Initialize() - { - base.Initialize(); - - const string CreateMediaStreamsTableCommand - = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, Rotation INT NULL, PRIMARY KEY (ItemId, StreamIndex))"; - - const string CreateMediaAttachmentsTableCommand - = "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))"; - - string[] queries = - { - "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)", - - "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))", - "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)", - "create index if not exists idx_AncestorIds5 on AncestorIds(AncestorIdText,ItemId)", - - "create table if not exists ItemValues (ItemId GUID NOT NULL, Type INT NOT NULL, Value TEXT NOT NULL, CleanValue TEXT NOT NULL)", - - "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)", - - "drop index if exists idxPeopleItemId", - "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)", - "create index if not exists idxPeopleName on People(Name)", - - "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", - - CreateMediaStreamsTableCommand, - CreateMediaAttachmentsTableCommand, - - "pragma shrink_memory" - }; - - string[] postQueries = - { - "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", - "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", - - "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)", - "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)", - "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)", - - // covering index - "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)", - - // series - "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)", - - // series counts - // seriesdateplayed sort order - "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)", - - // live tv programs - "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)", - - // covering index for getitemvalues - "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)", - - // used by movie suggestions - "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)", - "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)", - - // latest items - "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)", - "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)", - - // resume - "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)", - - // items by name - "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)", - "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)", - - // Used to update inherited tags - "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)", - - "CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type)", - "CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder)" - }; - - using (var connection = GetConnection()) - using (var transaction = connection.BeginTransaction()) - { - connection.Execute(string.Join(';', queries)); - - var existingColumnNames = GetColumnNames(connection, "AncestorIds"); - AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, "TypedBaseItems"); - - AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "NormalizationGain", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, "ItemValues"); - AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, ChaptersTableName); - AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, "MediaStreams"); - AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames); - AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "Rotation", "INT", existingColumnNames); - - connection.Execute(string.Join(';', postQueries)); - - transaction.Commit(); - } - } - - public void SaveImages(BaseItem item) - { - ArgumentNullException.ThrowIfNull(item); - - CheckDisposed(); - - var images = SerializeImages(item.ImageInfos); - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - using var saveImagesStatement = PrepareStatement(connection, "Update TypedBaseItems set Images=@Images where guid=@Id"); - saveImagesStatement.TryBind("@Id", item.Id); - saveImagesStatement.TryBind("@Images", images); - - saveImagesStatement.ExecuteNonQuery(); - transaction.Commit(); - } - - /// - /// Saves the items. - /// - /// The items. - /// The cancellation token. - /// - /// or is null. - /// - public void SaveItems(IReadOnlyList items, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(items); - - cancellationToken.ThrowIfCancellationRequested(); - - CheckDisposed(); - - var itemsLen = items.Count; - var tuples = new ValueTuple, BaseItem, string, List>[itemsLen]; - for (int i = 0; i < itemsLen; i++) - { - var item = items[i]; - var ancestorIds = item.SupportsAncestors ? - item.GetAncestorIds().Distinct().ToList() : - null; - - var topParent = item.GetTopParent(); - - var userdataKey = item.GetUserDataKeys().FirstOrDefault(); - var inheritedTags = item.GetInheritedTags(); - - tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags); - } - - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - SaveItemsInTransaction(connection, tuples); - transaction.Commit(); - } - - private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List AncestorIds, BaseItem TopParent, string UserDataKey, List InheritedTags)> tuples) - { - using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText)) - using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId")) - { - var requiresReset = false; - foreach (var tuple in tuples) - { - if (requiresReset) - { - saveItemStatement.Parameters.Clear(); - deleteAncestorsStatement.Parameters.Clear(); - } - - var item = tuple.Item; - var topParent = tuple.TopParent; - var userDataKey = tuple.UserDataKey; - - SaveItem(item, topParent, userDataKey, saveItemStatement); - - var inheritedTags = tuple.InheritedTags; - - if (item.SupportsAncestors) - { - UpdateAncestors(item.Id, tuple.AncestorIds, db, deleteAncestorsStatement); - } - - UpdateItemValues(item.Id, GetItemValuesToSave(item, inheritedTags), db); - - requiresReset = true; - } - } - } - - private string GetPathToSave(string path) - { - if (path is null) - { - return null; - } - - return _appHost.ReverseVirtualPath(path); - } - - private string RestorePath(string path) - { - return _appHost.ExpandVirtualPath(path); - } - - private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, SqliteCommand saveItemStatement) - { - Type type = item.GetType(); - - saveItemStatement.TryBind("@guid", item.Id); - saveItemStatement.TryBind("@type", type.FullName); - - if (TypeRequiresDeserialization(type)) - { - saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions), true); - } - else - { - saveItemStatement.TryBindNull("@data"); - } - - saveItemStatement.TryBind("@Path", GetPathToSave(item.Path)); - - if (item is IHasStartDate hasStartDate) - { - saveItemStatement.TryBind("@StartDate", hasStartDate.StartDate); - } - else - { - saveItemStatement.TryBindNull("@StartDate"); - } - - if (item.EndDate.HasValue) - { - saveItemStatement.TryBind("@EndDate", item.EndDate.Value); - } - else - { - saveItemStatement.TryBindNull("@EndDate"); - } - - saveItemStatement.TryBind("@ChannelId", item.ChannelId.IsEmpty() ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture)); - - if (item is IHasProgramAttributes hasProgramAttributes) - { - saveItemStatement.TryBind("@IsMovie", hasProgramAttributes.IsMovie); - saveItemStatement.TryBind("@IsSeries", hasProgramAttributes.IsSeries); - saveItemStatement.TryBind("@EpisodeTitle", hasProgramAttributes.EpisodeTitle); - saveItemStatement.TryBind("@IsRepeat", hasProgramAttributes.IsRepeat); - } - else - { - saveItemStatement.TryBindNull("@IsMovie"); - saveItemStatement.TryBindNull("@IsSeries"); - saveItemStatement.TryBindNull("@EpisodeTitle"); - saveItemStatement.TryBindNull("@IsRepeat"); - } - - saveItemStatement.TryBind("@CommunityRating", item.CommunityRating); - saveItemStatement.TryBind("@CustomRating", item.CustomRating); - saveItemStatement.TryBind("@IndexNumber", item.IndexNumber); - saveItemStatement.TryBind("@IsLocked", item.IsLocked); - saveItemStatement.TryBind("@Name", item.Name); - saveItemStatement.TryBind("@OfficialRating", item.OfficialRating); - saveItemStatement.TryBind("@MediaType", item.MediaType.ToString()); - saveItemStatement.TryBind("@Overview", item.Overview); - saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber); - saveItemStatement.TryBind("@PremiereDate", item.PremiereDate); - saveItemStatement.TryBind("@ProductionYear", item.ProductionYear); - - var parentId = item.ParentId; - if (parentId.IsEmpty()) - { - saveItemStatement.TryBindNull("@ParentId"); - } - else - { - saveItemStatement.TryBind("@ParentId", parentId); - } - - if (item.Genres.Length > 0) - { - saveItemStatement.TryBind("@Genres", string.Join('|', item.Genres)); - } - else - { - saveItemStatement.TryBindNull("@Genres"); - } - - saveItemStatement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue); - - saveItemStatement.TryBind("@SortName", item.SortName); - - saveItemStatement.TryBind("@ForcedSortName", item.ForcedSortName); - - saveItemStatement.TryBind("@RunTimeTicks", item.RunTimeTicks); - saveItemStatement.TryBind("@Size", item.Size); - - saveItemStatement.TryBind("@DateCreated", item.DateCreated); - saveItemStatement.TryBind("@DateModified", item.DateModified); - - saveItemStatement.TryBind("@PreferredMetadataLanguage", item.PreferredMetadataLanguage); - saveItemStatement.TryBind("@PreferredMetadataCountryCode", item.PreferredMetadataCountryCode); - - if (item.Width > 0) - { - saveItemStatement.TryBind("@Width", item.Width); - } - else - { - saveItemStatement.TryBindNull("@Width"); - } - - if (item.Height > 0) - { - saveItemStatement.TryBind("@Height", item.Height); - } - else - { - saveItemStatement.TryBindNull("@Height"); - } - - if (item.DateLastRefreshed != default(DateTime)) - { - saveItemStatement.TryBind("@DateLastRefreshed", item.DateLastRefreshed); - } - else - { - saveItemStatement.TryBindNull("@DateLastRefreshed"); - } - - if (item.DateLastSaved != default(DateTime)) - { - saveItemStatement.TryBind("@DateLastSaved", item.DateLastSaved); - } - else - { - saveItemStatement.TryBindNull("@DateLastSaved"); - } - - saveItemStatement.TryBind("@IsInMixedFolder", item.IsInMixedFolder); - - if (item.LockedFields.Length > 0) - { - saveItemStatement.TryBind("@LockedFields", string.Join('|', item.LockedFields)); - } - else - { - saveItemStatement.TryBindNull("@LockedFields"); - } - - if (item.Studios.Length > 0) - { - saveItemStatement.TryBind("@Studios", string.Join('|', item.Studios)); - } - else - { - saveItemStatement.TryBindNull("@Studios"); - } - - if (item.Audio.HasValue) - { - saveItemStatement.TryBind("@Audio", item.Audio.Value.ToString()); - } - else - { - saveItemStatement.TryBindNull("@Audio"); - } - - if (item is LiveTvChannel liveTvChannel) - { - saveItemStatement.TryBind("@ExternalServiceId", liveTvChannel.ServiceName); - } - else - { - saveItemStatement.TryBindNull("@ExternalServiceId"); - } - - if (item.Tags.Length > 0) - { - saveItemStatement.TryBind("@Tags", string.Join('|', item.Tags)); - } - else - { - saveItemStatement.TryBindNull("@Tags"); - } - - saveItemStatement.TryBind("@IsFolder", item.IsFolder); - - saveItemStatement.TryBind("@UnratedType", item.GetBlockUnratedType().ToString()); - - if (topParent is null) - { - saveItemStatement.TryBindNull("@TopParentId"); - } - else - { - saveItemStatement.TryBind("@TopParentId", topParent.Id.ToString("N", CultureInfo.InvariantCulture)); - } - - if (item is Trailer trailer && trailer.TrailerTypes.Length > 0) - { - saveItemStatement.TryBind("@TrailerTypes", string.Join('|', trailer.TrailerTypes)); - } - else - { - saveItemStatement.TryBindNull("@TrailerTypes"); - } - - saveItemStatement.TryBind("@CriticRating", item.CriticRating); - - if (string.IsNullOrWhiteSpace(item.Name)) - { - saveItemStatement.TryBindNull("@CleanName"); - } - else - { - saveItemStatement.TryBind("@CleanName", GetCleanValue(item.Name)); - } - - saveItemStatement.TryBind("@PresentationUniqueKey", item.PresentationUniqueKey); - saveItemStatement.TryBind("@OriginalTitle", item.OriginalTitle); - - if (item is Video video) - { - saveItemStatement.TryBind("@PrimaryVersionId", video.PrimaryVersionId); - } - else - { - saveItemStatement.TryBindNull("@PrimaryVersionId"); - } - - if (item is Folder folder && folder.DateLastMediaAdded.HasValue) - { - saveItemStatement.TryBind("@DateLastMediaAdded", folder.DateLastMediaAdded.Value); - } - else - { - saveItemStatement.TryBindNull("@DateLastMediaAdded"); - } - - saveItemStatement.TryBind("@Album", item.Album); - saveItemStatement.TryBind("@LUFS", item.LUFS); - saveItemStatement.TryBind("@NormalizationGain", item.NormalizationGain); - saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem); - - if (item is IHasSeries hasSeriesName) - { - saveItemStatement.TryBind("@SeriesName", hasSeriesName.SeriesName); - } - else - { - saveItemStatement.TryBindNull("@SeriesName"); - } - - if (string.IsNullOrWhiteSpace(userDataKey)) - { - saveItemStatement.TryBindNull("@UserDataKey"); - } - else - { - saveItemStatement.TryBind("@UserDataKey", userDataKey); - } - - if (item is Episode episode) - { - saveItemStatement.TryBind("@SeasonName", episode.SeasonName); - - var nullableSeasonId = episode.SeasonId.IsEmpty() ? (Guid?)null : episode.SeasonId; - - saveItemStatement.TryBind("@SeasonId", nullableSeasonId); - } - else - { - saveItemStatement.TryBindNull("@SeasonName"); - saveItemStatement.TryBindNull("@SeasonId"); - } - - if (item is IHasSeries hasSeries) - { - var nullableSeriesId = hasSeries.SeriesId.IsEmpty() ? (Guid?)null : hasSeries.SeriesId; - - saveItemStatement.TryBind("@SeriesId", nullableSeriesId); - saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey); - } - else - { - saveItemStatement.TryBindNull("@SeriesId"); - saveItemStatement.TryBindNull("@SeriesPresentationUniqueKey"); - } - - saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId); - saveItemStatement.TryBind("@Tagline", item.Tagline); - - saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item.ProviderIds)); - saveItemStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); - - if (item.ProductionLocations.Length > 0) - { - saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations)); - } - else - { - saveItemStatement.TryBindNull("@ProductionLocations"); - } - - if (item.ExtraIds.Length > 0) - { - saveItemStatement.TryBind("@ExtraIds", string.Join('|', item.ExtraIds)); - } - else - { - saveItemStatement.TryBindNull("@ExtraIds"); - } - - saveItemStatement.TryBind("@TotalBitrate", item.TotalBitrate); - if (item.ExtraType.HasValue) - { - saveItemStatement.TryBind("@ExtraType", item.ExtraType.Value.ToString()); - } - else - { - saveItemStatement.TryBindNull("@ExtraType"); - } - - string artists = null; - if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0) - { - artists = string.Join('|', hasArtists.Artists); - } - - saveItemStatement.TryBind("@Artists", artists); - - string albumArtists = null; - if (item is IHasAlbumArtist hasAlbumArtists - && hasAlbumArtists.AlbumArtists.Count > 0) - { - albumArtists = string.Join('|', hasAlbumArtists.AlbumArtists); - } - - saveItemStatement.TryBind("@AlbumArtists", albumArtists); - saveItemStatement.TryBind("@ExternalId", item.ExternalId); - - if (item is LiveTvProgram program) - { - saveItemStatement.TryBind("@ShowId", program.ShowId); - } - else - { - saveItemStatement.TryBindNull("@ShowId"); - } - - Guid ownerId = item.OwnerId; - if (ownerId.IsEmpty()) - { - saveItemStatement.TryBindNull("@OwnerId"); - } - else - { - saveItemStatement.TryBind("@OwnerId", ownerId); - } - - saveItemStatement.ExecuteNonQuery(); - } - - internal static string SerializeProviderIds(Dictionary providerIds) - { - StringBuilder str = new StringBuilder(); - foreach (var i in providerIds) - { - // Ideally we shouldn't need this IsNullOrWhiteSpace check, - // but we're seeing some cases of bad data slip through - if (string.IsNullOrWhiteSpace(i.Value)) - { - continue; - } - - str.Append(i.Key) - .Append('=') - .Append(i.Value) - .Append('|'); - } - - if (str.Length == 0) - { - return null; - } - - str.Length -= 1; // Remove last | - return str.ToString(); - } - - internal static void DeserializeProviderIds(string value, IHasProviderIds item) - { - if (string.IsNullOrWhiteSpace(value)) - { - return; - } - - foreach (var part in value.SpanSplit('|')) - { - var providerDelimiterIndex = part.IndexOf('='); - // Don't let empty values through - if (providerDelimiterIndex != -1 && part.Length != providerDelimiterIndex + 1) - { - item.SetProviderId(part[..providerDelimiterIndex].ToString(), part[(providerDelimiterIndex + 1)..].ToString()); - } - } - } - - internal string SerializeImages(ItemImageInfo[] images) - { - if (images.Length == 0) - { - return null; - } - - StringBuilder str = new StringBuilder(); - foreach (var i in images) - { - if (string.IsNullOrWhiteSpace(i.Path)) - { - continue; - } - - AppendItemImageInfo(str, i); - str.Append('|'); - } - - str.Length -= 1; // Remove last | - return str.ToString(); - } - - internal ItemImageInfo[] DeserializeImages(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return Array.Empty(); - } - - // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed - var valueSpan = value.AsSpan(); - var count = valueSpan.Count('|') + 1; - - var position = 0; - var result = new ItemImageInfo[count]; - foreach (var part in valueSpan.Split('|')) - { - var image = ItemImageInfoFromValueString(part); - - if (image is not null) - { - result[position++] = image; - } - } - - if (position == count) - { - return result; - } - - if (position == 0) - { - return Array.Empty(); - } - - // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array. - return result[..position]; - } - - private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image) - { - const char Delimiter = '*'; - - var path = image.Path ?? string.Empty; - - bldr.Append(GetPathToSave(path)) - .Append(Delimiter) - .Append(image.DateModified.Ticks) - .Append(Delimiter) - .Append(image.Type) - .Append(Delimiter) - .Append(image.Width) - .Append(Delimiter) - .Append(image.Height); - - var hash = image.BlurHash; - if (!string.IsNullOrEmpty(hash)) - { - bldr.Append(Delimiter) - // Replace delimiters with other characters. - // This can be removed when we migrate to a proper DB. - .Append(hash.Replace(Delimiter, '/').Replace('|', '\\')); - } - } - - internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan value) - { - const char Delimiter = '*'; - - var nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - return null; - } - - ReadOnlySpan path = value[..nextSegment]; - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - return null; - } - - ReadOnlySpan dateModified = value[..nextSegment]; - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - nextSegment = value.Length; - } - - ReadOnlySpan imageType = value[..nextSegment]; - - var image = new ItemImageInfo - { - Path = RestorePath(path.ToString()) - }; - - if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks) - && ticks >= DateTime.MinValue.Ticks - && ticks <= DateTime.MaxValue.Ticks) - { - image.DateModified = new DateTime(ticks, DateTimeKind.Utc); - } - else - { - return null; - } - - if (Enum.TryParse(imageType, true, out ImageType type)) - { - image.Type = type; - } - else - { - return null; - } - - // Optional parameters: width*height*blurhash - if (nextSegment + 1 < value.Length - 1) - { - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1 || nextSegment == value.Length) - { - return image; - } - - ReadOnlySpan widthSpan = value[..nextSegment]; - - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - nextSegment = value.Length; - } - - ReadOnlySpan heightSpan = value[..nextSegment]; - - if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width) - && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height)) - { - image.Width = width; - image.Height = height; - } - - if (nextSegment < value.Length - 1) - { - value = value[(nextSegment + 1)..]; - var length = value.Length; - - Span blurHashSpan = stackalloc char[length]; - for (int i = 0; i < length; i++) - { - var c = value[i]; - blurHashSpan[i] = c switch - { - '/' => Delimiter, - '\\' => '|', - _ => c - }; - } - - image.BlurHash = new string(blurHashSpan); - } - } - - return image; - } - - /// - /// Internal retrieve from items or users table. - /// - /// The id. - /// BaseItem. - /// is null. - /// is . - public BaseItem RetrieveItem(Guid id) - { - if (id.IsEmpty()) - { - throw new ArgumentException("Guid can't be empty", nameof(id)); - } - - CheckDisposed(); - - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery)) - { - statement.TryBind("@guid", id); - - foreach (var row in statement.ExecuteQuery()) - { - return GetItem(row, new InternalItemsQuery()); - } - } - - return null; - } - private bool TypeRequiresDeserialization(Type type) { if (_config.Configuration.SkipDeserializationForBasicTypes) @@ -1304,586 +238,6 @@ namespace Emby.Server.Implementations.Data && type != typeof(MusicAlbum); } - private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query) - { - return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query), false); - } - - private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields, bool skipDeserialization) - { - var typeString = reader.GetString(0); - - var type = _typeMapper.GetType(typeString); - - if (type is null) - { - return null; - } - - BaseItem item = null; - - if (TypeRequiresDeserialization(type) && !skipDeserialization) - { - try - { - item = JsonSerializer.Deserialize(reader.GetStream(1), type, _jsonOptions) as BaseItem; - } - catch (JsonException ex) - { - Logger.LogError(ex, "Error deserializing item with JSON: {Data}", reader.GetString(1)); - } - } - - if (item is null) - { - try - { - item = Activator.CreateInstance(type) as BaseItem; - } - catch - { - } - } - - if (item is null) - { - return null; - } - - var index = 2; - - if (queryHasStartDate) - { - if (item is IHasStartDate hasStartDate && reader.TryReadDateTime(index, out var startDate)) - { - hasStartDate.StartDate = startDate; - } - - index++; - } - - if (reader.TryReadDateTime(index++, out var endDate)) - { - item.EndDate = endDate; - } - - if (reader.TryGetGuid(index, out var guid)) - { - item.ChannelId = guid; - } - - index++; - - if (enableProgramAttributes) - { - if (item is IHasProgramAttributes hasProgramAttributes) - { - if (reader.TryGetBoolean(index++, out var isMovie)) - { - hasProgramAttributes.IsMovie = isMovie; - } - - if (reader.TryGetBoolean(index++, out var isSeries)) - { - hasProgramAttributes.IsSeries = isSeries; - } - - if (reader.TryGetString(index++, out var episodeTitle)) - { - hasProgramAttributes.EpisodeTitle = episodeTitle; - } - - if (reader.TryGetBoolean(index++, out var isRepeat)) - { - hasProgramAttributes.IsRepeat = isRepeat; - } - } - else - { - index += 4; - } - } - - if (reader.TryGetSingle(index++, out var communityRating)) - { - item.CommunityRating = communityRating; - } - - if (HasField(query, ItemFields.CustomRating)) - { - if (reader.TryGetString(index++, out var customRating)) - { - item.CustomRating = customRating; - } - } - - if (reader.TryGetInt32(index++, out var indexNumber)) - { - item.IndexNumber = indexNumber; - } - - if (HasField(query, ItemFields.Settings)) - { - if (reader.TryGetBoolean(index++, out var isLocked)) - { - item.IsLocked = isLocked; - } - - if (reader.TryGetString(index++, out var preferredMetadataLanguage)) - { - item.PreferredMetadataLanguage = preferredMetadataLanguage; - } - - if (reader.TryGetString(index++, out var preferredMetadataCountryCode)) - { - item.PreferredMetadataCountryCode = preferredMetadataCountryCode; - } - } - - if (HasField(query, ItemFields.Width)) - { - if (reader.TryGetInt32(index++, out var width)) - { - item.Width = width; - } - } - - if (HasField(query, ItemFields.Height)) - { - if (reader.TryGetInt32(index++, out var height)) - { - item.Height = height; - } - } - - if (HasField(query, ItemFields.DateLastRefreshed)) - { - if (reader.TryReadDateTime(index++, out var dateLastRefreshed)) - { - item.DateLastRefreshed = dateLastRefreshed; - } - } - - if (reader.TryGetString(index++, out var name)) - { - item.Name = name; - } - - if (reader.TryGetString(index++, out var restorePath)) - { - item.Path = RestorePath(restorePath); - } - - if (reader.TryReadDateTime(index++, out var premiereDate)) - { - item.PremiereDate = premiereDate; - } - - if (HasField(query, ItemFields.Overview)) - { - if (reader.TryGetString(index++, out var overview)) - { - item.Overview = overview; - } - } - - if (reader.TryGetInt32(index++, out var parentIndexNumber)) - { - item.ParentIndexNumber = parentIndexNumber; - } - - if (reader.TryGetInt32(index++, out var productionYear)) - { - item.ProductionYear = productionYear; - } - - if (reader.TryGetString(index++, out var officialRating)) - { - item.OfficialRating = officialRating; - } - - if (HasField(query, ItemFields.SortName)) - { - if (reader.TryGetString(index++, out var forcedSortName)) - { - item.ForcedSortName = forcedSortName; - } - } - - if (reader.TryGetInt64(index++, out var runTimeTicks)) - { - item.RunTimeTicks = runTimeTicks; - } - - if (reader.TryGetInt64(index++, out var size)) - { - item.Size = size; - } - - if (HasField(query, ItemFields.DateCreated)) - { - if (reader.TryReadDateTime(index++, out var dateCreated)) - { - item.DateCreated = dateCreated; - } - } - - if (reader.TryReadDateTime(index++, out var dateModified)) - { - item.DateModified = dateModified; - } - - item.Id = reader.GetGuid(index++); - - if (HasField(query, ItemFields.Genres)) - { - if (reader.TryGetString(index++, out var genres)) - { - item.Genres = genres.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - } - - if (reader.TryGetGuid(index++, out var parentId)) - { - item.ParentId = parentId; - } - - if (reader.TryGetString(index++, out var audioString)) - { - if (Enum.TryParse(audioString, true, out ProgramAudio audio)) - { - item.Audio = audio; - } - } - - // TODO: Even if not needed by apps, the server needs it internally - // But get this excluded from contexts where it is not needed - if (hasServiceName) - { - if (item is LiveTvChannel liveTvChannel) - { - if (reader.TryGetString(index, out var serviceName)) - { - liveTvChannel.ServiceName = serviceName; - } - } - - index++; - } - - if (reader.TryGetBoolean(index++, out var isInMixedFolder)) - { - item.IsInMixedFolder = isInMixedFolder; - } - - if (HasField(query, ItemFields.DateLastSaved)) - { - if (reader.TryReadDateTime(index++, out var dateLastSaved)) - { - item.DateLastSaved = dateLastSaved; - } - } - - if (HasField(query, ItemFields.Settings)) - { - if (reader.TryGetString(index++, out var lockedFields)) - { - List fields = null; - foreach (var i in lockedFields.AsSpan().Split('|')) - { - if (Enum.TryParse(i, true, out MetadataField parsedValue)) - { - (fields ??= new List()).Add(parsedValue); - } - } - - item.LockedFields = fields?.ToArray() ?? Array.Empty(); - } - } - - if (HasField(query, ItemFields.Studios)) - { - if (reader.TryGetString(index++, out var studios)) - { - item.Studios = studios.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - } - - if (HasField(query, ItemFields.Tags)) - { - if (reader.TryGetString(index++, out var tags)) - { - item.Tags = tags.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - } - - if (hasTrailerTypes) - { - if (item is Trailer trailer) - { - if (reader.TryGetString(index, out var trailerTypes)) - { - List types = null; - foreach (var i in trailerTypes.AsSpan().Split('|')) - { - if (Enum.TryParse(i, true, out TrailerType parsedValue)) - { - (types ??= new List()).Add(parsedValue); - } - } - - trailer.TrailerTypes = types?.ToArray() ?? Array.Empty(); - } - } - - index++; - } - - if (HasField(query, ItemFields.OriginalTitle)) - { - if (reader.TryGetString(index++, out var originalTitle)) - { - item.OriginalTitle = originalTitle; - } - } - - if (item is Video video) - { - if (reader.TryGetString(index, out var primaryVersionId)) - { - video.PrimaryVersionId = primaryVersionId; - } - } - - index++; - - if (HasField(query, ItemFields.DateLastMediaAdded)) - { - if (item is Folder folder && reader.TryReadDateTime(index, out var dateLastMediaAdded)) - { - folder.DateLastMediaAdded = dateLastMediaAdded; - } - - index++; - } - - if (reader.TryGetString(index++, out var album)) - { - item.Album = album; - } - - if (reader.TryGetSingle(index++, out var lUFS)) - { - item.LUFS = lUFS; - } - - if (reader.TryGetSingle(index++, out var normalizationGain)) - { - item.NormalizationGain = normalizationGain; - } - - if (reader.TryGetSingle(index++, out var criticRating)) - { - item.CriticRating = criticRating; - } - - if (reader.TryGetBoolean(index++, out var isVirtualItem)) - { - item.IsVirtualItem = isVirtualItem; - } - - if (item is IHasSeries hasSeriesName) - { - if (reader.TryGetString(index, out var seriesName)) - { - hasSeriesName.SeriesName = seriesName; - } - } - - index++; - - if (hasEpisodeAttributes) - { - if (item is Episode episode) - { - if (reader.TryGetString(index, out var seasonName)) - { - episode.SeasonName = seasonName; - } - - index++; - if (reader.TryGetGuid(index, out var seasonId)) - { - episode.SeasonId = seasonId; - } - } - else - { - index++; - } - - index++; - } - - var hasSeries = item as IHasSeries; - if (hasSeriesFields) - { - if (hasSeries is not null) - { - if (reader.TryGetGuid(index, out var seriesId)) - { - hasSeries.SeriesId = seriesId; - } - } - - index++; - } - - if (HasField(query, ItemFields.PresentationUniqueKey)) - { - if (reader.TryGetString(index++, out var presentationUniqueKey)) - { - item.PresentationUniqueKey = presentationUniqueKey; - } - } - - if (HasField(query, ItemFields.InheritedParentalRatingValue)) - { - if (reader.TryGetInt32(index++, out var parentalRating)) - { - item.InheritedParentalRatingValue = parentalRating; - } - } - - if (HasField(query, ItemFields.ExternalSeriesId)) - { - if (reader.TryGetString(index++, out var externalSeriesId)) - { - item.ExternalSeriesId = externalSeriesId; - } - } - - if (HasField(query, ItemFields.Taglines)) - { - if (reader.TryGetString(index++, out var tagLine)) - { - item.Tagline = tagLine; - } - } - - if (item.ProviderIds.Count == 0 && reader.TryGetString(index, out var providerIds)) - { - DeserializeProviderIds(providerIds, item); - } - - index++; - - if (query.DtoOptions.EnableImages) - { - if (item.ImageInfos.Length == 0 && reader.TryGetString(index, out var imageInfos)) - { - item.ImageInfos = DeserializeImages(imageInfos); - } - - index++; - } - - if (HasField(query, ItemFields.ProductionLocations)) - { - if (reader.TryGetString(index++, out var productionLocations)) - { - item.ProductionLocations = productionLocations.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - } - - if (HasField(query, ItemFields.ExtraIds)) - { - if (reader.TryGetString(index++, out var extraIds)) - { - item.ExtraIds = SplitToGuids(extraIds); - } - } - - if (reader.TryGetInt32(index++, out var totalBitrate)) - { - item.TotalBitrate = totalBitrate; - } - - if (reader.TryGetString(index++, out var extraTypeString)) - { - if (Enum.TryParse(extraTypeString, true, out ExtraType extraType)) - { - item.ExtraType = extraType; - } - } - - if (hasArtistFields) - { - if (item is IHasArtist hasArtists && reader.TryGetString(index, out var artists)) - { - hasArtists.Artists = artists.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - - index++; - - if (item is IHasAlbumArtist hasAlbumArtists && reader.TryGetString(index, out var albumArtists)) - { - hasAlbumArtists.AlbumArtists = albumArtists.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - - index++; - } - - if (reader.TryGetString(index++, out var externalId)) - { - item.ExternalId = externalId; - } - - if (HasField(query, ItemFields.SeriesPresentationUniqueKey)) - { - if (hasSeries is not null) - { - if (reader.TryGetString(index, out var seriesPresentationUniqueKey)) - { - hasSeries.SeriesPresentationUniqueKey = seriesPresentationUniqueKey; - } - } - - index++; - } - - if (enableProgramAttributes) - { - if (item is LiveTvProgram program && reader.TryGetString(index, out var showId)) - { - program.ShowId = showId; - } - - index++; - } - - if (reader.TryGetGuid(index, out var ownerId)) - { - item.OwnerId = ownerId; - } - - return item; - } - - private static Guid[] SplitToGuids(string value) - { - var ids = value.Split('|'); - - var result = new Guid[ids.Length]; - - for (var i = 0; i < result.Length; i++) - { - result[i] = new Guid(ids[i]); - } - - return result; - } - /// public List GetChapters(BaseItem item) { diff --git a/Jellyfin.Data/Entities/AncestorId.cs b/Jellyfin.Data/Entities/AncestorId.cs new file mode 100644 index 0000000000..dc83b763ee --- /dev/null +++ b/Jellyfin.Data/Entities/AncestorId.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities; + +#pragma warning disable CA1708 // Identifiers should differ by more than case +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public class AncestorId +{ + public Guid Id { get; set; } + + public Guid ItemId { get; set; } + + public required BaseItem Item { get; set; } + + public string? AncestorIdText { get; set; } +} diff --git a/Jellyfin.Data/Entities/AttachmentStreamInfo.cs b/Jellyfin.Data/Entities/AttachmentStreamInfo.cs new file mode 100644 index 0000000000..d2483548b8 --- /dev/null +++ b/Jellyfin.Data/Entities/AttachmentStreamInfo.cs @@ -0,0 +1,21 @@ +using System; + +namespace Jellyfin.Data.Entities; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public class AttachmentStreamInfo +{ + public required Guid ItemId { get; set; } + + public required int Index { get; set; } + + public required string Codec { get; set; } + + public string? CodecTag { get; set; } + + public string? Comment { get; set; } + + public string? Filename { get; set; } + + public string? MimeType { get; set; } +} diff --git a/Jellyfin.Data/Entities/BaseItem.cs b/Jellyfin.Data/Entities/BaseItem.cs new file mode 100644 index 0000000000..c0c88b2e63 --- /dev/null +++ b/Jellyfin.Data/Entities/BaseItem.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public class BaseItem +{ + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + + public Guid Id { get; set; } + + public required string Type { get; set; } + + public IReadOnlyList? Data { get; set; } + + public Guid? ParentId { get; set; } + + public string? Path { get; set; } + + public DateTime StartDate { get; set; } + + public DateTime EndDate { get; set; } + + public string? ChannelId { get; set; } + + public bool IsMovie { get; set; } + + public float? CommunityRating { get; set; } + + public string? CustomRating { get; set; } + + public int? IndexNumber { get; set; } + + public bool IsLocked { get; set; } + + public string? Name { get; set; } + + public string? OfficialRating { get; set; } + + public string? MediaType { get; set; } + + public string? Overview { get; set; } + + public int? ParentIndexNumber { get; set; } + + public DateTime? PremiereDate { get; set; } + + public int? ProductionYear { get; set; } + + public string? Genres { get; set; } + + public string? SortName { get; set; } + + public string? ForcedSortName { get; set; } + + public long? RunTimeTicks { get; set; } + + public DateTime? DateCreated { get; set; } + + public DateTime? DateModified { get; set; } + + public bool IsSeries { get; set; } + + public string? EpisodeTitle { get; set; } + + public bool IsRepeat { get; set; } + + public string? PreferredMetadataLanguage { get; set; } + + public string? PreferredMetadataCountryCode { get; set; } + + public DateTime? DateLastRefreshed { get; set; } + + public DateTime? DateLastSaved { get; set; } + + public bool IsInMixedFolder { get; set; } + + public string? LockedFields { get; set; } + + public string? Studios { get; set; } + + public string? Audio { get; set; } + + public string? ExternalServiceId { get; set; } + + public string? Tags { get; set; } + + public bool IsFolder { get; set; } + + public int? InheritedParentalRatingValue { get; set; } + + public string? UnratedType { get; set; } + + public string? TopParentId { get; set; } + + public string? TrailerTypes { get; set; } + + public float? CriticRating { get; set; } + + public string? CleanName { get; set; } + + public string? PresentationUniqueKey { get; set; } + + public string? OriginalTitle { get; set; } + + public string? PrimaryVersionId { get; set; } + + public DateTime? DateLastMediaAdded { get; set; } + + public string? Album { get; set; } + + public float? LUFS { get; set; } + + public float? NormalizationGain { get; set; } + + public bool IsVirtualItem { get; set; } + + public string? SeriesName { get; set; } + + public string? UserDataKey { get; set; } + + public string? SeasonName { get; set; } + + public Guid? SeasonId { get; set; } + + public Guid? SeriesId { get; set; } + + public string? ExternalSeriesId { get; set; } + + public string? Tagline { get; set; } + + public string? ProviderIds { get; set; } + + public string? Images { get; set; } + + public string? ProductionLocations { get; set; } + + public string? ExtraIds { get; set; } + + public int? TotalBitrate { get; set; } + + public string? ExtraType { get; set; } + + public string? Artists { get; set; } + + public string? AlbumArtists { get; set; } + + public string? ExternalId { get; set; } + + public string? SeriesPresentationUniqueKey { get; set; } + + public string? ShowId { get; set; } + + public string? OwnerId { get; set; } + + public int? Width { get; set; } + + public int? Height { get; set; } + + public long? Size { get; set; } +} diff --git a/Jellyfin.Data/Entities/Chapter.cs b/Jellyfin.Data/Entities/Chapter.cs new file mode 100644 index 0000000000..6822b19021 --- /dev/null +++ b/Jellyfin.Data/Entities/Chapter.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public class Chapter +{ + public Guid ItemId { get; set; } + + public required int ChapterIndex { get; set; } + + public required long StartPositionTicks { get; set; } + + public string? Name { get; set; } + + public string? ImagePath { get; set; } + + public DateTime? ImageDateModified { get; set; } +} diff --git a/Jellyfin.Data/Entities/ItemValue.cs b/Jellyfin.Data/Entities/ItemValue.cs new file mode 100644 index 0000000000..a3c0908bbe --- /dev/null +++ b/Jellyfin.Data/Entities/ItemValue.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities; + +public class ItemValue +{ + public Guid ItemId { get; set; } + public required BaseItem Item { get; set; } + + public required int Type { get; set; } + public required string Value { get; set; } + public required string CleanValue { get; set; } +} diff --git a/Jellyfin.Data/Entities/MediaStreamInfo.cs b/Jellyfin.Data/Entities/MediaStreamInfo.cs new file mode 100644 index 0000000000..3b89ca62f8 --- /dev/null +++ b/Jellyfin.Data/Entities/MediaStreamInfo.cs @@ -0,0 +1,101 @@ +using System; + +namespace Jellyfin.Data.Entities; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public class MediaStreamInfo +{ + public Guid ItemId { get; set; } + + public required BaseItem Item { get; set; } + + public int StreamIndex { get; set; } + + public string? StreamType { get; set; } + + public string? Codec { get; set; } + + public string? Language { get; set; } + + public string? ChannelLayout { get; set; } + + public string? Profile { get; set; } + + public string? AspectRatio { get; set; } + + public string? Path { get; set; } + + public bool IsInterlaced { get; set; } + + public required int BitRate { get; set; } + + public required int Channels { get; set; } + + public required int SampleRate { get; set; } + + public bool IsDefault { get; set; } + + public bool IsForced { get; set; } + + public bool IsExternal { get; set; } + + public required int Height { get; set; } + + public required int Width { get; set; } + + public required float AverageFrameRate { get; set; } + + public required float RealFrameRate { get; set; } + + public required float Level { get; set; } + + public string? PixelFormat { get; set; } + + public required int BitDepth { get; set; } + + public required bool IsAnamorphic { get; set; } + + public required int RefFrames { get; set; } + + public required string CodecTag { get; set; } + + public required string Comment { get; set; } + + public required string NalLengthSize { get; set; } + + public required bool IsAvc { get; set; } + + public required string Title { get; set; } + + public required string TimeBase { get; set; } + + public required string CodecTimeBase { get; set; } + + public required string ColorPrimaries { get; set; } + + public required string ColorSpace { get; set; } + + public required string ColorTransfer { get; set; } + + public required int DvVersionMajor { get; set; } + + public required int DvVersionMinor { get; set; } + + public required int DvProfile { get; set; } + + public required int DvLevel { get; set; } + + public required int RpuPresentFlag { get; set; } + + public required int ElPresentFlag { get; set; } + + public required int BlPresentFlag { get; set; } + + public required int DvBlSignalCompatibilityId { get; set; } + + public required bool IsHearingImpaired { get; set; } + + public required int Rotation { get; set; } + + public string? KeyFrames { get; set; } +} diff --git a/Jellyfin.Data/Entities/People.cs b/Jellyfin.Data/Entities/People.cs new file mode 100644 index 0000000000..72c39699b2 --- /dev/null +++ b/Jellyfin.Data/Entities/People.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities; +public class People +{ + public Guid ItemId { get; set; } + + public required string Name { get; set; } + public string? Role { get; set; } + public string? PersonType { get; set; } + public int? SortOrder { get; set; } + public int? ListOrder { get; set; } +} diff --git a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs new file mode 100644 index 0000000000..4ad842e0bc --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs @@ -0,0 +1,753 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading; +using Jellyfin.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.LiveTv; +using Microsoft.EntityFrameworkCore; +using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; +using BaseItemEntity = Jellyfin.Data.Entities.BaseItem; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// Handles all storage logic for BaseItems. +/// +public class BaseItemManager +{ + private readonly IDbContextFactory _dbProvider; + private readonly IServerApplicationHost _appHost; + + /// + /// This holds all the types in the running assemblies + /// so that we can de-serialize properly when we don't have strong types. + /// + private static readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary(); + + /// + /// Initializes a new instance of the class. + /// + /// The db factory. + public BaseItemManager(IDbContextFactory dbProvider, IServerApplicationHost appHost) + { + _dbProvider = dbProvider; + _appHost = appHost; + } + + /// + /// Gets the type. + /// + /// Name of the type. + /// Type. + /// typeName is null. + private static Type? GetType(string typeName) + { + ArgumentException.ThrowIfNullOrEmpty(typeName); + + return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies() + .Select(a => a.GetType(k)) + .FirstOrDefault(t => t is not null)); + } + + /// + /// Saves the items. + /// + /// The items. + /// The cancellation token. + /// + /// or is null. + /// + public void UpdateOrInsertItems(IReadOnlyList items, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(items); + cancellationToken.ThrowIfCancellationRequested(); + + var itemsLen = items.Count; + var tuples = new (BaseItemDto Item, List? AncestorIds, BaseItemDto TopParent, string? UserDataKey, List InheritedTags)[itemsLen]; + for (int i = 0; i < itemsLen; i++) + { + var item = items[i]; + var ancestorIds = item.SupportsAncestors ? + item.GetAncestorIds().Distinct().ToList() : + null; + + var topParent = item.GetTopParent(); + + var userdataKey = item.GetUserDataKeys().FirstOrDefault(); + var inheritedTags = item.GetInheritedTags(); + + tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags); + } + + using var context = _dbProvider.CreateDbContext(); + foreach (var item in tuples) + { + var entity = Map(item.Item); + context.BaseItems.Add(entity); + + if (item.Item.SupportsAncestors && item.AncestorIds != null) + { + foreach (var ancestorId in item.AncestorIds) + { + context.AncestorIds.Add(new Data.Entities.AncestorId() + { + Item = entity, + AncestorIdText = ancestorId.ToString(), + Id = ancestorId + }); + } + } + + var itemValues = GetItemValuesToSave(item.Item, item.InheritedTags); + context.ItemValues.Where(e => e.ItemId.Equals(entity.Id)).ExecuteDelete(); + foreach (var itemValue in itemValues) + { + context.ItemValues.Add(new() + { + Item = entity, + Type = itemValue.MagicNumber, + Value = itemValue.Value, + CleanValue = GetCleanValue(itemValue.Value) + }); + } + } + + context.SaveChanges(true); + } + + public BaseItemDto? GetSingle(Guid id) + { + if (id.IsEmpty()) + { + throw new ArgumentException("Guid can't be empty", nameof(id)); + } + + using var context = _dbProvider.CreateDbContext(); + var item = context.BaseItems.FirstOrDefault(e => e.Id.Equals(id)); + if (item is null) + { + return null; + } + + return DeserialiseBaseItem(item); + } + + private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity) + { + var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unkown type."); + var dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type.");; + return Map(baseItemEntity, dto); + } + + /// + /// Maps a Entity to the DTO. + /// + /// The entity. + /// The dto base instance. + /// The dto to map. + public BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto) + { + dto.Id = entity.Id; + dto.ParentId = entity.ParentId.GetValueOrDefault(); + dto.Path = entity.Path; + dto.EndDate = entity.EndDate; + dto.CommunityRating = entity.CommunityRating; + dto.CustomRating = entity.CustomRating; + dto.IndexNumber = entity.IndexNumber; + dto.IsLocked = entity.IsLocked; + dto.Name = entity.Name; + dto.OfficialRating = entity.OfficialRating; + dto.Overview = entity.Overview; + dto.ParentIndexNumber = entity.ParentIndexNumber; + dto.PremiereDate = entity.PremiereDate; + dto.ProductionYear = entity.ProductionYear; + dto.SortName = entity.SortName; + dto.ForcedSortName = entity.ForcedSortName; + dto.RunTimeTicks = entity.RunTimeTicks; + dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage; + dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode; + dto.IsInMixedFolder = entity.IsInMixedFolder; + dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue; + dto.CriticRating = entity.CriticRating; + dto.PresentationUniqueKey = entity.PresentationUniqueKey; + dto.OriginalTitle = entity.OriginalTitle; + dto.Album = entity.Album; + dto.LUFS = entity.LUFS; + dto.NormalizationGain = entity.NormalizationGain; + dto.IsVirtualItem = entity.IsVirtualItem; + dto.ExternalSeriesId = entity.ExternalSeriesId; + dto.Tagline = entity.Tagline; + dto.TotalBitrate = entity.TotalBitrate; + dto.ExternalId = entity.ExternalId; + dto.Size = entity.Size; + dto.Genres = entity.Genres?.Split('|'); + dto.DateCreated = entity.DateCreated.GetValueOrDefault(); + dto.DateModified = entity.DateModified.GetValueOrDefault(); + dto.ChannelId = string.IsNullOrWhiteSpace(entity.ChannelId) ? Guid.Empty : Guid.Parse(entity.ChannelId); + dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault(); + dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault(); + dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : Guid.Parse(entity.OwnerId); + dto.Width = entity.Width.GetValueOrDefault(); + dto.Height = entity.Height.GetValueOrDefault(); + if (entity.ProviderIds is not null) + { + DeserializeProviderIds(entity.ProviderIds, dto); + } + + if (entity.ExtraType is not null) + { + dto.ExtraType = Enum.Parse(entity.ExtraType); + } + + if (entity.LockedFields is not null) + { + List? fields = null; + foreach (var i in entity.LockedFields.AsSpan().Split('|')) + { + if (Enum.TryParse(i, true, out MetadataField parsedValue)) + { + (fields ??= new List()).Add(parsedValue); + } + } + + dto.LockedFields = fields?.ToArray() ?? Array.Empty(); + } + + if (entity.Audio is not null) + { + dto.Audio = Enum.Parse(entity.Audio); + } + + dto.ExtraIds = entity.ExtraIds?.Split('|').Select(e => Guid.Parse(e)).ToArray(); + dto.ProductionLocations = entity.ProductionLocations?.Split('|'); + dto.Studios = entity.Studios?.Split('|'); + dto.Tags = entity.Tags?.Split('|'); + + if (dto is IHasProgramAttributes hasProgramAttributes) + { + hasProgramAttributes.IsMovie = entity.IsMovie; + hasProgramAttributes.IsSeries = entity.IsSeries; + hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle; + hasProgramAttributes.IsRepeat = entity.IsRepeat; + } + + if (dto is LiveTvChannel liveTvChannel) + { + liveTvChannel.ServiceName = entity.ExternalServiceId; + } + + if (dto is Trailer trailer) + { + List? types = null; + foreach (var i in entity.TrailerTypes.AsSpan().Split('|')) + { + if (Enum.TryParse(i, true, out TrailerType parsedValue)) + { + (types ??= new List()).Add(parsedValue); + } + } + + trailer.TrailerTypes = types?.ToArray() ?? Array.Empty(); + } + + if (dto is Video video) + { + video.PrimaryVersionId = entity.PrimaryVersionId; + } + + if (dto is IHasSeries hasSeriesName) + { + hasSeriesName.SeriesName = entity.SeriesName; + hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault(); + hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey; + } + + if (dto is Episode episode) + { + episode.SeasonName = entity.SeasonName; + episode.SeasonId = entity.SeasonId.GetValueOrDefault(); + } + + if (dto is IHasArtist hasArtists) + { + hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries); + } + + if (dto is IHasAlbumArtist hasAlbumArtists) + { + hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries); + } + + if (dto is LiveTvProgram program) + { + program.ShowId = entity.ShowId; + } + + if (entity.Images is not null) + { + dto.ImageInfos = DeserializeImages(entity.Images); + } + + // dto.Type = entity.Type; + // dto.Data = entity.Data; + // dto.MediaType = entity.MediaType; + if (dto is IHasStartDate hasStartDate) + { + hasStartDate.StartDate = entity.StartDate; + } + + // Fields that are present in the DB but are never actually used + // dto.UnratedType = entity.UnratedType; + // dto.TopParentId = entity.TopParentId; + // dto.CleanName = entity.CleanName; + // dto.UserDataKey = entity.UserDataKey; + + if (dto is Folder folder) + { + folder.DateLastMediaAdded = entity.DateLastMediaAdded; + } + + return dto; + } + + /// + /// Maps a Entity to the DTO. + /// + /// The entity. + /// The dto to map. + public BaseItemEntity Map(BaseItemDto dto) + { + var entity = new BaseItemEntity() + { + Type = dto.GetType().ToString(), + }; + entity.Id = dto.Id; + entity.ParentId = dto.ParentId; + entity.Path = GetPathToSave(dto.Path); + entity.EndDate = dto.EndDate.GetValueOrDefault(); + entity.CommunityRating = dto.CommunityRating; + entity.CustomRating = dto.CustomRating; + entity.IndexNumber = dto.IndexNumber; + entity.IsLocked = dto.IsLocked; + entity.Name = dto.Name; + entity.OfficialRating = dto.OfficialRating; + entity.Overview = dto.Overview; + entity.ParentIndexNumber = dto.ParentIndexNumber; + entity.PremiereDate = dto.PremiereDate; + entity.ProductionYear = dto.ProductionYear; + entity.SortName = dto.SortName; + entity.ForcedSortName = dto.ForcedSortName; + entity.RunTimeTicks = dto.RunTimeTicks; + entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage; + entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode; + entity.IsInMixedFolder = dto.IsInMixedFolder; + entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue; + entity.CriticRating = dto.CriticRating; + entity.PresentationUniqueKey = dto.PresentationUniqueKey; + entity.OriginalTitle = dto.OriginalTitle; + entity.Album = dto.Album; + entity.LUFS = dto.LUFS; + entity.NormalizationGain = dto.NormalizationGain; + entity.IsVirtualItem = dto.IsVirtualItem; + entity.ExternalSeriesId = dto.ExternalSeriesId; + entity.Tagline = dto.Tagline; + entity.TotalBitrate = dto.TotalBitrate; + entity.ExternalId = dto.ExternalId; + entity.Size = dto.Size; + entity.Genres = string.Join('|', dto.Genres); + entity.DateCreated = dto.DateCreated; + entity.DateModified = dto.DateModified; + entity.ChannelId = dto.ChannelId.ToString(); + entity.DateLastRefreshed = dto.DateLastRefreshed; + entity.DateLastSaved = dto.DateLastSaved; + entity.OwnerId = dto.OwnerId.ToString(); + entity.Width = dto.Width; + entity.Height = dto.Height; + entity.ProviderIds = SerializeProviderIds(dto.ProviderIds); + + entity.Audio = dto.Audio?.ToString(); + entity.ExtraType = dto.ExtraType?.ToString(); + + entity.ExtraIds = string.Join('|', dto.ExtraIds); + entity.ProductionLocations = string.Join('|', dto.ProductionLocations); + entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null; + entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null; + entity.LockedFields = dto.LockedFields is not null ? string.Join('|', dto.LockedFields) : null; + + if (dto is IHasProgramAttributes hasProgramAttributes) + { + entity.IsMovie = hasProgramAttributes.IsMovie; + entity.IsSeries = hasProgramAttributes.IsSeries; + entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle; + entity.IsRepeat = hasProgramAttributes.IsRepeat; + } + + if (dto is LiveTvChannel liveTvChannel) + { + entity.ExternalServiceId = liveTvChannel.ServiceName; + } + + if (dto is Trailer trailer) + { + entity.LockedFields = trailer.LockedFields is not null ? string.Join('|', trailer.LockedFields) : null; + } + + if (dto is Video video) + { + entity.PrimaryVersionId = video.PrimaryVersionId; + } + + if (dto is IHasSeries hasSeriesName) + { + entity.SeriesName = hasSeriesName.SeriesName; + entity.SeriesId = hasSeriesName.SeriesId; + entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey; + } + + if (dto is Episode episode) + { + entity.SeasonName = episode.SeasonName; + entity.SeasonId = episode.SeasonId; + } + + if (dto is IHasArtist hasArtists) + { + entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null; + } + + if (dto is IHasAlbumArtist hasAlbumArtists) + { + entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists) : null; + } + + if (dto is LiveTvProgram program) + { + entity.ShowId = program.ShowId; + } + + if (dto.ImageInfos is not null) + { + entity.Images = SerializeImages(dto.ImageInfos); + } + + // dto.Type = entity.Type; + // dto.Data = entity.Data; + // dto.MediaType = entity.MediaType; + if (dto is IHasStartDate hasStartDate) + { + entity.StartDate = hasStartDate.StartDate; + } + + // Fields that are present in the DB but are never actually used + // dto.UnratedType = entity.UnratedType; + // dto.TopParentId = entity.TopParentId; + // dto.CleanName = entity.CleanName; + // dto.UserDataKey = entity.UserDataKey; + + if (dto is Folder folder) + { + entity.DateLastMediaAdded = folder.DateLastMediaAdded; + entity.IsFolder = folder.IsFolder; + } + + return entity; + } + + private string GetCleanValue(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return value; + } + + return value.RemoveDiacritics().ToLowerInvariant(); + } + + private List<(int MagicNumber, string Value)> GetItemValuesToSave(BaseItem item, List inheritedTags) + { + var list = new List<(int, string)>(); + + if (item is IHasArtist hasArtist) + { + list.AddRange(hasArtist.Artists.Select(i => (0, i))); + } + + if (item is IHasAlbumArtist hasAlbumArtist) + { + list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i))); + } + + list.AddRange(item.Genres.Select(i => (2, i))); + list.AddRange(item.Studios.Select(i => (3, i))); + list.AddRange(item.Tags.Select(i => (4, i))); + + // keywords was 5 + + list.AddRange(inheritedTags.Select(i => (6, i))); + + // Remove all invalid values. + list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2)); + + return list; + } + + internal static string? SerializeProviderIds(Dictionary providerIds) + { + StringBuilder str = new StringBuilder(); + foreach (var i in providerIds) + { + // Ideally we shouldn't need this IsNullOrWhiteSpace check, + // but we're seeing some cases of bad data slip through + if (string.IsNullOrWhiteSpace(i.Value)) + { + continue; + } + + str.Append(i.Key) + .Append('=') + .Append(i.Value) + .Append('|'); + } + + if (str.Length == 0) + { + return null; + } + + str.Length -= 1; // Remove last | + return str.ToString(); + } + + internal static void DeserializeProviderIds(string value, IHasProviderIds item) + { + if (string.IsNullOrWhiteSpace(value)) + { + return; + } + + foreach (var part in value.SpanSplit('|')) + { + var providerDelimiterIndex = part.IndexOf('='); + // Don't let empty values through + if (providerDelimiterIndex != -1 && part.Length != providerDelimiterIndex + 1) + { + item.SetProviderId(part[..providerDelimiterIndex].ToString(), part[(providerDelimiterIndex + 1)..].ToString()); + } + } + } + + internal string? SerializeImages(ItemImageInfo[] images) + { + if (images.Length == 0) + { + return null; + } + + StringBuilder str = new StringBuilder(); + foreach (var i in images) + { + if (string.IsNullOrWhiteSpace(i.Path)) + { + continue; + } + + AppendItemImageInfo(str, i); + str.Append('|'); + } + + str.Length -= 1; // Remove last | + return str.ToString(); + } + + internal ItemImageInfo[] DeserializeImages(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return Array.Empty(); + } + + // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed + var valueSpan = value.AsSpan(); + var count = valueSpan.Count('|') + 1; + + var position = 0; + var result = new ItemImageInfo[count]; + foreach (var part in valueSpan.Split('|')) + { + var image = ItemImageInfoFromValueString(part); + + if (image is not null) + { + result[position++] = image; + } + } + + if (position == count) + { + return result; + } + + if (position == 0) + { + return Array.Empty(); + } + + // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array. + return result[..position]; + } + + private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image) + { + const char Delimiter = '*'; + + var path = image.Path ?? string.Empty; + + bldr.Append(GetPathToSave(path)) + .Append(Delimiter) + .Append(image.DateModified.Ticks) + .Append(Delimiter) + .Append(image.Type) + .Append(Delimiter) + .Append(image.Width) + .Append(Delimiter) + .Append(image.Height); + + var hash = image.BlurHash; + if (!string.IsNullOrEmpty(hash)) + { + bldr.Append(Delimiter) + // Replace delimiters with other characters. + // This can be removed when we migrate to a proper DB. + .Append(hash.Replace(Delimiter, '/').Replace('|', '\\')); + } + } + + private string? GetPathToSave(string path) + { + if (path is null) + { + return null; + } + + return _appHost.ReverseVirtualPath(path); + } + + private string RestorePath(string path) + { + return _appHost.ExpandVirtualPath(path); + } + + internal ItemImageInfo? ItemImageInfoFromValueString(ReadOnlySpan value) + { + const char Delimiter = '*'; + + var nextSegment = value.IndexOf(Delimiter); + if (nextSegment == -1) + { + return null; + } + + ReadOnlySpan path = value[..nextSegment]; + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf(Delimiter); + if (nextSegment == -1) + { + return null; + } + + ReadOnlySpan dateModified = value[..nextSegment]; + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf(Delimiter); + if (nextSegment == -1) + { + nextSegment = value.Length; + } + + ReadOnlySpan imageType = value[..nextSegment]; + + var image = new ItemImageInfo + { + Path = RestorePath(path.ToString()) + }; + + if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks) + && ticks >= DateTime.MinValue.Ticks + && ticks <= DateTime.MaxValue.Ticks) + { + image.DateModified = new DateTime(ticks, DateTimeKind.Utc); + } + else + { + return null; + } + + if (Enum.TryParse(imageType, true, out ImageType type)) + { + image.Type = type; + } + else + { + return null; + } + + // Optional parameters: width*height*blurhash + if (nextSegment + 1 < value.Length - 1) + { + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf(Delimiter); + if (nextSegment == -1 || nextSegment == value.Length) + { + return image; + } + + ReadOnlySpan widthSpan = value[..nextSegment]; + + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf(Delimiter); + if (nextSegment == -1) + { + nextSegment = value.Length; + } + + ReadOnlySpan heightSpan = value[..nextSegment]; + + if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width) + && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height)) + { + image.Width = width; + image.Height = height; + } + + if (nextSegment < value.Length - 1) + { + value = value[(nextSegment + 1)..]; + var length = value.Length; + + Span blurHashSpan = stackalloc char[length]; + for (int i = 0; i < length; i++) + { + var c = value[i]; + blurHashSpan[i] = c switch + { + '/' => Delimiter, + '\\' => '|', + _ => c + }; + } + + image.BlurHash = new string(blurHashSpan); + } + } + + return image; + } +} diff --git a/Jellyfin.Server.Implementations/Item/ChapterManager.cs b/Jellyfin.Server.Implementations/Item/ChapterManager.cs new file mode 100644 index 0000000000..273cc96bae --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/ChapterManager.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jellyfin.Data.Entities; +using MediaBrowser.Model.Entities; +using Microsoft.EntityFrameworkCore; +using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; + +namespace Jellyfin.Server.Implementations.Item; + +public class ChapterManager +{ + private readonly IDbContextFactory _dbProvider; + + public ChapterManager(IDbContextFactory dbProvider) + { + _dbProvider = dbProvider; + } + + public IReadOnlyList GetChapters(BaseItemDto baseItemDto) + { + using var context = _dbProvider.CreateDbContext(); + return context.Chapters.Where(e => e.ItemId.Equals(baseItemDto.Id)).Select(Map).ToList(); + } + + private Chapter Map(ChapterInfo chapterInfo, int index, Guid itemId) + { + return new Chapter() + { + ChapterIndex = index, + StartPositionTicks = chapterInfo.StartPositionTicks, + ImageDateModified = chapterInfo.ImageDateModified, + ImagePath = chapterInfo.ImagePath, + ItemId = itemId, + Name = chapterInfo.Name + }; + } + + private ChapterInfo Map(Chapter chapterInfo, BaseItemDto baseItem) + { + var info = new ChapterInfo() + { + StartPositionTicks = chapterInfo.StartPositionTicks, + ImageDateModified = chapterInfo.ImageDateModified.GetValueOrDefault(), + ImagePath = chapterInfo.ImagePath, + Name = chapterInfo.Name, + }; + info.ImageTag = _imageProcessor.GetImageCacheTag(baseItem, info); + return info; + } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index 8e2c21fbc8..01f059db4d 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -93,6 +93,41 @@ public class JellyfinDbContext : DbContext /// public DbSet UserData => Set(); + /// + /// Gets the containing the user data. + /// + public DbSet AncestorIds => Set(); + + /// + /// Gets the containing the user data. + /// + public DbSet AttachmentStreamInfos => Set(); + + /// + /// Gets the containing the user data. + /// + public DbSet BaseItems => Set(); + + /// + /// Gets the containing the user data. + /// + public DbSet Chapters => Set(); + + /// + /// Gets the containing the user data. + /// + public DbSet ItemValues => Set(); + + /// + /// Gets the containing the user data. + /// + public DbSet MediaStreamInfos => Set(); + + /// + /// Gets the containing the user data. + /// + public DbSet Peoples => Set(); + /*public DbSet Artwork => Set(); public DbSet Books => Set(); diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs new file mode 100644 index 0000000000..b7fe909dd4 --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs @@ -0,0 +1,20 @@ +using System; +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + +/// +/// AncestorId configuration. +/// +public class AncestorIdConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(e => new { e.ItemId, e.Id }); + builder.HasIndex(e => e.Id); + builder.HasIndex(e => new { e.ItemId, e.AncestorIdText }); + } +} diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs new file mode 100644 index 0000000000..c0f09670d7 --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs @@ -0,0 +1,42 @@ +using System; +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + +/// +/// Configuration for BaseItem. +/// +public class BaseItemConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasNoKey(); + builder.HasIndex(e => e.Path); + builder.HasIndex(e => e.ParentId); + builder.HasIndex(e => e.PresentationUniqueKey); + builder.HasIndex(e => new { e.Id, e.Type, e.IsFolder, e.IsVirtualItem }); + builder.HasIndex(e => new { e.UserDataKey, e.Type }); + + // covering index + builder.HasIndex(e => new { e.TopParentId, e.Id }); + // series + builder.HasIndex(e => new { e.Type, e.SeriesPresentationUniqueKey, e.PresentationUniqueKey, e.SortName }); + // series counts + // seriesdateplayed sort order + builder.HasIndex(e => new { e.Type, e.SeriesPresentationUniqueKey, e.IsFolder, e.IsVirtualItem }); + // live tv programs + builder.HasIndex(e => new { e.Type, e.TopParentId, e.StartDate }); + // covering index for getitemvalues + builder.HasIndex(e => new { e.Type, e.TopParentId, e.Id }); + // used by movie suggestions + builder.HasIndex(e => new { e.Type, e.TopParentId, e.PresentationUniqueKey }); + // latest items + builder.HasIndex(e => new { e.Type, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey, e.DateCreated }); + builder.HasIndex(e => new { e.IsFolder, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey, e.DateCreated }); + // resume + builder.HasIndex(e => new { e.MediaType, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey }); + } +} diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs new file mode 100644 index 0000000000..0e7c88931a --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs @@ -0,0 +1,20 @@ +using System; +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + + +/// +/// Chapter configuration. +/// +public class ChapterConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasNoKey(); + builder.HasIndex(e => new { e.ItemId, e.ChapterIndex }); + } +} diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs new file mode 100644 index 0000000000..a7de6ec327 --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs @@ -0,0 +1,20 @@ +using System; +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + +/// +/// itemvalues Configuration. +/// +public class ItemValuesConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasNoKey(); + builder.HasIndex(e => new { e.ItemId, e.Type, e.CleanValue }); + builder.HasIndex(e => new { e.ItemId, e.Type, e.Value }); + } +} diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs new file mode 100644 index 0000000000..f6cd39c248 --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs @@ -0,0 +1,20 @@ +using System; +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + +/// +/// People configuration. +/// +public class PeopleConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasNoKey(); + builder.HasIndex(e => new { e.ItemId, e.ListOrder }); + builder.HasIndex(e => e.Name); + } +} From 7a2427bf07f9036d62c88a75855cd6dc7e8e3064 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 5 Sep 2024 12:55:15 +0200 Subject: [PATCH 003/457] Add SessionInfoDto, DeviceInfoDto and implement JsonDelimitedArrayConverter.Write --- .../Session/SessionManager.cs | 137 ++++++++++++- Jellyfin.Api/Controllers/DevicesController.cs | 10 +- Jellyfin.Api/Controllers/SessionController.cs | 83 ++------ Jellyfin.Data/Dtos/DeviceOptionsDto.cs | 33 ++-- .../Devices/DeviceManager.cs | 85 ++++++-- .../Authentication/AuthenticationResult.cs | 33 ++-- .../Devices/IDeviceManager.cs | 150 ++++++++------ .../AuthenticationResultEventArgs.cs | 3 +- .../Outbound/SessionsMessage.cs | 5 +- .../Session/ISessionManager.cs | 11 ++ .../Session/SessionInfo.cs | 120 +++++++++-- MediaBrowser.Model/Devices/DeviceInfo.cs | 139 +++++++------ .../Dto}/ClientCapabilitiesDto.cs | 20 +- MediaBrowser.Model/Dto/DeviceInfoDto.cs | 83 ++++++++ MediaBrowser.Model/Dto/SessionInfoDto.cs | 186 ++++++++++++++++++ .../Converters/JsonDelimitedArrayConverter.cs | 65 ++++-- .../JsonCommaDelimitedArrayTests.cs | 16 +- 17 files changed, 862 insertions(+), 317 deletions(-) rename {Jellyfin.Api/Models/SessionDtos => MediaBrowser.Model/Dto}/ClientCapabilitiesDto.cs (77%) create mode 100644 MediaBrowser.Model/Dto/DeviceInfoDto.cs create mode 100644 MediaBrowser.Model/Dto/SessionInfoDto.cs diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 72e164b521..6bcbe3ceba 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -68,13 +66,29 @@ namespace Emby.Server.Implementations.Session private Timer _inactiveTimer; private DtoOptions _itemInfoDtoOptions; - private bool _disposed = false; + private bool _disposed; + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. public SessionManager( ILogger logger, IEventManager eventManager, IUserDataManager userDataManager, - IServerConfigurationManager config, + IServerConfigurationManager serverConfigurationManager, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, @@ -88,7 +102,7 @@ namespace Emby.Server.Implementations.Session _logger = logger; _eventManager = eventManager; _userDataManager = userDataManager; - _config = config; + _config = serverConfigurationManager; _libraryManager = libraryManager; _userManager = userManager; _musicManager = musicManager; @@ -508,7 +522,10 @@ namespace Emby.Server.Implementations.Session deviceName = "Network Device"; } - var deviceOptions = _deviceManager.GetDeviceOptions(deviceId); + var deviceOptions = _deviceManager.GetDeviceOptions(deviceId) ?? new() + { + DeviceId = deviceId + }; if (string.IsNullOrEmpty(deviceOptions.CustomName)) { sessionInfo.DeviceName = deviceName; @@ -1076,6 +1093,42 @@ namespace Emby.Server.Implementations.Session return session; } + private SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo) + { + return new SessionInfoDto + { + PlayState = sessionInfo.PlayState, + AdditionalUsers = sessionInfo.AdditionalUsers, + Capabilities = _deviceManager.ToClientCapabilitiesDto(sessionInfo.Capabilities), + RemoteEndPoint = sessionInfo.RemoteEndPoint, + PlayableMediaTypes = sessionInfo.PlayableMediaTypes, + Id = sessionInfo.Id, + UserId = sessionInfo.UserId, + UserName = sessionInfo.UserName, + Client = sessionInfo.Client, + LastActivityDate = sessionInfo.LastActivityDate, + LastPlaybackCheckIn = sessionInfo.LastPlaybackCheckIn, + LastPausedDate = sessionInfo.LastPausedDate, + DeviceName = sessionInfo.DeviceName, + DeviceType = sessionInfo.DeviceType, + NowPlayingItem = sessionInfo.NowPlayingItem, + NowViewingItem = sessionInfo.NowViewingItem, + DeviceId = sessionInfo.DeviceId, + ApplicationVersion = sessionInfo.ApplicationVersion, + TranscodingInfo = sessionInfo.TranscodingInfo, + IsActive = sessionInfo.IsActive, + SupportsMediaControl = sessionInfo.SupportsMediaControl, + SupportsRemoteControl = sessionInfo.SupportsRemoteControl, + NowPlayingQueue = sessionInfo.NowPlayingQueue, + NowPlayingQueueFullItems = sessionInfo.NowPlayingQueueFullItems, + HasCustomDeviceName = sessionInfo.HasCustomDeviceName, + PlaylistItemId = sessionInfo.PlaylistItemId, + ServerId = sessionInfo.ServerId, + UserPrimaryImageTag = sessionInfo.UserPrimaryImageTag, + SupportedCommands = sessionInfo.SupportedCommands + }; + } + /// public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken) { @@ -1393,7 +1446,7 @@ namespace Emby.Server.Implementations.Session UserName = user.Username }; - session.AdditionalUsers = [..session.AdditionalUsers, newUser]; + session.AdditionalUsers = [.. session.AdditionalUsers, newUser]; } } @@ -1505,7 +1558,7 @@ namespace Emby.Server.Implementations.Session var returnResult = new AuthenticationResult { User = _userManager.GetUserDto(user, request.RemoteEndPoint), - SessionInfo = session, + SessionInfo = ToSessionInfoDto(session), AccessToken = token, ServerId = _appHost.SystemId }; @@ -1800,6 +1853,74 @@ namespace Emby.Server.Implementations.Session return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false); } + /// + public IReadOnlyList GetSessions( + Guid userId, + string deviceId, + int? activeWithinSeconds, + Guid? controllableUserToCheck) + { + var result = Sessions; + var user = _userManager.GetUserById(userId); + if (!string.IsNullOrEmpty(deviceId)) + { + result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); + } + + if (!controllableUserToCheck.IsNullOrEmpty()) + { + result = result.Where(i => i.SupportsRemoteControl); + + var controlledUser = _userManager.GetUserById(controllableUserToCheck.Value); + if (controlledUser is null) + { + return []; + } + + if (!controlledUser.HasPermission(PermissionKind.EnableSharedDeviceControl)) + { + // Controlled user has device sharing disabled + result = result.Where(i => !i.UserId.IsEmpty()); + } + + if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) + { + // User cannot control other user's sessions, validate user id. + result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(controllableUserToCheck.Value)); + } + + result = result.Where(i => + { + if (!string.IsNullOrWhiteSpace(i.DeviceId) && !_deviceManager.CanAccessDevice(user, i.DeviceId)) + { + return false; + } + + return true; + }); + } + else if (!user.HasPermission(PermissionKind.IsAdministrator)) + { + // Request isn't from administrator, limit to "own" sessions. + result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId)); + + // Don't report acceleration type for non-admin users. + result = result.Select(r => + { + r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none; + return r; + }); + } + + if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) + { + var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value); + result = result.Where(i => i.LastActivityDate >= minActiveDate); + } + + return result.Select(ToSessionInfoDto).ToList(); + } + /// public Task SendMessageToAdminSessions(SessionMessageType name, T data, CancellationToken cancellationToken) { diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 2a2ab4ad16..50050262f0 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -1,15 +1,13 @@ using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Data.Dtos; -using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Queries; using MediaBrowser.Common.Api; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -47,7 +45,7 @@ public class DevicesController : BaseJellyfinApiController /// An containing the list of devices. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetDevices([FromQuery] Guid? userId) + public ActionResult> GetDevices([FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); return _deviceManager.GetDevicesForUser(userId); @@ -63,7 +61,7 @@ public class DevicesController : BaseJellyfinApiController [HttpGet("Info")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceInfo([FromQuery, Required] string id) + public ActionResult GetDeviceInfo([FromQuery, Required] string id) { var deviceInfo = _deviceManager.GetDevice(id); if (deviceInfo is null) @@ -84,7 +82,7 @@ public class DevicesController : BaseJellyfinApiController [HttpGet("Options")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceOptions([FromQuery, Required] string id) + public ActionResult GetDeviceOptions([FromQuery, Required] string id) { var deviceInfo = _deviceManager.GetDeviceOptions(id); if (deviceInfo is null) diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 942bdeb9e8..91a879b8ed 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -1,18 +1,13 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Api.Models.SessionDtos; using Jellyfin.Data.Enums; -using Jellyfin.Extensions; using MediaBrowser.Common.Api; -using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; @@ -32,22 +27,18 @@ public class SessionController : BaseJellyfinApiController { private readonly ISessionManager _sessionManager; private readonly IUserManager _userManager; - private readonly IDeviceManager _deviceManager; /// /// Initializes a new instance of the class. /// /// Instance of interface. /// Instance of interface. - /// Instance of interface. public SessionController( ISessionManager sessionManager, - IUserManager userManager, - IDeviceManager deviceManager) + IUserManager userManager) { _sessionManager = sessionManager; _userManager = userManager; - _deviceManager = deviceManager; } /// @@ -57,77 +48,25 @@ public class SessionController : BaseJellyfinApiController /// Filter by device Id. /// Optional. Filter by sessions that were active in the last n seconds. /// List of sessions returned. - /// An with the available sessions. + /// An with the available sessions. [HttpGet("Sessions")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetSessions( + public ActionResult> GetSessions( [FromQuery] Guid? controllableByUserId, [FromQuery] string? deviceId, [FromQuery] int? activeWithinSeconds) { - var result = _sessionManager.Sessions; - var isRequestingFromAdmin = User.IsInRole(UserRoles.Administrator); + Guid? controllableUserToCheck = controllableByUserId is null ? null : RequestHelpers.GetUserId(User, controllableByUserId); + var result = _sessionManager.GetSessions( + User.GetUserId(), + deviceId, + activeWithinSeconds, + controllableUserToCheck); - if (!string.IsNullOrEmpty(deviceId)) + if (result.Count == 0) { - result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); - } - - if (!controllableByUserId.IsNullOrEmpty()) - { - result = result.Where(i => i.SupportsRemoteControl); - - var user = _userManager.GetUserById(controllableByUserId.Value); - if (user is null) - { - return NotFound(); - } - - if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) - { - // User cannot control other user's sessions, validate user id. - result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(RequestHelpers.GetUserId(User, controllableByUserId))); - } - - if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl)) - { - result = result.Where(i => !i.UserId.IsEmpty()); - } - - result = result.Where(i => - { - if (!string.IsNullOrWhiteSpace(i.DeviceId)) - { - if (!_deviceManager.CanAccessDevice(user, i.DeviceId)) - { - return false; - } - } - - return true; - }); - } - else if (!isRequestingFromAdmin) - { - // Request isn't from administrator, limit to "own" sessions. - result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(User.GetUserId())); - } - - if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) - { - var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value); - result = result.Where(i => i.LastActivityDate >= minActiveDate); - } - - // Request isn't from administrator, don't report acceleration type. - if (!isRequestingFromAdmin) - { - result = result.Select(r => - { - r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none; - return r; - }); + return NotFound(); } return Ok(result); diff --git a/Jellyfin.Data/Dtos/DeviceOptionsDto.cs b/Jellyfin.Data/Dtos/DeviceOptionsDto.cs index 392ef5ff4e..aad5787097 100644 --- a/Jellyfin.Data/Dtos/DeviceOptionsDto.cs +++ b/Jellyfin.Data/Dtos/DeviceOptionsDto.cs @@ -1,23 +1,22 @@ -namespace Jellyfin.Data.Dtos +namespace Jellyfin.Data.Dtos; + +/// +/// A dto representing custom options for a device. +/// +public class DeviceOptionsDto { /// - /// A dto representing custom options for a device. + /// Gets or sets the id. /// - public class DeviceOptionsDto - { - /// - /// Gets or sets the id. - /// - public int Id { get; set; } + public int Id { get; set; } - /// - /// Gets or sets the device id. - /// - public string? DeviceId { get; set; } + /// + /// Gets or sets the device id. + /// + public string? DeviceId { get; set; } - /// - /// Gets or sets the custom name. - /// - public string? CustomName { get; set; } - } + /// + /// Gets or sets the custom name. + /// + public string? CustomName { get; set; } } diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 415c04bbf1..d3bff2936c 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Enums; @@ -13,6 +14,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; using Microsoft.EntityFrameworkCore; @@ -68,7 +70,7 @@ namespace Jellyfin.Server.Implementations.Devices } /// - public async Task UpdateDeviceOptions(string deviceId, string deviceName) + public async Task UpdateDeviceOptions(string deviceId, string? deviceName) { DeviceOptions? deviceOptions; var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); @@ -105,29 +107,37 @@ namespace Jellyfin.Server.Implementations.Devices } /// - public DeviceOptions GetDeviceOptions(string deviceId) + public DeviceOptionsDto? GetDeviceOptions(string deviceId) { - _deviceOptions.TryGetValue(deviceId, out var deviceOptions); + if (_deviceOptions.TryGetValue(deviceId, out var deviceOptions)) + { + return ToDeviceOptionsDto(deviceOptions); + } - return deviceOptions ?? new DeviceOptions(deviceId); + return null; } /// - public ClientCapabilities GetCapabilities(string deviceId) + public ClientCapabilities GetCapabilities(string? deviceId) { + if (deviceId is null) + { + return new(); + } + return _capabilitiesMap.TryGetValue(deviceId, out ClientCapabilities? result) ? result - : new ClientCapabilities(); + : new(); } /// - public DeviceInfo? GetDevice(string id) + public DeviceInfoDto? GetDevice(string id) { var device = _devices.Values.Where(d => d.DeviceId == id).OrderByDescending(d => d.DateLastActivity).FirstOrDefault(); _deviceOptions.TryGetValue(id, out var deviceOption); var deviceInfo = device is null ? null : ToDeviceInfo(device, deviceOption); - return deviceInfo; + return deviceInfo is null ? null : ToDeviceInfoDto(deviceInfo); } /// @@ -166,7 +176,7 @@ namespace Jellyfin.Server.Implementations.Devices } /// - public QueryResult GetDevicesForUser(Guid? userId) + public QueryResult GetDevicesForUser(Guid? userId) { IEnumerable devices = _devices.Values .OrderByDescending(d => d.DateLastActivity) @@ -187,9 +197,11 @@ namespace Jellyfin.Server.Implementations.Devices { _deviceOptions.TryGetValue(device.DeviceId, out var option); return ToDeviceInfo(device, option); - }).ToArray(); + }) + .Select(ToDeviceInfoDto) + .ToArray(); - return new QueryResult(array); + return new QueryResult(array); } /// @@ -235,13 +247,9 @@ namespace Jellyfin.Server.Implementations.Devices private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null) { var caps = GetCapabilities(authInfo.DeviceId); - var user = _userManager.GetUserById(authInfo.UserId); - if (user is null) - { - throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found"); - } + var user = _userManager.GetUserById(authInfo.UserId) ?? throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found"); - return new DeviceInfo + return new() { AppName = authInfo.AppName, AppVersion = authInfo.AppVersion, @@ -254,5 +262,48 @@ namespace Jellyfin.Server.Implementations.Devices CustomName = options?.CustomName, }; } + + private DeviceOptionsDto ToDeviceOptionsDto(DeviceOptions options) + { + return new() + { + Id = options.Id, + DeviceId = options.DeviceId, + CustomName = options.CustomName, + }; + } + + private DeviceInfoDto ToDeviceInfoDto(DeviceInfo info) + { + return new() + { + Name = info.Name, + CustomName = info.CustomName, + AccessToken = info.AccessToken, + Id = info.Id, + LastUserName = info.LastUserName, + AppName = info.AppName, + AppVersion = info.AppVersion, + LastUserId = info.LastUserId, + DateLastActivity = info.DateLastActivity, + Capabilities = ToClientCapabilitiesDto(info.Capabilities), + IconUrl = info.IconUrl + }; + } + + /// + public ClientCapabilitiesDto ToClientCapabilitiesDto(ClientCapabilities capabilities) + { + return new() + { + PlayableMediaTypes = capabilities.PlayableMediaTypes, + SupportedCommands = capabilities.SupportedCommands, + SupportsMediaControl = capabilities.SupportsMediaControl, + SupportsPersistentIdentifier = capabilities.SupportsPersistentIdentifier, + DeviceProfile = capabilities.DeviceProfile, + AppStoreUrl = capabilities.AppStoreUrl, + IconUrl = capabilities.IconUrl + }; + } } } diff --git a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs index 635e4eb3d7..daf4d96313 100644 --- a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs +++ b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs @@ -1,20 +1,31 @@ #nullable disable -#pragma warning disable CS1591 - -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; -namespace MediaBrowser.Controller.Authentication +namespace MediaBrowser.Controller.Authentication; + +/// +/// A class representing an authentication result. +/// +public class AuthenticationResult { - public class AuthenticationResult - { - public UserDto User { get; set; } + /// + /// Gets or sets the user. + /// + public UserDto User { get; set; } - public SessionInfo SessionInfo { get; set; } + /// + /// Gets or sets the session info. + /// + public SessionInfoDto SessionInfo { get; set; } - public string AccessToken { get; set; } + /// + /// Gets or sets the access token. + /// + public string AccessToken { get; set; } - public string ServerId { get; set; } - } + /// + /// Gets or sets the server id. + /// + public string ServerId { get; set; } } diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 5566421cbe..cade53d994 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -1,81 +1,117 @@ -#nullable disable - -#pragma warning disable CS1591 - using System; using System.Threading.Tasks; +using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; -namespace MediaBrowser.Controller.Devices +namespace MediaBrowser.Controller.Devices; + +/// +/// Device manager interface. +/// +public interface IDeviceManager { - public interface IDeviceManager - { - event EventHandler>> DeviceOptionsUpdated; + /// + /// Event handler for updated device options. + /// + event EventHandler>> DeviceOptionsUpdated; - /// - /// Creates a new device. - /// - /// The device to create. - /// A representing the creation of the device. - Task CreateDevice(Device device); + /// + /// Creates a new device. + /// + /// The device to create. + /// A representing the creation of the device. + Task CreateDevice(Device device); - /// - /// Saves the capabilities. - /// - /// The device id. - /// The capabilities. - void SaveCapabilities(string deviceId, ClientCapabilities capabilities); + /// + /// Saves the capabilities. + /// + /// The device id. + /// The capabilities. + void SaveCapabilities(string deviceId, ClientCapabilities capabilities); - /// - /// Gets the capabilities. - /// - /// The device id. - /// ClientCapabilities. - ClientCapabilities GetCapabilities(string deviceId); + /// + /// Gets the capabilities. + /// + /// The device id. + /// ClientCapabilities. + ClientCapabilities GetCapabilities(string? deviceId); - /// - /// Gets the device information. - /// - /// The identifier. - /// DeviceInfo. - DeviceInfo GetDevice(string id); + /// + /// Gets the device information. + /// + /// The identifier. + /// DeviceInfoDto. + DeviceInfoDto? GetDevice(string id); - /// - /// Gets devices based on the provided query. - /// - /// The device query. - /// A representing the retrieval of the devices. - QueryResult GetDevices(DeviceQuery query); + /// + /// Gets devices based on the provided query. + /// + /// The device query. + /// A representing the retrieval of the devices. + QueryResult GetDevices(DeviceQuery query); - QueryResult GetDeviceInfos(DeviceQuery query); + /// + /// Gets device infromation based on the provided query. + /// + /// The device query. + /// A representing the retrieval of the device information. + QueryResult GetDeviceInfos(DeviceQuery query); - /// - /// Gets the devices. - /// - /// The user's id, or null. - /// IEnumerable<DeviceInfo>. - QueryResult GetDevicesForUser(Guid? userId); + /// + /// Gets the device information. + /// + /// The user's id, or null. + /// IEnumerable<DeviceInfoDto>. + QueryResult GetDevicesForUser(Guid? userId); - Task DeleteDevice(Device device); + /// + /// Deletes a device. + /// + /// The device. + /// A representing the deletion of the device. + Task DeleteDevice(Device device); - Task UpdateDevice(Device device); + /// + /// Updates a device. + /// + /// The device. + /// A representing the update of the device. + Task UpdateDevice(Device device); - /// - /// Determines whether this instance [can access device] the specified user identifier. - /// - /// The user to test. - /// The device id to test. - /// Whether the user can access the device. - bool CanAccessDevice(User user, string deviceId); + /// + /// Determines whether this instance [can access device] the specified user identifier. + /// + /// The user to test. + /// The device id to test. + /// Whether the user can access the device. + bool CanAccessDevice(User user, string deviceId); - Task UpdateDeviceOptions(string deviceId, string deviceName); + /// + /// Updates the options of a device. + /// + /// The device id. + /// The device name. + /// A representing the update of the device options. + Task UpdateDeviceOptions(string deviceId, string? deviceName); - DeviceOptions GetDeviceOptions(string deviceId); - } + /// + /// Gets the options of a device. + /// + /// The device id. + /// of the device. + DeviceOptionsDto? GetDeviceOptions(string deviceId); + + /// + /// Gets the dto for client capabilites. + /// + /// The client capabilities. + /// of the device. + ClientCapabilitiesDto ToClientCapabilitiesDto(ClientCapabilities capabilities); } diff --git a/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs index 357ef9406d..1542c58b35 100644 --- a/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs +++ b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs @@ -1,6 +1,5 @@ using System; using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; namespace MediaBrowser.Controller.Events.Authentication; @@ -29,7 +28,7 @@ public class AuthenticationResultEventArgs : EventArgs /// /// Gets or sets the session information. /// - public SessionInfo? SessionInfo { get; set; } + public SessionInfoDto? SessionInfo { get; set; } /// /// Gets or sets the server id. diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs index 3504831b87..8330745418 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.ComponentModel; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; @@ -8,13 +9,13 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// /// Sessions message. /// -public class SessionsMessage : OutboundWebSocketMessage> +public class SessionsMessage : OutboundWebSocketMessage> { /// /// Initializes a new instance of the class. /// /// Session info. - public SessionsMessage(IReadOnlyList data) + public SessionsMessage(IReadOnlyList data) : base(data) { } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 5a47236f92..f2e98dd787 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities.Security; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; @@ -292,6 +293,16 @@ namespace MediaBrowser.Controller.Session /// SessionInfo. SessionInfo GetSession(string deviceId, string client, string version); + /// + /// Gets all sessions available to a user. + /// + /// The session identifier. + /// The device id. + /// Active within session limit. + /// Filter for sessions remote controllable for this user. + /// IReadOnlyList{SessionInfoDto}. + IReadOnlyList GetSessions(Guid userId, string deviceId, int? activeWithinSeconds, Guid? controllableUserToCheck); + /// /// Gets the session by authentication token. /// diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 9e33588187..3ba1bfce42 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Linq; @@ -27,28 +25,45 @@ namespace MediaBrowser.Controller.Session private readonly ISessionManager _sessionManager; private readonly ILogger _logger; - private readonly object _progressLock = new object(); + private readonly object _progressLock = new(); private Timer _progressTimer; private PlaybackProgressInfo _lastProgressInfo; - private bool _disposed = false; + private bool _disposed; + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. public SessionInfo(ISessionManager sessionManager, ILogger logger) { _sessionManager = sessionManager; _logger = logger; - AdditionalUsers = Array.Empty(); + AdditionalUsers = []; PlayState = new PlayerStateInfo(); - SessionControllers = Array.Empty(); - NowPlayingQueue = Array.Empty(); - NowPlayingQueueFullItems = Array.Empty(); + SessionControllers = []; + NowPlayingQueue = []; + NowPlayingQueueFullItems = []; } + /// + /// Gets or sets the play state. + /// + /// The play state. public PlayerStateInfo PlayState { get; set; } - public SessionUserInfo[] AdditionalUsers { get; set; } + /// + /// Gets or sets the additional users. + /// + /// The additional users. + public IReadOnlyList AdditionalUsers { get; set; } + /// + /// Gets or sets the client capabilities. + /// + /// The client capabilities. public ClientCapabilities Capabilities { get; set; } /// @@ -67,7 +82,7 @@ namespace MediaBrowser.Controller.Session { if (Capabilities is null) { - return Array.Empty(); + return []; } return Capabilities.PlayableMediaTypes; @@ -134,9 +149,17 @@ namespace MediaBrowser.Controller.Session /// The now playing item. public BaseItemDto NowPlayingItem { get; set; } + /// + /// Gets or sets the now playing queue full items. + /// + /// The now playing queue full items. [JsonIgnore] public BaseItem FullNowPlayingItem { get; set; } + /// + /// Gets or sets the now viewing item. + /// + /// The now viewing item. public BaseItemDto NowViewingItem { get; set; } /// @@ -156,8 +179,12 @@ namespace MediaBrowser.Controller.Session /// /// The session controller. [JsonIgnore] - public ISessionController[] SessionControllers { get; set; } + public IReadOnlyList SessionControllers { get; set; } + /// + /// Gets or sets the transcoding info. + /// + /// The transcoding info. public TranscodingInfo TranscodingInfo { get; set; } /// @@ -177,7 +204,7 @@ namespace MediaBrowser.Controller.Session } } - if (controllers.Length > 0) + if (controllers.Count > 0) { return false; } @@ -186,6 +213,10 @@ namespace MediaBrowser.Controller.Session } } + /// + /// Gets a value indicating whether the session supports media control. + /// + /// true if this session supports media control; otherwise, false. public bool SupportsMediaControl { get @@ -208,6 +239,10 @@ namespace MediaBrowser.Controller.Session } } + /// + /// Gets a value indicating whether the session supports remote control. + /// + /// true if this session supports remote control; otherwise, false. public bool SupportsRemoteControl { get @@ -230,16 +265,40 @@ namespace MediaBrowser.Controller.Session } } + /// + /// Gets or sets the now playing queue. + /// + /// The now playing queue. public IReadOnlyList NowPlayingQueue { get; set; } + /// + /// Gets or sets the now playing queue full items. + /// + /// The now playing queue full items. public IReadOnlyList NowPlayingQueueFullItems { get; set; } + /// + /// Gets or sets a value indicating whether the session has a custom device name. + /// + /// true if this session has a custom device name; otherwise, false. public bool HasCustomDeviceName { get; set; } + /// + /// Gets or sets the playlist item id. + /// + /// The splaylist item id. public string PlaylistItemId { get; set; } + /// + /// Gets or sets the server id. + /// + /// The server id. public string ServerId { get; set; } + /// + /// Gets or sets the user primary image tag. + /// + /// The user primary image tag. public string UserPrimaryImageTag { get; set; } /// @@ -247,8 +306,14 @@ namespace MediaBrowser.Controller.Session /// /// The supported commands. public IReadOnlyList SupportedCommands - => Capabilities is null ? Array.Empty() : Capabilities.SupportedCommands; + => Capabilities is null ? [] : Capabilities.SupportedCommands; + /// + /// Ensures a controller of type exists. + /// + /// Class to register. + /// The factory. + /// Tuple{ISessionController, bool}. public Tuple EnsureController(Func factory) { var controllers = SessionControllers.ToList(); @@ -261,18 +326,27 @@ namespace MediaBrowser.Controller.Session } var newController = factory(this); - _logger.LogDebug("Creating new {0}", newController.GetType().Name); + _logger.LogDebug("Creating new {Factory}", newController.GetType().Name); controllers.Add(newController); - SessionControllers = controllers.ToArray(); + SessionControllers = [.. controllers]; return new Tuple(newController, true); } + /// + /// Adds a controller to the session. + /// + /// The controller. public void AddController(ISessionController controller) { - SessionControllers = [..SessionControllers, controller]; + SessionControllers = [.. SessionControllers, controller]; } + /// + /// Gets a value indicating whether the session contains a user. + /// + /// The user id to check. + /// true if this session contains the user; otherwise, false. public bool ContainsUser(Guid userId) { if (UserId.Equals(userId)) @@ -291,6 +365,11 @@ namespace MediaBrowser.Controller.Session return false; } + /// + /// Starts automatic progressing. + /// + /// The playback progress info. + /// The supported commands. public void StartAutomaticProgress(PlaybackProgressInfo progressInfo) { if (_disposed) @@ -359,6 +438,9 @@ namespace MediaBrowser.Controller.Session } } + /// + /// Stops automatic progressing. + /// public void StopAutomaticProgress() { lock (_progressLock) @@ -373,6 +455,10 @@ namespace MediaBrowser.Controller.Session } } + /// + /// Disposes the instance async. + /// + /// ValueTask. public async ValueTask DisposeAsync() { _disposed = true; @@ -380,7 +466,7 @@ namespace MediaBrowser.Controller.Session StopAutomaticProgress(); var controllers = SessionControllers.ToList(); - SessionControllers = Array.Empty(); + SessionControllers = []; foreach (var controller in controllers) { diff --git a/MediaBrowser.Model/Devices/DeviceInfo.cs b/MediaBrowser.Model/Devices/DeviceInfo.cs index 4962992a0a..1155986138 100644 --- a/MediaBrowser.Model/Devices/DeviceInfo.cs +++ b/MediaBrowser.Model/Devices/DeviceInfo.cs @@ -1,69 +1,84 @@ -#nullable disable -#pragma warning disable CS1591 - using System; using MediaBrowser.Model.Session; -namespace MediaBrowser.Model.Devices +namespace MediaBrowser.Model.Devices; + +/// +/// A class for device Information. +/// +public class DeviceInfo { - public class DeviceInfo + /// + /// Initializes a new instance of the class. + /// + public DeviceInfo() { - public DeviceInfo() - { - Capabilities = new ClientCapabilities(); - } - - public string Name { get; set; } - - public string CustomName { get; set; } - - /// - /// Gets or sets the access token. - /// - public string AccessToken { get; set; } - - /// - /// Gets or sets the identifier. - /// - /// The identifier. - public string Id { get; set; } - - /// - /// Gets or sets the last name of the user. - /// - /// The last name of the user. - public string LastUserName { get; set; } - - /// - /// Gets or sets the name of the application. - /// - /// The name of the application. - public string AppName { get; set; } - - /// - /// Gets or sets the application version. - /// - /// The application version. - public string AppVersion { get; set; } - - /// - /// Gets or sets the last user identifier. - /// - /// The last user identifier. - public Guid LastUserId { get; set; } - - /// - /// Gets or sets the date last modified. - /// - /// The date last modified. - public DateTime DateLastActivity { get; set; } - - /// - /// Gets or sets the capabilities. - /// - /// The capabilities. - public ClientCapabilities Capabilities { get; set; } - - public string IconUrl { get; set; } + Capabilities = new ClientCapabilities(); } + + /// + /// Gets or sets the name. + /// + /// The name. + public string? Name { get; set; } + + /// + /// Gets or sets the custom name. + /// + /// The custom name. + public string? CustomName { get; set; } + + /// + /// Gets or sets the access token. + /// + /// The access token. + public string? AccessToken { get; set; } + + /// + /// Gets or sets the identifier. + /// + /// The identifier. + public string? Id { get; set; } + + /// + /// Gets or sets the last name of the user. + /// + /// The last name of the user. + public string? LastUserName { get; set; } + + /// + /// Gets or sets the name of the application. + /// + /// The name of the application. + public string? AppName { get; set; } + + /// + /// Gets or sets the application version. + /// + /// The application version. + public string? AppVersion { get; set; } + + /// + /// Gets or sets the last user identifier. + /// + /// The last user identifier. + public Guid? LastUserId { get; set; } + + /// + /// Gets or sets the date last modified. + /// + /// The date last modified. + public DateTime? DateLastActivity { get; set; } + + /// + /// Gets or sets the capabilities. + /// + /// The capabilities. + public ClientCapabilities Capabilities { get; set; } + + /// + /// Gets or sets the icon URL. + /// + /// The icon URL. + public string? IconUrl { get; set; } } diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs similarity index 77% rename from Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs rename to MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs index c699c469d9..5963ed270d 100644 --- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs +++ b/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs @@ -1,13 +1,11 @@ -using System; using System.Collections.Generic; -using System.ComponentModel; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Session; -namespace Jellyfin.Api.Models.SessionDtos; +namespace MediaBrowser.Model.Dto; /// /// Client capabilities dto. @@ -18,13 +16,13 @@ public class ClientCapabilitiesDto /// Gets or sets the list of playable media types. /// [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList PlayableMediaTypes { get; set; } = Array.Empty(); + public IReadOnlyList PlayableMediaTypes { get; set; } = []; /// /// Gets or sets the list of supported commands. /// [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList SupportedCommands { get; set; } = Array.Empty(); + public IReadOnlyList SupportedCommands { get; set; } = []; /// /// Gets or sets a value indicating whether session supports media control. @@ -51,18 +49,6 @@ public class ClientCapabilitiesDto /// public string? IconUrl { get; set; } -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - // TODO: Remove after 10.9 - [Obsolete("Unused")] - [DefaultValue(false)] - public bool? SupportsContentUploading { get; set; } = false; - - // TODO: Remove after 10.9 - [Obsolete("Unused")] - [DefaultValue(false)] - public bool? SupportsSync { get; set; } = false; -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member - /// /// Convert the dto to the full model. /// diff --git a/MediaBrowser.Model/Dto/DeviceInfoDto.cs b/MediaBrowser.Model/Dto/DeviceInfoDto.cs new file mode 100644 index 0000000000..ac7a731a90 --- /dev/null +++ b/MediaBrowser.Model/Dto/DeviceInfoDto.cs @@ -0,0 +1,83 @@ +using System; + +namespace MediaBrowser.Model.Dto; + +/// +/// A DTO representing device information. +/// +public class DeviceInfoDto +{ + /// + /// Initializes a new instance of the class. + /// + public DeviceInfoDto() + { + Capabilities = new ClientCapabilitiesDto(); + } + + /// + /// Gets or sets the name. + /// + /// The name. + public string? Name { get; set; } + + /// + /// Gets or sets the custom name. + /// + /// The custom name. + public string? CustomName { get; set; } + + /// + /// Gets or sets the access token. + /// + /// The access token. + public string? AccessToken { get; set; } + + /// + /// Gets or sets the identifier. + /// + /// The identifier. + public string? Id { get; set; } + + /// + /// Gets or sets the last name of the user. + /// + /// The last name of the user. + public string? LastUserName { get; set; } + + /// + /// Gets or sets the name of the application. + /// + /// The name of the application. + public string? AppName { get; set; } + + /// + /// Gets or sets the application version. + /// + /// The application version. + public string? AppVersion { get; set; } + + /// + /// Gets or sets the last user identifier. + /// + /// The last user identifier. + public Guid? LastUserId { get; set; } + + /// + /// Gets or sets the date last modified. + /// + /// The date last modified. + public DateTime? DateLastActivity { get; set; } + + /// + /// Gets or sets the capabilities. + /// + /// The capabilities. + public ClientCapabilitiesDto Capabilities { get; set; } + + /// + /// Gets or sets the icon URL. + /// + /// The icon URL. + public string? IconUrl { get; set; } +} diff --git a/MediaBrowser.Model/Dto/SessionInfoDto.cs b/MediaBrowser.Model/Dto/SessionInfoDto.cs new file mode 100644 index 0000000000..2496c933a2 --- /dev/null +++ b/MediaBrowser.Model/Dto/SessionInfoDto.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Data.Enums; +using MediaBrowser.Model.Session; + +namespace MediaBrowser.Model.Dto; + +/// +/// Session info DTO. +/// +public class SessionInfoDto +{ + /// + /// Gets or sets the play state. + /// + /// The play state. + public PlayerStateInfo? PlayState { get; set; } + + /// + /// Gets or sets the additional users. + /// + /// The additional users. + public IReadOnlyList? AdditionalUsers { get; set; } + + /// + /// Gets or sets the client capabilities. + /// + /// The client capabilities. + public ClientCapabilitiesDto? Capabilities { get; set; } + + /// + /// Gets or sets the remote end point. + /// + /// The remote end point. + public string? RemoteEndPoint { get; set; } + + /// + /// Gets or sets the playable media types. + /// + /// The playable media types. + public IReadOnlyList PlayableMediaTypes { get; set; } = []; + + /// + /// Gets or sets the id. + /// + /// The id. + public string? Id { get; set; } + + /// + /// Gets or sets the user id. + /// + /// The user id. + public Guid UserId { get; set; } + + /// + /// Gets or sets the username. + /// + /// The username. + public string? UserName { get; set; } + + /// + /// Gets or sets the type of the client. + /// + /// The type of the client. + public string? Client { get; set; } + + /// + /// Gets or sets the last activity date. + /// + /// The last activity date. + public DateTime LastActivityDate { get; set; } + + /// + /// Gets or sets the last playback check in. + /// + /// The last playback check in. + public DateTime LastPlaybackCheckIn { get; set; } + + /// + /// Gets or sets the last paused date. + /// + /// The last paused date. + public DateTime? LastPausedDate { get; set; } + + /// + /// Gets or sets the name of the device. + /// + /// The name of the device. + public string? DeviceName { get; set; } + + /// + /// Gets or sets the type of the device. + /// + /// The type of the device. + public string? DeviceType { get; set; } + + /// + /// Gets or sets the now playing item. + /// + /// The now playing item. + public BaseItemDto? NowPlayingItem { get; set; } + + /// + /// Gets or sets the now viewing item. + /// + /// The now viewing item. + public BaseItemDto? NowViewingItem { get; set; } + + /// + /// Gets or sets the device id. + /// + /// The device id. + public string? DeviceId { get; set; } + + /// + /// Gets or sets the application version. + /// + /// The application version. + public string? ApplicationVersion { get; set; } + + /// + /// Gets or sets the transcoding info. + /// + /// The transcoding info. + public TranscodingInfo? TranscodingInfo { get; set; } + + /// + /// Gets or sets a value indicating whether this session is active. + /// + /// true if this session is active; otherwise, false. + public bool IsActive { get; set; } + + /// + /// Gets or sets a value indicating whether the session supports media control. + /// + /// true if this session supports media control; otherwise, false. + public bool SupportsMediaControl { get; set; } + + /// + /// Gets or sets a value indicating whether the session supports remote control. + /// + /// true if this session supports remote control; otherwise, false. + public bool SupportsRemoteControl { get; set; } + + /// + /// Gets or sets the now playing queue. + /// + /// The now playing queue. + public IReadOnlyList? NowPlayingQueue { get; set; } + + /// + /// Gets or sets the now playing queue full items. + /// + /// The now playing queue full items. + public IReadOnlyList? NowPlayingQueueFullItems { get; set; } + + /// + /// Gets or sets a value indicating whether the session has a custom device name. + /// + /// true if this session has a custom device name; otherwise, false. + public bool HasCustomDeviceName { get; set; } + + /// + /// Gets or sets the playlist item id. + /// + /// The splaylist item id. + public string? PlaylistItemId { get; set; } + + /// + /// Gets or sets the server id. + /// + /// The server id. + public string? ServerId { get; set; } + + /// + /// Gets or sets the user primary image tag. + /// + /// The user primary image tag. + public string? UserPrimaryImageTag { get; set; } + + /// + /// Gets or sets the supported commands. + /// + /// The supported commands. + public IReadOnlyList SupportedCommands { get; set; } = []; +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs index 1466d3a71a..b9477ce6b7 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; @@ -35,38 +37,27 @@ namespace Jellyfin.Extensions.Json.Converters var stringEntries = reader.GetString()!.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries); if (stringEntries.Length == 0) { - return Array.Empty(); + return []; } - var parsedValues = new object[stringEntries.Length]; - var convertedCount = 0; + var typedValues = new List(); for (var i = 0; i < stringEntries.Length; i++) { try { - parsedValues[i] = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim()) ?? throw new FormatException(); - convertedCount++; + var parsedValue = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim()); + if (parsedValue is not null) + { + typedValues.Add((T)parsedValue); + } } catch (FormatException) { - // TODO log when upgraded to .Net6 - // https://github.com/dotnet/runtime/issues/42975 - // _logger.LogDebug(e, "Error converting value."); + // Ignore unconvertable inputs } } - var typedValues = new T[convertedCount]; - var typedValueIndex = 0; - for (var i = 0; i < stringEntries.Length; i++) - { - if (parsedValues[i] is not null) - { - typedValues.SetValue(parsedValues[i], typedValueIndex); - typedValueIndex++; - } - } - - return typedValues; + return [.. typedValues]; } return JsonSerializer.Deserialize(ref reader, options); @@ -75,7 +66,39 @@ namespace Jellyfin.Extensions.Json.Converters /// public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options) { - throw new NotImplementedException(); + if (value is not null) + { + writer.WriteStartArray(); + if (value.Length > 0) + { + var toWrite = value.Length - 1; + foreach (var it in value) + { + var wrote = false; + if (it is not null) + { + writer.WriteStringValue(it.ToString()); + wrote = true; + } + + if (toWrite > 0) + { + if (wrote) + { + writer.WriteStringValue(Delimiter.ToString()); + } + + toWrite--; + } + } + } + + writer.WriteEndArray(); + } + else + { + writer.WriteNullValue(); + } } } } diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs index 61105b42b2..9fc0158235 100644 --- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs @@ -41,7 +41,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { "a", "b", "c" } + Value = ["a", "b", "c"] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions); @@ -53,7 +53,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { "a", "b", "c" } + Value = ["a", "b", "c"] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a, b, c"" }", _jsonOptions); @@ -65,7 +65,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,MoveDown"" }", _jsonOptions); @@ -77,7 +77,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", _jsonOptions); @@ -89,7 +89,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,TotallyNotAVallidCommand,MoveDown"" }", _jsonOptions); @@ -101,7 +101,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp, MoveDown"" }", _jsonOptions); @@ -113,7 +113,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { "a", "b", "c" } + Value = ["a", "b", "c"] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""a"",""b"",""c""] }", _jsonOptions); @@ -125,7 +125,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters { var desiredValue = new GenericBodyArrayModel { - Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] }; var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions); From 0a982e2bfdd6f72dbe9c0bcb09db9890a314a7af Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 18 Sep 2024 16:04:29 +0200 Subject: [PATCH 004/457] Return empty response instead of not found --- Jellyfin.Api/Controllers/SessionController.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 91a879b8ed..72eb93eff2 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -64,11 +64,6 @@ public class SessionController : BaseJellyfinApiController activeWithinSeconds, controllableUserToCheck); - if (result.Count == 0) - { - return NotFound(); - } - return Ok(result); } From ffa1c370fd4b92df15609cd3706b8ebcff930e0d Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 18 Sep 2024 16:10:13 +0200 Subject: [PATCH 005/457] Fix permission checks --- Emby.Server.Implementations/Session/SessionManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 6bcbe3ceba..55e4856692 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1886,7 +1886,7 @@ namespace Emby.Server.Implementations.Session if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) { // User cannot control other user's sessions, validate user id. - result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(controllableUserToCheck.Value)); + result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(user.Id)); } result = result.Where(i => @@ -1903,7 +1903,10 @@ namespace Emby.Server.Implementations.Session { // Request isn't from administrator, limit to "own" sessions. result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId)); + } + if (!user.HasPermission(PermissionKind.IsAdministrator)) + { // Don't report acceleration type for non-admin users. result = result.Select(r => { From 5bfb7b5d1143f5bbf60d91f645f2bc78d4626016 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 18 Sep 2024 16:18:14 +0200 Subject: [PATCH 006/457] Remove invalid test --- .../Controllers/SessionControllerTests.cs | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs deleted file mode 100644 index c267d3dd35..0000000000 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/SessionControllerTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Net; -using System.Threading.Tasks; -using Xunit; - -namespace Jellyfin.Server.Integration.Tests.Controllers; - -public class SessionControllerTests : IClassFixture -{ - private readonly JellyfinApplicationFactory _factory; - private static string? _accessToken; - - public SessionControllerTests(JellyfinApplicationFactory factory) - { - _factory = factory; - } - - [Fact] - public async Task GetSessions_NonExistentUserId_NotFound() - { - var client = _factory.CreateClient(); - client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - - using var response = await client.GetAsync($"Sessions?controllableByUserId={Guid.NewGuid()}"); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } -} From 9c4bf48b4ea85aac30d96d70a198247e56a6323d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Mesk=C3=B3?= Date: Wed, 18 Sep 2024 15:29:42 +0000 Subject: [PATCH 007/457] Translated using Weblate (Hungarian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hu/ --- .../Localization/Core/hu.json | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index 2c8533ac65..f205e8b64c 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -1,13 +1,13 @@ { "Albums": "Albumok", - "AppDeviceValues": "Program: {0}, Eszköz: {1}", + "AppDeviceValues": "Program: {0}, eszköz: {1}", "Application": "Alkalmazás", "Artists": "Előadók", "AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve", "Books": "Könyvek", "CameraImageUploadedFrom": "Új kamerakép lett feltöltve innen: {0}", "Channels": "Csatornák", - "ChapterNameValue": "Jelenet {0}", + "ChapterNameValue": "{0}. jelenet", "Collections": "Gyűjtemények", "DeviceOfflineWithName": "{0} kijelentkezett", "DeviceOnlineWithName": "{0} belépett", @@ -15,31 +15,31 @@ "Favorites": "Kedvencek", "Folders": "Könyvtárak", "Genres": "Műfajok", - "HeaderAlbumArtists": "Album előadók", + "HeaderAlbumArtists": "Albumelőadók", "HeaderContinueWatching": "Megtekintés folytatása", - "HeaderFavoriteAlbums": "Kedvenc Albumok", - "HeaderFavoriteArtists": "Kedvenc Előadók", - "HeaderFavoriteEpisodes": "Kedvenc Epizódok", - "HeaderFavoriteShows": "Kedvenc Sorozatok", - "HeaderFavoriteSongs": "Kedvenc Dalok", + "HeaderFavoriteAlbums": "Kedvenc albumok", + "HeaderFavoriteArtists": "Kedvenc előadók", + "HeaderFavoriteEpisodes": "Kedvenc epizódok", + "HeaderFavoriteShows": "Kedvenc sorozatok", + "HeaderFavoriteSongs": "Kedvenc számok", "HeaderLiveTV": "Élő TV", "HeaderNextUp": "Következik", - "HeaderRecordingGroups": "Felvevő Csoportok", - "HomeVideos": "Otthoni Videók", - "Inherit": "Örökölt", - "ItemAddedWithName": "{0} hozzáadva a könyvtárhoz", - "ItemRemovedWithName": "{0} eltávolítva a könyvtárból", + "HeaderRecordingGroups": "Felvételi csoportok", + "HomeVideos": "Otthoni videók", + "Inherit": "Öröklés", + "ItemAddedWithName": "{0} hozzáadva a médiatárhoz", + "ItemRemovedWithName": "{0} eltávolítva a médiatárból", "LabelIpAddressValue": "IP-cím: {0}", "LabelRunningTimeValue": "Lejátszási idő: {0}", "Latest": "Legújabb", "MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve lett", "MessageApplicationUpdatedTo": "A Jellyfin kiszolgáló frissítve lett a következőre: {0}", "MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve lett: {0}", - "MessageServerConfigurationUpdated": "Kiszolgálókonfiguráció frissítve lett", + "MessageServerConfigurationUpdated": "A kiszolgálókonfiguráció frissítve lett", "MixedContent": "Vegyes tartalom", "Movies": "Filmek", "Music": "Zenék", - "MusicVideos": "Zenei videóklippek", + "MusicVideos": "Zenei videóklipek", "NameInstallFailed": "{0} sikertelen telepítés", "NameSeasonNumber": "{0}. évad", "NameSeasonUnknown": "Ismeretlen évad", @@ -56,7 +56,7 @@ "NotificationOptionPluginUninstalled": "Bővítmény eltávolítva", "NotificationOptionPluginUpdateInstalled": "Bővítményfrissítés telepítve", "NotificationOptionServerRestartRequired": "A kiszolgáló újraindítása szükséges", - "NotificationOptionTaskFailed": "Ütemezett feladat hiba", + "NotificationOptionTaskFailed": "Hiba az ütemezett feladatban", "NotificationOptionUserLockedOut": "Felhasználó tiltva", "NotificationOptionVideoPlayback": "Videólejátszás elkezdve", "NotificationOptionVideoPlaybackStopped": "Videólejátszás leállítva", @@ -107,7 +107,7 @@ "TaskCleanCache": "Gyorsítótár könyvtárának ürítése", "TasksChannelsCategory": "Internetes csatornák", "TasksApplicationCategory": "Alkalmazás", - "TasksLibraryCategory": "Könyvtár", + "TasksLibraryCategory": "Médiatár", "TasksMaintenanceCategory": "Karbantartás", "TaskDownloadMissingSubtitlesDescription": "A metaadat-konfiguráció alapján ellenőrzi és letölti a hiányzó feliratokat az internetről.", "TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése", @@ -119,19 +119,22 @@ "Undefined": "Meghatározatlan", "Forced": "Kényszerített", "Default": "Alapértelmezett", - "TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a könyvtár beolvasása után, vagy egyéb, adatbázis-módosítást igénylő változtatások végrehajtása javíthatja a teljesítményt.", + "TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a médiatár beolvasása, vagy egyéb adatbázis-módosítást igénylő változtatás végrehajtása után, javíthatja a teljesítményt.", "TaskOptimizeDatabase": "Adatbázis optimalizálása", "TaskKeyframeExtractor": "Kulcsképkockák kibontása", "TaskKeyframeExtractorDescription": "Kibontja a kulcsképkockákat a videófájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.", "External": "Külső", "HearingImpaired": "Hallássérült", - "TaskRefreshTrickplayImages": "Trickplay képek generálása", + "TaskRefreshTrickplayImages": "Trickplay képek előállítása", "TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz.", - "TaskAudioNormalization": "Hangerő Normalizáció", + "TaskAudioNormalization": "Hangerő-normalizálás", "TaskCleanCollectionsAndPlaylistsDescription": "Nem létező elemek törlése a gyűjteményekből és lejátszási listákról.", - "TaskAudioNormalizationDescription": "Hangerő normalizációs adatok keresése.", + "TaskAudioNormalizationDescription": "Hangerő-normalizálási adatok keresése.", "TaskCleanCollectionsAndPlaylists": "Gyűjtemények és lejátszási listák optimalizálása", - "TaskExtractMediaSegments": "Média szegmens felismerése", + "TaskExtractMediaSegments": "Médiaszegmens felismerése", "TaskDownloadMissingLyrics": "Hiányzó szöveg letöltése", - "TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése" + "TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése", + "TaskMoveTrickplayImages": "Trickplay képek helyének átköltöztetése", + "TaskMoveTrickplayImagesDescription": "A médiatár-beállításoknak megfelelően áthelyezi a meglévő trickplay fájlokat.", + "TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből." } From 03aa37731b39135e3b3be3f8751f3018d7427d10 Mon Sep 17 00:00:00 2001 From: Brian Howe <30811239+bhowe34@users.noreply.github.com> Date: Thu, 19 Sep 2024 08:12:32 -0500 Subject: [PATCH 008/457] Watch library directories with perm errors (#10684) --- Emby.Server.Implementations/IO/LibraryMonitor.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 31617d1a5f..6af2a553d6 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -314,6 +314,12 @@ namespace Emby.Server.Implementations.IO var ex = e.GetException(); var dw = (FileSystemWatcher)sender; + if (ex is UnauthorizedAccessException unauthorizedAccessException) + { + _logger.LogError(unauthorizedAccessException, "Permission error for Directory watcher: {Path}", dw.Path); + return; + } + _logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path); DisposeWatcher(dw, true); From 93db8990d951649c1d25003e2859776ac80e7440 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Thu, 19 Sep 2024 21:14:18 +0800 Subject: [PATCH 009/457] Enable HEVC RExt HW decoding for 4:2:2/4:4:4 content (#12664) --- .../MediaEncoding/EncodingHelper.cs | 175 ++++++++++++++---- .../Configuration/EncodingOptions.cs | 12 ++ 2 files changed, 147 insertions(+), 40 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 88aa888a1e..e26bcf21e0 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -299,7 +299,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.VideoStream is null || !options.EnableTonemapping - || GetVideoColorBitDepth(state) != 10 + || GetVideoColorBitDepth(state) < 10 || !_mediaEncoder.SupportsFilter("tonemapx")) { return false; @@ -312,7 +312,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.VideoStream is null || !options.EnableTonemapping - || GetVideoColorBitDepth(state) != 10) + || GetVideoColorBitDepth(state) < 10) { return false; } @@ -354,7 +354,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.VideoStream is null || !options.EnableVppTonemapping - || GetVideoColorBitDepth(state) != 10) + || GetVideoColorBitDepth(state) < 10) { return false; } @@ -377,7 +377,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.VideoStream is null || !options.EnableVideoToolboxTonemapping - || GetVideoColorBitDepth(state) != 10) + || GetVideoColorBitDepth(state) < 10) { return false; } @@ -388,6 +388,25 @@ namespace MediaBrowser.Controller.MediaEncoding && state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.HDR10Plus or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG; } + private bool IsVideoStreamHevcRext(EncodingJobInfo state) + { + var videoStream = state.VideoStream; + if (videoStream is null) + { + return false; + } + + return string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) + && (string.Equals(videoStream.Profile, "Rext", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv422p", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv422p10le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv422p12le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase)); + } + /// /// Gets the name of the output video codec. /// @@ -3659,7 +3678,8 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add($"transpose_cuda=dir={tranposeDir}"); } - var outFormat = doCuTonemap ? string.Empty : "yuv420p"; + var isRext = IsVideoStreamHevcRext(state); + var outFormat = doCuTonemap ? (isRext ? "p010" : string.Empty) : "yuv420p"; var hwScaleFilter = GetHwScaleFilter("scale", "cuda", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); // hw scale mainFilters.Add(hwScaleFilter); @@ -4091,6 +4111,8 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (isD3d11vaDecoder || isQsvDecoder) { + var isRext = IsVideoStreamHevcRext(state); + var twoPassVppTonemap = isRext; var doVppProcamp = false; var procampParams = string.Empty; if (doVppTonemap) @@ -4100,21 +4122,21 @@ namespace MediaBrowser.Controller.MediaEncoding && options.VppTonemappingBrightness <= 100) { procampParams += $":brightness={options.VppTonemappingBrightness}"; - doVppProcamp = true; + twoPassVppTonemap = doVppProcamp = true; } if (options.VppTonemappingContrast > 1 && options.VppTonemappingContrast <= 10) { procampParams += $":contrast={options.VppTonemappingContrast}"; - doVppProcamp = true; + twoPassVppTonemap = doVppProcamp = true; } procampParams += doVppProcamp ? ":procamp=1:async_depth=2" : string.Empty; } - var outFormat = doOclTonemap ? (doVppTranspose ? "p010" : string.Empty) : "nv12"; - outFormat = (doVppTonemap && doVppProcamp) ? "p010" : outFormat; + var outFormat = doOclTonemap ? ((doVppTranspose || isRext) ? "p010" : string.Empty) : "nv12"; + outFormat = twoPassVppTonemap ? "p010" : outFormat; var swapOutputWandH = doVppTranspose && swapWAndH; var hwScalePrefix = (doVppTranspose || doVppTonemap) ? "vpp" : "scale"; @@ -4127,7 +4149,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTonemap) { - hwScaleFilter += doVppProcamp ? procampParams : ":tonemap=1"; + hwScaleFilter += doVppProcamp ? procampParams : (twoPassVppTonemap ? string.Empty : ":tonemap=1"); } if (isD3d11vaDecoder) @@ -4151,7 +4173,7 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add(hwScaleFilter); // hw tonemap(w/ procamp) - if (doVppTonemap && doVppProcamp) + if (doVppTonemap && twoPassVppTonemap) { mainFilters.Add("vpp_qsv=tonemap=1:format=nv12:async_depth=2"); } @@ -4346,6 +4368,7 @@ namespace MediaBrowser.Controller.MediaEncoding else if (isVaapiDecoder || isQsvDecoder) { var hwFilterSuffix = isVaapiDecoder ? "vaapi" : "qsv"; + var isRext = IsVideoStreamHevcRext(state); // INPUT vaapi/qsv surface(vram) // hw deint @@ -4362,6 +4385,8 @@ namespace MediaBrowser.Controller.MediaEncoding } var outFormat = doOclTonemap ? ((isQsvDecoder && doVppTranspose) ? "p010" : string.Empty) : "nv12"; + outFormat = (doTonemap && isRext) ? "p010" : outFormat; + var swapOutputWandH = isQsvDecoder && doVppTranspose && swapWAndH; var hwScalePrefix = (isQsvDecoder && doVppTranspose) ? "vpp" : "scale"; var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, hwFilterSuffix, outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); @@ -4658,6 +4683,8 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (isVaapiDecoder) { + var isRext = IsVideoStreamHevcRext(state); + // INPUT vaapi surface(vram) // hw deint if (doDeintH2645) @@ -4672,7 +4699,7 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add($"transpose_vaapi=dir={tranposeDir}"); } - var outFormat = doTonemap ? string.Empty : "nv12"; + var outFormat = doTonemap ? (isRext ? "p010" : string.Empty) : "nv12"; var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); // allocate extra pool sizes for vaapi vpp @@ -5974,7 +6001,11 @@ namespace MediaBrowser.Controller.MediaEncoding var decoderName = decoderPrefix + '_' + decoderSuffix; var isCodecAvailable = _mediaEncoder.SupportsDecoder(decoderName) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase); - if (bitDepth == 10 && isCodecAvailable) + + // VideoToolbox decoders have built-in SW fallback + if (bitDepth == 10 + && isCodecAvailable + && (options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox)) { if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase) && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase) @@ -6050,17 +6081,40 @@ namespace MediaBrowser.Controller.MediaEncoding && ffmpegVersion >= _minFFmpegDisplayRotationOption; var stripRotationDataArgs = stripRotationData ? " -display_rotation 0" : string.Empty; - if (bitDepth == 10 && isCodecAvailable) + // VideoToolbox decoders have built-in SW fallback + if (isCodecAvailable + && (options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox)) { if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase) - && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase) - && !options.EnableDecodingColorDepth10Hevc) + && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase)) { - return null; + if (IsVideoStreamHevcRext(state)) + { + if (bitDepth <= 10 && !options.EnableDecodingColorDepth10HevcRext) + { + return null; + } + + if (bitDepth == 12 && !options.EnableDecodingColorDepth12HevcRext) + { + return null; + } + + if (hardwareAccelerationType == HardwareAccelerationType.vaapi + && !_mediaEncoder.IsVaapiDeviceInteliHD) + { + return null; + } + } + else if (bitDepth == 10 && !options.EnableDecodingColorDepth10Hevc) + { + return null; + } } if (string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase) && options.HardwareDecodingCodecs.Contains("vp9", StringComparison.OrdinalIgnoreCase) + && bitDepth == 10 && !options.EnableDecodingColorDepth10Vp9) { return null; @@ -6172,6 +6226,14 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsQsv = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsQsv = is8bitSwFormatsQsv || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsQsv = is8_10bitSwFormatsQsv + || string.Equals("yuv422p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); // TODO: add more 8/10bit and 4:4:4 formats for Qsv after finishing the ffcheck tool if (is8bitSwFormatsQsv) @@ -6200,12 +6262,6 @@ namespace MediaBrowser.Controller.MediaEncoding if (is8_10bitSwFormatsQsv) { - if (string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)) - { - return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "qsv", "hevc", bitDepth); - } - if (string.Equals(videoStream.Codec, "vp9", StringComparison.OrdinalIgnoreCase)) { return GetHwaccelType(state, options, "vp9", bitDepth, hwSurface) + GetHwDecoderName(options, "vp9", "qsv", "vp9", bitDepth); @@ -6217,6 +6273,15 @@ namespace MediaBrowser.Controller.MediaEncoding } } + if (is8_10_12bitSwFormatsQsv) + { + if (string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)) + { + return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "qsv", "hevc", bitDepth); + } + } + return null; } @@ -6232,6 +6297,11 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsNvdec = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsNvdec = is8bitSwFormatsNvdec || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsNvdec = is8_10bitSwFormatsNvdec + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); // TODO: add more 8/10/12bit and 4:4:4 formats for Nvdec after finishing the ffcheck tool if (is8bitSwFormatsNvdec) @@ -6265,12 +6335,6 @@ namespace MediaBrowser.Controller.MediaEncoding if (is8_10bitSwFormatsNvdec) { - if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) - || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) - { - return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "cuvid", "hevc", bitDepth); - } - if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { return GetHwaccelType(state, options, "vp9", bitDepth, hwSurface) + GetHwDecoderName(options, "vp9", "cuvid", "vp9", bitDepth); @@ -6282,6 +6346,15 @@ namespace MediaBrowser.Controller.MediaEncoding } } + if (is8_10_12bitSwFormatsNvdec) + { + if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) + || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + { + return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "cuvid", "hevc", bitDepth); + } + } + return null; } @@ -6356,6 +6429,14 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsVaapi = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsVaapi = is8bitSwFormatsVaapi || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsVaapi = is8_10bitSwFormatsVaapi + || string.Equals("yuv422p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); if (is8bitSwFormatsVaapi) { @@ -6383,12 +6464,6 @@ namespace MediaBrowser.Controller.MediaEncoding if (is8_10bitSwFormatsVaapi) { - if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) - || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) - { - return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface); - } - if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { return GetHwaccelType(state, options, "vp9", bitDepth, hwSurface); @@ -6400,6 +6475,15 @@ namespace MediaBrowser.Controller.MediaEncoding } } + if (is8_10_12bitSwFormatsVaapi) + { + if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) + || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + { + return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface); + } + } + return null; } @@ -6414,6 +6498,14 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsVt = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsVt = is8_10bitSwFormatsVt + || string.Equals("yuv422p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", 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(); @@ -6434,16 +6526,19 @@ namespace MediaBrowser.Controller.MediaEncoding return GetHwaccelType(state, options, "h264", bitDepth, useHwSurface); } + if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + { + return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface); + } + } + + if (is8_10_12bitSwFormatsVt) + { if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface); } - - if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) - { - return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface); - } } return null; diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index d67a2479fb..2720c0bdf6 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -46,6 +46,8 @@ public class EncodingOptions DeinterlaceMethod = DeinterlaceMethod.yadif; EnableDecodingColorDepth10Hevc = true; EnableDecodingColorDepth10Vp9 = true; + EnableDecodingColorDepth10HevcRext = false; + EnableDecodingColorDepth12HevcRext = false; // Enhanced Nvdec or system native decoder is required for DoVi to SDR tone-mapping. EnableEnhancedNvdecDecoder = true; PreferSystemNativeHwDecoder = true; @@ -234,6 +236,16 @@ public class EncodingOptions /// public bool EnableDecodingColorDepth10Vp9 { get; set; } + /// + /// Gets or sets a value indicating whether 8/10bit HEVC RExt decoding is enabled. + /// + public bool EnableDecodingColorDepth10HevcRext { get; set; } + + /// + /// Gets or sets a value indicating whether 12bit HEVC RExt decoding is enabled. + /// + public bool EnableDecodingColorDepth12HevcRext { get; set; } + /// /// Gets or sets a value indicating whether the enhanced NVDEC is enabled. /// From d4bde14a0143ae60db5187084dbb8cbf93e77cfd Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Thu, 19 Sep 2024 16:46:59 +0200 Subject: [PATCH 010/457] Update src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs Co-authored-by: Bond-009 --- .../Json/Converters/JsonDelimitedArrayConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs index b9477ce6b7..936a5a97c4 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs @@ -57,7 +57,7 @@ namespace Jellyfin.Extensions.Json.Converters } } - return [.. typedValues]; + return typedValues.ToArray(); } return JsonSerializer.Deserialize(ref reader, options); From 2c0520b5401620bc121e1e4424e3b4a3f08656f2 Mon Sep 17 00:00:00 2001 From: Bas <44002186+854562@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:41:47 +0000 Subject: [PATCH 011/457] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- .../Localization/Core/nl.json | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 1522720dc5..7d101195b7 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -16,13 +16,13 @@ "Folders": "Mappen", "Genres": "Genres", "HeaderAlbumArtists": "Albumartiesten", - "HeaderContinueWatching": "Kijken hervatten", + "HeaderContinueWatching": "Verderkijken", "HeaderFavoriteAlbums": "Favoriete albums", "HeaderFavoriteArtists": "Favoriete artiesten", "HeaderFavoriteEpisodes": "Favoriete afleveringen", - "HeaderFavoriteShows": "Favoriete shows", + "HeaderFavoriteShows": "Favoriete series", "HeaderFavoriteSongs": "Favoriete nummers", - "HeaderLiveTV": "Live TV", + "HeaderLiveTV": "Live-tv", "HeaderNextUp": "Volgende", "HeaderRecordingGroups": "Opnamegroepen", "HomeVideos": "Homevideo's", @@ -34,8 +34,8 @@ "Latest": "Nieuwste", "MessageApplicationUpdated": "Jellyfin Server is bijgewerkt", "MessageApplicationUpdatedTo": "Jellyfin Server is bijgewerkt naar {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Sectie {0} van de server configuratie is bijgewerkt", - "MessageServerConfigurationUpdated": "Server configuratie is bijgewerkt", + "MessageNamedServerConfigurationUpdatedWithValue": "Sectie {0} van de serverconfiguratie is bijgewerkt", + "MessageServerConfigurationUpdated": "Serverconfiguratie is bijgewerkt", "MixedContent": "Gemengde inhoud", "Movies": "Films", "Music": "Muziek", @@ -50,12 +50,12 @@ "NotificationOptionAudioPlaybackStopped": "Muziek gestopt", "NotificationOptionCameraImageUploaded": "Camera-afbeelding geüpload", "NotificationOptionInstallationFailed": "Installatie mislukt", - "NotificationOptionNewLibraryContent": "Nieuwe content toegevoegd", - "NotificationOptionPluginError": "Plug-in fout", + "NotificationOptionNewLibraryContent": "Nieuwe inhoud toegevoegd", + "NotificationOptionPluginError": "Plug-in-fout", "NotificationOptionPluginInstalled": "Plug-in geïnstalleerd", "NotificationOptionPluginUninstalled": "Plug-in verwijderd", "NotificationOptionPluginUpdateInstalled": "Plug-in-update geïnstalleerd", - "NotificationOptionServerRestartRequired": "Server herstart nodig", + "NotificationOptionServerRestartRequired": "Herstarten server vereist", "NotificationOptionTaskFailed": "Geplande taak mislukt", "NotificationOptionUserLockedOut": "Gebruiker is vergrendeld", "NotificationOptionVideoPlayback": "Afspelen van video gestart", @@ -72,16 +72,16 @@ "ServerNameNeedsToBeRestarted": "{0} moet herstart worden", "Shows": "Series", "Songs": "Nummers", - "StartupEmbyServerIsLoading": "Jellyfin Server is aan het laden, probeer het later opnieuw.", + "StartupEmbyServerIsLoading": "Jellyfin Server is aan het laden. Probeer het later opnieuw.", "SubtitleDownloadFailureForItem": "Downloaden van ondertiteling voor {0} is mislukt", - "SubtitleDownloadFailureFromForItem": "Ondertitels konden niet gedownload worden van {0} voor {1}", + "SubtitleDownloadFailureFromForItem": "Ondertiteling kon niet gedownload worden van {0} voor {1}", "Sync": "Synchronisatie", "System": "Systeem", "TvShows": "TV-series", "User": "Gebruiker", "UserCreatedWithName": "Gebruiker {0} is aangemaakt", "UserDeletedWithName": "Gebruiker {0} is verwijderd", - "UserDownloadingItemWithValues": "{0} download {1}", + "UserDownloadingItemWithValues": "{0} downloadt {1}", "UserLockedOutWithName": "Gebruikersaccount {0} is vergrendeld", "UserOfflineFromDevice": "Verbinding van {0} met {1} is verbroken", "UserOnlineFromDevice": "{0} heeft verbinding met {1}", @@ -90,7 +90,7 @@ "UserStartedPlayingItemWithValues": "{0} speelt {1} af op {2}", "UserStoppedPlayingItemWithValues": "{0} heeft afspelen van {1} gestopt op {2}", "ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek", - "ValueSpecialEpisodeName": "Speciaal - {0}", + "ValueSpecialEpisodeName": "Special - {0}", "VersionNumber": "Versie {0}", "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.", "TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden", From 7ab7f69916a7c207f290e5e931a6c7749dd4413b Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Fri, 20 Sep 2024 03:22:53 +0800 Subject: [PATCH 012/457] Enable key-frame only decoding for RKMPP trickplay Signed-off-by: nyanmisaka --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 6f87692521..517d135d3b 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -804,7 +804,8 @@ namespace MediaBrowser.MediaEncoding.Encoder || (hardwareAccelerationType == HardwareAccelerationType.amf && OperatingSystem.IsWindows()) || (hardwareAccelerationType == HardwareAccelerationType.qsv && options.PreferSystemNativeHwDecoder) || hardwareAccelerationType == HardwareAccelerationType.vaapi - || hardwareAccelerationType == HardwareAccelerationType.videotoolbox; + || hardwareAccelerationType == HardwareAccelerationType.videotoolbox + || hardwareAccelerationType == HardwareAccelerationType.rkmpp; if (!supportsKeyFrameOnly) { // Disable hardware acceleration when the hardware decoder does not support keyframe only mode. @@ -930,13 +931,14 @@ namespace MediaBrowser.MediaEncoding.Encoder // Final command arguments var args = string.Format( CultureInfo.InvariantCulture, - "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}-f {6} \"{7}\"", + "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}{5}{6}-f {7} \"{8}\"", inputArg, filterParam, outputThreads.GetValueOrDefault(_threads), vidEncoder, encoderQualityOption + encoderQuality + " ", vidEncoder.Contains("videotoolbox", StringComparison.InvariantCultureIgnoreCase) ? "-allow_sw 1 " : string.Empty, // allow_sw fallback for some intel macs + EncoderVersion >= new Version(5, 1) ? "-fps_mode passthrough " : "-vsync passthrough ", // passthrough timestamp "image2", outputPath); From e615b56a70d88c441589bb9f1fd0159e97ea4b1a Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Fri, 20 Sep 2024 21:01:58 +0800 Subject: [PATCH 013/457] Fix RKMPP 2pass scaling in Trickplay (#12675) --- .../MediaEncoding/EncodingHelper.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index e26bcf21e0..d8c4f2f850 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -5570,13 +5570,22 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doOclTonemap ? "p010" : "nv12"; var hwScalePrefix = doRkVppTranspose ? "vpp" : "scale"; var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); - var hwScaleFilter2 = GetHwScaleFilter(hwScalePrefix, "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var doScaling = GetHwScaleFilter(hwScalePrefix, "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); if (!hasSubs || doRkVppTranspose || !isFullAfbcPipeline - || !string.IsNullOrEmpty(hwScaleFilter2)) + || !string.IsNullOrEmpty(doScaling)) { + // RGA3 hardware only support (1/8 ~ 8) scaling in each blit operation, + // but in Trickplay there's a case: (3840/320 == 12), enable 2pass for it + if (!string.IsNullOrEmpty(doScaling) + && !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f)) + { + var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/8:h=ih/8:format={outFormat}:afbc=1"; + mainFilters.Add(hwScaleFilterFirstPass); + } + if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose) { hwScaleFilter += $":transpose={tranposeDir}"; From 41fb696ef6801af786ae7f6f3fe1c7a10121114b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Sep 2024 07:46:31 -0600 Subject: [PATCH 014/457] Update github/codeql-action action to v3.26.8 (#12672) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index de5503b843..551dbae987 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '8.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/init@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/autobuild@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7 + uses: github/codeql-action/analyze@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 From 9ff7575c85d05714a497502f95ee38f6f3b87752 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Fri, 20 Sep 2024 15:46:44 +0200 Subject: [PATCH 015/457] Fix metadata merge for BoxSets (#12583) --- MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs index df9d15ec20..32ab7716f7 100644 --- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs +++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs @@ -54,7 +54,14 @@ namespace MediaBrowser.Providers.BoxSets if (mergeMetadataSettings) { - targetItem.LinkedChildren = sourceItem.LinkedChildren; + if (replaceData || targetItem.LinkedChildren.Length == 0) + { + targetItem.LinkedChildren = sourceItem.LinkedChildren; + } + else + { + targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray(); + } } } From 78638c72fb2fd15e7c9277bb37f5a6a93b9cc0a9 Mon Sep 17 00:00:00 2001 From: l00d3r Date: Sat, 21 Sep 2024 11:52:24 +0000 Subject: [PATCH 016/457] Translated using Weblate (Estonian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/et/ --- Emby.Server.Implementations/Localization/Core/et.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index 075bcc9a44..fcb12718a0 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -102,7 +102,7 @@ "Forced": "Sunnitud", "Folders": "Kaustad", "Favorites": "Lemmikud", - "FailedLoginAttemptWithUserName": "{0} - sisselogimine nurjus", + "FailedLoginAttemptWithUserName": "Sisselogimine nurjus aadressilt {0}", "DeviceOnlineWithName": "{0} on ühendatud", "DeviceOfflineWithName": "{0} katkestas ühenduse", "Default": "Vaikimisi", From 092e9e29f1d94a8b6a88868d56c5b4801cbe5e35 Mon Sep 17 00:00:00 2001 From: l00d3r Date: Sat, 21 Sep 2024 12:32:53 +0000 Subject: [PATCH 017/457] Translated using Weblate (Estonian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/et/ --- Emby.Server.Implementations/Localization/Core/et.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index fcb12718a0..7ca4e431d9 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -129,5 +129,7 @@ "TaskAudioNormalization": "Heli Normaliseerimine", "TaskAudioNormalizationDescription": "Skaneerib faile heli normaliseerimise andmete jaoks.", "TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest asjad, mida enam ei eksisteeri.", - "TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid" + "TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid", + "TaskDownloadMissingLyrics": "Lae alla puuduolev lüürika", + "TaskDownloadMissingLyricsDescription": "Lae lauludele alla lüürika" } From 38f80edc8011783d6544a9c1bd6401e3fe13ff57 Mon Sep 17 00:00:00 2001 From: l00d3r Date: Sat, 21 Sep 2024 12:37:52 +0000 Subject: [PATCH 018/457] Translated using Weblate (Estonian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/et/ --- Emby.Server.Implementations/Localization/Core/et.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index 7ca4e431d9..3b2bb70a95 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -131,5 +131,9 @@ "TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest asjad, mida enam ei eksisteeri.", "TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid", "TaskDownloadMissingLyrics": "Lae alla puuduolev lüürika", - "TaskDownloadMissingLyricsDescription": "Lae lauludele alla lüürika" + "TaskDownloadMissingLyricsDescription": "Lae lauludele alla lüürika", + "TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.", + "TaskExtractMediaSegments": "Meediasegmentide skaneerimine", + "TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.", + "TaskMoveTrickplayImages": "Migreeri trickplay piltide asukoht" } From d944f415f3cc0e5433d94b11a16684ca3f0131ec Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 22 Sep 2024 00:34:47 +0800 Subject: [PATCH 019/457] Let HLS Controller decide if subtitle should be burn in Previously, we predicted whether the subtitle should be burned in with transcode reasons, but that was not accurate because the actual transcoding codec is only determined after the client has requested the stream. This pass through the option to the `DynamicHlsController` to handle the subtitle burn-in during the actual transcoding process. Now the client should be responsible to conditionally load the subtitle when this option is enabled. --- .../Controllers/DynamicHlsController.cs | 63 ++++++++++++------- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 6 ++ .../MediaEncoding/BaseEncodingJobOptions.cs | 2 + .../MediaEncoding/EncodingHelper.cs | 38 ++++++----- MediaBrowser.Model/Dlna/StreamBuilder.cs | 18 +----- MediaBrowser.Model/Dlna/StreamInfo.cs | 7 ++- 6 files changed, 81 insertions(+), 53 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index db1d866985..924f010e40 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -158,6 +158,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The max height. /// Optional. Whether to enable subtitles in the manifest. /// Optional. Whether to enable Audio Encoding. + /// Whether to always burn in subtitles when transcoding. /// Hls live stream retrieved. /// A containing the hls file. [HttpGet("Videos/{itemId}/live.m3u8")] @@ -216,7 +217,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] bool? enableSubtitlesInManifest, - [FromQuery] bool enableAudioVbrEncoding = true) + [FromQuery] bool enableAudioVbrEncoding = true, + [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) { VideoRequestDto streamingRequest = new VideoRequestDto { @@ -251,7 +253,7 @@ public class DynamicHlsController : BaseJellyfinApiController Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, RequireAvc = requireAvc ?? false, @@ -271,7 +273,8 @@ public class DynamicHlsController : BaseJellyfinApiController MaxHeight = maxHeight, MaxWidth = maxWidth, EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true, - EnableAudioVbrEncoding = enableAudioVbrEncoding + EnableAudioVbrEncoding = enableAudioVbrEncoding, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding }; // CTS lifecycle is managed internally. @@ -398,6 +401,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// Enable adaptive bitrate streaming. /// Enable trickplay image playlists being added to master playlist. /// Whether to enable Audio Encoding. + /// Whether to always burn in subtitles when transcoding. /// Video stream returned. /// A containing the playlist file. [HttpGet("Videos/{itemId}/master.m3u8")] @@ -457,7 +461,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] Dictionary streamOptions, [FromQuery] bool enableAdaptiveBitrateStreaming = true, [FromQuery] bool enableTrickplay = true, - [FromQuery] bool enableAudioVbrEncoding = true) + [FromQuery] bool enableAudioVbrEncoding = true, + [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) { var streamingRequest = new HlsVideoRequestDto { @@ -493,7 +498,7 @@ public class DynamicHlsController : BaseJellyfinApiController MaxHeight = maxHeight, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, RequireAvc = requireAvc ?? false, @@ -512,7 +517,8 @@ public class DynamicHlsController : BaseJellyfinApiController StreamOptions = streamOptions, EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming, EnableTrickplay = enableTrickplay, - EnableAudioVbrEncoding = enableAudioVbrEncoding + EnableAudioVbrEncoding = enableAudioVbrEncoding, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding }; return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); @@ -572,6 +578,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The streaming options. /// Enable adaptive bitrate streaming. /// Optional. Whether to enable Audio Encoding. + /// Whether to always burn in subtitles when transcoding. /// Audio stream returned. /// A containing the playlist file. [HttpGet("Audio/{itemId}/master.m3u8")] @@ -629,7 +636,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, [FromQuery] bool enableAdaptiveBitrateStreaming = true, - [FromQuery] bool enableAudioVbrEncoding = true) + [FromQuery] bool enableAudioVbrEncoding = true, + [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) { var streamingRequest = new HlsAudioRequestDto { @@ -663,7 +671,7 @@ public class DynamicHlsController : BaseJellyfinApiController Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, RequireAvc = requireAvc ?? false, @@ -681,7 +689,8 @@ public class DynamicHlsController : BaseJellyfinApiController Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming, - EnableAudioVbrEncoding = enableAudioVbrEncoding + EnableAudioVbrEncoding = enableAudioVbrEncoding, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding }; return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); @@ -741,6 +750,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The . /// Optional. The streaming options. /// Optional. Whether to enable Audio Encoding. + /// Whether to always burn in subtitles when transcoding. /// Video stream returned. /// A containing the audio file. [HttpGet("Videos/{itemId}/main.m3u8")] @@ -797,7 +807,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, - [FromQuery] bool enableAudioVbrEncoding = true) + [FromQuery] bool enableAudioVbrEncoding = true, + [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) { using var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new VideoRequestDto @@ -834,7 +845,7 @@ public class DynamicHlsController : BaseJellyfinApiController MaxHeight = maxHeight, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, RequireAvc = requireAvc ?? false, @@ -851,7 +862,8 @@ public class DynamicHlsController : BaseJellyfinApiController VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, - EnableAudioVbrEncoding = enableAudioVbrEncoding + EnableAudioVbrEncoding = enableAudioVbrEncoding, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding }; return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource) @@ -911,6 +923,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The . /// Optional. The streaming options. /// Optional. Whether to enable Audio Encoding. + /// Whether to always burn in subtitles when transcoding. /// Audio stream returned. /// A containing the audio file. [HttpGet("Audio/{itemId}/main.m3u8")] @@ -966,7 +979,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, - [FromQuery] bool enableAudioVbrEncoding = true) + [FromQuery] bool enableAudioVbrEncoding = true, + [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) { using var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new StreamingRequestDto @@ -1001,7 +1015,7 @@ public class DynamicHlsController : BaseJellyfinApiController Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, RequireAvc = requireAvc ?? false, @@ -1018,7 +1032,8 @@ public class DynamicHlsController : BaseJellyfinApiController VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, - EnableAudioVbrEncoding = enableAudioVbrEncoding + EnableAudioVbrEncoding = enableAudioVbrEncoding, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding }; return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource) @@ -1084,6 +1099,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The . /// Optional. The streaming options. /// Optional. Whether to enable Audio Encoding. + /// Whether to always burn in subtitles when transcoding. /// Video stream returned. /// A containing the audio file. [HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] @@ -1146,7 +1162,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, - [FromQuery] bool enableAudioVbrEncoding = true) + [FromQuery] bool enableAudioVbrEncoding = true, + [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) { var streamingRequest = new VideoRequestDto { @@ -1185,7 +1202,7 @@ public class DynamicHlsController : BaseJellyfinApiController MaxHeight = maxHeight, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, RequireAvc = requireAvc ?? false, @@ -1202,7 +1219,8 @@ public class DynamicHlsController : BaseJellyfinApiController VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, - EnableAudioVbrEncoding = enableAudioVbrEncoding + EnableAudioVbrEncoding = enableAudioVbrEncoding, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding }; return await GetDynamicSegment(streamingRequest, segmentId) @@ -1267,6 +1285,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The . /// Optional. The streaming options. /// Optional. Whether to enable Audio Encoding. + /// Whether to always burn in subtitles when transcoding. /// Video stream returned. /// A containing the audio file. [HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] @@ -1328,7 +1347,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, - [FromQuery] bool enableAudioVbrEncoding = true) + [FromQuery] bool enableAudioVbrEncoding = true, + [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) { var streamingRequest = new StreamingRequestDto { @@ -1365,7 +1385,7 @@ public class DynamicHlsController : BaseJellyfinApiController Height = height, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, RequireAvc = requireAvc ?? false, @@ -1382,7 +1402,8 @@ public class DynamicHlsController : BaseJellyfinApiController VideoStreamIndex = videoStreamIndex, Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, - EnableAudioVbrEncoding = enableAudioVbrEncoding + EnableAudioVbrEncoding = enableAudioVbrEncoding, + AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding }; return await GetDynamicSegment(streamingRequest, segmentId) diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 5050cab418..2d9ecd4f08 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -293,6 +293,7 @@ public class MediaInfoHelper mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; mediaSource.TranscodingContainer = streamInfo.Container; mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; + mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true"; } else { @@ -310,6 +311,11 @@ public class MediaInfoHelper { mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; } + + if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding) + { + mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true"; + } } } diff --git a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs index f77186e25c..20f51ddb71 100644 --- a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs @@ -193,6 +193,8 @@ namespace MediaBrowser.Controller.MediaEncoding public bool EnableAudioVbrEncoding { get; set; } + public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; } + public string GetOption(string qualifier, string name) { var value = GetOption(qualifier + "-" + name); diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index d8c4f2f850..f8ba8ddd80 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -941,7 +941,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // DVBSUB uses the fixed canvas size 720x576 if (state.SubtitleStream is not null - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + && ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream && !string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase)) { @@ -1240,7 +1240,7 @@ namespace MediaBrowser.Controller.MediaEncoding // sub2video for external graphical subtitles if (state.SubtitleStream is not null - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + && ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleStream.IsExternal) { @@ -2554,7 +2554,7 @@ namespace MediaBrowser.Controller.MediaEncoding } var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive; - if (state.SubtitleStream is not null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode && !isCopyingTimestamps) + if (state.SubtitleStream is not null && state.SubtitleStream.IsTextSubtitleStream && ShouldEncodeSubtitle(state) && !isCopyingTimestamps) { var seconds = TimeSpan.FromTicks(state.StartTimeTicks ?? 0).TotalSeconds; @@ -2755,7 +2755,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.AudioStream.IsExternal) { bool hasExternalGraphicsSubs = state.SubtitleStream is not null - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + && ShouldEncodeSubtitle(state) && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream; int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1; @@ -3475,7 +3475,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doToneMap = IsSwTonemapAvailable(state, options); var requireDoviReshaping = doToneMap && state.VideoStream.VideoRangeType == VideoRangeType.DOVI; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; @@ -3618,7 +3618,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doCuTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -3824,7 +3824,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -4064,7 +4064,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doOclTonemap = !doVppTonemap && IsHwTonemapAvailable(state, options); var doTonemap = doVppTonemap || doOclTonemap; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -4320,7 +4320,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doTonemap = doVaVppTonemap || doOclTonemap; var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -4636,7 +4636,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doTonemap = doVaVppTonemap || doOclTonemap; var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -4858,7 +4858,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doVkTonemap = IsVulkanHwTonemapAvailable(state, options); var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -5091,7 +5091,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; @@ -5339,7 +5339,7 @@ namespace MediaBrowser.Controller.MediaEncoding var hwScaleFilter = GetHwScaleFilter("scale", "vt", scaleFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -5511,7 +5511,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream != null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -5722,7 +5722,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Empty; } - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; @@ -7156,7 +7156,7 @@ namespace MediaBrowser.Controller.MediaEncoding args += keyFrameArg; - var hasGraphicalSubs = state.SubtitleStream is not null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasGraphicalSubs = state.SubtitleStream is not null && !state.SubtitleStream.IsTextSubtitleStream && ShouldEncodeSubtitle(state); var hasCopyTs = false; @@ -7361,5 +7361,11 @@ namespace MediaBrowser.Controller.MediaEncoding { return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase); } + + private static bool ShouldEncodeSubtitle(EncodingJobInfo state) + { + return state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + || (state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding && !IsCopyCodec(state.OutputVideoCodec)); + } } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index bf122dcc7f..6c45f19468 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -639,7 +639,8 @@ namespace MediaBrowser.Model.Dlna RunTimeTicks = item.RunTimeTicks, Context = options.Context, DeviceProfile = options.Profile, - SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles) + SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles), + AlwaysBurnInSubtitleWhenTranscoding = options.AlwaysBurnInSubtitleWhenTranscoding }; var subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitle, playlistItem.SubtitleStreamIndex.Value) : null; @@ -767,20 +768,7 @@ namespace MediaBrowser.Model.Dlna if (subtitleStream is not null) { var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol); - - if (options.AlwaysBurnInSubtitleWhenTranscoding && (playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) != 0) - { - playlistItem.SubtitleDeliveryMethod = SubtitleDeliveryMethod.Encode; - foreach (SubtitleProfile profile in options.Profile.SubtitleProfiles) - { - profile.Method = SubtitleDeliveryMethod.Encode; - } - } - else - { - playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; - } - + playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method; playlistItem.SubtitleFormat = subtitleProfile.Format; playlistItem.SubtitleCodecs = [subtitleProfile.Format]; } diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 3be6860880..1ae4e1962d 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -270,6 +270,11 @@ public class StreamInfo /// public bool EnableAudioVbrEncoding { get; set; } + /// + /// Gets or sets a value indicating whether always burn in subtitles when transcoding. + /// + public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; } + /// /// Gets a value indicating whether the stream is direct. /// @@ -953,7 +958,7 @@ public class StreamInfo 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.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.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)); From c3e889cd41e534b15e0930e2e76fcf12238e5f16 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 22 Sep 2024 01:11:00 +0800 Subject: [PATCH 020/457] Conditionally add burn in option for remote source --- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 2d9ecd4f08..4adda0b695 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -293,7 +293,10 @@ public class MediaInfoHelper mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; mediaSource.TranscodingContainer = streamInfo.Container; mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true"; + if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding) + { + mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true"; + } } else { From 1346ebc13448e4e4e4096ac31320abf7cf3a6baa Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 22 Sep 2024 03:13:30 +0800 Subject: [PATCH 021/457] Don't add subtitle option to audio endpoint --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 924f010e40..05c9faddf3 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -578,7 +578,6 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The streaming options. /// Enable adaptive bitrate streaming. /// Optional. Whether to enable Audio Encoding. - /// Whether to always burn in subtitles when transcoding. /// Audio stream returned. /// A containing the playlist file. [HttpGet("Audio/{itemId}/master.m3u8")] @@ -636,8 +635,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, [FromQuery] bool enableAdaptiveBitrateStreaming = true, - [FromQuery] bool enableAudioVbrEncoding = true, - [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) + [FromQuery] bool enableAudioVbrEncoding = true) { var streamingRequest = new HlsAudioRequestDto { @@ -690,7 +688,7 @@ public class DynamicHlsController : BaseJellyfinApiController StreamOptions = streamOptions, EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming, EnableAudioVbrEncoding = enableAudioVbrEncoding, - AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding + AlwaysBurnInSubtitleWhenTranscoding = false }; return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); From 4502024468ecd9a795cd482f4fbda30b35ee58de Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 22 Sep 2024 04:41:30 +0800 Subject: [PATCH 022/457] Remove all subtitle options from audio endpoints Co-authored-by: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com> --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 05c9faddf3..48fda471a6 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -921,7 +921,6 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The . /// Optional. The streaming options. /// Optional. Whether to enable Audio Encoding. - /// Whether to always burn in subtitles when transcoding. /// Audio stream returned. /// A containing the audio file. [HttpGet("Audio/{itemId}/main.m3u8")] @@ -977,8 +976,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, - [FromQuery] bool enableAudioVbrEncoding = true, - [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) + [FromQuery] bool enableAudioVbrEncoding = true) { using var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new StreamingRequestDto @@ -1031,7 +1029,7 @@ public class DynamicHlsController : BaseJellyfinApiController Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, EnableAudioVbrEncoding = enableAudioVbrEncoding, - AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding + AlwaysBurnInSubtitleWhenTranscoding = false }; return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource) @@ -1283,7 +1281,6 @@ public class DynamicHlsController : BaseJellyfinApiController /// Optional. The . /// Optional. The streaming options. /// Optional. Whether to enable Audio Encoding. - /// Whether to always burn in subtitles when transcoding. /// Video stream returned. /// A containing the audio file. [HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] @@ -1345,8 +1342,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? videoStreamIndex, [FromQuery] EncodingContext? context, [FromQuery] Dictionary streamOptions, - [FromQuery] bool enableAudioVbrEncoding = true, - [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false) + [FromQuery] bool enableAudioVbrEncoding = true) { var streamingRequest = new StreamingRequestDto { @@ -1401,7 +1397,7 @@ public class DynamicHlsController : BaseJellyfinApiController Context = context ?? EncodingContext.Streaming, StreamOptions = streamOptions, EnableAudioVbrEncoding = enableAudioVbrEncoding, - AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding + AlwaysBurnInSubtitleWhenTranscoding = false }; return await GetDynamicSegment(streamingRequest, segmentId) From 56cf1a581c7f0ffc19fca284a13878d231136aaa Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 22 Sep 2024 10:01:47 +0800 Subject: [PATCH 023/457] Better bitrate and resolution normalization (#12644) --- Jellyfin.Api/Helpers/StreamingHelpers.cs | 8 ++- .../MediaEncoding/EncodingHelper.cs | 8 ++- .../Dlna/ResolutionNormalizer.cs | 71 ++++++++++--------- 3 files changed, 51 insertions(+), 36 deletions(-) diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 3cc6a393bc..3a5db2f3fb 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -219,11 +219,17 @@ public static class StreamingHelpers } else { + var h264EquivalentBitrate = EncodingHelper.ScaleBitrate( + state.OutputVideoBitrate.Value, + state.ActualOutputVideoCodec, + "h264"); var resolution = ResolutionNormalizer.Normalize( state.VideoStream?.BitRate, state.OutputVideoBitrate.Value, + h264EquivalentBitrate, state.VideoRequest.MaxWidth, - state.VideoRequest.MaxHeight); + state.VideoRequest.MaxHeight, + state.TargetFramerate); state.VideoRequest.MaxWidth = resolution.MaxWidth; state.VideoRequest.MaxHeight = resolution.MaxHeight; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index f8ba8ddd80..788bd03351 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2400,7 +2400,7 @@ namespace MediaBrowser.Controller.MediaEncoding return 1; } - private static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec) + public static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec) { var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec); var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec); @@ -2424,6 +2424,12 @@ namespace MediaBrowser.Controller.MediaEncoding { scaleFactor = Math.Max(scaleFactor, 2); } + else if (bitrate >= 30000000) + { + // Don't scale beyond 30Mbps, it is hardly visually noticeable for most codecs with our prefer speed encoding + // and will cause extremely high bitrate to be used for av1->h264 transcoding that will overload clients and encoders + scaleFactor = 1; + } return Convert.ToInt32(scaleFactor * bitrate); } diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs index 5d7daa81aa..1a636b2403 100644 --- a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs +++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs @@ -2,28 +2,33 @@ #pragma warning disable CS1591 using System; +using System.Linq; namespace MediaBrowser.Model.Dlna { public static class ResolutionNormalizer { - private static readonly ResolutionConfiguration[] Configurations = - new[] - { - new ResolutionConfiguration(426, 320000), - new ResolutionConfiguration(640, 400000), - new ResolutionConfiguration(720, 950000), - new ResolutionConfiguration(1280, 2500000), - new ResolutionConfiguration(1920, 4000000), - new ResolutionConfiguration(2560, 20000000), - new ResolutionConfiguration(3840, 35000000) - }; + // Please note: all bitrate here are in the scale of SDR h264 bitrate at 30fps + private static readonly ResolutionConfiguration[] _configurations = + [ + new ResolutionConfiguration(416, 365000), + new ResolutionConfiguration(640, 730000), + new ResolutionConfiguration(768, 1100000), + new ResolutionConfiguration(960, 3000000), + new ResolutionConfiguration(1280, 6000000), + new ResolutionConfiguration(1920, 13500000), + new ResolutionConfiguration(2560, 28000000), + new ResolutionConfiguration(3840, 50000000) + ]; public static ResolutionOptions Normalize( int? inputBitrate, int outputBitrate, + int h264EquivalentOutputBitrate, int? maxWidth, - int? maxHeight) + int? maxHeight, + float? targetFps, + bool isHdr = false) // We are not doing HDR transcoding for now, leave for future use { // If the bitrate isn't changing, then don't downscale the resolution if (inputBitrate.HasValue && outputBitrate >= inputBitrate.Value) @@ -38,16 +43,26 @@ namespace MediaBrowser.Model.Dlna } } - var resolutionConfig = GetResolutionConfiguration(outputBitrate); - if (resolutionConfig is not null) - { - var originvalValue = maxWidth; + var referenceBitrate = h264EquivalentOutputBitrate * (30.0f / (targetFps ?? 30.0f)); - maxWidth = Math.Min(resolutionConfig.MaxWidth, maxWidth ?? resolutionConfig.MaxWidth); - if (!originvalValue.HasValue || originvalValue.Value != maxWidth.Value) - { - maxHeight = null; - } + if (isHdr) + { + referenceBitrate *= 0.8f; + } + + var resolutionConfig = GetResolutionConfiguration(Convert.ToInt32(referenceBitrate)); + + if (resolutionConfig is null) + { + return new ResolutionOptions { MaxWidth = maxWidth, MaxHeight = maxHeight }; + } + + var originWidthValue = maxWidth; + + maxWidth = Math.Min(resolutionConfig.MaxWidth, maxWidth ?? resolutionConfig.MaxWidth); + if (!originWidthValue.HasValue || originWidthValue.Value != maxWidth.Value) + { + maxHeight = null; } return new ResolutionOptions @@ -59,19 +74,7 @@ namespace MediaBrowser.Model.Dlna private static ResolutionConfiguration GetResolutionConfiguration(int outputBitrate) { - ResolutionConfiguration previousOption = null; - - foreach (var config in Configurations) - { - if (outputBitrate <= config.MaxBitrate) - { - return previousOption ?? config; - } - - previousOption = config; - } - - return null; + return _configurations.FirstOrDefault(config => outputBitrate <= config.MaxBitrate); } } } From 62606e46b538138d2d8c5b901344cdecc069c5c6 Mon Sep 17 00:00:00 2001 From: Nyanmisaka <799610810@qq.com> Date: Sun, 22 Sep 2024 09:56:44 +0000 Subject: [PATCH 024/457] Translated using Weblate (Chinese (Simplified Han script)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- Emby.Server.Implementations/Localization/Core/zh-CN.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index cbec0979ac..a406a73b78 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -125,15 +125,15 @@ "TaskKeyframeExtractor": "关键帧提取器", "External": "外部", "HearingImpaired": "听力障碍", - "TaskRefreshTrickplayImages": "生成时间轴缩略图", - "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。", + "TaskRefreshTrickplayImages": "生成特技播放预览图", + "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成特技播放预览图。", "TaskCleanCollectionsAndPlaylists": "清理合集和播放列表", "TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。", "TaskAudioNormalization": "音频标准化", "TaskAudioNormalizationDescription": "扫描文件以寻找音频标准化数据。", "TaskDownloadMissingLyrics": "下载缺失的歌词", "TaskDownloadMissingLyricsDescription": "下载歌曲歌词", - "TaskMoveTrickplayImages": "迁移时间轴缩略图的存储位置", + "TaskMoveTrickplayImages": "迁移特技播放预览图的存储位置", "TaskExtractMediaSegments": "媒体片段扫描", "TaskExtractMediaSegmentsDescription": "从支持 MediaSegment 的插件中提取或获取媒体片段。", "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的特技播放文件。" From b162e9290ba7e4265010079708e158399356635c Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Sun, 22 Sep 2024 22:58:15 +0800 Subject: [PATCH 025/457] Fix the diff between requested and actual resolution in RKMPP (#12680) --- .../MediaEncoding/EncodingHelper.cs | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 788bd03351..5322b2635d 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -5574,9 +5574,8 @@ namespace MediaBrowser.Controller.MediaEncoding var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap; var swapOutputWandH = doRkVppTranspose && swapWAndH; var outFormat = doOclTonemap ? "p010" : "nv12"; - var hwScalePrefix = doRkVppTranspose ? "vpp" : "scale"; - var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); - var doScaling = GetHwScaleFilter(hwScalePrefix, "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var hwScaleFilter = GetHwScaleFilter("vpp", "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var doScaling = GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); if (!hasSubs || doRkVppTranspose @@ -5588,7 +5587,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(doScaling) && !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f)) { - var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/8:h=ih/8:format={outFormat}:afbc=1"; + var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={outFormat}:afbc=1"; mainFilters.Add(hwScaleFilterFirstPass); } @@ -5980,19 +5979,6 @@ namespace MediaBrowser.Controller.MediaEncoding } } - var whichCodec = videoStream.Codec; - if (string.Equals(whichCodec, "avc", StringComparison.OrdinalIgnoreCase)) - { - whichCodec = "h264"; - } - else if (string.Equals(whichCodec, "h265", StringComparison.OrdinalIgnoreCase)) - { - whichCodec = "hevc"; - } - - // Avoid a second attempt if no hardware acceleration is being used - options.HardwareDecodingCodecs = options.HardwareDecodingCodecs.Where(c => !string.Equals(c, whichCodec, StringComparison.OrdinalIgnoreCase)).ToArray(); - // leave blank so ffmpeg will decide return null; } From bcc818f397789be7d487e58726a6dd415546f194 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 22 Sep 2024 16:58:23 +0200 Subject: [PATCH 026/457] Fix DeviceProfile.Id should be nullable (#12679) --- MediaBrowser.Model/Dlna/DeviceProfile.cs | 2 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index f689576222..995b7633a9 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -22,7 +22,7 @@ public class DeviceProfile /// /// Gets or sets the unique internal identifier. /// - public Guid Id { get; set; } + public Guid? Id { get; set; } /// /// Gets or sets the maximum allowed bitrate for all streamed content. diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 6c45f19468..4a666ac47b 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -65,7 +65,7 @@ namespace MediaBrowser.Model.Dlna if (streamInfo is not null) { streamInfo.DeviceId = options.DeviceId; - streamInfo.DeviceProfileId = options.Profile.Id.ToString("N", CultureInfo.InvariantCulture); + streamInfo.DeviceProfileId = options.Profile.Id?.ToString("N", CultureInfo.InvariantCulture); streams.Add(streamInfo); } } @@ -240,7 +240,7 @@ namespace MediaBrowser.Model.Dlna foreach (var stream in streams) { stream.DeviceId = options.DeviceId; - stream.DeviceProfileId = options.Profile.Id.ToString("N", CultureInfo.InvariantCulture); + stream.DeviceProfileId = options.Profile.Id?.ToString("N", CultureInfo.InvariantCulture); } return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0); From 39747ee80b2f3f59274f54eeb60b56139936a942 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 22 Sep 2024 18:25:25 +0200 Subject: [PATCH 027/457] Fix media segment operation name (#12682) --- Jellyfin.Api/Controllers/MediaSegmentsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs index e97704d48d..3dc5167a2e 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs @@ -45,7 +45,7 @@ public class MediaSegmentsController : BaseJellyfinApiController [HttpGet("{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetSegmentsAsync( + public async Task>> GetItemSegments( [FromRoute, Required] Guid itemId, [FromQuery] IEnumerable? includeSegmentTypes = null) { From 4dcae54035dbdcb621793bf71b6161c934d97d59 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 22 Sep 2024 18:25:43 +0200 Subject: [PATCH 028/457] Fix GetTrickplayTileImage operation name (#12681) --- Jellyfin.Api/Controllers/TrickplayController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs index c1ff0f3401..2cf66144ce 100644 --- a/Jellyfin.Api/Controllers/TrickplayController.cs +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -80,7 +80,7 @@ public class TrickplayController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] - public async Task GetTrickplayTileImageAsync( + public async Task GetTrickplayTileImage( [FromRoute, Required] Guid itemId, [FromRoute, Required] int width, [FromRoute, Required] int index, From e21592f473845a587312dc08935f623d4d8f745b Mon Sep 17 00:00:00 2001 From: hoanghuy309 Date: Sun, 22 Sep 2024 18:06:29 +0000 Subject: [PATCH 029/457] Translated using Weblate (Vietnamese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/vi/ --- Emby.Server.Implementations/Localization/Core/vi.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index 32e2f4bab1..f890ea74dc 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -131,5 +131,9 @@ "TaskAudioNormalization": "Chuẩn Hóa Âm Thanh", "TaskAudioNormalizationDescription": "Quét tập tin để tìm dữ liệu chuẩn hóa âm thanh.", "TaskDownloadMissingLyricsDescription": "Tải xuống lời cho bài hát", - "TaskDownloadMissingLyrics": "Tải xuống lời bị thiếu" + "TaskDownloadMissingLyrics": "Tải xuống lời bị thiếu", + "TaskExtractMediaSegmentsDescription": "Trích xuất hoặc lấy các phân đoạn phương tiện từ các plugin hỗ trợ MediaSegment.", + "TaskMoveTrickplayImages": "Di chuyển vị trí hình ảnh Trickplay", + "TaskMoveTrickplayImagesDescription": "Di chuyển các tập tin trickplay hiện có theo cài đặt thư viện.", + "TaskExtractMediaSegments": "Quét Phân Đoạn Phương Tiện" } From c108e5c4854e0b0a83361b05c39827186c545eb4 Mon Sep 17 00:00:00 2001 From: Nyanmisaka <799610810@qq.com> Date: Sun, 22 Sep 2024 19:49:50 +0000 Subject: [PATCH 030/457] Translated using Weblate (Chinese (Simplified Han script)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- Emby.Server.Implementations/Localization/Core/zh-CN.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index a406a73b78..3256569e01 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -125,16 +125,16 @@ "TaskKeyframeExtractor": "关键帧提取器", "External": "外部", "HearingImpaired": "听力障碍", - "TaskRefreshTrickplayImages": "生成特技播放预览图", - "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成特技播放预览图。", + "TaskRefreshTrickplayImages": "生成进度条预览图", + "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成进度条预览图。", "TaskCleanCollectionsAndPlaylists": "清理合集和播放列表", "TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。", "TaskAudioNormalization": "音频标准化", "TaskAudioNormalizationDescription": "扫描文件以寻找音频标准化数据。", "TaskDownloadMissingLyrics": "下载缺失的歌词", "TaskDownloadMissingLyricsDescription": "下载歌曲歌词", - "TaskMoveTrickplayImages": "迁移特技播放预览图的存储位置", + "TaskMoveTrickplayImages": "迁移进度条预览图的存储位置", "TaskExtractMediaSegments": "媒体片段扫描", "TaskExtractMediaSegmentsDescription": "从支持 MediaSegment 的插件中提取或获取媒体片段。", - "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的特技播放文件。" + "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的进度条预览图文件。" } From 5d5afe10e8094f14e5443e1fd3295bdf325f1566 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Mon, 23 Sep 2024 08:50:18 +0800 Subject: [PATCH 031/457] Fix incorrect input range for certain hw JPEG encoders (#12683) --- .../MediaEncoding/EncodingHelper.cs | 81 ++++++++++++++----- 1 file changed, 62 insertions(+), 19 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 5322b2635d..e9e8ed6182 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -71,6 +71,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegAdvancedTonemapMode = new Version(7, 0, 1); private readonly Version _minFFmpegAlteredVaVkInterop = new Version(7, 0, 1); private readonly Version _minFFmpegQsvVppTonemapOption = new Version(7, 0, 1); + private readonly Version _minFFmpegQsvVppOutRangeOption = new Version(7, 0, 1); private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled); @@ -3297,7 +3298,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Empty; } - public string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat) + private string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat, bool forceFullRange) { if (string.IsNullOrEmpty(hwTonemapSuffix)) { @@ -3307,7 +3308,7 @@ namespace MediaBrowser.Controller.MediaEncoding var args = string.Empty; var algorithm = options.TonemappingAlgorithm.ToString().ToLowerInvariant(); var mode = options.TonemappingMode.ToString().ToLowerInvariant(); - var range = options.TonemappingRange; + var range = forceFullRange ? TonemappingRange.pc : options.TonemappingRange; var rangeString = range.ToString().ToLowerInvariant(); if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase)) @@ -3377,7 +3378,7 @@ namespace MediaBrowser.Controller.MediaEncoding rangeString); } - public string GetLibplaceboFilter( + private string GetLibplaceboFilter( EncodingOptions options, string videoFormat, bool doTonemap, @@ -3386,7 +3387,8 @@ namespace MediaBrowser.Controller.MediaEncoding int? requestedWidth, int? requestedHeight, int? requestedMaxWidth, - int? requestedMaxHeight) + int? requestedMaxHeight, + bool forceFullRange) { var (outWidth, outHeight) = GetFixedOutputSize( videoWidth, @@ -3411,7 +3413,7 @@ namespace MediaBrowser.Controller.MediaEncoding var algorithm = options.TonemappingAlgorithm; var algorithmString = "clip"; var mode = options.TonemappingMode; - var range = options.TonemappingRange; + var range = forceFullRange ? TonemappingRange.pc : options.TonemappingRange; if (algorithm == TonemappingAlgorithm.bt2390) { @@ -3616,6 +3618,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isNvencEncoder = vidEncoder.Contains("nvenc", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isNvencEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isCuInCuOut = isNvDecoder && isNvencEncoder; var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.ReferenceFrameRate ?? 60) <= 30; @@ -3694,7 +3697,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw tonemap if (doCuTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "cuda", "yuv420p"); + var tonemapFilter = GetHwTonemapFilter(options, "cuda", "yuv420p", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -3823,6 +3826,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isAmfEncoder = vidEncoder.Contains("amf", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isAmfEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isDxInDxOut = isD3d11vaDecoder && isAmfEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -3902,7 +3906,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4061,6 +4065,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isHwDecoder = isD3d11vaDecoder || isQsvDecoder; var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isQsvEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isQsvInQsvOut = isHwDecoder && isQsvEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -4119,6 +4124,8 @@ namespace MediaBrowser.Controller.MediaEncoding { var isRext = IsVideoStreamHevcRext(state); var twoPassVppTonemap = isRext; + var doVppFullRangeOut = isMjpegEncoder + && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption; var doVppProcamp = false; var procampParams = string.Empty; if (doVppTonemap) @@ -4145,7 +4152,7 @@ namespace MediaBrowser.Controller.MediaEncoding outFormat = twoPassVppTonemap ? "p010" : outFormat; var swapOutputWandH = doVppTranspose && swapWAndH; - var hwScalePrefix = (doVppTranspose || doVppTonemap) ? "vpp" : "scale"; + var hwScalePrefix = (doVppTranspose || doVppTonemap || doVppFullRangeOut) ? "vpp" : "scale"; var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "qsv", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTranspose) @@ -4153,6 +4160,11 @@ namespace MediaBrowser.Controller.MediaEncoding hwScaleFilter += $":transpose={tranposeDir}"; } + if (!string.IsNullOrEmpty(hwScaleFilter) && doVppFullRangeOut) + { + hwScaleFilter += ":out_range=pc"; + } + if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTonemap) { hwScaleFilter += doVppProcamp ? procampParams : (twoPassVppTonemap ? string.Empty : ":tonemap=1"); @@ -4200,7 +4212,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4317,6 +4329,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isHwDecoder = isVaapiDecoder || isQsvDecoder; var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isQsvEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isQsvInQsvOut = isHwDecoder && isQsvEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -4375,6 +4388,8 @@ namespace MediaBrowser.Controller.MediaEncoding { var hwFilterSuffix = isVaapiDecoder ? "vaapi" : "qsv"; var isRext = IsVideoStreamHevcRext(state); + var doVppFullRangeOut = isMjpegEncoder + && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption; // INPUT vaapi/qsv surface(vram) // hw deint @@ -4394,7 +4409,7 @@ namespace MediaBrowser.Controller.MediaEncoding outFormat = (doTonemap && isRext) ? "p010" : outFormat; var swapOutputWandH = isQsvDecoder && doVppTranspose && swapWAndH; - var hwScalePrefix = (isQsvDecoder && doVppTranspose) ? "vpp" : "scale"; + var hwScalePrefix = (isQsvDecoder && (doVppTranspose || doVppFullRangeOut)) ? "vpp" : "scale"; var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, hwFilterSuffix, outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); if (!string.IsNullOrEmpty(hwScaleFilter) && isQsvDecoder && doVppTranspose) @@ -4402,6 +4417,13 @@ namespace MediaBrowser.Controller.MediaEncoding hwScaleFilter += $":transpose={tranposeDir}"; } + if (!string.IsNullOrEmpty(hwScaleFilter) + && ((isVaapiDecoder && isMjpegEncoder) + || (isQsvDecoder && doVppFullRangeOut))) + { + hwScaleFilter += ":out_range=pc"; + } + // allocate extra pool sizes for vaapi vpp scale if (!string.IsNullOrEmpty(hwScaleFilter) && isVaapiDecoder) { @@ -4422,7 +4444,7 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add("format=vaapi"); } - var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); if (isQsvDecoder) @@ -4442,7 +4464,7 @@ namespace MediaBrowser.Controller.MediaEncoding // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4633,6 +4655,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isVaapiEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isVaInVaOut = isVaapiDecoder && isVaapiEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -4708,6 +4731,11 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doTonemap ? (isRext ? "p010" : string.Empty) : "nv12"; var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) + { + hwScaleFilter += ":out_range=pc"; + } + // allocate extra pool sizes for vaapi vpp if (!string.IsNullOrEmpty(hwScaleFilter)) { @@ -4721,7 +4749,7 @@ namespace MediaBrowser.Controller.MediaEncoding // vaapi vpp tonemap if (doVaVppTonemap && isVaapiDecoder) { - var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4734,7 +4762,7 @@ namespace MediaBrowser.Controller.MediaEncoding // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4858,6 +4886,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isVaapiEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); @@ -4953,6 +4982,12 @@ namespace MediaBrowser.Controller.MediaEncoding // hw scale var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", "nv12", false, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) + { + hwScaleFilter += ":out_range=pc"; + } + mainFilters.Add(hwScaleFilter); } } @@ -4973,7 +5008,7 @@ namespace MediaBrowser.Controller.MediaEncoding // vk libplacebo if (doVkTonemap || hasSubs) { - var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, isMjpegEncoder); mainFilters.Add(libplaceboFilter); mainFilters.Add("format=vulkan"); } @@ -5088,6 +5123,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isVaapiEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isVaInVaOut = isVaapiDecoder && isVaapiEncoder; var isi965Driver = _mediaEncoder.IsVaapiDeviceInteli965; var isAmdDriver = _mediaEncoder.IsVaapiDeviceAmd; @@ -5149,6 +5185,11 @@ namespace MediaBrowser.Controller.MediaEncoding outFormat = doOclTonemap ? string.Empty : "nv12"; var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) + { + hwScaleFilter += ":out_range=pc"; + } + // allocate extra pool sizes for vaapi vpp if (!string.IsNullOrEmpty(hwScaleFilter)) { @@ -5177,7 +5218,7 @@ namespace MediaBrowser.Controller.MediaEncoding // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -5303,6 +5344,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase); var isVtDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase); + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var inW = state.VideoStream?.Width; var inH = state.VideoStream?.Height; @@ -5384,7 +5426,7 @@ namespace MediaBrowser.Controller.MediaEncoding // Metal tonemap if (doMetalTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "videotoolbox", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "videotoolbox", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -5507,6 +5549,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isRkmppEncoder = vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase); var isSwDecoder = !isRkmppDecoder; var isSwEncoder = !isRkmppEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isDrmInDrmOut = isRkmppDecoder && isRkmppEncoder; var isEncoderSupportAfbc = isRkmppEncoder && (vidEncoder.Contains("h264", StringComparison.OrdinalIgnoreCase) @@ -5573,7 +5616,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap; var swapOutputWandH = doRkVppTranspose && swapWAndH; - var outFormat = doOclTonemap ? "p010" : "nv12"; + var outFormat = doOclTonemap ? "p010" : (isMjpegEncoder ? "bgra" : "nv12"); // RGA only support full range in rgb fmts var hwScaleFilter = GetHwScaleFilter("vpp", "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); var doScaling = GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); @@ -5616,7 +5659,7 @@ namespace MediaBrowser.Controller.MediaEncoding // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } From 9ec85a0f180dd3ea1582ad60f3fb8d55861f2fdf Mon Sep 17 00:00:00 2001 From: elfalem Date: Sun, 22 Sep 2024 20:50:29 -0400 Subject: [PATCH 032/457] Allow Playlists access for users with allowed tags configured (#12686) --- CONTRIBUTORS.md | 1 + MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5b94e04e15..a9deb1c4a2 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -191,6 +191,7 @@ - [pret0rian8](https://github.com/pret0rian) - [jaina heartles](https://github.com/heartles) - [oxixes](https://github.com/oxixes) + - [elfalem](https://github.com/elfalem) # Emby Contributors diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 414488853f..eb605f6c87 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1608,7 +1608,7 @@ namespace MediaBrowser.Controller.Entities } var parent = GetParents().FirstOrDefault() ?? this; - if (parent is UserRootFolder or AggregateFolder) + if (parent is UserRootFolder or AggregateFolder or UserView) { return true; } From aed00733f88fb9a50fc51816fd05a4438c9d5c42 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Sep 2024 19:44:37 -0600 Subject: [PATCH 033/457] Update dependency xunit to 2.9.1 (#12687) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Cody Robibero --- Directory.Packages.props | 2 +- tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs | 8 ++++---- .../Parsers/EpisodeNfoProviderTests.cs | 2 +- .../Parsers/MovieNfoParserTests.cs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2c16bc9d15..b16b7d78c0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -86,6 +86,6 @@ - + \ No newline at end of file diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index 3005a4416c..6b13986957 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -25,8 +25,8 @@ namespace Jellyfin.Naming.Tests.Video files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Single(result.Where(v => v.ExtraType is null)); - Assert.Single(result.Where(v => v.ExtraType is not null)); + Assert.Single(result, v => v.ExtraType is null); + Assert.Single(result, v => v.ExtraType is not null); } [Fact] @@ -44,8 +44,8 @@ namespace Jellyfin.Naming.Tests.Video files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), _namingOptions).ToList(); - Assert.Single(result.Where(v => v.ExtraType is null)); - Assert.Single(result.Where(v => v.ExtraType is not null)); + Assert.Single(result, v => v.ExtraType is null); + Assert.Single(result, v => v.ExtraType is not null); Assert.Equal(2, result[0].AlternateVersions.Count); } diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs index 3721d1f7ac..12d6e1934d 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs @@ -157,7 +157,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers _parser.Fetch(result, "Test Data/Sonarr-Thumb.nfo", CancellationToken.None); - Assert.Single(result.RemoteImages.Where(x => x.Type == ImageType.Primary)); + Assert.Single(result.RemoteImages, x => x.Type == ImageType.Primary); Assert.Equal("https://artworks.thetvdb.com/banners/episodes/359095/7081317.jpg", result.RemoteImages.First(x => x.Type == ImageType.Primary).Url); } diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs index 5bc4abd06d..075c70da88 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs @@ -220,7 +220,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers _parser.Fetch(result, "Test Data/Fanart.nfo", CancellationToken.None); - Assert.Single(result.RemoteImages.Where(x => x.Type == ImageType.Backdrop)); + Assert.Single(result.RemoteImages, x => x.Type == ImageType.Backdrop); Assert.Equal("https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg", result.RemoteImages.First(x => x.Type == ImageType.Backdrop).Url); } From 8a456bf8957923b934873bab89e3984512a5425b Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com> Date: Mon, 23 Sep 2024 18:01:45 +0300 Subject: [PATCH 034/457] Escape quotes in the subtitle path (#12690) --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 517d135d3b..7ae1fbbb10 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -1104,7 +1104,11 @@ namespace MediaBrowser.MediaEncoding.Encoder // https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping // We need to double escape - return path.Replace('\\', '/').Replace(":", "\\:", StringComparison.Ordinal).Replace("'", @"'\\\''", StringComparison.Ordinal); + return path + .Replace('\\', '/') + .Replace(":", "\\:", StringComparison.Ordinal) + .Replace("'", @"'\\\''", StringComparison.Ordinal) + .Replace("\"", "\\\"", StringComparison.Ordinal); } /// From a0204ada2f97b73f932e1ec40befd6cfa75e8e16 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Mon, 23 Sep 2024 23:02:31 +0800 Subject: [PATCH 035/457] Fix intel Xe kernel driver cannot be used with QSV (#12691) --- .../MediaEncoding/EncodingHelper.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index e9e8ed6182..b05d2ffb7b 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -72,6 +72,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegAlteredVaVkInterop = new Version(7, 0, 1); private readonly Version _minFFmpegQsvVppTonemapOption = new Version(7, 0, 1); private readonly Version _minFFmpegQsvVppOutRangeOption = new Version(7, 0, 1); + private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1); private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled); @@ -872,13 +873,15 @@ namespace MediaBrowser.Controller.MediaEncoding options); } - private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string srcDeviceAlias, string alias) + private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string vendorId, string srcDeviceAlias, string alias) { alias ??= VaapiAlias; + var haveVendorId = !string.IsNullOrEmpty(vendorId) + && _mediaEncoder.EncoderVersion >= _minFFmpegVaapiDeviceVendorId; - // 'renderNodePath' has higher priority than 'kernelDriver' + // Priority: 'renderNodePath' > 'vendorId' > 'kernelDriver' var driverOpts = string.IsNullOrEmpty(renderNodePath) - ? (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver) + ? (haveVendorId ? $",vendor_id={vendorId}" : (string.IsNullOrEmpty(kernelDriver) ? string.Empty : $",kernel_driver={kernelDriver}")) : renderNodePath; // 'driver' behaves similarly to env LIBVA_DRIVER_NAME @@ -913,7 +916,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (OperatingSystem.IsLinux()) { // derive qsv from vaapi device - return GetVaapiDeviceArgs(renderNodePath, "iHD", "i915", null, VaapiAlias) + arg + "@" + VaapiAlias; + return GetVaapiDeviceArgs(renderNodePath, "iHD", "i915", "0x8086", null, VaapiAlias) + arg + "@" + VaapiAlias; } if (OperatingSystem.IsWindows()) @@ -1008,14 +1011,14 @@ namespace MediaBrowser.Controller.MediaEncoding if (_mediaEncoder.IsVaapiDeviceInteliHD) { - args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "iHD", null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "iHD", null, null, null, VaapiAlias)); } else if (_mediaEncoder.IsVaapiDeviceInteli965) { // Only override i965 since it has lower priority than iHD in libva lookup. Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965"); Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965"); - args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "i965", null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "i965", null, null, null, VaapiAlias)); } var filterDevArgs = string.Empty; @@ -1039,7 +1042,7 @@ namespace MediaBrowser.Controller.MediaEncoding && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier) { args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias)); - args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(null, null, null, null, DrmAlias, VaapiAlias)); args.Append(GetVulkanDeviceArgs(0, null, DrmAlias, VulkanAlias)); // libplacebo wants an explicitly set vulkan filter device. @@ -1047,7 +1050,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else { - args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, null, VaapiAlias)); filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias); if (doOclTonemap) From 0539fdc5e3a164b694d12d8d3f8437e2cc5b6457 Mon Sep 17 00:00:00 2001 From: gnattu Date: Mon, 23 Sep 2024 23:09:08 +0800 Subject: [PATCH 036/457] Fix libx264/libx265 auto preset (#12692) --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b05d2ffb7b..557eb5e329 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1444,7 +1444,13 @@ namespace MediaBrowser.Controller.MediaEncoding var encoderPreset = preset ?? defaultPreset; if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || isLibX265) { - param += " -preset " + encoderPreset.ToString().ToLowerInvariant(); + var presetString = encoderPreset switch + { + EncoderPreset.auto => EncoderPreset.veryfast.ToString().ToLowerInvariant(), + _ => encoderPreset.ToString().ToLowerInvariant() + }; + + param += " -preset " + presetString; int encodeCrf = encodingOptions.H264Crf; if (isLibX265) From 3c639c2e80f2a17eea3f5f1a70c1b287bc99aba4 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Mon, 23 Sep 2024 09:09:23 -0600 Subject: [PATCH 037/457] Tweak Trickplay migration for speed (#12643) --- .../Trickplay/TrickplayManager.cs | 8 ++- .../Migrations/Routines/MoveTrickplayFiles.cs | 59 ++++++++++++++----- .../Trickplay/ITrickplayManager.cs | 4 +- .../Trickplay/TrickplayMoveImagesTask.cs | 51 +++++++++------- 4 files changed, 83 insertions(+), 39 deletions(-) diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 861037c1fe..73e31279f4 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -455,16 +455,18 @@ public class TrickplayManager : ITrickplayManager } /// - public async Task> GetTrickplayItemsAsync() + public async Task> GetTrickplayItemsAsync(int limit, int offset) { - List trickplayItems; + IReadOnlyList trickplayItems; var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { trickplayItems = await dbContext.TrickplayInfos .AsNoTracking() - .Select(i => i.ItemId) + .OrderBy(i => i.ItemId) + .Skip(offset) + .Take(limit) .ToListAsync() .ConfigureAwait(false); } diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs index 301541b6ce..c1a9e88949 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs @@ -1,10 +1,15 @@ using System; +using System.Diagnostics; 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; using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.Routines; @@ -16,6 +21,7 @@ public class MoveTrickplayFiles : IMigrationRoutine private readonly ITrickplayManager _trickplayManager; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. @@ -23,11 +29,13 @@ public class MoveTrickplayFiles : IMigrationRoutine /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - public MoveTrickplayFiles(ITrickplayManager trickplayManager, IFileSystem fileSystem, ILibraryManager libraryManager) + /// The logger. + public MoveTrickplayFiles(ITrickplayManager trickplayManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILogger logger) { _trickplayManager = trickplayManager; _fileSystem = fileSystem; _libraryManager = libraryManager; + _logger = logger; } /// @@ -42,26 +50,49 @@ public class MoveTrickplayFiles : IMigrationRoutine /// public void Perform() { - var trickplayItems = _trickplayManager.GetTrickplayItemsAsync().GetAwaiter().GetResult(); - foreach (var itemId in trickplayItems) - { - var resolutions = _trickplayManager.GetTrickplayResolutions(itemId).GetAwaiter().GetResult(); - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - continue; - } + const int Limit = 100; + int itemCount = 0, offset = 0, previousCount; - foreach (var resolution in resolutions) + var sw = Stopwatch.StartNew(); + var trickplayQuery = new InternalItemsQuery + { + MediaTypes = [MediaType.Video], + SourceTypes = [SourceType.Library], + IsVirtualItem = false, + IsFolder = false + }; + + do + { + var trickplayInfos = _trickplayManager.GetTrickplayItemsAsync(Limit, offset).GetAwaiter().GetResult(); + previousCount = trickplayInfos.Count; + offset += Limit; + + trickplayQuery.ItemIds = trickplayInfos.Select(i => i.ItemId).Distinct().ToArray(); + var items = _libraryManager.GetItemList(trickplayQuery); + foreach (var trickplayInfo in trickplayInfos) { - var oldPath = GetOldTrickplayDirectory(item, resolution.Key); - var newPath = _trickplayManager.GetTrickplayDirectory(item, resolution.Value.TileWidth, resolution.Value.TileHeight, resolution.Value.Width, false); + var item = items.OfType