Migrate to trickplay table to EF. Rename vars/methods/members to have consistent use of tile and thumbnail

This commit is contained in:
Nick 2023-06-26 17:40:10 -07:00
parent a2a144869d
commit ab20ceaad6
20 changed files with 1004 additions and 298 deletions

View file

@ -78,7 +78,6 @@ using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Controller.TV; using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.BdInfo;
@ -97,7 +96,6 @@ using MediaBrowser.Providers.Lyric;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.Tmdb; using MediaBrowser.Providers.Plugins.Tmdb;
using MediaBrowser.Providers.Subtitles; using MediaBrowser.Providers.Subtitles;
using MediaBrowser.Providers.Trickplay;
using MediaBrowser.XbmcMetadata.Providers; using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -593,7 +591,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>();
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();

View file

@ -26,7 +26,6 @@ using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
@ -48,7 +47,6 @@ namespace Emby.Server.Implementations.Data
{ {
private const string FromText = " from TypedBaseItems A"; private const string FromText = " from TypedBaseItems A";
private const string ChaptersTableName = "Chapters2"; private const string ChaptersTableName = "Chapters2";
private const string TrickplayTableName = "Trickplay";
private const string SaveItemCommandText = private const string SaveItemCommandText =
@"replace into TypedBaseItems @"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 " + 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, CreateMediaStreamsTableCommand,
CreateMediaAttachmentsTableCommand, 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) private static bool EnableJoinUserData(InternalItemsQuery query)
{ {
if (query.User is null) if (query.User is null)

View file

@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
@ -52,6 +53,7 @@ namespace Emby.Server.Implementations.Dto
private readonly Lazy<ILiveTvManager> _livetvManagerFactory; private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
private readonly ILyricManager _lyricManager; private readonly ILyricManager _lyricManager;
private readonly ITrickplayManager _trickplayManager;
public DtoService( public DtoService(
ILogger<DtoService> logger, ILogger<DtoService> logger,
@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
IApplicationHost appHost, IApplicationHost appHost,
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
Lazy<ILiveTvManager> livetvManagerFactory, Lazy<ILiveTvManager> livetvManagerFactory,
ILyricManager lyricManager) ILyricManager lyricManager,
ITrickplayManager trickplayManager)
{ {
_logger = logger; _logger = logger;
_libraryManager = libraryManager; _libraryManager = libraryManager;
@ -75,6 +78,7 @@ namespace Emby.Server.Implementations.Dto
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory; _livetvManagerFactory = livetvManagerFactory;
_lyricManager = lyricManager; _lyricManager = lyricManager;
_trickplayManager = trickplayManager;
} }
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@ -1060,9 +1064,11 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.Trickplay)) 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. // 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. // 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) if (video.ExtraType.HasValue)

View file

@ -2,6 +2,7 @@ using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Net.Mime; using System.Net.Mime;
using System.Text; using System.Text;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -42,18 +43,18 @@ public class TrickplayController : BaseJellyfinApiController
/// <param name="itemId">The item id.</param> /// <param name="itemId">The item id.</param>
/// <param name="width">The width of a single tile.</param> /// <param name="width">The width of a single tile.</param>
/// <param name="mediaSourceId">The media version id, if using an alternate version.</param> /// <param name="mediaSourceId">The media version id, if using an alternate version.</param>
/// <response code="200">Tiles stream returned.</response> /// <response code="200">Tiles playlist returned.</response>
/// <returns>A <see cref="FileResult"/> containing the trickplay tiles file.</returns> /// <returns>A <see cref="FileResult"/> containing the trickplay playlist file.</returns>
[HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")] [HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesPlaylistFile] [ProducesPlaylistFile]
public ActionResult GetTrickplayHlsPlaylist( public async Task<ActionResult> GetTrickplayHlsPlaylist(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromRoute, Required] int width, [FromRoute, Required] int width,
[FromQuery] Guid? mediaSourceId) [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)) if (string.IsNullOrEmpty(playlist))
{ {
@ -64,20 +65,20 @@ public class TrickplayController : BaseJellyfinApiController
} }
/// <summary> /// <summary>
/// Gets a trickplay tile grid image. /// Gets a trickplay tile image.
/// </summary> /// </summary>
/// <param name="itemId">The item id.</param> /// <param name="itemId">The item id.</param>
/// <param name="width">The width of a single tile.</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> /// <param name="mediaSourceId">The media version id, if using an alternate version.</param>
/// <response code="200">Tiles image returned.</response> /// <response code="200">Tile image returned.</response>
/// <response code="200">Tiles image not found at specified index.</response> /// <response code="200">Tile image not found at specified index.</response>
/// <returns>A <see cref="FileResult"/> containing the trickplay tiles image.</returns> /// <returns>A <see cref="FileResult"/> containing the trickplay tiles image.</returns>
[HttpGet("Videos/{itemId}/Trickplay/{width}/{index}.jpg")] [HttpGet("Videos/{itemId}/Trickplay/{width}/{index}.jpg")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile] [ProducesImageFile]
public ActionResult GetTrickplayGridImage( public ActionResult GetTrickplayTileImage(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromRoute, Required] int width, [FromRoute, Required] int width,
[FromRoute, Required] int index, [FromRoute, Required] int index,

View file

@ -308,8 +308,8 @@ public class DynamicHlsHelper
if (!isLiveStream && (state.VideoRequest?.EnableTrickplay).GetValueOrDefault(false)) if (!isLiveStream && (state.VideoRequest?.EnableTrickplay).GetValueOrDefault(false))
{ {
var sourceId = Guid.Parse(state.Request.MediaSourceId); var sourceId = Guid.Parse(state.Request.MediaSourceId);
var tilesResolutions = _trickplayManager.GetTilesResolutions(sourceId); var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false);
AddTrickplay(state, tilesResolutions, builder, _httpContextAccessor.HttpContext.User); AddTrickplay(state, trickplayResolutions, builder, _httpContextAccessor.HttpContext.User);
} }
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); 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. /// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution.
/// </summary> /// </summary>
/// <param name="state">StreamState of the current stream.</param> /// <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="builder">StringBuilder to append the field to.</param>
/// <param name="user">Http user context.</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}\""; 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 width = resolution.Key;
var tilesInfo = resolution.Value; var trickplayInfo = resolution.Value;
var url = string.Format( var url = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
@ -566,9 +566,9 @@ public class DynamicHlsHelper
var line = string.Format( var line = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
playlistFormat, playlistFormat,
tilesInfo.Bandwidth.ToString(CultureInfo.InvariantCulture), trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
tilesInfo.Width.ToString(CultureInfo.InvariantCulture), trickplayInfo.Width.ToString(CultureInfo.InvariantCulture),
tilesInfo.Height.ToString(CultureInfo.InvariantCulture), trickplayInfo.Height.ToString(CultureInfo.InvariantCulture),
url); url);
builder.AppendLine(line); builder.AppendLine(line);

View file

@ -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; }
}

View file

@ -78,6 +78,11 @@ public class JellyfinDbContext : DbContext
/// </summary> /// </summary>
public DbSet<User> Users => Set<User>(); 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<Artwork> Artwork => Set<Artwork>();
public DbSet<Book> Books => Set<Book>(); public DbSet<Book> Books => Set<Book>();

View file

@ -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
}
}
}

View file

@ -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");
}
}
}

View file

@ -1,4 +1,4 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{ {
@ -442,6 +442,37 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("DeviceOptions"); 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 => modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")

View file

@ -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 });
}
}
}

View file

@ -6,19 +6,20 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Trickplay; using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Trickplay; namespace Jellyfin.Server.Implementations.Trickplay;
/// <summary> /// <summary>
/// ITrickplayManager implementation. /// ITrickplayManager implementation.
@ -26,13 +27,13 @@ namespace MediaBrowser.Providers.Trickplay;
public class TrickplayManager : ITrickplayManager public class TrickplayManager : ITrickplayManager
{ {
private readonly ILogger<TrickplayManager> _logger; private readonly ILogger<TrickplayManager> _logger;
private readonly IItemRepository _itemRepo;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly EncodingHelper _encodingHelper; private readonly EncodingHelper _encodingHelper;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly IImageEncoder _imageEncoder; private readonly IImageEncoder _imageEncoder;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private static readonly SemaphoreSlim _resourcePool = new(1, 1); private static readonly SemaphoreSlim _resourcePool = new(1, 1);
private static readonly string[] _trickplayImgExtensions = { ".jpg" }; private static readonly string[] _trickplayImgExtensions = { ".jpg" };
@ -41,31 +42,31 @@ public class TrickplayManager : ITrickplayManager
/// Initializes a new instance of the <see cref="TrickplayManager"/> class. /// Initializes a new instance of the <see cref="TrickplayManager"/> class.
/// </summary> /// </summary>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
/// <param name="mediaEncoder">The media encoder.</param> /// <param name="mediaEncoder">The media encoder.</param>
/// <param name="fileSystem">The file systen.</param> /// <param name="fileSystem">The file systen.</param>
/// <param name="encodingHelper">The encoding helper.</param> /// <param name="encodingHelper">The encoding helper.</param>
/// <param name="libraryManager">The library manager.</param> /// <param name="libraryManager">The library manager.</param>
/// <param name="config">The server configuration manager.</param> /// <param name="config">The server configuration manager.</param>
/// <param name="imageEncoder">The image encoder.</param> /// <param name="imageEncoder">The image encoder.</param>
/// <param name="dbProvider">The database provider.</param>
public TrickplayManager( public TrickplayManager(
ILogger<TrickplayManager> logger, ILogger<TrickplayManager> logger,
IItemRepository itemRepo,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
IFileSystem fileSystem, IFileSystem fileSystem,
EncodingHelper encodingHelper, EncodingHelper encodingHelper,
ILibraryManager libraryManager, ILibraryManager libraryManager,
IServerConfigurationManager config, IServerConfigurationManager config,
IImageEncoder imageEncoder) IImageEncoder imageEncoder,
IDbContextFactory<JellyfinDbContext> dbProvider)
{ {
_logger = logger; _logger = logger;
_itemRepo = itemRepo;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_encodingHelper = encodingHelper; _encodingHelper = encodingHelper;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_config = config; _config = config;
_imageEncoder = imageEncoder; _imageEncoder = imageEncoder;
_dbProvider = dbProvider;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -105,7 +106,7 @@ public class TrickplayManager : ITrickplayManager
try 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); _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
return; return;
@ -152,14 +153,16 @@ public class TrickplayManager : ITrickplayManager
// Create tiles // Create tiles
var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); 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 // Save tiles info
try 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); _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
} }
else 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) if (images.Count == 0)
{ {
@ -200,48 +203,48 @@ public class TrickplayManager : ITrickplayManager
Directory.CreateDirectory(workDir); Directory.CreateDirectory(workDir);
var tilesInfo = new TrickplayTilesInfo var trickplayInfo = new TrickplayInfo
{ {
Width = width, Width = width,
Interval = options.Interval, Interval = options.Interval,
TileWidth = options.TileWidth, TileWidth = options.TileWidth,
TileHeight = options.TileHeight, TileHeight = options.TileHeight,
TileCount = images.Count, ThumbnailCount = images.Count,
// Set during image generation // Set during image generation
Height = 0, Height = 0,
Bandwidth = 0 Bandwidth = 0
}; };
/* /*
* Generate trickplay tile grids from sets of images * Generate trickplay tiles from sets of thumbnails
*/ */
var imageOptions = new ImageCollageOptions var imageOptions = new ImageCollageOptions
{ {
Width = tilesInfo.TileWidth, Width = trickplayInfo.TileWidth,
Height = tilesInfo.TileHeight Height = trickplayInfo.TileHeight
}; };
var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
var requiredTileGrids = (int)Math.Ceiling((double)images.Count / tilesPerGrid); 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 // Set output/input paths
var tileGridPath = Path.Combine(workDir, $"{i}.jpg"); var tilePath = Path.Combine(workDir, $"{i}.jpg");
imageOptions.OutputPath = tileGridPath; imageOptions.OutputPath = tilePath;
imageOptions.InputPaths = images.Skip(i * tilesPerGrid).Take(tilesPerGrid).ToList(); imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(thumbnailsPerTile).ToList();
// Generate image and use returned height for tiles info // Generate image and use returned height for tiles info
var height = _imageEncoder.CreateTrickplayGrid(imageOptions, options.JpegQuality, tilesInfo.Width, tilesInfo.Height != 0 ? tilesInfo.Height : null); var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
if (tilesInfo.Height == 0) if (trickplayInfo.Height == 0)
{ {
tilesInfo.Height = height; trickplayInfo.Height = height;
} }
// Update bitrate // Update bitrate
var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000)); var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000));
tilesInfo.Bandwidth = Math.Max(tilesInfo.Bandwidth, bitrate); trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
} }
/* /*
@ -249,7 +252,7 @@ public class TrickplayManager : ITrickplayManager
*/ */
Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName); 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)) if (Directory.Exists(outputDir))
{ {
Directory.Delete(outputDir, true); Directory.Delete(outputDir, true);
@ -257,7 +260,7 @@ public class TrickplayManager : ITrickplayManager
MoveDirectory(workDir, outputDir); MoveDirectory(workDir, outputDir);
return tilesInfo; return trickplayInfo;
} }
private bool CanGenerateTrickplay(Video video, int interval) private bool CanGenerateTrickplay(Video video, int interval)
@ -299,21 +302,62 @@ public class TrickplayManager : ITrickplayManager
} }
/// <inheritdoc /> /// <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 /> /// <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 /> /// <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 /> /// <inheritdoc />
@ -323,42 +367,41 @@ public class TrickplayManager : ITrickplayManager
} }
/// <inheritdoc /> /// <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); var trickplayResolutions = await GetTrickplayResolutions(itemId).ConfigureAwait(false);
if (tilesResolutions is not null && tilesResolutions.TryGetValue(width, out var tilesInfo)) if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo))
{ {
var builder = new StringBuilder(128); 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 urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}";
const string decimalFormat = "{0:0.###}"; const string decimalFormat = "{0:0.###}";
var resolution = $"{tilesInfo.Width}x{tilesInfo.Height}"; var resolution = $"{trickplayInfo.Width}x{trickplayInfo.Height}";
var layout = $"{tilesInfo.TileWidth}x{tilesInfo.TileHeight}"; var layout = $"{trickplayInfo.TileWidth}x{trickplayInfo.TileHeight}";
var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
var tileDuration = tilesInfo.Interval / 1000d; var thumbnailDuration = trickplayInfo.Interval / 1000d;
var infDuration = tileDuration * tilesPerGrid; var infDuration = thumbnailDuration * thumbnailsPerTile;
var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid); var tileCount = (int)Math.Ceiling((decimal)trickplayInfo.ThumbnailCount / thumbnailsPerTile);
builder builder
.AppendLine("#EXTM3U") .AppendLine("#EXTM3U")
.Append("#EXT-X-TARGETDURATION:") .Append("#EXT-X-TARGETDURATION:")
.AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture)) .AppendLine(tileCount.ToString(CultureInfo.InvariantCulture))
.AppendLine("#EXT-X-VERSION:7") .AppendLine("#EXT-X-VERSION:7")
.AppendLine("#EXT-X-MEDIA-SEQUENCE:1") .AppendLine("#EXT-X-MEDIA-SEQUENCE:1")
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD") .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
.AppendLine("#EXT-X-IMAGES-ONLY"); .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. // All tiles prior to the last must contain full amount of thumbnails (no black).
// The final grid will be 0 < count <= maxTiles if (i == tileCount - 1)
if (i == tileGridCount - 1)
{ {
tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid); thumbnailsPerTile = trickplayInfo.ThumbnailCount - (i * thumbnailsPerTile);
infDuration = tileDuration * tilesPerGrid; infDuration = thumbnailDuration * thumbnailsPerTile;
} }
// EXTINF // EXTINF
@ -374,7 +417,7 @@ public class TrickplayManager : ITrickplayManager
.Append(",LAYOUT=") .Append(",LAYOUT=")
.Append(layout) .Append(layout)
.Append(",DURATION=") .Append(",DURATION=")
.AppendFormat(CultureInfo.InvariantCulture, decimalFormat, tileDuration) .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, thumbnailDuration)
.AppendLine(); .AppendLine();
// URL // URL

View file

@ -11,6 +11,7 @@ using Jellyfin.Server.Implementations.Activity;
using Jellyfin.Server.Implementations.Devices; using Jellyfin.Server.Implementations.Devices;
using Jellyfin.Server.Implementations.Events; using Jellyfin.Server.Implementations.Events;
using Jellyfin.Server.Implementations.Security; using Jellyfin.Server.Implementations.Security;
using Jellyfin.Server.Implementations.Trickplay;
using Jellyfin.Server.Implementations.Users; using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.BaseItemManager; using MediaBrowser.Controller.BaseItemManager;
@ -21,6 +22,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Activity; using MediaBrowser.Model.Activity;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -77,6 +79,7 @@ namespace Jellyfin.Server
serviceCollection.AddSingleton<IUserManager, UserManager>(); serviceCollection.AddSingleton<IUserManager, UserManager>();
serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>(); serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>();
serviceCollection.AddSingleton<IDeviceManager, DeviceManager>(); serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>();
// TODO search the assemblies instead of adding them manually? // TODO search the assemblies instead of adding them manually?
serviceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>(); serviceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>();

View file

@ -2,7 +2,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
namespace MediaBrowser.Controller.Drawing namespace MediaBrowser.Controller.Drawing
@ -84,13 +83,13 @@ namespace MediaBrowser.Controller.Drawing
void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops); void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops);
/// <summary> /// <summary>
/// Creates a new jpeg trickplay grid image. /// Creates a new trickplay tile image.
/// </summary> /// </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="quality">The image encode quality.</param>
/// <param name="imgWidth">The width of a single trickplay image.</param> /// <param name="imgWidth">The width of a single trickplay thumbnail.</param>
/// <param name="imgHeight">Optional height of a single trickplay image, if it is known.</param> /// <param name="imgHeight">Optional height of a single trickplay thumbnail, if it is known.</param>
/// <returns>Height of single decoded trickplay image.</returns> /// <returns>Height of single decoded trickplay thumbnail.</returns>
int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight); int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight);
} }
} }

View file

@ -61,27 +61,6 @@ namespace MediaBrowser.Controller.Persistence
/// <param name="chapters">The list of chapters to save.</param> /// <param name="chapters">The list of chapters to save.</param>
void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters); 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> /// <summary>
/// Gets the media streams. /// Gets the media streams.
/// </summary> /// </summary>

View file

@ -2,8 +2,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Trickplay; namespace MediaBrowser.Controller.Trickplay;
@ -13,7 +13,7 @@ namespace MediaBrowser.Controller.Trickplay;
public interface ITrickplayManager public interface ITrickplayManager
{ {
/// <summary> /// <summary>
/// Generate or replace trickplay data. /// Generates new trickplay images and metadata.
/// </summary> /// </summary>
/// <param name="video">The video.</param> /// <param name="video">The video.</param>
/// <param name="replace">Whether or not existing data should be replaced.</param> /// <param name="replace">Whether or not existing data should be replaced.</param>
@ -26,28 +26,28 @@ public interface ITrickplayManager
/// </summary> /// </summary>
/// <param name="itemId">The item.</param> /// <param name="itemId">The item.</param>
/// <returns>Map of width resolutions to trickplay tiles info.</returns> /// <returns>Map of width resolutions to trickplay tiles info.</returns>
Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId); Task<Dictionary<int, TrickplayInfo>> GetTrickplayResolutions(Guid itemId);
/// <summary> /// <summary>
/// Saves trickplay tiles info. /// Saves trickplay info.
/// </summary> /// </summary>
/// <param name="itemId">The item.</param> /// <param name="info">The trickplay info.</param>
/// <param name="tilesInfo">The trickplay tiles info.</param> /// <returns>Task.</returns>
void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); Task SaveTrickplayInfo(TrickplayInfo info);
/// <summary> /// <summary>
/// Gets the trickplay manifest. /// Gets all trickplay infos for all media streams of an item.
/// </summary> /// </summary>
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
/// <returns>A map of media source id to a map of tile width to tile info.</returns> /// <returns>A map of media source id to a map of tile width to trickplay info.</returns>
Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item); Task<Dictionary<Guid, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item);
/// <summary> /// <summary>
/// Gets the path to a trickplay tiles image. /// Gets the path to a trickplay tile image.
/// </summary> /// </summary>
/// <param name="item">The item.</param> /// <param name="item">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="index">The tile grid's index.</param> /// <param name="index">The tile's index.</param>
/// <returns>The absolute path.</returns> /// <returns>The absolute path.</returns>
string GetTrickplayTilePath(BaseItem item, int width, int index); string GetTrickplayTilePath(BaseItem item, int width, int index);
@ -55,8 +55,8 @@ public interface ITrickplayManager
/// Gets the trickplay HLS playlist. /// Gets the trickplay HLS playlist.
/// </summary> /// </summary>
/// <param name="itemId">The item.</param> /// <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> /// <param name="apiKey">Optional api key of the requesting user.</param>
/// <returns>The text content of the .m3u8 playlist.</returns> /// <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);
} }

View file

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -572,7 +573,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the trickplay manifest. /// Gets or sets the trickplay manifest.
/// </summary> /// </summary>
/// <value>The trickplay manifest.</value> /// <value>The trickplay manifest.</value>
public Dictionary<string, Dictionary<int, TrickplayTilesInfo>> Trickplay { get; set; } public Dictionary<string, Dictionary<int, TrickplayInfo>> Trickplay { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type of the location. /// Gets or sets the type of the location.

View file

@ -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; }
}

View file

@ -531,7 +531,7 @@ public class SkiaEncoder : IImageEncoder
} }
/// <inheritdoc /> /// <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 paths = options.InputPaths;
var tileWidth = options.Width; var tileWidth = options.Width;

View file

@ -50,7 +50,7 @@ public class NullImageEncoder : IImageEncoder
} }
/// <inheritdoc /> /// <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(); throw new NotImplementedException();
} }