diff options
20 files changed, 1004 insertions, 298 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 1e0bb0cd6..7969577bc 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -78,7 +78,6 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.SyncPlay; -using MediaBrowser.Controller.Trickplay; using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; @@ -97,7 +96,6 @@ using MediaBrowser.Providers.Lyric; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.Tmdb; using MediaBrowser.Providers.Subtitles; -using MediaBrowser.Providers.Trickplay; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -593,7 +591,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); - serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>(); serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 8ec24522b..d1fbea95a 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -26,7 +26,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Extensions; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; @@ -48,7 +47,6 @@ namespace Emby.Server.Implementations.Data { private const string FromText = " from TypedBaseItems A"; private const string ChaptersTableName = "Chapters2"; - private const string TrickplayTableName = "Trickplay"; private const string SaveItemCommandText = @"replace into TypedBaseItems @@ -384,8 +382,6 @@ namespace Emby.Server.Implementations.Data "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", - "create table if not exists " + TrickplayTableName + " (ItemId GUID, Width INT NOT NULL, Height INT NOT NULL, TileWidth INT NOT NULL, TileHeight INT NOT NULL, TileCount INT NOT NULL, Interval INT NOT NULL, Bandwidth INT NOT NULL, PRIMARY KEY (ItemId, Width))", - CreateMediaStreamsTableCommand, CreateMediaAttachmentsTableCommand, @@ -2138,126 +2134,6 @@ namespace Emby.Server.Implementations.Data } } - /// <inheritdoc /> - public Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId) - { - CheckDisposed(); - - var tilesResolutions = new Dictionary<int, TrickplayTilesInfo>(); - using (var connection = GetConnection(true)) - { - using (var statement = PrepareStatement(connection, "select Width,Height,TileWidth,TileHeight,TileCount,Interval,Bandwidth from " + TrickplayTableName + " where ItemId = @ItemId order by Width asc")) - { - statement.TryBind("@ItemId", itemId); - - foreach (var row in statement.ExecuteQuery()) - { - TrickplayTilesInfo tilesInfo = GetTrickplayTilesInfo(row); - tilesResolutions[tilesInfo.Width] = tilesInfo; - } - } - } - - return tilesResolutions; - } - - /// <inheritdoc /> - public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo) - { - CheckDisposed(); - - ArgumentNullException.ThrowIfNull(tilesInfo); - - var idBlob = itemId.ToByteArray(); - using (var connection = GetConnection(false)) - { - connection.RunInTransaction( - db => - { - // Delete old tiles info - db.Execute("delete from " + TrickplayTableName + " where ItemId=@ItemId and Width=@Width", idBlob, tilesInfo.Width); - db.Execute( - "insert into " + TrickplayTableName + " values (@ItemId, @Width, @Height, @TileWidth, @TileHeight, @TileCount, @Interval, @Bandwidth)", - idBlob, - tilesInfo.Width, - tilesInfo.Height, - tilesInfo.TileWidth, - tilesInfo.TileHeight, - tilesInfo.TileCount, - tilesInfo.Interval, - tilesInfo.Bandwidth); - }, - TransactionMode); - } - } - - /// <inheritdoc /> - public Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item) - { - CheckDisposed(); - - var trickplayManifest = new Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>>(); - foreach (var mediaSource in item.GetMediaSources(false)) - { - var mediaSourceId = Guid.Parse(mediaSource.Id); - var tilesResolutions = GetTilesResolutions(mediaSourceId); - - if (tilesResolutions.Count > 0) - { - trickplayManifest[mediaSourceId] = tilesResolutions; - } - } - - return trickplayManifest; - } - - /// <summary> - /// Gets the trickplay tiles info. - /// </summary> - /// <param name="reader">The reader.</param> - /// <returns>TrickplayTilesInfo.</returns> - private TrickplayTilesInfo GetTrickplayTilesInfo(IReadOnlyList<ResultSetValue> reader) - { - var tilesInfo = new TrickplayTilesInfo(); - - if (reader.TryGetInt32(0, out var width)) - { - tilesInfo.Width = width; - } - - if (reader.TryGetInt32(1, out var height)) - { - tilesInfo.Height = height; - } - - if (reader.TryGetInt32(2, out var tileWidth)) - { - tilesInfo.TileWidth = tileWidth; - } - - if (reader.TryGetInt32(3, out var tileHeight)) - { - tilesInfo.TileHeight = tileHeight; - } - - if (reader.TryGetInt32(4, out var tileCount)) - { - tilesInfo.TileCount = tileCount; - } - - if (reader.TryGetInt32(5, out var interval)) - { - tilesInfo.Interval = interval; - } - - if (reader.TryGetInt32(6, out var bandwidth)) - { - tilesInfo.Bandwidth = bandwidth; - } - - return tilesInfo; - } - private static bool EnableJoinUserData(InternalItemsQuery query) { if (query.User is null) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 1687fa442..933b95df0 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; @@ -52,6 +53,7 @@ namespace Emby.Server.Implementations.Dto private readonly Lazy<ILiveTvManager> _livetvManagerFactory; private readonly ILyricManager _lyricManager; + private readonly ITrickplayManager _trickplayManager; public DtoService( ILogger<DtoService> logger, @@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto IApplicationHost appHost, IMediaSourceManager mediaSourceManager, Lazy<ILiveTvManager> livetvManagerFactory, - ILyricManager lyricManager) + ILyricManager lyricManager, + ITrickplayManager trickplayManager) { _logger = logger; _libraryManager = libraryManager; @@ -75,6 +78,7 @@ namespace Emby.Server.Implementations.Dto _mediaSourceManager = mediaSourceManager; _livetvManagerFactory = livetvManagerFactory; _lyricManager = lyricManager; + _trickplayManager = trickplayManager; } private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; @@ -1060,9 +1064,11 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.Trickplay)) { + var manifest = _trickplayManager.GetTrickplayManifest(item).ConfigureAwait(false).GetAwaiter().GetResult(); + // To stay consistent with other fields, this must go from a Guid to a non-dashed string. // This does not seem to occur automatically to dictionaries like it does with other Guid fields. - dto.Trickplay = _itemRepo.GetTrickplayManifest(item).ToDictionary(x => x.Key.ToString("N", CultureInfo.InvariantCulture), y => y.Value); + dto.Trickplay = manifest.ToDictionary(x => x.Key.ToString("N", CultureInfo.InvariantCulture), y => y.Value); } if (video.ExtraType.HasValue) diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs index 36464d726..e4f8f076e 100644 --- a/Jellyfin.Api/Controllers/TrickplayController.cs +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Net.Mime; using System.Text; +using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Extensions; using MediaBrowser.Controller.Library; @@ -42,18 +43,18 @@ public class TrickplayController : BaseJellyfinApiController /// <param name="itemId">The item id.</param> /// <param name="width">The width of a single tile.</param> /// <param name="mediaSourceId">The media version id, if using an alternate version.</param> - /// <response code="200">Tiles stream returned.</response> - /// <returns>A <see cref="FileResult"/> containing the trickplay tiles file.</returns> + /// <response code="200">Tiles playlist returned.</response> + /// <returns>A <see cref="FileResult"/> containing the trickplay playlist file.</returns> [HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesPlaylistFile] - public ActionResult GetTrickplayHlsPlaylist( + public async Task<ActionResult> GetTrickplayHlsPlaylist( [FromRoute, Required] Guid itemId, [FromRoute, Required] int width, [FromQuery] Guid? mediaSourceId) { - string? playlist = _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken()); + string? playlist = await _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken()).ConfigureAwait(false); if (string.IsNullOrEmpty(playlist)) { @@ -64,20 +65,20 @@ public class TrickplayController : BaseJellyfinApiController } /// <summary> - /// Gets a trickplay tile grid image. + /// Gets a trickplay tile image. /// </summary> /// <param name="itemId">The item id.</param> /// <param name="width">The width of a single tile.</param> - /// <param name="index">The index of the desired tile grid.</param> + /// <param name="index">The index of the desired tile.</param> /// <param name="mediaSourceId">The media version id, if using an alternate version.</param> - /// <response code="200">Tiles image returned.</response> - /// <response code="200">Tiles image not found at specified index.</response> + /// <response code="200">Tile image returned.</response> + /// <response code="200">Tile image not found at specified index.</response> /// <returns>A <see cref="FileResult"/> containing the trickplay tiles image.</returns> [HttpGet("Videos/{itemId}/Trickplay/{width}/{index}.jpg")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] - public ActionResult GetTrickplayGridImage( + public ActionResult GetTrickplayTileImage( [FromRoute, Required] Guid itemId, [FromRoute, Required] int width, [FromRoute, Required] int index, diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index b1657aeae..bd3091055 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -308,8 +308,8 @@ public class DynamicHlsHelper if (!isLiveStream && (state.VideoRequest?.EnableTrickplay).GetValueOrDefault(false)) { var sourceId = Guid.Parse(state.Request.MediaSourceId); - var tilesResolutions = _trickplayManager.GetTilesResolutions(sourceId); - AddTrickplay(state, tilesResolutions, builder, _httpContextAccessor.HttpContext.User); + var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false); + AddTrickplay(state, trickplayResolutions, builder, _httpContextAccessor.HttpContext.User); } return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); @@ -544,17 +544,17 @@ public class DynamicHlsHelper /// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution. /// </summary> /// <param name="state">StreamState of the current stream.</param> - /// <param name="tilesResolutions">Dictionary of widths to corresponding tiles info.</param> + /// <param name="trickplayResolutions">Dictionary of widths to corresponding tiles info.</param> /// <param name="builder">StringBuilder to append the field to.</param> /// <param name="user">Http user context.</param> - private void AddTrickplay(StreamState state, Dictionary<int, TrickplayTilesInfo> tilesResolutions, StringBuilder builder, ClaimsPrincipal user) + private void AddTrickplay(StreamState state, Dictionary<int, TrickplayInfo> trickplayResolutions, StringBuilder builder, ClaimsPrincipal user) { const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3}\""; - foreach (var resolution in tilesResolutions) + foreach (var resolution in trickplayResolutions) { var width = resolution.Key; - var tilesInfo = resolution.Value; + var trickplayInfo = resolution.Value; var url = string.Format( CultureInfo.InvariantCulture, @@ -566,9 +566,9 @@ public class DynamicHlsHelper var line = string.Format( CultureInfo.InvariantCulture, playlistFormat, - tilesInfo.Bandwidth.ToString(CultureInfo.InvariantCulture), - tilesInfo.Width.ToString(CultureInfo.InvariantCulture), - tilesInfo.Height.ToString(CultureInfo.InvariantCulture), + trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture), + trickplayInfo.Width.ToString(CultureInfo.InvariantCulture), + trickplayInfo.Height.ToString(CultureInfo.InvariantCulture), url); builder.AppendLine(line); diff --git a/Jellyfin.Data/Entities/TrickplayInfo.cs b/Jellyfin.Data/Entities/TrickplayInfo.cs new file mode 100644 index 000000000..64e7da1b5 --- /dev/null +++ b/Jellyfin.Data/Entities/TrickplayInfo.cs @@ -0,0 +1,75 @@ +using System; +using System.Text.Json.Serialization; + +namespace Jellyfin.Data.Entities; + +/// <summary> +/// An entity representing the metadata for a group of trickplay tiles. +/// </summary> +public class TrickplayInfo +{ + /// <summary> + /// Gets or sets the id of the associated item. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + [JsonIgnore] + public Guid ItemId { get; set; } + + /// <summary> + /// Gets or sets width of an individual thumbnail. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public int Width { get; set; } + + /// <summary> + /// Gets or sets height of an individual thumbnail. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public int Height { get; set; } + + /// <summary> + /// Gets or sets amount of thumbnails per row. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public int TileWidth { get; set; } + + /// <summary> + /// Gets or sets amount of thumbnails per column. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public int TileHeight { get; set; } + + /// <summary> + /// Gets or sets total amount of non-black thumbnails. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public int ThumbnailCount { get; set; } + + /// <summary> + /// Gets or sets interval in milliseconds between each trickplay thumbnail. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public int Interval { get; set; } + + /// <summary> + /// Gets or sets peak bandwith usage in bits per second. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public int Bandwidth { get; set; } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index 0d91707e3..ea99af004 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -78,6 +78,11 @@ public class JellyfinDbContext : DbContext /// </summary> public DbSet<User> Users => Set<User>(); + /// <summary> + /// Gets the <see cref="DbSet{TEntity}"/> containing the trickplay metadata. + /// </summary> + public DbSet<TrickplayInfo> TrickplayInfos => Set<TrickplayInfo>(); + /*public DbSet<Artwork> Artwork => Set<Artwork>(); public DbSet<Book> Books => Set<Book>(); diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs new file mode 100644 index 000000000..28baf1992 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs @@ -0,0 +1,681 @@ +// <auto-generated /> +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20230626233818_AddTrickplayInfos")] + partial class AddTrickplayInfos + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.7"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property<double>("EndHour") + .HasColumnType("REAL"); + + b.Property<double>("StartHour") + .HasColumnType("REAL"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<string>("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<int>("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<string>("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<string>("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<string>("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.Property<string>("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<string>("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<bool>("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property<int?>("IndexBy") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<int>("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property<bool>("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property<bool>("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property<int>("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property<int>("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property<string>("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property<int>("Order") + .HasColumnType("INTEGER"); + + b.Property<int>("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("LastModified") + .HasColumnType("TEXT"); + + b.Property<string>("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<int?>("IndexBy") + .HasColumnType("INTEGER"); + + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<bool>("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property<string>("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<int>("SortOrder") + .HasColumnType("INTEGER"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.Property<int>("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("Kind") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.Property<bool>("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<int>("Kind") + .HasColumnType("INTEGER"); + + b.Property<Guid?>("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<Guid?>("UserId") + .HasColumnType("TEXT"); + + b.Property<string>("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property<string>("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<string>("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property<DateTime>("DateModified") + .HasColumnType("TEXT"); + + b.Property<string>("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property<string>("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property<bool>("IsActive") + .HasColumnType("INTEGER"); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<string>("CustomName") + .HasColumnType("TEXT"); + + b.Property<string>("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<int>("Width") + .HasColumnType("INTEGER"); + + b.Property<int>("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property<int>("Height") + .HasColumnType("INTEGER"); + + b.Property<int>("Interval") + .HasColumnType("INTEGER"); + + b.Property<int>("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property<int>("TileHeight") + .HasColumnType("INTEGER"); + + b.Property<int>("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property<string>("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<string>("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<bool>("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property<bool>("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property<bool>("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property<bool>("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property<long>("InternalId") + .HasColumnType("INTEGER"); + + b.Property<int>("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property<DateTime?>("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property<DateTime?>("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property<int?>("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property<int>("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property<int?>("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property<bool>("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property<string>("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property<string>("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<bool>("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property<bool>("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property<int?>("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property<int>("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property<int>("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property<string>("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs new file mode 100644 index 000000000..76b12de08 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// <inheritdoc /> + public partial class AddTrickplayInfos : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TrickplayInfos", + columns: table => new + { + ItemId = table.Column<Guid>(type: "TEXT", nullable: false), + Width = table.Column<int>(type: "INTEGER", nullable: false), + Height = table.Column<int>(type: "INTEGER", nullable: false), + TileWidth = table.Column<int>(type: "INTEGER", nullable: false), + TileHeight = table.Column<int>(type: "INTEGER", nullable: false), + ThumbnailCount = table.Column<int>(type: "INTEGER", nullable: false), + Interval = table.Column<int>(type: "INTEGER", nullable: false), + Bandwidth = table.Column<int>(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TrickplayInfos", x => new { x.ItemId, x.Width }); + }); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TrickplayInfos"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index d23508096..3c06e1cfc 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -1,4 +1,4 @@ -// <auto-generated /> +// <auto-generated /> 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", "7.0.5"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.7"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -442,6 +442,37 @@ namespace Jellyfin.Server.Implementations.Migrations b.ToTable("DeviceOptions"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property<Guid>("ItemId") + .HasColumnType("TEXT"); + + b.Property<int>("Width") + .HasColumnType("INTEGER"); + + b.Property<int>("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property<int>("Height") + .HasColumnType("INTEGER"); + + b.Property<int>("Interval") + .HasColumnType("INTEGER"); + + b.Property<int>("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property<int>("TileHeight") + .HasColumnType("INTEGER"); + + b.Property<int>("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => { b.Property<Guid>("Id") diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs new file mode 100644 index 000000000..dc1c17e5e --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs @@ -0,0 +1,18 @@ +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration +{ + /// <summary> + /// FluentAPI configuration for the TrickplayInfo entity. + /// </summary> + public class TrickplayInfoConfiguration : IEntityTypeConfiguration<TrickplayInfo> + { + /// <inheritdoc/> + public void Configure(EntityTypeBuilder<TrickplayInfo> builder) + { + builder.HasKey(info => new { info.ItemId, info.Width }); + } + } +} diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 3ea7c00d0..34b27e21a 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -6,19 +6,20 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Trickplay; +namespace Jellyfin.Server.Implementations.Trickplay; /// <summary> /// ITrickplayManager implementation. @@ -26,13 +27,13 @@ namespace MediaBrowser.Providers.Trickplay; public class TrickplayManager : ITrickplayManager { private readonly ILogger<TrickplayManager> _logger; - private readonly IItemRepository _itemRepo; private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; private readonly EncodingHelper _encodingHelper; private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _config; private readonly IImageEncoder _imageEncoder; + private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; private static readonly SemaphoreSlim _resourcePool = new(1, 1); private static readonly string[] _trickplayImgExtensions = { ".jpg" }; @@ -41,31 +42,31 @@ public class TrickplayManager : ITrickplayManager /// Initializes a new instance of the <see cref="TrickplayManager"/> class. /// </summary> /// <param name="logger">The logger.</param> - /// <param name="itemRepo">The item repository.</param> /// <param name="mediaEncoder">The media encoder.</param> /// <param name="fileSystem">The file systen.</param> /// <param name="encodingHelper">The encoding helper.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="config">The server configuration manager.</param> /// <param name="imageEncoder">The image encoder.</param> + /// <param name="dbProvider">The database provider.</param> public TrickplayManager( ILogger<TrickplayManager> logger, - IItemRepository itemRepo, IMediaEncoder mediaEncoder, IFileSystem fileSystem, EncodingHelper encodingHelper, ILibraryManager libraryManager, IServerConfigurationManager config, - IImageEncoder imageEncoder) + IImageEncoder imageEncoder, + IDbContextFactory<JellyfinDbContext> dbProvider) { _logger = logger; - _itemRepo = itemRepo; _mediaEncoder = mediaEncoder; _fileSystem = fileSystem; _encodingHelper = encodingHelper; _libraryManager = libraryManager; _config = config; _imageEncoder = imageEncoder; + _dbProvider = dbProvider; } /// <inheritdoc /> @@ -105,7 +106,7 @@ public class TrickplayManager : ITrickplayManager try { - if (!replace && Directory.Exists(outputDir) && GetTilesResolutions(video.Id).ContainsKey(width)) + if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width)) { _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id); return; @@ -152,14 +153,16 @@ public class TrickplayManager : ITrickplayManager // Create tiles var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); - var tilesInfo = CreateTiles(images, width, options, tilesTempDir, outputDir); + var trickplayInfo = CreateTiles(images, width, options, tilesTempDir, outputDir); // Save tiles info try { - if (tilesInfo is not null) + if (trickplayInfo is not null) { - SaveTilesInfo(video.Id, tilesInfo); + trickplayInfo.ItemId = video.Id; + await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false); + _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath); } else @@ -191,7 +194,7 @@ public class TrickplayManager : ITrickplayManager } } - private TrickplayTilesInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string workDir, string outputDir) + private TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string workDir, string outputDir) { if (images.Count == 0) { @@ -200,48 +203,48 @@ public class TrickplayManager : ITrickplayManager Directory.CreateDirectory(workDir); - var tilesInfo = new TrickplayTilesInfo + var trickplayInfo = new TrickplayInfo { Width = width, Interval = options.Interval, TileWidth = options.TileWidth, TileHeight = options.TileHeight, - TileCount = images.Count, + ThumbnailCount = images.Count, // Set during image generation Height = 0, Bandwidth = 0 }; /* - * Generate trickplay tile grids from sets of images + * Generate trickplay tiles from sets of thumbnails */ var imageOptions = new ImageCollageOptions { - Width = tilesInfo.TileWidth, - Height = tilesInfo.TileHeight + Width = trickplayInfo.TileWidth, + Height = trickplayInfo.TileHeight }; - var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; - var requiredTileGrids = (int)Math.Ceiling((double)images.Count / tilesPerGrid); + var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight; + var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile); - for (int i = 0; i < requiredTileGrids; i++) + for (int i = 0; i < requiredTiles; i++) { // Set output/input paths - var tileGridPath = Path.Combine(workDir, $"{i}.jpg"); + var tilePath = Path.Combine(workDir, $"{i}.jpg"); - imageOptions.OutputPath = tileGridPath; - imageOptions.InputPaths = images.Skip(i * tilesPerGrid).Take(tilesPerGrid).ToList(); + imageOptions.OutputPath = tilePath; + imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(thumbnailsPerTile).ToList(); // Generate image and use returned height for tiles info - var height = _imageEncoder.CreateTrickplayGrid(imageOptions, options.JpegQuality, tilesInfo.Width, tilesInfo.Height != 0 ? tilesInfo.Height : null); - if (tilesInfo.Height == 0) + var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null); + if (trickplayInfo.Height == 0) { - tilesInfo.Height = height; + trickplayInfo.Height = height; } // Update bitrate - var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000)); - tilesInfo.Bandwidth = Math.Max(tilesInfo.Bandwidth, bitrate); + var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000)); + trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate); } /* @@ -249,7 +252,7 @@ public class TrickplayManager : ITrickplayManager */ Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName); - // Replace existing tile grids if they already exist + // Replace existing tiles if they already exist if (Directory.Exists(outputDir)) { Directory.Delete(outputDir, true); @@ -257,7 +260,7 @@ public class TrickplayManager : ITrickplayManager MoveDirectory(workDir, outputDir); - return tilesInfo; + return trickplayInfo; } private bool CanGenerateTrickplay(Video video, int interval) @@ -299,21 +302,62 @@ public class TrickplayManager : ITrickplayManager } /// <inheritdoc /> - public Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId) + public async Task<Dictionary<int, TrickplayInfo>> GetTrickplayResolutions(Guid itemId) { - return _itemRepo.GetTilesResolutions(itemId); + var trickplayResolutions = new Dictionary<int, TrickplayInfo>(); + + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var trickplayInfos = await dbContext.TrickplayInfos + .AsNoTracking() + .Where(i => i.ItemId.Equals(itemId)) + .ToListAsync() + .ConfigureAwait(false); + + foreach (var info in trickplayInfos) + { + trickplayResolutions[info.Width] = info; + } + } + + return trickplayResolutions; } /// <inheritdoc /> - public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo) + public async Task SaveTrickplayInfo(TrickplayInfo info) { - _itemRepo.SaveTilesInfo(itemId, tilesInfo); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var oldInfo = await dbContext.TrickplayInfos.FindAsync(info.ItemId, info.Width).ConfigureAwait(false); + if (oldInfo is not null) + { + dbContext.TrickplayInfos.Remove(oldInfo); + } + + dbContext.Add(info); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } } /// <inheritdoc /> - public Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item) + public async Task<Dictionary<Guid, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item) { - return _itemRepo.GetTrickplayManifest(item); + var trickplayManifest = new Dictionary<Guid, Dictionary<int, TrickplayInfo>>(); + foreach (var mediaSource in item.GetMediaSources(false)) + { + var mediaSourceId = Guid.Parse(mediaSource.Id); + var trickplayResolutions = await GetTrickplayResolutions(mediaSourceId).ConfigureAwait(false); + + if (trickplayResolutions.Count > 0) + { + trickplayManifest[mediaSourceId] = trickplayResolutions; + } + } + + return trickplayManifest; } /// <inheritdoc /> @@ -323,42 +367,41 @@ public class TrickplayManager : ITrickplayManager } /// <inheritdoc /> - public string? GetHlsPlaylist(Guid itemId, int width, string? apiKey) + public async Task<string?> GetHlsPlaylist(Guid itemId, int width, string? apiKey) { - var tilesResolutions = GetTilesResolutions(itemId); - if (tilesResolutions is not null && tilesResolutions.TryGetValue(width, out var tilesInfo)) + var trickplayResolutions = await GetTrickplayResolutions(itemId).ConfigureAwait(false); + if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo)) { var builder = new StringBuilder(128); - if (tilesInfo.TileCount > 0) + if (trickplayInfo.ThumbnailCount > 0) { const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}"; const string decimalFormat = "{0:0.###}"; - var resolution = $"{tilesInfo.Width}x{tilesInfo.Height}"; - var layout = $"{tilesInfo.TileWidth}x{tilesInfo.TileHeight}"; - var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; - var tileDuration = tilesInfo.Interval / 1000d; - var infDuration = tileDuration * tilesPerGrid; - var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid); + var resolution = $"{trickplayInfo.Width}x{trickplayInfo.Height}"; + var layout = $"{trickplayInfo.TileWidth}x{trickplayInfo.TileHeight}"; + var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight; + var thumbnailDuration = trickplayInfo.Interval / 1000d; + var infDuration = thumbnailDuration * thumbnailsPerTile; + var tileCount = (int)Math.Ceiling((decimal)trickplayInfo.ThumbnailCount / thumbnailsPerTile); builder .AppendLine("#EXTM3U") .Append("#EXT-X-TARGETDURATION:") - .AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture)) + .AppendLine(tileCount.ToString(CultureInfo.InvariantCulture)) .AppendLine("#EXT-X-VERSION:7") .AppendLine("#EXT-X-MEDIA-SEQUENCE:1") .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD") .AppendLine("#EXT-X-IMAGES-ONLY"); - for (int i = 0; i < tileGridCount; i++) + for (int i = 0; i < tileCount; i++) { - // All tile grids before the last one must contain full amount of tiles. - // The final grid will be 0 < count <= maxTiles - if (i == tileGridCount - 1) + // All tiles prior to the last must contain full amount of thumbnails (no black). + if (i == tileCount - 1) { - tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid); - infDuration = tileDuration * tilesPerGrid; + thumbnailsPerTile = trickplayInfo.ThumbnailCount - (i * thumbnailsPerTile); + infDuration = thumbnailDuration * thumbnailsPerTile; } // EXTINF @@ -374,7 +417,7 @@ public class TrickplayManager : ITrickplayManager .Append(",LAYOUT=") .Append(layout) .Append(",DURATION=") - .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, tileDuration) + .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, thumbnailDuration) .AppendLine(); // URL diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 939376dd8..18d924aa8 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -11,6 +11,7 @@ using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Devices; using Jellyfin.Server.Implementations.Events; using Jellyfin.Server.Implementations.Security; +using Jellyfin.Server.Implementations.Trickplay; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Controller; using MediaBrowser.Controller.BaseItemManager; @@ -21,6 +22,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Activity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -77,6 +79,7 @@ namespace Jellyfin.Server serviceCollection.AddSingleton<IUserManager, UserManager>(); serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>(); serviceCollection.AddSingleton<IDeviceManager, DeviceManager>(); + serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>(); // TODO search the assemblies instead of adding them manually? serviceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>(); diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 42c680761..c7bfbdb53 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Drawing; namespace MediaBrowser.Controller.Drawing @@ -84,13 +83,13 @@ namespace MediaBrowser.Controller.Drawing void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops); /// <summary> - /// Creates a new jpeg trickplay grid image. + /// Creates a new trickplay tile image. /// </summary> - /// <param name="options">The options to use when creating the image. Width and Height are a quantity of tiles in this case, not pixels.</param> + /// <param name="options">The options to use when creating the image. Width and Height are a quantity of thumbnails in this case, not pixels.</param> /// <param name="quality">The image encode quality.</param> - /// <param name="imgWidth">The width of a single trickplay image.</param> - /// <param name="imgHeight">Optional height of a single trickplay image, if it is known.</param> - /// <returns>Height of single decoded trickplay image.</returns> - int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight); + /// <param name="imgWidth">The width of a single trickplay thumbnail.</param> + /// <param name="imgHeight">Optional height of a single trickplay thumbnail, if it is known.</param> + /// <returns>Height of single decoded trickplay thumbnail.</returns> + int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight); } } diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 11eb4932c..2c52b2b45 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -62,27 +62,6 @@ namespace MediaBrowser.Controller.Persistence void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters); /// <summary> - /// Get available trickplay resolutions and corresponding info. - /// </summary> - /// <param name="itemId">The item.</param> - /// <returns>Map of width resolutions to trickplay tiles info.</returns> - Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId); - - /// <summary> - /// Saves trickplay tiles info. - /// </summary> - /// <param name="itemId">The item.</param> - /// <param name="tilesInfo">The trickplay tiles info.</param> - void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); - - /// <summary> - /// Gets trickplay data for an item. - /// </summary> - /// <param name="item">The item.</param> - /// <returns>A map of media source id to a map of tile width to tile info.</returns> - Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item); - - /// <summary> /// Gets the media streams. /// </summary> /// <param name="query">The query.</param> diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs index 8d36fc3ff..0a1e780b2 100644 --- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Trickplay; @@ -13,7 +13,7 @@ namespace MediaBrowser.Controller.Trickplay; public interface ITrickplayManager { /// <summary> - /// Generate or replace trickplay data. + /// Generates new trickplay images and metadata. /// </summary> /// <param name="video">The video.</param> /// <param name="replace">Whether or not existing data should be replaced.</param> @@ -26,28 +26,28 @@ public interface ITrickplayManager /// </summary> /// <param name="itemId">The item.</param> /// <returns>Map of width resolutions to trickplay tiles info.</returns> - Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId); + Task<Dictionary<int, TrickplayInfo>> GetTrickplayResolutions(Guid itemId); /// <summary> - /// Saves trickplay tiles info. + /// Saves trickplay info. /// </summary> - /// <param name="itemId">The item.</param> - /// <param name="tilesInfo">The trickplay tiles info.</param> - void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); + /// <param name="info">The trickplay info.</param> + /// <returns>Task.</returns> + Task SaveTrickplayInfo(TrickplayInfo info); /// <summary> - /// Gets the trickplay manifest. + /// Gets all trickplay infos for all media streams of an item. /// </summary> /// <param name="item">The item.</param> - /// <returns>A map of media source id to a map of tile width to tile info.</returns> - Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item); + /// <returns>A map of media source id to a map of tile width to trickplay info.</returns> + Task<Dictionary<Guid, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item); /// <summary> - /// Gets the path to a trickplay tiles image. + /// Gets the path to a trickplay tile image. /// </summary> /// <param name="item">The item.</param> - /// <param name="width">The width of a single tile.</param> - /// <param name="index">The tile grid's index.</param> + /// <param name="width">The width of a single thumbnail.</param> + /// <param name="index">The tile's index.</param> /// <returns>The absolute path.</returns> string GetTrickplayTilePath(BaseItem item, int width, int index); @@ -55,8 +55,8 @@ public interface ITrickplayManager /// Gets the trickplay HLS playlist. /// </summary> /// <param name="itemId">The item.</param> - /// <param name="width">The width of a single tile.</param> + /// <param name="width">The width of a single thumbnail.</param> /// <param name="apiKey">Optional api key of the requesting user.</param> /// <returns>The text content of the .m3u8 playlist.</returns> - string? GetHlsPlaylist(Guid itemId, int width, string? apiKey); + Task<string?> GetHlsPlaylist(Guid itemId, int width, string? apiKey); } diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 3db9cb08b..287966dd0 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; @@ -572,7 +573,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the trickplay manifest. /// </summary> /// <value>The trickplay manifest.</value> - public Dictionary<string, Dictionary<int, TrickplayTilesInfo>> Trickplay { get; set; } + public Dictionary<string, Dictionary<int, TrickplayInfo>> Trickplay { get; set; } /// <summary> /// Gets or sets the type of the location. diff --git a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs deleted file mode 100644 index 86d37787f..000000000 --- a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace MediaBrowser.Model.Entities; - -/// <summary> -/// Class TrickplayTilesInfo. -/// </summary> -public class TrickplayTilesInfo -{ - /// <summary> - /// Gets or sets width of an individual tile. - /// </summary> - /// <value>The width.</value> - public int Width { get; set; } - - /// <summary> - /// Gets or sets height of an individual tile. - /// </summary> - /// <value>The height.</value> - public int Height { get; set; } - - /// <summary> - /// Gets or sets amount of tiles per row. - /// </summary> - /// <value>The tile grid's width.</value> - public int TileWidth { get; set; } - - /// <summary> - /// Gets or sets amount of tiles per column. - /// </summary> - /// <value>The tile grid's height.</value> - public int TileHeight { get; set; } - - /// <summary> - /// Gets or sets total amount of non-black tiles. - /// </summary> - /// <value>The tile count.</value> - public int TileCount { get; set; } - - /// <summary> - /// Gets or sets interval in milliseconds between each trickplay tile. - /// </summary> - /// <value>The interval.</value> - public int Interval { get; set; } - - /// <summary> - /// Gets or sets peak bandwith usage in bits per second. - /// </summary> - /// <value>The bandwidth.</value> - public int Bandwidth { get; set; } -} diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 2facf0f37..b387c437b 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -531,7 +531,7 @@ public class SkiaEncoder : IImageEncoder } /// <inheritdoc /> - public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) + public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) { var paths = options.InputPaths; var tileWidth = options.Width; diff --git a/src/Jellyfin.Drawing/NullImageEncoder.cs b/src/Jellyfin.Drawing/NullImageEncoder.cs index 15345e1bc..1495661c1 100644 --- a/src/Jellyfin.Drawing/NullImageEncoder.cs +++ b/src/Jellyfin.Drawing/NullImageEncoder.cs @@ -50,7 +50,7 @@ public class NullImageEncoder : IImageEncoder } /// <inheritdoc /> - public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) + public int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) { throw new NotImplementedException(); } |
