From 0e2c362078c5b0babaa0fd254106452e6d67ebe8 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Tue, 30 May 2023 14:23:02 -0700 Subject: Move SkiaSharp related code to Jellyfin.Drawing and IImageEncoder --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 79 ++++++++++++++++++++++++++++++++ src/Jellyfin.Drawing/NullImageEncoder.cs | 6 +++ 2 files changed, 85 insertions(+) (limited to 'src') diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 2d980db18..2facf0f37 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -2,14 +2,18 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; +using System.Security.Cryptography.Xml; using BlurHashSharp.SkiaSharp; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Drawing; using Microsoft.Extensions.Logging; using SkiaSharp; +using static System.Net.Mime.MediaTypeNames; using SKSvg = SkiaSharp.Extended.Svg.SKSvg; namespace Jellyfin.Drawing.Skia; @@ -526,6 +530,81 @@ public class SkiaEncoder : IImageEncoder splashBuilder.GenerateSplash(posters, backdrops, outputPath); } + /// + public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) + { + var paths = options.InputPaths; + var tileWidth = options.Width; + var tileHeight = options.Height; + + if (paths.Count < 1) + { + throw new ArgumentException("InputPaths cannot be empty."); + } + else if (paths.Count > tileWidth * tileHeight) + { + throw new ArgumentException($"InputPaths contains more images than would fit on {tileWidth}x{tileHeight} grid."); + } + + // If no height provided, use height of first image. + if (!imgHeight.HasValue) + { + using var firstImg = Decode(paths[0], false, null, out _); + + if (firstImg is null) + { + throw new InvalidDataException("Could not decode image data."); + } + + if (firstImg.Width != imgWidth) + { + throw new InvalidOperationException("Image width does not match provided width."); + } + + imgHeight = firstImg.Height; + } + + // Make horizontal strips using every provided image. + using var tileGrid = new SKBitmap(imgWidth * tileWidth, imgHeight.Value * tileHeight); + using var canvas = new SKCanvas(tileGrid); + + var imgIndex = 0; + for (var y = 0; y < tileHeight; y++) + { + for (var x = 0; x < tileWidth; x++) + { + if (imgIndex >= paths.Count) + { + break; + } + + using var img = Decode(paths[imgIndex++], false, null, out _); + + if (img is null) + { + throw new InvalidDataException("Could not decode image data."); + } + + if (img.Width != imgWidth) + { + throw new InvalidOperationException("Image width does not match provided width."); + } + + if (img.Height != imgHeight) + { + throw new InvalidOperationException("Image height does not match first image height."); + } + + canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value); + } + } + + using var outputStream = new SKFileWStream(options.OutputPath); + tileGrid.Encode(outputStream, SKEncodedImageFormat.Jpeg, quality); + + return imgHeight.Value; + } + private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options) { try diff --git a/src/Jellyfin.Drawing/NullImageEncoder.cs b/src/Jellyfin.Drawing/NullImageEncoder.cs index 171128bed..15345e1bc 100644 --- a/src/Jellyfin.Drawing/NullImageEncoder.cs +++ b/src/Jellyfin.Drawing/NullImageEncoder.cs @@ -49,6 +49,12 @@ public class NullImageEncoder : IImageEncoder throw new NotImplementedException(); } + /// + public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight) + { + throw new NotImplementedException(); + } + /// public string GetImageBlurHash(int xComp, int yComp, string path) { -- cgit v1.2.3 From ab20ceaad65b2e72fe6e823aa6086e2c6ac36844 Mon Sep 17 00:00:00 2001 From: Nick <20588554+nicknsy@users.noreply.github.com> Date: Mon, 26 Jun 2023 17:40:10 -0700 Subject: Migrate to trickplay table to EF. Rename vars/methods/members to have consistent use of tile and thumbnail --- Emby.Server.Implementations/ApplicationHost.cs | 3 - .../Data/SqliteItemRepository.cs | 124 ---- Emby.Server.Implementations/Dto/DtoService.cs | 10 +- Jellyfin.Api/Controllers/TrickplayController.cs | 19 +- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 18 +- Jellyfin.Data/Entities/TrickplayInfo.cs | 75 +++ .../JellyfinDbContext.cs | 5 + .../20230626233818_AddTrickplayInfos.Designer.cs | 681 +++++++++++++++++++++ .../Migrations/20230626233818_AddTrickplayInfos.cs | 40 ++ .../Migrations/JellyfinDbModelSnapshot.cs | 35 +- .../TrickplayInfoConfiguration.cs | 18 + .../Trickplay/TrickplayManager.cs | 468 ++++++++++++++ Jellyfin.Server/CoreAppHost.cs | 3 + MediaBrowser.Controller/Drawing/IImageEncoder.cs | 13 +- .../Persistence/IItemRepository.cs | 21 - .../Trickplay/ITrickplayManager.cs | 30 +- MediaBrowser.Model/Dto/BaseItemDto.cs | 3 +- MediaBrowser.Model/Entities/TrickplayTilesInfo.cs | 49 -- .../Trickplay/TrickplayManager.cs | 425 ------------- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 2 +- src/Jellyfin.Drawing/NullImageEncoder.cs | 2 +- 21 files changed, 1375 insertions(+), 669 deletions(-) create mode 100644 Jellyfin.Data/Entities/TrickplayInfo.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs create mode 100644 Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs delete mode 100644 MediaBrowser.Model/Entities/TrickplayTilesInfo.cs delete mode 100644 MediaBrowser.Providers/Trickplay/TrickplayManager.cs (limited to 'src') 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(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); 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 } } - /// - public Dictionary GetTilesResolutions(Guid itemId) - { - CheckDisposed(); - - var tilesResolutions = new Dictionary(); - 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; - } - - /// - 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); - } - } - - /// - public Dictionary> GetTrickplayManifest(BaseItem item) - { - CheckDisposed(); - - var trickplayManifest = new Dictionary>(); - 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; - } - - /// - /// Gets the trickplay tiles info. - /// - /// The reader. - /// TrickplayTilesInfo. - private TrickplayTilesInfo GetTrickplayTilesInfo(IReadOnlyList 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 _livetvManagerFactory; private readonly ILyricManager _lyricManager; + private readonly ITrickplayManager _trickplayManager; public DtoService( ILogger logger, @@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto IApplicationHost appHost, IMediaSourceManager mediaSourceManager, Lazy 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 /// The item id. /// The width of a single tile. /// The media version id, if using an alternate version. - /// Tiles stream returned. - /// A containing the trickplay tiles file. + /// Tiles playlist returned. + /// A containing the trickplay playlist file. [HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesPlaylistFile] - public ActionResult GetTrickplayHlsPlaylist( + public async Task 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 } /// - /// Gets a trickplay tile grid image. + /// Gets a trickplay tile image. /// /// The item id. /// The width of a single tile. - /// The index of the desired tile grid. + /// The index of the desired tile. /// The media version id, if using an alternate version. - /// Tiles image returned. - /// Tiles image not found at specified index. + /// Tile image returned. + /// Tile image not found at specified index. /// A containing the trickplay tiles image. [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. /// /// StreamState of the current stream. - /// Dictionary of widths to corresponding tiles info. + /// Dictionary of widths to corresponding tiles info. /// StringBuilder to append the field to. /// Http user context. - private void AddTrickplay(StreamState state, Dictionary tilesResolutions, StringBuilder builder, ClaimsPrincipal user) + private void AddTrickplay(StreamState state, Dictionary 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; + +/// +/// An entity representing the metadata for a group of trickplay tiles. +/// +public class TrickplayInfo +{ + /// + /// Gets or sets the id of the associated item. + /// + /// + /// Required. + /// + [JsonIgnore] + public Guid ItemId { get; set; } + + /// + /// Gets or sets width of an individual thumbnail. + /// + /// + /// Required. + /// + public int Width { get; set; } + + /// + /// Gets or sets height of an individual thumbnail. + /// + /// + /// Required. + /// + public int Height { get; set; } + + /// + /// Gets or sets amount of thumbnails per row. + /// + /// + /// Required. + /// + public int TileWidth { get; set; } + + /// + /// Gets or sets amount of thumbnails per column. + /// + /// + /// Required. + /// + public int TileHeight { get; set; } + + /// + /// Gets or sets total amount of non-black thumbnails. + /// + /// + /// Required. + /// + public int ThumbnailCount { get; set; } + + /// + /// Gets or sets interval in milliseconds between each trickplay thumbnail. + /// + /// + /// Required. + /// + public int Interval { get; set; } + + /// + /// Gets or sets peak bandwith usage in bits per second. + /// + /// + /// Required. + /// + 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 /// public DbSet Users => Set(); + /// + /// Gets the containing the trickplay metadata. + /// + public DbSet TrickplayInfos => Set(); + /*public DbSet Artwork => Set(); public DbSet Books => Set(); 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 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.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 +{ + /// + public partial class AddTrickplayInfos : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TrickplayInfos", + columns: table => new + { + ItemId = table.Column(type: "TEXT", nullable: false), + Width = table.Column(type: "INTEGER", nullable: false), + Height = table.Column(type: "INTEGER", nullable: false), + TileWidth = table.Column(type: "INTEGER", nullable: false), + TileHeight = table.Column(type: "INTEGER", nullable: false), + ThumbnailCount = table.Column(type: "INTEGER", nullable: false), + Interval = table.Column(type: "INTEGER", nullable: false), + Bandwidth = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TrickplayInfos", x => new { x.ItemId, x.Width }); + }); + } + + /// + 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 @@ -// +// 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("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => { b.Property("Id") 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 +{ + /// + /// FluentAPI configuration for the TrickplayInfo entity. + /// + public class TrickplayInfoConfiguration : IEntityTypeConfiguration + { + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(info => new { info.ItemId, info.Width }); + } + } +} diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs new file mode 100644 index 000000000..34b27e21a --- /dev/null +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -0,0 +1,468 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +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.Trickplay; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Implementations.Trickplay; + +/// +/// ITrickplayManager implementation. +/// +public class TrickplayManager : ITrickplayManager +{ + private readonly ILogger _logger; + 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 _dbProvider; + + private static readonly SemaphoreSlim _resourcePool = new(1, 1); + private static readonly string[] _trickplayImgExtensions = { ".jpg" }; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The media encoder. + /// The file systen. + /// The encoding helper. + /// The library manager. + /// The server configuration manager. + /// The image encoder. + /// The database provider. + public TrickplayManager( + ILogger logger, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + EncodingHelper encodingHelper, + ILibraryManager libraryManager, + IServerConfigurationManager config, + IImageEncoder imageEncoder, + IDbContextFactory dbProvider) + { + _logger = logger; + _mediaEncoder = mediaEncoder; + _fileSystem = fileSystem; + _encodingHelper = encodingHelper; + _libraryManager = libraryManager; + _config = config; + _imageEncoder = imageEncoder; + _dbProvider = dbProvider; + } + + /// + public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken) + { + _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); + + var options = _config.Configuration.TrickplayOptions; + foreach (var width in options.WidthResolutions) + { + cancellationToken.ThrowIfCancellationRequested(); + await RefreshTrickplayDataInternal( + video, + replace, + width, + options, + cancellationToken).ConfigureAwait(false); + } + } + + private async Task RefreshTrickplayDataInternal( + Video video, + bool replace, + int width, + TrickplayOptions options, + CancellationToken cancellationToken) + { + if (!CanGenerateTrickplay(video, options.Interval)) + { + return; + } + + var imgTempDir = string.Empty; + var outputDir = GetTrickplayDirectory(video, width); + + await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + 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; + } + + // Extract images + // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay. + var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id)); + + if (mediaSource is null) + { + _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id); + return; + } + + var mediaPath = mediaSource.Path; + var mediaStream = mediaSource.VideoStream; + var container = mediaSource.Container; + + _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id); + imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated( + mediaPath, + container, + mediaSource, + mediaStream, + width, + TimeSpan.FromMilliseconds(options.Interval), + options.EnableHwAcceleration, + options.ProcessThreads, + options.Qscale, + options.ProcessPriority, + _encodingHelper, + cancellationToken).ConfigureAwait(false); + + if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir)) + { + throw new InvalidOperationException("Null or invalid directory from media encoder."); + } + + var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false) + .Select(i => i.FullName) + .OrderBy(i => i) + .ToList(); + + // Create tiles + var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); + var trickplayInfo = CreateTiles(images, width, options, tilesTempDir, outputDir); + + // Save tiles info + try + { + if (trickplayInfo is not null) + { + trickplayInfo.ItemId = video.Id; + await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false); + + _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath); + } + else + { + throw new InvalidOperationException("Null trickplay tiles info from CreateTiles."); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while saving trickplay tiles info."); + + // Make sure no files stay in metadata folders on failure + // if tiles info wasn't saved. + Directory.Delete(outputDir, true); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating trickplay images."); + } + finally + { + _resourcePool.Release(); + + if (!string.IsNullOrEmpty(imgTempDir)) + { + Directory.Delete(imgTempDir, true); + } + } + } + + private TrickplayInfo CreateTiles(List images, int width, TrickplayOptions options, string workDir, string outputDir) + { + if (images.Count == 0) + { + throw new ArgumentException("Can't create trickplay from 0 images."); + } + + Directory.CreateDirectory(workDir); + + var trickplayInfo = new TrickplayInfo + { + Width = width, + Interval = options.Interval, + TileWidth = options.TileWidth, + TileHeight = options.TileHeight, + ThumbnailCount = images.Count, + // Set during image generation + Height = 0, + Bandwidth = 0 + }; + + /* + * Generate trickplay tiles from sets of thumbnails + */ + var imageOptions = new ImageCollageOptions + { + Width = trickplayInfo.TileWidth, + Height = trickplayInfo.TileHeight + }; + + var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight; + var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile); + + for (int i = 0; i < requiredTiles; i++) + { + // Set output/input paths + var tilePath = Path.Combine(workDir, $"{i}.jpg"); + + imageOptions.OutputPath = tilePath; + imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(thumbnailsPerTile).ToList(); + + // Generate image and use returned height for tiles info + var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null); + if (trickplayInfo.Height == 0) + { + trickplayInfo.Height = height; + } + + // Update 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); + } + + /* + * Move trickplay tiles to output directory + */ + Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName); + + // Replace existing tiles if they already exist + if (Directory.Exists(outputDir)) + { + Directory.Delete(outputDir, true); + } + + MoveDirectory(workDir, outputDir); + + return trickplayInfo; + } + + private bool CanGenerateTrickplay(Video video, int interval) + { + var videoType = video.VideoType; + if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay) + { + return false; + } + + if (video.IsPlaceHolder) + { + return false; + } + + if (video.IsShortcut) + { + return false; + } + + if (!video.IsCompleteMedia) + { + return false; + } + + if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks) + { + return false; + } + + var libraryOptions = _libraryManager.GetLibraryOptions(video); + if (libraryOptions is null || !libraryOptions.EnableTrickplayImageExtraction) + { + return false; + } + + // Can't extract images if there are no video streams + return video.GetMediaStreams().Count > 0; + } + + /// + public async Task> GetTrickplayResolutions(Guid itemId) + { + var trickplayResolutions = new Dictionary(); + + 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; + } + + /// + public async Task SaveTrickplayInfo(TrickplayInfo info) + { + 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); + } + } + + /// + public async Task>> GetTrickplayManifest(BaseItem item) + { + var trickplayManifest = new Dictionary>(); + 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; + } + + /// + public string GetTrickplayTilePath(BaseItem item, int width, int index) + { + return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg"); + } + + /// + public async Task GetHlsPlaylist(Guid itemId, int width, string? apiKey) + { + var trickplayResolutions = await GetTrickplayResolutions(itemId).ConfigureAwait(false); + if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo)) + { + var builder = new StringBuilder(128); + + if (trickplayInfo.ThumbnailCount > 0) + { + const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}"; + const string decimalFormat = "{0:0.###}"; + + 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(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 < tileCount; i++) + { + // All tiles prior to the last must contain full amount of thumbnails (no black). + if (i == tileCount - 1) + { + thumbnailsPerTile = trickplayInfo.ThumbnailCount - (i * thumbnailsPerTile); + infDuration = thumbnailDuration * thumbnailsPerTile; + } + + // EXTINF + builder + .Append("#EXTINF:") + .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration) + .AppendLine(","); + + // EXT-X-TILES + builder + .Append("#EXT-X-TILES:RESOLUTION=") + .Append(resolution) + .Append(",LAYOUT=") + .Append(layout) + .Append(",DURATION=") + .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, thumbnailDuration) + .AppendLine(); + + // URL + builder + .AppendFormat( + CultureInfo.InvariantCulture, + urlFormat, + width.ToString(CultureInfo.InvariantCulture), + i.ToString(CultureInfo.InvariantCulture), + itemId.ToString("N"), + apiKey) + .AppendLine(); + } + + builder.AppendLine("#EXT-X-ENDLIST"); + return builder.ToString(); + } + } + + return null; + } + + private string GetTrickplayDirectory(BaseItem item, int? width = null) + { + var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay"); + + return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path; + } + + private void MoveDirectory(string source, string destination) + { + try + { + Directory.Move(source, destination); + } + catch (IOException) + { + // Cross device move requires a copy + Directory.CreateDirectory(destination); + foreach (string file in Directory.GetFiles(source)) + { + File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true); + } + + Directory.Delete(source, true); + } + } +} 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(); serviceCollection.AddScoped(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); // TODO search the assemblies instead of adding them manually? serviceCollection.AddSingleton(); 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 posters, IReadOnlyList backdrops); /// - /// Creates a new jpeg trickplay grid image. + /// Creates a new trickplay tile image. /// - /// The options to use when creating the image. Width and Height are a quantity of tiles in this case, not pixels. + /// The options to use when creating the image. Width and Height are a quantity of thumbnails in this case, not pixels. /// The image encode quality. - /// The width of a single trickplay image. - /// Optional height of a single trickplay image, if it is known. - /// Height of single decoded trickplay image. - int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight); + /// The width of a single trickplay thumbnail. + /// Optional height of a single trickplay thumbnail, if it is known. + /// Height of single decoded trickplay thumbnail. + 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 @@ -61,27 +61,6 @@ namespace MediaBrowser.Controller.Persistence /// The list of chapters to save. void SaveChapters(Guid id, IReadOnlyList chapters); - /// - /// Get available trickplay resolutions and corresponding info. - /// - /// The item. - /// Map of width resolutions to trickplay tiles info. - Dictionary GetTilesResolutions(Guid itemId); - - /// - /// Saves trickplay tiles info. - /// - /// The item. - /// The trickplay tiles info. - void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); - - /// - /// Gets trickplay data for an item. - /// - /// The item. - /// A map of media source id to a map of tile width to tile info. - Dictionary> GetTrickplayManifest(BaseItem item); - /// /// Gets the media streams. /// 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 { /// - /// Generate or replace trickplay data. + /// Generates new trickplay images and metadata. /// /// The video. /// Whether or not existing data should be replaced. @@ -26,28 +26,28 @@ public interface ITrickplayManager /// /// The item. /// Map of width resolutions to trickplay tiles info. - Dictionary GetTilesResolutions(Guid itemId); + Task> GetTrickplayResolutions(Guid itemId); /// - /// Saves trickplay tiles info. + /// Saves trickplay info. /// - /// The item. - /// The trickplay tiles info. - void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); + /// The trickplay info. + /// Task. + Task SaveTrickplayInfo(TrickplayInfo info); /// - /// Gets the trickplay manifest. + /// Gets all trickplay infos for all media streams of an item. /// /// The item. - /// A map of media source id to a map of tile width to tile info. - Dictionary> GetTrickplayManifest(BaseItem item); + /// A map of media source id to a map of tile width to trickplay info. + Task>> GetTrickplayManifest(BaseItem item); /// - /// Gets the path to a trickplay tiles image. + /// Gets the path to a trickplay tile image. /// /// The item. - /// The width of a single tile. - /// The tile grid's index. + /// The width of a single thumbnail. + /// The tile's index. /// The absolute path. string GetTrickplayTilePath(BaseItem item, int width, int index); @@ -55,8 +55,8 @@ public interface ITrickplayManager /// Gets the trickplay HLS playlist. /// /// The item. - /// The width of a single tile. + /// The width of a single thumbnail. /// Optional api key of the requesting user. /// The text content of the .m3u8 playlist. - string? GetHlsPlaylist(Guid itemId, int width, string? apiKey); + Task 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. /// /// The trickplay manifest. - public Dictionary> Trickplay { get; set; } + public Dictionary> Trickplay { get; set; } /// /// 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; - -/// -/// Class TrickplayTilesInfo. -/// -public class TrickplayTilesInfo -{ - /// - /// Gets or sets width of an individual tile. - /// - /// The width. - public int Width { get; set; } - - /// - /// Gets or sets height of an individual tile. - /// - /// The height. - public int Height { get; set; } - - /// - /// Gets or sets amount of tiles per row. - /// - /// The tile grid's width. - public int TileWidth { get; set; } - - /// - /// Gets or sets amount of tiles per column. - /// - /// The tile grid's height. - public int TileHeight { get; set; } - - /// - /// Gets or sets total amount of non-black tiles. - /// - /// The tile count. - public int TileCount { get; set; } - - /// - /// Gets or sets interval in milliseconds between each trickplay tile. - /// - /// The interval. - public int Interval { get; set; } - - /// - /// Gets or sets peak bandwith usage in bits per second. - /// - /// The bandwidth. - public int Bandwidth { get; set; } -} diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs deleted file mode 100644 index 3ea7c00d0..000000000 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ /dev/null @@ -1,425 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -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.Extensions.Logging; - -namespace MediaBrowser.Providers.Trickplay; - -/// -/// ITrickplayManager implementation. -/// -public class TrickplayManager : ITrickplayManager -{ - private readonly ILogger _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 static readonly SemaphoreSlim _resourcePool = new(1, 1); - private static readonly string[] _trickplayImgExtensions = { ".jpg" }; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The item repository. - /// The media encoder. - /// The file systen. - /// The encoding helper. - /// The library manager. - /// The server configuration manager. - /// The image encoder. - public TrickplayManager( - ILogger logger, - IItemRepository itemRepo, - IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - EncodingHelper encodingHelper, - ILibraryManager libraryManager, - IServerConfigurationManager config, - IImageEncoder imageEncoder) - { - _logger = logger; - _itemRepo = itemRepo; - _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _encodingHelper = encodingHelper; - _libraryManager = libraryManager; - _config = config; - _imageEncoder = imageEncoder; - } - - /// - public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken) - { - _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); - - var options = _config.Configuration.TrickplayOptions; - foreach (var width in options.WidthResolutions) - { - cancellationToken.ThrowIfCancellationRequested(); - await RefreshTrickplayDataInternal( - video, - replace, - width, - options, - cancellationToken).ConfigureAwait(false); - } - } - - private async Task RefreshTrickplayDataInternal( - Video video, - bool replace, - int width, - TrickplayOptions options, - CancellationToken cancellationToken) - { - if (!CanGenerateTrickplay(video, options.Interval)) - { - return; - } - - var imgTempDir = string.Empty; - var outputDir = GetTrickplayDirectory(video, width); - - await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - if (!replace && Directory.Exists(outputDir) && GetTilesResolutions(video.Id).ContainsKey(width)) - { - _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id); - return; - } - - // Extract images - // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay. - var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id)); - - if (mediaSource is null) - { - _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id); - return; - } - - var mediaPath = mediaSource.Path; - var mediaStream = mediaSource.VideoStream; - var container = mediaSource.Container; - - _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id); - imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated( - mediaPath, - container, - mediaSource, - mediaStream, - width, - TimeSpan.FromMilliseconds(options.Interval), - options.EnableHwAcceleration, - options.ProcessThreads, - options.Qscale, - options.ProcessPriority, - _encodingHelper, - cancellationToken).ConfigureAwait(false); - - if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir)) - { - throw new InvalidOperationException("Null or invalid directory from media encoder."); - } - - var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false) - .Select(i => i.FullName) - .OrderBy(i => i) - .ToList(); - - // Create tiles - var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); - var tilesInfo = CreateTiles(images, width, options, tilesTempDir, outputDir); - - // Save tiles info - try - { - if (tilesInfo is not null) - { - SaveTilesInfo(video.Id, tilesInfo); - _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath); - } - else - { - throw new InvalidOperationException("Null trickplay tiles info from CreateTiles."); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error while saving trickplay tiles info."); - - // Make sure no files stay in metadata folders on failure - // if tiles info wasn't saved. - Directory.Delete(outputDir, true); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating trickplay images."); - } - finally - { - _resourcePool.Release(); - - if (!string.IsNullOrEmpty(imgTempDir)) - { - Directory.Delete(imgTempDir, true); - } - } - } - - private TrickplayTilesInfo CreateTiles(List images, int width, TrickplayOptions options, string workDir, string outputDir) - { - if (images.Count == 0) - { - throw new ArgumentException("Can't create trickplay from 0 images."); - } - - Directory.CreateDirectory(workDir); - - var tilesInfo = new TrickplayTilesInfo - { - Width = width, - Interval = options.Interval, - TileWidth = options.TileWidth, - TileHeight = options.TileHeight, - TileCount = images.Count, - // Set during image generation - Height = 0, - Bandwidth = 0 - }; - - /* - * Generate trickplay tile grids from sets of images - */ - var imageOptions = new ImageCollageOptions - { - Width = tilesInfo.TileWidth, - Height = tilesInfo.TileHeight - }; - - var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight; - var requiredTileGrids = (int)Math.Ceiling((double)images.Count / tilesPerGrid); - - for (int i = 0; i < requiredTileGrids; i++) - { - // Set output/input paths - var tileGridPath = Path.Combine(workDir, $"{i}.jpg"); - - imageOptions.OutputPath = tileGridPath; - imageOptions.InputPaths = images.Skip(i * tilesPerGrid).Take(tilesPerGrid).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) - { - tilesInfo.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); - } - - /* - * Move trickplay tiles to output directory - */ - Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName); - - // Replace existing tile grids if they already exist - if (Directory.Exists(outputDir)) - { - Directory.Delete(outputDir, true); - } - - MoveDirectory(workDir, outputDir); - - return tilesInfo; - } - - private bool CanGenerateTrickplay(Video video, int interval) - { - var videoType = video.VideoType; - if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay) - { - return false; - } - - if (video.IsPlaceHolder) - { - return false; - } - - if (video.IsShortcut) - { - return false; - } - - if (!video.IsCompleteMedia) - { - return false; - } - - if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks) - { - return false; - } - - var libraryOptions = _libraryManager.GetLibraryOptions(video); - if (libraryOptions is null || !libraryOptions.EnableTrickplayImageExtraction) - { - return false; - } - - // Can't extract images if there are no video streams - return video.GetMediaStreams().Count > 0; - } - - /// - public Dictionary GetTilesResolutions(Guid itemId) - { - return _itemRepo.GetTilesResolutions(itemId); - } - - /// - public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo) - { - _itemRepo.SaveTilesInfo(itemId, tilesInfo); - } - - /// - public Dictionary> GetTrickplayManifest(BaseItem item) - { - return _itemRepo.GetTrickplayManifest(item); - } - - /// - public string GetTrickplayTilePath(BaseItem item, int width, int index) - { - return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg"); - } - - /// - public string? GetHlsPlaylist(Guid itemId, int width, string? apiKey) - { - var tilesResolutions = GetTilesResolutions(itemId); - if (tilesResolutions is not null && tilesResolutions.TryGetValue(width, out var tilesInfo)) - { - var builder = new StringBuilder(128); - - if (tilesInfo.TileCount > 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); - - builder - .AppendLine("#EXTM3U") - .Append("#EXT-X-TARGETDURATION:") - .AppendLine(tileGridCount.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++) - { - // 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) - { - tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid); - infDuration = tileDuration * tilesPerGrid; - } - - // EXTINF - builder - .Append("#EXTINF:") - .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration) - .AppendLine(","); - - // EXT-X-TILES - builder - .Append("#EXT-X-TILES:RESOLUTION=") - .Append(resolution) - .Append(",LAYOUT=") - .Append(layout) - .Append(",DURATION=") - .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, tileDuration) - .AppendLine(); - - // URL - builder - .AppendFormat( - CultureInfo.InvariantCulture, - urlFormat, - width.ToString(CultureInfo.InvariantCulture), - i.ToString(CultureInfo.InvariantCulture), - itemId.ToString("N"), - apiKey) - .AppendLine(); - } - - builder.AppendLine("#EXT-X-ENDLIST"); - return builder.ToString(); - } - } - - return null; - } - - private string GetTrickplayDirectory(BaseItem item, int? width = null) - { - var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay"); - - return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path; - } - - private void MoveDirectory(string source, string destination) - { - try - { - Directory.Move(source, destination); - } - catch (IOException) - { - // Cross device move requires a copy - Directory.CreateDirectory(destination); - foreach (string file in Directory.GetFiles(source)) - { - File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true); - } - - Directory.Delete(source, true); - } - } -} 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 } /// - 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 } /// - 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(); } -- cgit v1.2.3 From b176beb88e22a36cc056439ac2a4df4fbe68f2c1 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Fri, 6 Oct 2023 00:40:09 +0200 Subject: Reduce string allocations Some simple changes to reduce the number of allocated strings --- Emby.Dlna/DlnaManager.cs | 2 +- Emby.Naming/ExternalFiles/ExternalPathParser.cs | 2 +- Emby.Naming/Video/StubResolver.cs | 7 ++--- Emby.Photos/PhotoProvider.cs | 2 +- .../IO/ManagedFileSystem.cs | 6 ++-- .../Library/LibraryManager.cs | 4 +-- .../Library/Resolvers/Audio/AudioResolver.cs | 6 ++-- .../Library/Resolvers/BaseVideoResolver.cs | 2 +- .../Library/Resolvers/Books/BookResolver.cs | 9 +++--- .../MediaEncoder/EncodingManager.cs | 2 +- .../Playlists/PlaylistManager.cs | 16 ++++------ .../Updates/InstallationManager.cs | 3 +- Jellyfin.Api/Controllers/DynamicHlsController.cs | 4 +-- Jellyfin.Api/Controllers/HlsSegmentController.cs | 9 +++--- Jellyfin.Api/Helpers/StreamingHelpers.cs | 10 +++--- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 2 +- .../MediaEncoding/EncodingHelper.cs | 18 +++++------ .../Attachments/AttachmentExtractor.cs | 36 +++------------------- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 6 ++-- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 2 +- src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 12 ++++---- 21 files changed, 63 insertions(+), 97 deletions(-) (limited to 'src') diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index 99b3e6e7e..d67cb67b5 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -228,7 +228,7 @@ namespace Emby.Dlna try { return _fileSystem.GetFilePaths(path) - .Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase)) + .Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase)) .Select(i => ParseProfileFile(i, type)) .Where(i => i is not null) .ToList()!; // We just filtered out all the nulls diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs index 953129671..4080ba10d 100644 --- a/Emby.Naming/ExternalFiles/ExternalPathParser.cs +++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs @@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles return null; } - var extension = Path.GetExtension(path); + var extension = Path.GetExtension(path.AsSpan()); if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) && !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))) { diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs index f7ba606e3..4b9df19b0 100644 --- a/Emby.Naming/Video/StubResolver.cs +++ b/Emby.Naming/Video/StubResolver.cs @@ -26,19 +26,18 @@ namespace Emby.Naming.Video return false; } - var extension = Path.GetExtension(path); + var extension = Path.GetExtension(path.AsSpan()); if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { return false; } - path = Path.GetFileNameWithoutExtension(path); - var token = Path.GetExtension(path).TrimStart('.'); + var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.'); foreach (var rule in options.StubTypes) { - if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase)) + if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)) { stubType = rule.StubType; return true; diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs index f54066c57..27329a7f2 100644 --- a/Emby.Photos/PhotoProvider.cs +++ b/Emby.Photos/PhotoProvider.cs @@ -61,7 +61,7 @@ namespace Emby.Photos item.SetImagePath(ImageType.Primary, item.Path); // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs - if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase)) + if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase)) { try { diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 18b00ce0b..4178936ce 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -103,15 +103,17 @@ namespace Emby.Server.Implementations.IO return filePath; } + var filePathSpan = filePath.AsSpan(); + // relative path if (firstChar == '\\') { - filePath = filePath.Substring(1); + filePathSpan = filePathSpan.Slice(1); } try { - return Path.GetFullPath(Path.Combine(folderPath, filePath)); + return Path.GetFullPath(Path.Join(folderPath, filePathSpan)); } catch (ArgumentException) { diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index b0a4a4151..845e7e351 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1162,7 +1162,7 @@ namespace Emby.Server.Implementations.Library Name = Path.GetFileName(dir), Locations = _fileSystem.GetFilePaths(dir, false) - .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) + .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase)) .Select(i => { try @@ -3135,7 +3135,7 @@ namespace Emby.Server.Implementations.Library } var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true) - .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) + .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase)) .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrEmpty(shortcut)) diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index a74f82475..862f144e6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -94,9 +94,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (AudioFileParser.IsAudioFile(args.Path, _namingOptions)) { - var extension = Path.GetExtension(args.Path); + var extension = Path.GetExtension(args.Path.AsSpan()); - if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase)) + if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase)) { // if audio file exists of same name, return null return null; @@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (item is not null) { - item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase); + item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase); item.IsInMixedFolder = true; } diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 381796d0e..779cfd5be 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers return false; } - return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase)); + return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase)); } /// diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 042422c6f..73861ff59 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -32,9 +32,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books return GetBook(args); } - var extension = Path.GetExtension(args.Path); + var extension = Path.GetExtension(args.Path.AsSpan()); - if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) + if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { // It's a book return new Book @@ -51,12 +51,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books { var bookFiles = args.FileSystemChildren.Where(f => { - var fileExtension = Path.GetExtension(f.FullName) - ?? string.Empty; + var fileExtension = Path.GetExtension(f.FullName.AsSpan()); return _validExtensions.Contains( fileExtension, - StringComparer.OrdinalIgnoreCase); + StringComparison.OrdinalIgnoreCase); }).ToList(); // Don't return a Book if there is more (or less) than one document in the directory diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 7732e32d0..896f47923 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -222,7 +222,7 @@ namespace Emby.Server.Implementations.MediaEncoder { var deadImages = images .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase) - .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) + .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase)) .ToList(); foreach (var image in deadImages) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 0cb450cdf..649c49924 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -327,9 +327,9 @@ namespace Emby.Server.Implementations.Playlists // this is probably best done as a metadata provider // saving a file over itself will require some work to prevent this from happening when not needed var playlistPath = item.Path; - var extension = Path.GetExtension(playlistPath); + var extension = Path.GetExtension(playlistPath.AsSpan()); - if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase)) + if (extension.Equals(".wpl", StringComparison.OrdinalIgnoreCase)) { var playlist = new WplPlaylist(); foreach (var child in item.GetLinkedChildren()) @@ -362,8 +362,7 @@ namespace Emby.Server.Implementations.Playlists string text = new WplContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase)) { var playlist = new ZplPlaylist(); foreach (var child in item.GetLinkedChildren()) @@ -396,8 +395,7 @@ namespace Emby.Server.Implementations.Playlists string text = new ZplContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".m3u", StringComparison.OrdinalIgnoreCase)) { var playlist = new M3uPlaylist { @@ -428,8 +426,7 @@ namespace Emby.Server.Implementations.Playlists string text = new M3uContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase)) { var playlist = new M3uPlaylist(); playlist.IsExtended = true; @@ -458,8 +455,7 @@ namespace Emby.Server.Implementations.Playlists string text = new M3uContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase)) { var playlist = new PlsPlaylist(); foreach (var child in item.GetLinkedChildren()) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 6c198b6f9..77d385ba1 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -504,8 +504,7 @@ namespace Emby.Server.Implementations.Updates private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken) { - var extension = Path.GetExtension(package.SourceUrl); - if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase)) + if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase)) { _logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl); return; diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 065a4ce5c..4026a38f8 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -2041,9 +2041,9 @@ public class DynamicHlsController : BaseJellyfinApiController return null; } - var playlistFilename = Path.GetFileNameWithoutExtension(playlist); + var playlistFilename = Path.GetFileNameWithoutExtension(playlist.AsSpan()); - var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length); + var indexString = Path.GetFileNameWithoutExtension(file.Name.AsSpan()).Slice(playlistFilename.Length); return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture); } diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index d7cec865e..6eedfd8c7 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -59,7 +59,7 @@ public class HlsSegmentController : BaseJellyfinApiController public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId) { // TODO: Deprecate with new iOS app - var file = segmentId + Path.GetExtension(Request.Path); + var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan())); var transcodePath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodePath, file)); var fileDir = Path.GetDirectoryName(file); @@ -85,11 +85,12 @@ public class HlsSegmentController : BaseJellyfinApiController [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId) { - var file = playlistId + Path.GetExtension(Request.Path); + var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan())); var transcodePath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodePath, file)); var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8") + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) + || Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase)) { return BadRequest("Invalid segment."); } @@ -138,7 +139,7 @@ public class HlsSegmentController : BaseJellyfinApiController [FromRoute, Required] string segmentId, [FromRoute, Required] string segmentContainer) { - var file = segmentId + Path.GetExtension(Request.Path); + var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan())); var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file)); diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 782cd6568..d15fe4fab 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -243,7 +243,7 @@ public static class StreamingHelpers ? GetOutputFileExtension(state, mediaSource) : ("." + state.OutputContainer); - state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId); + state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId); return state; } @@ -418,11 +418,11 @@ public static class StreamingHelpers /// System.String. private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource) { - var ext = Path.GetExtension(state.RequestedUrl); + var ext = Path.GetExtension(state.RequestedUrl.AsSpan()); - if (!string.IsNullOrEmpty(ext)) + if (ext.IsEmpty) { - return ext; + return null; } // Try to infer based on the desired video codec @@ -504,7 +504,7 @@ public static class StreamingHelpers /// The device id. /// The play session id. /// The complete file path, including the folder, for the transcoding file. - private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId) + private static string GetOutputFilePath(StreamState state, string? outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId) { var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}"; diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 73ebb396d..c16a586d6 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -538,7 +538,7 @@ public class TranscodingJobHelper : IDisposable await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); } - if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase)) + if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase)) { string subtitlePath = state.SubtitleStream.Path; string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal)); diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index c311d3b8a..3be7a4252 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -548,25 +548,25 @@ namespace MediaBrowser.Controller.MediaEncoding /// System.Nullable{VideoCodecs}. public string InferVideoCodec(string url) { - var ext = Path.GetExtension(url); + var ext = Path.GetExtension(url.AsSpan()); - if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".asf", StringComparison.OrdinalIgnoreCase)) { return "wmv"; } - if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".webm", StringComparison.OrdinalIgnoreCase)) { // TODO: this may not always mean VP8, as the codec ages return "vp8"; } - if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".ogg", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ogv", StringComparison.OrdinalIgnoreCase)) { return "theora"; } - if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".m3u8", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ts", StringComparison.OrdinalIgnoreCase)) { return "h264"; } @@ -1080,10 +1080,10 @@ namespace MediaBrowser.Controller.MediaEncoding && state.SubtitleStream.IsExternal) { var subtitlePath = state.SubtitleStream.Path; - var subtitleExtension = Path.GetExtension(subtitlePath); + var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan()); - if (string.Equals(subtitleExtension, ".sub", StringComparison.OrdinalIgnoreCase) - || string.Equals(subtitleExtension, ".sup", StringComparison.OrdinalIgnoreCase)) + if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase) + || subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase)) { var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); if (File.Exists(idxFile)) @@ -6033,7 +6033,7 @@ namespace MediaBrowser.Controller.MediaEncoding var format = string.Empty; var keyFrame = string.Empty; - if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase) + if (Path.GetExtension(outputPath.AsSpan()).Equals(".mp4", StringComparison.OrdinalIgnoreCase) && state.BaseRequest.Context == EncodingContext.Streaming) { // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 989e386a5..0ec0c84d4 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -1,4 +1,3 @@ -#nullable disable #pragma warning disable CS1591 using System; @@ -23,7 +22,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Attachments { - public class AttachmentExtractor : IAttachmentExtractor, IDisposable + public sealed class AttachmentExtractor : IAttachmentExtractor { private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; @@ -34,8 +33,6 @@ namespace MediaBrowser.MediaEncoding.Attachments private readonly ConcurrentDictionary _semaphoreLocks = new ConcurrentDictionary(); - private bool _disposed = false; - public AttachmentExtractor( ILogger logger, IApplicationPaths appPaths, @@ -296,7 +293,7 @@ namespace MediaBrowser.MediaEncoding.Attachments ArgumentException.ThrowIfNullOrEmpty(outputPath); - Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputPath))); var processArgs = string.Format( CultureInfo.InvariantCulture, @@ -391,33 +388,8 @@ namespace MediaBrowser.MediaEncoding.Attachments filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture); } - var prefix = filename.Substring(0, 1); - return Path.Combine(_appPaths.DataPath, "attachments", prefix, filename); - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - } - - _disposed = true; + var prefix = filename.AsSpan(0, 1); + return Path.Join(_appPaths.DataPath, "attachments", prefix, filename); } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 346e97ae1..59c350561 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -316,10 +316,8 @@ namespace MediaBrowser.MediaEncoding.Encoder { var files = _fileSystem.GetFilePaths(path, recursive); - var excludeExtensions = new[] { ".c" }; - - return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase) - && !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty)); + return files.FirstOrDefault(i => Path.GetFileNameWithoutExtension(i.AsSpan()).Equals(filename, StringComparison.OrdinalIgnoreCase) + && !Path.GetExtension(i.AsSpan()).Equals(".c", StringComparison.OrdinalIgnoreCase)); } catch (Exception) { diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 126c0503e..416f60217 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -188,7 +188,7 @@ public class SkiaEncoder : IImageEncoder return path; } - var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path)); + var tempPath = Path.Combine(_appPaths.TempDirectory, string.Concat(Guid.NewGuid().ToString(), Path.GetExtension(path.AsSpan()))); var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid."); Directory.CreateDirectory(directory); File.Copy(path, tempPath, true); diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index 6dff7aa9b..4aff26c16 100644 --- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -38,25 +38,25 @@ public partial class StripCollageBuilder { ArgumentNullException.ThrowIfNull(outputPath); - var ext = Path.GetExtension(outputPath); + var ext = Path.GetExtension(outputPath.AsSpan()); - if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase) - || string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".jpg", StringComparison.OrdinalIgnoreCase) + || ext.Equals(".jpeg", StringComparison.OrdinalIgnoreCase)) { return SKEncodedImageFormat.Jpeg; } - if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".webp", StringComparison.OrdinalIgnoreCase)) { return SKEncodedImageFormat.Webp; } - if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".gif", StringComparison.OrdinalIgnoreCase)) { return SKEncodedImageFormat.Gif; } - if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase)) + if (ext.Equals(".bmp", StringComparison.OrdinalIgnoreCase)) { return SKEncodedImageFormat.Bmp; } -- cgit v1.2.3 From c707baed8366cd44ea80713cf93e5f92971815bf Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 5 Oct 2023 23:57:11 +0200 Subject: Jellyfin.Drawing minor improvements Reduce duplicate/dead code --- MediaBrowser.Controller/Drawing/IImageProcessor.cs | 11 ----- .../Drawing/ImageProcessingOptions.cs | 3 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 10 +--- .../Drawing/ImageFormatExtensions.cs | 17 +++++++ .../MediaInfo/EmbeddedImageProvider.cs | 6 --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 14 +----- src/Jellyfin.Drawing/ImageProcessor.cs | 56 +--------------------- .../Drawing/ImageFormatExtensionsTests.cs | 13 +++++ 8 files changed, 37 insertions(+), 93 deletions(-) (limited to 'src') diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index cdc3d52b9..0d1e2a5a0 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; @@ -70,14 +69,6 @@ namespace MediaBrowser.Controller.Drawing string? GetImageCacheTag(User user); - /// - /// Processes the image. - /// - /// The options. - /// To stream. - /// Task. - Task ProcessImage(ImageProcessingOptions options, Stream toStream); - /// /// Processes the image. /// @@ -97,7 +88,5 @@ namespace MediaBrowser.Controller.Drawing /// The options. /// The library name to draw onto the collage. void CreateImageCollage(ImageCollageOptions options, string? libraryName); - - bool SupportsTransparency(string path); } } diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs index 7912c5e87..953cfe698 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -119,7 +119,8 @@ namespace MediaBrowser.Controller.Drawing private bool IsFormatSupported(string originalImagePath) { var ext = Path.GetExtension(originalImagePath); - return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, "." + outputFormat, StringComparison.OrdinalIgnoreCase)); + ext = ext.Replace(".jpeg", ".jpg", StringComparison.OrdinalIgnoreCase); + return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, outputFormat.GetExtension(), StringComparison.OrdinalIgnoreCase)); } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4bff19665..26f47a18f 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -650,15 +650,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { ArgumentException.ThrowIfNullOrEmpty(inputPath); - var outputExtension = targetFormat switch - { - ImageFormat.Bmp => ".bmp", - ImageFormat.Gif => ".gif", - ImageFormat.Jpg => ".jpg", - ImageFormat.Png => ".png", - ImageFormat.Webp => ".webp", - _ => ".jpg" - }; + var outputExtension = targetFormat?.GetExtension() ?? ".jpg"; var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension); Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); diff --git a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs index 68a5c2534..1bb24112e 100644 --- a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs +++ b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs @@ -24,4 +24,21 @@ public static class ImageFormatExtensions ImageFormat.Webp => "image/webp", _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat)) }; + + /// + /// Returns the correct extension for this . + /// + /// This . + /// The is an invalid enumeration value. + /// The correct extension for this . + public static string GetExtension(this ImageFormat format) + => format switch + { + ImageFormat.Bmp => ".bmp", + ImageFormat.Gif => ".gif", + ImageFormat.Jpg => ".jpg", + ImageFormat.Png => ".png", + ImageFormat.Webp => ".webp", + _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat)) + }; } diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs index c24f4e2fc..0bfee07fd 100644 --- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs @@ -204,16 +204,10 @@ namespace MediaBrowser.Providers.MediaInfo ? Path.GetExtension(attachmentStream.FileName) : MimeTypes.ToExtension(attachmentStream.MimeType); - if (string.IsNullOrEmpty(extension)) - { - extension = ".jpg"; - } - ImageFormat format = extension switch { ".bmp" => ImageFormat.Bmp, ".gif" => ImageFormat.Gif, - ".jpg" => ImageFormat.Jpg, ".png" => ImageFormat.Png, ".webp" => ImageFormat.Webp, _ => ImageFormat.Jpg diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 416f60217..03f90da8e 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -200,20 +200,10 @@ public class SkiaEncoder : IImageEncoder { if (!orientation.HasValue) { - return SKEncodedOrigin.TopLeft; + return SKEncodedOrigin.Default; } - return orientation.Value switch - { - ImageOrientation.TopRight => SKEncodedOrigin.TopRight, - ImageOrientation.RightTop => SKEncodedOrigin.RightTop, - ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom, - ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop, - ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom, - ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight, - ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft, - _ => SKEncodedOrigin.TopLeft - }; + return (SKEncodedOrigin)orientation.Value; } /// diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index 4f16e294b..65a8f4e83 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -107,22 +107,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable /// public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation; - /// - public async Task ProcessImage(ImageProcessingOptions options, Stream toStream) - { - var file = await ProcessImage(options).ConfigureAwait(false); - using var fileStream = AsyncFile.OpenRead(file.Path); - await fileStream.CopyToAsync(toStream).ConfigureAwait(false); - } - /// public IReadOnlyCollection GetSupportedImageOutputFormats() => _imageEncoder.SupportedOutputFormats; - /// - public bool SupportsTransparency(string path) - => _transparentImageTypes.Contains(Path.GetExtension(path)); - /// public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options) { @@ -224,7 +212,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable } } - return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); + return (cacheFilePath, outputFormat.GetMimeType(), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); } catch (Exception ex) { @@ -262,17 +250,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable return ImageFormat.Jpg; } - private string GetMimeType(ImageFormat format, string path) - => format switch - { - ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"), - ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"), - ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"), - ImageFormat.Png => MimeTypes.GetMimeType("i.png"), - ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"), - _ => MimeTypes.GetMimeType(path) - }; - /// /// Gets the cache file path based on a set of parameters. /// @@ -374,7 +351,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable filename.Append(",v="); filename.Append(Version); - return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant()); + return GetCachePath(ResizedImageCachePath, filename.ToString(), format.GetExtension()); } /// @@ -471,35 +448,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable return Task.FromResult((originalImagePath, dateModified)); } - // TODO _mediaEncoder.ConvertImage is not implemented - // if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat)) - // { - // try - // { - // string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); - // - // string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png"; - // var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension); - // - // var file = _fileSystem.GetFileInfo(outputPath); - // if (!file.Exists) - // { - // await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false); - // dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath); - // } - // else - // { - // dateModified = file.LastWriteTimeUtc; - // } - // - // originalImagePath = outputPath; - // } - // catch (Exception ex) - // { - // _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath); - // } - // } - return Task.FromResult((originalImagePath, dateModified)); } diff --git a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs index a5bdb42d8..198ad5a27 100644 --- a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs +++ b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs @@ -30,4 +30,17 @@ public static class ImageFormatExtensionsTests [InlineData((ImageFormat)5)] public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format) => Assert.Throws(() => format.GetMimeType()); + + [Theory] + [MemberData(nameof(GetAllImageFormats))] + public static void GetExtension_Valid_Valid(ImageFormat format) + => Assert.Null(Record.Exception(() => format.GetExtension())); + + [Theory] + [InlineData((ImageFormat)int.MinValue)] + [InlineData((ImageFormat)int.MaxValue)] + [InlineData((ImageFormat)(-1))] + [InlineData((ImageFormat)5)] + public static void GetExtension_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format) + => Assert.Throws(() => format.GetExtension()); } -- cgit v1.2.3 From 4be0f4267d41a4948815ff9a7ca055c1b16dffe1 Mon Sep 17 00:00:00 2001 From: cvium Date: Wed, 25 Oct 2023 23:05:47 +0200 Subject: chore(deps): use Svg.Skia instead of the deprecated SkiaSharp.Svg --- Directory.Packages.props | 6 ++---- .../Jellyfin.Drawing.Skia.csproj | 4 ++-- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 19 +++++++++---------- 3 files changed, 13 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/Directory.Packages.props b/Directory.Packages.props index 0619c4ef9..d4d2e8455 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,9 +2,7 @@ true - - @@ -72,9 +70,9 @@ - + @@ -90,4 +88,4 @@ - + \ No newline at end of file diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index c465c4ad0..09e103c6b 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -18,11 +18,11 @@ + - - + diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 554fbe941..21abe6e45 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -2,19 +2,15 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; -using System.Security.Cryptography.Xml; using BlurHashSharp.SkiaSharp; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Drawing; using Microsoft.Extensions.Logging; using SkiaSharp; -using static System.Net.Mime.MediaTypeNames; -using SKSvg = SkiaSharp.Extended.Svg.SKSvg; +using Svg.Skia; namespace Jellyfin.Drawing.Skia; @@ -123,10 +119,16 @@ public class SkiaEncoder : IImageEncoder var extension = Path.GetExtension(path.AsSpan()); if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase)) { - var svg = new SKSvg(); + using var svg = new SKSvg(); try { using var picture = svg.Load(path); + if (picture is null) + { + _logger.LogError("Unable to determine image dimensions for {FilePath}", path); + return default; + } + return new ImageDimensions(Convert.ToInt32(picture.CullRect.Width), Convert.ToInt32(picture.CullRect.Height)); } catch (FormatException skiaColorException) @@ -289,10 +291,7 @@ public class SkiaEncoder : IImageEncoder private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin) { - var needsFlip = origin == SKEncodedOrigin.LeftBottom - || origin == SKEncodedOrigin.LeftTop - || origin == SKEncodedOrigin.RightBottom - || origin == SKEncodedOrigin.RightTop; + var needsFlip = origin is SKEncodedOrigin.LeftBottom or SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightBottom or SKEncodedOrigin.RightTop; var rotated = needsFlip ? new SKBitmap(bitmap.Height, bitmap.Width) : new SKBitmap(bitmap.Width, bitmap.Height); -- cgit v1.2.3 From 0a284dc0ab3d8503df88b96c5c74640710942a76 Mon Sep 17 00:00:00 2001 From: cvium Date: Wed, 25 Oct 2023 23:29:05 +0200 Subject: refactor: cache the resize image filter --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 44 ++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 554fbe941..b8290c5fc 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -27,6 +27,30 @@ public class SkiaEncoder : IImageEncoder private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; + private static readonly SKImageFilter _imageFilter; + +#pragma warning disable CA1810 + static SkiaEncoder() +#pragma warning restore CA1810 + { + var kernel = new[] + { + 0, -.1f, 0, + -.1f, 1.4f, -.1f, + 0, -.1f, 0, + }; + + var kernelSize = new SKSizeI(3, 3); + var kernelOffset = new SKPointI(1, 1); + _imageFilter = SKImageFilter.CreateMatrixConvolution( + kernelSize, + kernel, + 1f, + 0f, + kernelOffset, + SKShaderTileMode.Clamp, + true); + } /// /// Initializes a new instance of the class. @@ -357,25 +381,7 @@ public class SkiaEncoder : IImageEncoder IsDither = isDither }; - var kernel = new float[9] - { - 0, -.1f, 0, - -.1f, 1.4f, -.1f, - 0, -.1f, 0, - }; - - var kernelSize = new SKSizeI(3, 3); - var kernelOffset = new SKPointI(1, 1); - - paint.ImageFilter = SKImageFilter.CreateMatrixConvolution( - kernelSize, - kernel, - 1f, - 0f, - kernelOffset, - SKShaderTileMode.Clamp, - true); - + paint.ImageFilter = _imageFilter; canvas.DrawBitmap( source, SKRect.Create(0, 0, source.Width, source.Height), -- cgit v1.2.3 From a9ef103c95a7460031879726f4afda3013ca6619 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 5 Nov 2023 02:01:14 +0100 Subject: Add IDisposableAnalyzers to more projects --- Emby.Dlna/Emby.Dlna.csproj | 6 +++++- Emby.Dlna/PlayTo/Device.cs | 5 ++--- Emby.Dlna/PlayTo/PlayToController.cs | 13 ++++++------- Emby.Naming/Emby.Naming.csproj | 6 +++++- Emby.Photos/Emby.Photos.csproj | 8 ++++++-- .../Emby.Server.Implementations.csproj | 7 ++++++- Jellyfin.Api/Jellyfin.Api.csproj | 6 +++++- .../ActivityLogWebSocketListener.cs | 5 ++++- .../ScheduledTasksWebSocketListener.cs | 7 +++++-- .../SessionInfoWebSocketListener.cs | 17 ++++++++++------- Jellyfin.Data/Jellyfin.Data.csproj | 6 +++++- Jellyfin.Networking/Jellyfin.Networking.csproj | 6 +++++- .../Jellyfin.Server.Implementations.csproj | 2 +- Jellyfin.Server/Jellyfin.Server.csproj | 6 +++++- MediaBrowser.Common/MediaBrowser.Common.csproj | 6 +++++- .../MediaBrowser.Controller.csproj | 6 +++++- .../MediaBrowser.LocalMetadata.csproj | 6 +++++- .../MediaBrowser.MediaEncoding.csproj | 6 +++++- .../Subtitles/SubtitleEncoder.cs | 8 +++----- MediaBrowser.Model/MediaBrowser.Model.csproj | 7 ++++++- MediaBrowser.Providers/Manager/ProviderManager.cs | 10 +++++----- .../MediaBrowser.Providers.csproj | 6 +++++- .../MediaBrowser.XbmcMetadata.csproj | 4 ++++ src/Directory.Build.props | 21 +++++++++++++++++++++ .../Jellyfin.Drawing.Skia.csproj | 15 --------------- src/Jellyfin.Drawing/Jellyfin.Drawing.csproj | 15 --------------- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj | 16 ---------------- .../Jellyfin.MediaEncoding.Hls.csproj | 15 --------------- .../Jellyfin.MediaEncoding.Keyframes.csproj | 15 --------------- 29 files changed, 134 insertions(+), 122 deletions(-) create mode 100644 src/Directory.Build.props (limited to 'src') diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index aca239964..efbef0564 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -26,8 +26,12 @@ false - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index bb9b8b0fd..18fa19650 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -1248,11 +1248,10 @@ namespace Emby.Dlna.PlayTo if (disposing) { _timer?.Dispose(); + _timer = null; + Properties = null!; } - _timer = null; - Properties = null!; - _disposed = true; } diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index b1ad15cdc..df02fe631 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -683,16 +683,15 @@ namespace Emby.Dlna.PlayTo if (disposing) { + _device.PlaybackStart -= OnDevicePlaybackStart; + _device.PlaybackProgress -= OnDevicePlaybackProgress; + _device.PlaybackStopped -= OnDevicePlaybackStopped; + _device.MediaChanged -= OnDeviceMediaChanged; + _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft; + _device.OnDeviceUnavailable = null; _device.Dispose(); } - _device.PlaybackStart -= OnDevicePlaybackStart; - _device.PlaybackProgress -= OnDevicePlaybackProgress; - _device.PlaybackStopped -= OnDevicePlaybackStopped; - _device.MediaChanged -= OnDeviceMediaChanged; - _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft; - _device.OnDeviceUnavailable = null; - _disposed = true; } diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index f3973dad9..bc7548189 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -45,8 +45,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index 0f97a0686..5a04bbe49 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -24,14 +24,18 @@ true - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers - + diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 80263c139..b48e389ac 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -49,8 +49,13 @@ false - + + + all runtime; build; native; contentfiles; analyzers diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 7ac231885..03dd97367 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -24,8 +24,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs index 5b90d65d8..ba228cb00 100644 --- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs @@ -53,7 +53,10 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener protected override void Dispose(bool dispose) { - _activityManager.EntryCreated -= OnEntryCreated; + if (dispose) + { + _activityManager.EntryCreated -= OnEntryCreated; + } base.Dispose(dispose); } diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs index a9df2d671..37c108d5a 100644 --- a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs @@ -58,8 +58,11 @@ public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener protected override void Dispose(bool dispose) { - _taskManager.TaskExecuting -= OnTaskExecuting; - _taskManager.TaskCompleted -= OnTaskCompleted; + if (dispose) + { + _taskManager.TaskExecuting -= OnTaskExecuting; + _taskManager.TaskCompleted -= OnTaskCompleted; + } base.Dispose(dispose); } diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs index b403ff46d..3c2b86142 100644 --- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs @@ -57,13 +57,16 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener protected override void Dispose(bool dispose) { - _sessionManager.SessionStarted -= OnSessionManagerSessionStarted; - _sessionManager.SessionEnded -= OnSessionManagerSessionEnded; - _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart; - _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped; - _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress; - _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged; - _sessionManager.SessionActivity -= OnSessionManagerSessionActivity; + if (dispose) + { + _sessionManager.SessionStarted -= OnSessionManagerSessionStarted; + _sessionManager.SessionEnded -= OnSessionManagerSessionEnded; + _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart; + _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped; + _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress; + _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged; + _sessionManager.SessionActivity -= OnSessionManagerSessionActivity; + } base.Dispose(dispose); } diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 1bc5d8bf9..847853ca0 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -27,8 +27,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index 4cff5927f..43d08c37a 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -9,8 +9,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index fa6adb022..df1d5a3e1 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -6,7 +6,7 @@ true - + all diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 62abb8935..5479d2296 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -22,8 +22,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 7015d991f..7d0d7a173 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -43,8 +43,12 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 69c0d26b6..f9468f6cd 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -49,8 +49,12 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index 71cdea529..a39bc238a 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -20,8 +20,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index a0624fe76..1f39e88cd 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -29,8 +29,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 21fa4468e..8eea773d8 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -135,19 +135,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles var subtitleStream = mediaSource.MediaStreams .First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex); - var subtitle = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken) + var (stream, inputFormat) = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken) .ConfigureAwait(false); - var inputFormat = subtitle.Format; - // Return the original if the same format is being requested // Character encoding was already handled in GetSubtitleStream if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase)) { - return subtitle.Stream; + return stream; } - using (var stream = subtitle.Stream) + using (stream) { return ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps, cancellationToken); } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 58ba83a35..75c5bc6f0 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -48,8 +48,12 @@ - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers @@ -58,6 +62,7 @@ + diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index d0bb34d52..4ba884418 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -1096,13 +1096,13 @@ namespace MediaBrowser.Providers.Manager return; } - if (!_disposeCancellationTokenSource.IsCancellationRequested) - { - _disposeCancellationTokenSource.Cancel(); - } - if (disposing) { + if (!_disposeCancellationTokenSource.IsCancellationRequested) + { + _disposeCancellationTokenSource.Cancel(); + } + _disposeCancellationTokenSource.Dispose(); } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 7ef70f4b0..8471f6fa1 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -33,8 +33,12 @@ ../jellyfin.ruleset - + + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index 807234915..d7e34fd22 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -22,6 +22,10 @@ + + all + runtime; build; native; contentfiles; analyzers + all runtime; build; native; contentfiles; analyzers diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 000000000..ac2726ed5 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,21 @@ + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 09e103c6b..3c417f8ff 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -31,19 +31,4 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj index 2a5e24a44..d7ef6f8e7 100644 --- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj +++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj @@ -21,19 +21,4 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 36ae55ed2..997df6dbe 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -27,24 +27,8 @@ - - - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj index b792e7ec6..76dde1cf6 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj +++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj @@ -5,21 +5,6 @@ true - - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - - - - diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj index 09b1f8faa..0d91a447b 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -9,21 +9,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - - - - -- cgit v1.2.3