From 90b9e7f10bc267d8e9af79b94fc09b2b97460cbd Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Thu, 2 Nov 2023 20:13:18 +0100 Subject: [PATCH 01/37] feat: Add Media Segments --- .../Controllers/MediaSegmentController.cs | 133 +++++++++++ Jellyfin.Data/Entities/MediaSegment.cs | 50 +++++ Jellyfin.Data/Enums/MediaSegmentAction.cs | 33 +++ Jellyfin.Data/Enums/MediaSegmentType.cs | 33 +++ .../JellyfinDbContext.cs | 5 + .../MediaSegments/MediaSegmentsManager.cs | 210 ++++++++++++++++++ .../MediaSegmentConfiguration.cs | 39 ++++ Jellyfin.Server/CoreAppHost.cs | 3 + .../MediaSegments/IMediaSegmentsManager.cs | 47 ++++ windows | 1 + 10 files changed, 554 insertions(+) create mode 100644 Jellyfin.Api/Controllers/MediaSegmentController.cs create mode 100644 Jellyfin.Data/Entities/MediaSegment.cs create mode 100644 Jellyfin.Data/Enums/MediaSegmentAction.cs create mode 100644 Jellyfin.Data/Enums/MediaSegmentType.cs create mode 100644 Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs create mode 100644 MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs create mode 160000 windows diff --git a/Jellyfin.Api/Controllers/MediaSegmentController.cs b/Jellyfin.Api/Controllers/MediaSegmentController.cs new file mode 100644 index 0000000000..077f126d57 --- /dev/null +++ b/Jellyfin.Api/Controllers/MediaSegmentController.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using MediaBrowser.Model.MediaSegments; +using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers; + +/// +/// Media Segments controller. +/// +[Authorize] +public class MediaSegmentController : BaseJellyfinApiController +{ + private readonly IMediaSegmentsManager _mediaSegmentManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public MediaSegmentController( + IMediaSegmentsManager mediaSegmentManager) + { + _mediaSegmentManager = mediaSegmentManager; + } + + /// + /// Get all media segments. + /// + /// Optional: Just segments with itemId. + /// Optional: All segments of type. + /// Optional: All segments with typeIndex. + /// Segments returned. + /// An containing the queryresult of segments. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetSegments( + [FromQuery] Guid itemId, + [FromQuery] MediaSegmentType? type, + [FromQuery, DefaultValue(-1)] int typeIndex) + { + var list = _mediaSegmentManager.GetAllMediaSegments(itemId, typeIndex, type); + + return new QueryResult(list); + } + + /// + /// Create or update a media segment. You can update start/end/action. + /// + /// Start position of segment in seconds. + /// End position of segment in seconds. + /// Segment is associated with item id. + /// Segment type. + /// Optional: If you want to add a type multiple times to the same itemId increment it. + /// Optional: Creator recommends an action. + /// Segments returned. + /// Missing query parameter. + /// An containing the queryresult of segment. + [HttpPost("Segment")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task>> PostSegment( + [FromQuery, Required] double start, + [FromQuery, Required] double end, + [FromQuery, Required] Guid itemId, + [FromQuery, Required] MediaSegmentType type, + [FromQuery, DefaultValue(0)] int typeIndex, + [FromQuery, DefaultValue(MediaSegmentAction.Auto)] MediaSegmentAction action) + { + var newMediaSegment = new MediaSegment() + { + Start = start, + End = end, + ItemId = itemId, + Type = type, + TypeIndex = typeIndex, + Action = action + }; + + var segment = await _mediaSegmentManager.CreateMediaSegmentAsync(newMediaSegment).ConfigureAwait(false); + + return new QueryResult( + new List { segment }); + } + + /// + /// Create or update multiple media segments. See /MediaSegment/Segment for required properties. + /// + /// All segments that should be added. + /// Segments returned. + /// Invalid segments. + /// An containing the queryresult of segment. + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task>> PostSegments( + [FromBody, Required] IEnumerable segments) + { + var nsegments = await _mediaSegmentManager.CreateMediaSegmentsAsync(segments).ConfigureAwait(false); + + return new QueryResult(nsegments.ToList()); + } + + /// + /// Delete media segments. All query parameters can be freely defined. + /// + /// Optional: All segments with itemId. + /// Optional: All segments of type. + /// Optional: All segments with typeIndex. + /// Segments returned. + /// Missing query parameter. + /// An containing the queryresult of segments. + [HttpDelete] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task>> DeleteSegments( + [FromQuery] Guid itemId, + [FromQuery] MediaSegmentType? type, + [FromQuery, DefaultValue(-1)] int typeIndex) + { + var list = await _mediaSegmentManager.DeleteSegmentsAsync(itemId, typeIndex, type).ConfigureAwait(false); + + return new QueryResult(list); + } +} diff --git a/Jellyfin.Data/Entities/MediaSegment.cs b/Jellyfin.Data/Entities/MediaSegment.cs new file mode 100644 index 0000000000..79d37f3c4c --- /dev/null +++ b/Jellyfin.Data/Entities/MediaSegment.cs @@ -0,0 +1,50 @@ +#nullable disable +#pragma warning disable CS1591 + +using System; +using Jellyfin.Data.Enums; + +namespace Jellyfin.Data.Entities +{ + /// + /// Class MediaSegment. + /// + public class MediaSegment + { + /// + /// Gets or sets the start position in seconds. + /// + /// The start position. + public double Start { get; set; } + + /// + /// Gets or sets the end position in seconds. + /// + /// The end position. + public double End { get; set; } + + /// + /// Gets or sets the Type. + /// + /// The media segment type. + public MediaSegmentType Type { get; set; } + + /// + /// Gets or sets the TypeIndex which relates to the type. + /// + /// The type index. + public int TypeIndex { get; set; } + + /// + /// Gets or sets the associated ItemId. + /// + /// The id. + public Guid ItemId { get; set; } + + /// + /// Gets or sets the creator recommended action. Can be overwritten with user defined action. + /// + /// The media segment action. + public MediaSegmentAction Action { get; set; } + } +} diff --git a/Jellyfin.Data/Enums/MediaSegmentAction.cs b/Jellyfin.Data/Enums/MediaSegmentAction.cs new file mode 100644 index 0000000000..c4cff97885 --- /dev/null +++ b/Jellyfin.Data/Enums/MediaSegmentAction.cs @@ -0,0 +1,33 @@ +namespace Jellyfin.Data.Enums +{ + /// + /// Enum MediaSegmentAction. + /// + public enum MediaSegmentAction + { + /// + /// Auto, use default for type. + /// + Auto = 0, + + /// + /// None, do nothing with MediaSegment. + /// + None = 1, + + /// + /// Force skip the MediaSegment. + /// + Skip = 2, + + /// + /// Prompt user to skip the MediaSegment. + /// + Prompt = 3, + + /// + /// Mute the MediaSegment. + /// + Mute = 4, + } +} diff --git a/Jellyfin.Data/Enums/MediaSegmentType.cs b/Jellyfin.Data/Enums/MediaSegmentType.cs new file mode 100644 index 0000000000..b6ac2bede6 --- /dev/null +++ b/Jellyfin.Data/Enums/MediaSegmentType.cs @@ -0,0 +1,33 @@ +namespace Jellyfin.Data.Enums +{ + /// + /// Enum MediaSegmentType. + /// + public enum MediaSegmentType + { + /// + /// The Intro. + /// + Intro = 0, + + /// + /// The Outro. + /// + Outro = 1, + + /// + /// Recap of last tv show episode(s). + /// + Recap = 2, + + /// + /// The preview for the next tv show episode. + /// + Preview = 3, + + /// + /// Commercial that interrupt the viewer. + /// + Commercial = 4, + } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index ea99af0047..28f317956e 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -83,6 +83,11 @@ public class JellyfinDbContext : DbContext /// public DbSet TrickplayInfos => Set(); + /// + /// Gets the containing the media segments. + /// + public DbSet Segments => Set(); + /*public DbSet Artwork => Set(); public DbSet Books => Set(); diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs new file mode 100644 index 0000000000..d514534523 --- /dev/null +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs @@ -0,0 +1,210 @@ +#pragma warning disable CA1307 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.MediaSegments; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Implementations.MediaSegments +{ + /// + /// Manages the creation and retrieval of instances. + /// + public class MediaSegmentsManager : IMediaSegmentsManager + { + private readonly ILibraryManager _libraryManager; + private readonly IDbContextFactory _dbProvider; + private readonly IUserManager _userManager; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The database provider. + /// The user manager. + /// The logger. + public MediaSegmentsManager( + ILibraryManager libraryManager, + IDbContextFactory dbProvider, + IUserManager userManager, + ILogger logger) + { + _libraryManager = libraryManager; + _libraryManager.ItemRemoved += LibraryManagerItemRemoved; + + _dbProvider = dbProvider; + _userManager = userManager; + _logger = logger; + } + + // + // public event EventHandler>? OnUserUpdated; + + /// + public async Task CreateMediaSegmentAsync(MediaSegment segment) + { + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + ValidateSegment(segment); + + var found = dbContext.Segments.Where(s => s.ItemId.Equals(segment.ItemId) && s.Type.Equals(segment.Type) && s.TypeIndex.Equals(segment.TypeIndex)).FirstOrDefault(); + + AddOrUpdateSegment(dbContext, segment, found); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + return segment; + } + + /// + public async Task> CreateMediaSegmentsAsync(IEnumerable segments) + { + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var foundSegments = dbContext.Segments.Select(s => s); + + foreach (var segment in segments) + { + ValidateSegment(segment); + + var found = foundSegments.Where(s => s.ItemId.Equals(segment.ItemId) && s.Type.Equals(segment.Type) && s.TypeIndex.Equals(segment.TypeIndex)).FirstOrDefault(); + + AddOrUpdateSegment(dbContext, segment, found); + } + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + return segments; + } + + /// + public List GetAllMediaSegments(Guid itemId = default, int typeIndex = -1, MediaSegmentType? type = null) + { + var allSegments = new List(); + + var dbContext = _dbProvider.CreateDbContext(); + using (dbContext) + { + IQueryable queryable = dbContext.Segments.Select(s => s); + + if (!itemId.Equals(default)) + { + queryable = queryable.Where(s => s.ItemId.Equals(itemId)); + } + + if (!type.Equals(null)) + { + queryable = queryable.Where(s => s.Type.Equals(type)); + } + + if (typeIndex > -1) + { + queryable = queryable.Where(s => s.TypeIndex.Equals(typeIndex)); + } + + allSegments = queryable.AsNoTracking().ToList(); + } + + return allSegments; + } + + /// + /// Add or Update a segment in db. + /// The db context. + /// The segment. + /// The found segment. + /// + private void AddOrUpdateSegment(JellyfinDbContext dbContext, MediaSegment segment, MediaSegment? found) + { + if (found != null) + { + found.Start = segment.Start; + found.End = segment.End; + found.Action = segment.Action; + } + else + { + dbContext.Segments.Add(segment); + } + } + + /// + /// Validate a segment: itemId, start >= end and type. + /// + /// The segment to validate. + private void ValidateSegment(MediaSegment segment) + { + if (segment.ItemId.Equals(default)) + { + throw new ArgumentException($"itemId is default: itemId={segment.ItemId} for segment with type '{segment.Type}.{segment.TypeIndex}'"); + } + + if (segment.Start >= segment.End) + { + throw new ArgumentException($"start >= end: {segment.Start}>={segment.End} for segment itemId '{segment.ItemId}' with type '{segment.Type}.{segment.TypeIndex}'"); + } + } + + /// + /// Delete all segments when itemid is deleted from library. + /// TODO: Do not block. + /// + /// The sending entity. + /// The . + private void LibraryManagerItemRemoved(object? sender, ItemChangeEventArgs itemChangeEventArgs) + { + var task = Task.Run(async () => { await DeleteSegmentsAsync(itemChangeEventArgs.Item.Id).ConfigureAwait(false); }); + task.Wait(); + } + + /// + public async Task> DeleteSegmentsAsync(Guid itemId = default, int typeIndex = -1, MediaSegmentType? type = null) + { + var allSegments = new List(); + + if (itemId.Equals(default)) + { + throw new ArgumentException($"itemId are not set. Please provide at least one."); + } + + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + IQueryable queryable = dbContext.Segments.Select(s => s); + + if (!itemId.Equals(default)) + { + queryable = queryable.Where(s => s.ItemId.Equals(itemId)); + } + + if (!type.Equals(null)) + { + queryable = queryable.Where(s => s.Type.Equals(type)); + } + + if (typeIndex > -1) + { + queryable = queryable.Where(s => s.TypeIndex.Equals(typeIndex)); + } + + allSegments = queryable.AsNoTracking().ToList(); + + dbContext.Segments.RemoveRange(allSegments); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + return allSegments; + } + } +} diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs new file mode 100644 index 0000000000..023d3b98b4 --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs @@ -0,0 +1,39 @@ +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration +{ + /// + /// FluentAPI configuration for the MediaSegment entity. + /// + public class MediaSegmentConfiguration : IEntityTypeConfiguration + { + /// + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(s => s.Start) + .IsRequired(); + builder + .Property(s => s.End) + .IsRequired(); + builder + .Property(s => s.Type) + .IsRequired(); + builder + .Property(s => s.TypeIndex) + .IsRequired(); + builder + .Property(s => s.ItemId) + .IsRequired(); + builder + .Property(s => s.Action) + .IsRequired(); + builder + .HasKey(s => new { s.ItemId, s.Type, s.TypeIndex }); + builder + .HasIndex(s => s.ItemId); + } + } +} diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index d5b6e93b8e..2417aeb2ad 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -11,6 +11,7 @@ using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Devices; using Jellyfin.Server.Implementations.Events; +using Jellyfin.Server.Implementations.MediaSegments; using Jellyfin.Server.Implementations.Security; using Jellyfin.Server.Implementations.Trickplay; using Jellyfin.Server.Implementations.Users; @@ -26,6 +27,7 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Activity; +using MediaBrowser.Model.MediaSegments; using MediaBrowser.Providers.Lyric; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -86,6 +88,7 @@ namespace Jellyfin.Server serviceCollection.AddScoped(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); // TODO search the assemblies instead of adding them manually? serviceCollection.AddSingleton(); diff --git a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs new file mode 100644 index 0000000000..0baf506d91 --- /dev/null +++ b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs @@ -0,0 +1,47 @@ +#nullable disable + +#pragma warning disable CA1002, CS1591 + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; + +namespace MediaBrowser.Model.MediaSegments +{ + public interface IMediaSegmentsManager + { + /// + /// Create or update a media segment. + /// + /// The segment. + /// New MediaSegment. + Task CreateMediaSegmentAsync(MediaSegment segment); + + /// + /// Create multiple new media segment. + /// + /// List of segments. + /// New MediaSegment. + Task> CreateMediaSegmentsAsync(IEnumerable segments); + + /// + /// Get all media segments. + /// + /// Optional: Just segments with itemId. + /// Optional: The typeIndex. + /// Optional: The segment type. + /// List of MediaSegment. + List GetAllMediaSegments(Guid itemId = default, int typeIndex = -1, MediaSegmentType? type = null); + + /// + /// Delete Media Segments. + /// + /// Optional: The itemId. + /// Optional: The typeIndex. + /// Optional: The segment type. + /// Deleted segments. + Task> DeleteSegmentsAsync(Guid itemId = default, int typeIndex = -1, MediaSegmentType? type = null); + } +} diff --git a/windows b/windows new file mode 160000 index 0000000000..5d228697a1 --- /dev/null +++ b/windows @@ -0,0 +1 @@ +Subproject commit 5d228697a1e297c276b3a452cf50fff440c2a704 From e90eb200ceee22541d872fcbbf14c39b8243df89 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Thu, 2 Nov 2023 20:15:04 +0100 Subject: [PATCH 02/37] feat: Add db migration --- ...1102191433_CreateMediaSegments.Designer.cs | 712 ++++++++++++++++++ .../20231102191433_CreateMediaSegments.cs | 43 ++ .../Migrations/JellyfinDbModelSnapshot.cs | 31 +- 3 files changed, 784 insertions(+), 2 deletions(-) create mode 100644 Jellyfin.Server.Implementations/Migrations/20231102191433_CreateMediaSegments.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20231102191433_CreateMediaSegments.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20231102191433_CreateMediaSegments.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20231102191433_CreateMediaSegments.Designer.cs new file mode 100644 index 0000000000..2997d03e97 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20231102191433_CreateMediaSegments.Designer.cs @@ -0,0 +1,712 @@ +// +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("20231102191433_CreateMediaSegments")] + partial class CreateMediaSegments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("TypeIndex") + .HasColumnType("INTEGER"); + + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("End") + .HasColumnType("REAL"); + + b.Property("Start") + .HasColumnType("REAL"); + + b.HasKey("ItemId", "Type", "TypeIndex"); + + b.HasIndex("ItemId"); + + b.ToTable("Segments"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.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/20231102191433_CreateMediaSegments.cs b/Jellyfin.Server.Implementations/Migrations/20231102191433_CreateMediaSegments.cs new file mode 100644 index 0000000000..e124800e41 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20231102191433_CreateMediaSegments.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class CreateMediaSegments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Segments", + columns: table => new + { + Type = table.Column(type: "INTEGER", nullable: false), + TypeIndex = table.Column(type: "INTEGER", nullable: false), + ItemId = table.Column(type: "TEXT", nullable: false), + Start = table.Column(type: "REAL", nullable: false), + End = table.Column(type: "REAL", nullable: false), + Action = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Segments", x => new { x.ItemId, x.Type, x.TypeIndex }); + }); + + migrationBuilder.CreateIndex( + name: "IX_Segments_ItemId", + table: "Segments", + column: "ItemId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Segments"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index f725ababe9..75b427f7e2 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.11"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -270,6 +270,33 @@ namespace Jellyfin.Server.Implementations.Migrations b.ToTable("ItemDisplayPreferences"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("TypeIndex") + .HasColumnType("INTEGER"); + + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("End") + .HasColumnType("REAL"); + + b.Property("Start") + .HasColumnType("REAL"); + + b.HasKey("ItemId", "Type", "TypeIndex"); + + b.HasIndex("ItemId"); + + b.ToTable("Segments"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => { b.Property("Id") From 7c90219a34f328f06df3663ac841facb9318739e Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Fri, 3 Nov 2023 01:00:57 +0100 Subject: [PATCH 03/37] temporary: fix api key regression --- windows | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows b/windows index 5d228697a1..ffc1ffb7c8 160000 --- a/windows +++ b/windows @@ -1 +1 @@ -Subproject commit 5d228697a1e297c276b3a452cf50fff440c2a704 +Subproject commit ffc1ffb7c869beb409b8c52a5fc3d3b5edec6f1e From dd1e9a7a4eb849a4029ede69e5e1c3a103c72784 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Fri, 10 Nov 2023 13:05:58 +0100 Subject: [PATCH 04/37] feat: add streamIndex --- .../Controllers/MediaSegmentController.cs | 17 ++++++++++++----- Jellyfin.Data/Entities/MediaSegment.cs | 8 +++++++- .../MediaSegments/MediaSegmentsManager.cs | 19 ++++++++++++------- .../MediaSegmentConfiguration.cs | 5 ++++- .../MediaSegments/IMediaSegmentsManager.cs | 10 ++++++---- 5 files changed, 41 insertions(+), 18 deletions(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentController.cs b/Jellyfin.Api/Controllers/MediaSegmentController.cs index 077f126d57..a9ddaab367 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentController.cs @@ -35,7 +35,8 @@ public class MediaSegmentController : BaseJellyfinApiController /// /// Get all media segments. /// - /// Optional: Just segments with itemId. + /// Optional: Just segments with MediaSourceId. + /// Optional: Just segments with MediaStreamIndex. /// Optional: All segments of type. /// Optional: All segments with typeIndex. /// Segments returned. @@ -44,10 +45,11 @@ public class MediaSegmentController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSegments( [FromQuery] Guid itemId, + [FromQuery, DefaultValue(-1)] int streamIndex, [FromQuery] MediaSegmentType? type, [FromQuery, DefaultValue(-1)] int typeIndex) { - var list = _mediaSegmentManager.GetAllMediaSegments(itemId, typeIndex, type); + var list = _mediaSegmentManager.GetAllMediaSegments(itemId, streamIndex, typeIndex, type); return new QueryResult(list); } @@ -57,7 +59,8 @@ public class MediaSegmentController : BaseJellyfinApiController /// /// Start position of segment in seconds. /// End position of segment in seconds. - /// Segment is associated with item id. + /// Segment is associated with MediaSourceId. + /// Segment is associated with MediaStreamIndex. /// Segment type. /// Optional: If you want to add a type multiple times to the same itemId increment it. /// Optional: Creator recommends an action. @@ -71,6 +74,7 @@ public class MediaSegmentController : BaseJellyfinApiController [FromQuery, Required] double start, [FromQuery, Required] double end, [FromQuery, Required] Guid itemId, + [FromQuery, Required] int streamIndex, [FromQuery, Required] MediaSegmentType type, [FromQuery, DefaultValue(0)] int typeIndex, [FromQuery, DefaultValue(MediaSegmentAction.Auto)] MediaSegmentAction action) @@ -80,6 +84,7 @@ public class MediaSegmentController : BaseJellyfinApiController Start = start, End = end, ItemId = itemId, + StreamIndex = streamIndex, Type = type, TypeIndex = typeIndex, Action = action @@ -112,7 +117,8 @@ public class MediaSegmentController : BaseJellyfinApiController /// /// Delete media segments. All query parameters can be freely defined. /// - /// Optional: All segments with itemId. + /// Optional: All segments with MediaSourceId. + /// Segment is associated with MediaStreamIndex. /// Optional: All segments of type. /// Optional: All segments with typeIndex. /// Segments returned. @@ -123,10 +129,11 @@ public class MediaSegmentController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task>> DeleteSegments( [FromQuery] Guid itemId, + [FromQuery, DefaultValue(-1)] int streamIndex, [FromQuery] MediaSegmentType? type, [FromQuery, DefaultValue(-1)] int typeIndex) { - var list = await _mediaSegmentManager.DeleteSegmentsAsync(itemId, typeIndex, type).ConfigureAwait(false); + var list = await _mediaSegmentManager.DeleteSegmentsAsync(itemId, streamIndex, typeIndex, type).ConfigureAwait(false); return new QueryResult(list); } diff --git a/Jellyfin.Data/Entities/MediaSegment.cs b/Jellyfin.Data/Entities/MediaSegment.cs index 79d37f3c4c..d2306111a9 100644 --- a/Jellyfin.Data/Entities/MediaSegment.cs +++ b/Jellyfin.Data/Entities/MediaSegment.cs @@ -36,11 +36,17 @@ namespace Jellyfin.Data.Entities public int TypeIndex { get; set; } /// - /// Gets or sets the associated ItemId. + /// Gets or sets the associated MediaSourceId. /// /// The id. public Guid ItemId { get; set; } + /// + /// Gets or sets the associated MediaStreamIndex. + /// + /// The id. + public int StreamIndex { get; set; } + /// /// Gets or sets the creator recommended action. Can be overwritten with user defined action. /// diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs index d514534523..2026e340f7 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs @@ -77,7 +77,7 @@ namespace Jellyfin.Server.Implementations.MediaSegments { ValidateSegment(segment); - var found = foundSegments.Where(s => s.ItemId.Equals(segment.ItemId) && s.Type.Equals(segment.Type) && s.TypeIndex.Equals(segment.TypeIndex)).FirstOrDefault(); + var found = foundSegments.Where(s => s.ItemId.Equals(segment.ItemId) && s.StreamIndex.Equals(segment.StreamIndex) && s.Type.Equals(segment.Type) && s.TypeIndex.Equals(segment.TypeIndex)).FirstOrDefault(); AddOrUpdateSegment(dbContext, segment, found); } @@ -89,7 +89,7 @@ namespace Jellyfin.Server.Implementations.MediaSegments } /// - public List GetAllMediaSegments(Guid itemId = default, int typeIndex = -1, MediaSegmentType? type = null) + public List GetAllMediaSegments(Guid itemId = default, int streamIndex = -1, int typeIndex = -1, MediaSegmentType? type = null) { var allSegments = new List(); @@ -103,6 +103,11 @@ namespace Jellyfin.Server.Implementations.MediaSegments queryable = queryable.Where(s => s.ItemId.Equals(itemId)); } + if (!streamIndex.Equals(-1)) + { + queryable = queryable.Where(s => s.StreamIndex.Equals(streamIndex)); + } + if (!type.Equals(null)) { queryable = queryable.Where(s => s.Type.Equals(type)); @@ -169,23 +174,23 @@ namespace Jellyfin.Server.Implementations.MediaSegments } /// - public async Task> DeleteSegmentsAsync(Guid itemId = default, int typeIndex = -1, MediaSegmentType? type = null) + public async Task> DeleteSegmentsAsync(Guid itemId, int streamIndex = -1, int typeIndex = -1, MediaSegmentType? type = null) { var allSegments = new List(); if (itemId.Equals(default)) { - throw new ArgumentException($"itemId are not set. Please provide at least one."); + throw new ArgumentException($"itemId is not set. Please provide one."); } var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - IQueryable queryable = dbContext.Segments.Select(s => s); + IQueryable queryable = dbContext.Segments.Where(s => s.ItemId.Equals(itemId)); - if (!itemId.Equals(default)) + if (!streamIndex.Equals(-1)) { - queryable = queryable.Where(s => s.ItemId.Equals(itemId)); + queryable = queryable.Where(s => s.StreamIndex.Equals(streamIndex)); } if (!type.Equals(null)) diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs index 023d3b98b4..57f948b526 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs +++ b/Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs @@ -27,11 +27,14 @@ namespace Jellyfin.Server.Implementations.ModelConfiguration builder .Property(s => s.ItemId) .IsRequired(); + builder + .Property(s => s.StreamIndex) + .IsRequired(); builder .Property(s => s.Action) .IsRequired(); builder - .HasKey(s => new { s.ItemId, s.Type, s.TypeIndex }); + .HasKey(s => new { s.ItemId, s.StreamIndex, s.Type, s.TypeIndex }); builder .HasIndex(s => s.ItemId); } diff --git a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs index 0baf506d91..b338e2c994 100644 --- a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs +++ b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs @@ -29,19 +29,21 @@ namespace MediaBrowser.Model.MediaSegments /// /// Get all media segments. /// - /// Optional: Just segments with itemId. + /// Optional: Just segments with MediaSourceId. + /// Optional: Just segments with MediaStreamIndex. /// Optional: The typeIndex. /// Optional: The segment type. /// List of MediaSegment. - List GetAllMediaSegments(Guid itemId = default, int typeIndex = -1, MediaSegmentType? type = null); + List GetAllMediaSegments(Guid itemId = default, int streamIndex = -1, int typeIndex = -1, MediaSegmentType? type = null); /// /// Delete Media Segments. /// - /// Optional: The itemId. + /// Required: The MediaSourceId. + /// Optional: Just segments with MediaStreamIndex. /// Optional: The typeIndex. /// Optional: The segment type. /// Deleted segments. - Task> DeleteSegmentsAsync(Guid itemId = default, int typeIndex = -1, MediaSegmentType? type = null); + Task> DeleteSegmentsAsync(Guid itemId, int streamIndex = -1, int typeIndex = -1, MediaSegmentType? type = null); } } From 239f0bc6944a57b1bde035c1895609d333a6579c Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Fri, 10 Nov 2023 13:13:35 +0100 Subject: [PATCH 05/37] refactor: migrate to Ticks --- Jellyfin.Api/Controllers/MediaSegmentController.cs | 12 ++++++------ Jellyfin.Data/Entities/MediaSegment.cs | 8 ++++---- .../MediaSegments/MediaSegmentsManager.cs | 8 ++++---- .../ModelConfiguration/MediaSegmentConfiguration.cs | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentController.cs b/Jellyfin.Api/Controllers/MediaSegmentController.cs index a9ddaab367..7439b6ddae 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentController.cs @@ -57,8 +57,8 @@ public class MediaSegmentController : BaseJellyfinApiController /// /// Create or update a media segment. You can update start/end/action. /// - /// Start position of segment in seconds. - /// End position of segment in seconds. + /// Start position of segment in Ticks. + /// End position of segment in Ticks. /// Segment is associated with MediaSourceId. /// Segment is associated with MediaStreamIndex. /// Segment type. @@ -71,8 +71,8 @@ public class MediaSegmentController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task>> PostSegment( - [FromQuery, Required] double start, - [FromQuery, Required] double end, + [FromQuery, Required] long startTicks, + [FromQuery, Required] long endTicks, [FromQuery, Required] Guid itemId, [FromQuery, Required] int streamIndex, [FromQuery, Required] MediaSegmentType type, @@ -81,8 +81,8 @@ public class MediaSegmentController : BaseJellyfinApiController { var newMediaSegment = new MediaSegment() { - Start = start, - End = end, + StartTicks = startTicks, + EndTicks = endTicks, ItemId = itemId, StreamIndex = streamIndex, Type = type, diff --git a/Jellyfin.Data/Entities/MediaSegment.cs b/Jellyfin.Data/Entities/MediaSegment.cs index d2306111a9..2582899753 100644 --- a/Jellyfin.Data/Entities/MediaSegment.cs +++ b/Jellyfin.Data/Entities/MediaSegment.cs @@ -12,16 +12,16 @@ namespace Jellyfin.Data.Entities public class MediaSegment { /// - /// Gets or sets the start position in seconds. + /// Gets or sets the start position in Ticks. /// /// The start position. - public double Start { get; set; } + public long StartTicks { get; set; } /// - /// Gets or sets the end position in seconds. + /// Gets or sets the end position in Ticks. /// /// The end position. - public double End { get; set; } + public long EndTicks { get; set; } /// /// Gets or sets the Type. diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs index 2026e340f7..6680c01072 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs @@ -134,8 +134,8 @@ namespace Jellyfin.Server.Implementations.MediaSegments { if (found != null) { - found.Start = segment.Start; - found.End = segment.End; + found.StartTicks = segment.StartTicks; + found.EndTicks = segment.EndTicks; found.Action = segment.Action; } else @@ -155,9 +155,9 @@ namespace Jellyfin.Server.Implementations.MediaSegments throw new ArgumentException($"itemId is default: itemId={segment.ItemId} for segment with type '{segment.Type}.{segment.TypeIndex}'"); } - if (segment.Start >= segment.End) + if (segment.StartTicks >= segment.EndTicks) { - throw new ArgumentException($"start >= end: {segment.Start}>={segment.End} for segment itemId '{segment.ItemId}' with type '{segment.Type}.{segment.TypeIndex}'"); + throw new ArgumentException($"start >= end: {segment.StartTicks}>={segment.EndTicks} for segment itemId '{segment.ItemId}' with type '{segment.Type}.{segment.TypeIndex}'"); } } diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs index 57f948b526..14803eb303 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs +++ b/Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs @@ -13,10 +13,10 @@ namespace Jellyfin.Server.Implementations.ModelConfiguration public void Configure(EntityTypeBuilder builder) { builder - .Property(s => s.Start) + .Property(s => s.StartTicks) .IsRequired(); builder - .Property(s => s.End) + .Property(s => s.EndTicks) .IsRequired(); builder .Property(s => s.Type) From 4e3dffee66a096d8dc605c0efda07eb0c31b979d Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Sat, 11 Nov 2023 02:38:07 +0100 Subject: [PATCH 06/37] Revert "feat: Add db migration" This reverts commit 5c2fc9f1f687d5ff5c87972a157f890678ee8fab. --- ...1102191433_CreateMediaSegments.Designer.cs | 712 ------------------ .../20231102191433_CreateMediaSegments.cs | 43 -- .../Migrations/JellyfinDbModelSnapshot.cs | 31 +- 3 files changed, 2 insertions(+), 784 deletions(-) delete mode 100644 Jellyfin.Server.Implementations/Migrations/20231102191433_CreateMediaSegments.Designer.cs delete mode 100644 Jellyfin.Server.Implementations/Migrations/20231102191433_CreateMediaSegments.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20231102191433_CreateMediaSegments.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20231102191433_CreateMediaSegments.Designer.cs deleted file mode 100644 index 2997d03e97..0000000000 --- a/Jellyfin.Server.Implementations/Migrations/20231102191433_CreateMediaSegments.Designer.cs +++ /dev/null @@ -1,712 +0,0 @@ -// -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("20231102191433_CreateMediaSegments")] - partial class CreateMediaSegments - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); - - modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DayOfWeek") - .HasColumnType("INTEGER"); - - b.Property("EndHour") - .HasColumnType("REAL"); - - b.Property("StartHour") - .HasColumnType("REAL"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AccessSchedules"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("LogSeverity") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("Overview") - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ShortOverview") - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DateCreated"); - - b.ToTable("ActivityLogs"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("Key") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client", "Key") - .IsUnique(); - - b.ToTable("CustomItemDisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChromecastVersion") - .HasColumnType("INTEGER"); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("DashboardTheme") - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("EnableNextVideoInfoOverlay") - .HasColumnType("INTEGER"); - - b.Property("IndexBy") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("ScrollDirection") - .HasColumnType("INTEGER"); - - b.Property("ShowBackdrop") - .HasColumnType("INTEGER"); - - b.Property("ShowSidebar") - .HasColumnType("INTEGER"); - - b.Property("SkipBackwardLength") - .HasColumnType("INTEGER"); - - b.Property("SkipForwardLength") - .HasColumnType("INTEGER"); - - b.Property("TvHome") - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client") - .IsUnique(); - - b.ToTable("DisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DisplayPreferencesId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("DisplayPreferencesId"); - - b.ToTable("HomeSection"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("Path") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("ImageInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("IndexBy") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("RememberIndexing") - .HasColumnType("INTEGER"); - - b.Property("RememberSorting") - .HasColumnType("INTEGER"); - - b.Property("SortBy") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("ViewType") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("ItemDisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.Property("TypeIndex") - .HasColumnType("INTEGER"); - - b.Property("Action") - .HasColumnType("INTEGER"); - - b.Property("End") - .HasColumnType("REAL"); - - b.Property("Start") - .HasColumnType("REAL"); - - b.HasKey("ItemId", "Type", "TypeIndex"); - - b.HasIndex("ItemId"); - - b.ToTable("Segments"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("Permission_Permissions_Guid") - .HasColumnType("TEXT"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Permissions"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("Preference_Preferences_Guid") - .HasColumnType("TEXT"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(65535) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Preferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("DateLastActivity") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AccessToken") - .IsUnique(); - - b.ToTable("ApiKeys"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("AppName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property("AppVersion") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("DateLastActivity") - .HasColumnType("TEXT"); - - b.Property("DateModified") - .HasColumnType("TEXT"); - - b.Property("DeviceId") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("DeviceName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property("IsActive") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId"); - - b.HasIndex("AccessToken", "DateLastActivity"); - - b.HasIndex("DeviceId", "DateLastActivity"); - - b.HasIndex("UserId", "DeviceId"); - - b.ToTable("Devices"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CustomName") - .HasColumnType("TEXT"); - - b.Property("DeviceId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId") - .IsUnique(); - - b.ToTable("DeviceOptions"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("Width") - .HasColumnType("INTEGER"); - - b.Property("Bandwidth") - .HasColumnType("INTEGER"); - - b.Property("Height") - .HasColumnType("INTEGER"); - - b.Property("Interval") - .HasColumnType("INTEGER"); - - b.Property("ThumbnailCount") - .HasColumnType("INTEGER"); - - b.Property("TileHeight") - .HasColumnType("INTEGER"); - - b.Property("TileWidth") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId", "Width"); - - b.ToTable("TrickplayInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("AudioLanguagePreference") - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("AuthenticationProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("CastReceiverId") - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("DisplayCollectionsView") - .HasColumnType("INTEGER"); - - b.Property("DisplayMissingEpisodes") - .HasColumnType("INTEGER"); - - b.Property("EnableAutoLogin") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalPassword") - .HasColumnType("INTEGER"); - - b.Property("EnableNextEpisodeAutoPlay") - .HasColumnType("INTEGER"); - - b.Property("EnableUserPreferenceAccess") - .HasColumnType("INTEGER"); - - b.Property("HidePlayedInLatest") - .HasColumnType("INTEGER"); - - b.Property("InternalId") - .HasColumnType("INTEGER"); - - b.Property("InvalidLoginAttemptCount") - .HasColumnType("INTEGER"); - - b.Property("LastActivityDate") - .HasColumnType("TEXT"); - - b.Property("LastLoginDate") - .HasColumnType("TEXT"); - - b.Property("LoginAttemptsBeforeLockout") - .HasColumnType("INTEGER"); - - b.Property("MaxActiveSessions") - .HasColumnType("INTEGER"); - - b.Property("MaxParentalAgeRating") - .HasColumnType("INTEGER"); - - b.Property("MustUpdatePassword") - .HasColumnType("INTEGER"); - - b.Property("Password") - .HasMaxLength(65535) - .HasColumnType("TEXT"); - - b.Property("PasswordResetProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("PlayDefaultAudioTrack") - .HasColumnType("INTEGER"); - - b.Property("RememberAudioSelections") - .HasColumnType("INTEGER"); - - b.Property("RememberSubtitleSelections") - .HasColumnType("INTEGER"); - - b.Property("RemoteClientBitrateLimit") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SubtitleLanguagePreference") - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("SubtitleMode") - .HasColumnType("INTEGER"); - - b.Property("SyncPlayAccess") - .HasColumnType("INTEGER"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT") - .UseCollation("NOCASE"); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.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/20231102191433_CreateMediaSegments.cs b/Jellyfin.Server.Implementations/Migrations/20231102191433_CreateMediaSegments.cs deleted file mode 100644 index e124800e41..0000000000 --- a/Jellyfin.Server.Implementations/Migrations/20231102191433_CreateMediaSegments.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Jellyfin.Server.Implementations.Migrations -{ - /// - public partial class CreateMediaSegments : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Segments", - columns: table => new - { - Type = table.Column(type: "INTEGER", nullable: false), - TypeIndex = table.Column(type: "INTEGER", nullable: false), - ItemId = table.Column(type: "TEXT", nullable: false), - Start = table.Column(type: "REAL", nullable: false), - End = table.Column(type: "REAL", nullable: false), - Action = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Segments", x => new { x.ItemId, x.Type, x.TypeIndex }); - }); - - migrationBuilder.CreateIndex( - name: "IX_Segments_ItemId", - table: "Segments", - column: "ItemId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Segments"); - } - } -} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index 75b427f7e2..f725ababe9 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.13"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.11"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -270,33 +270,6 @@ namespace Jellyfin.Server.Implementations.Migrations b.ToTable("ItemDisplayPreferences"); }); - modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.Property("TypeIndex") - .HasColumnType("INTEGER"); - - b.Property("Action") - .HasColumnType("INTEGER"); - - b.Property("End") - .HasColumnType("REAL"); - - b.Property("Start") - .HasColumnType("REAL"); - - b.HasKey("ItemId", "Type", "TypeIndex"); - - b.HasIndex("ItemId"); - - b.ToTable("Segments"); - }); - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => { b.Property("Id") From 508a7516faec9d7b7058f32e17a2fb89b8673352 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Sat, 11 Nov 2023 02:39:28 +0100 Subject: [PATCH 07/37] feat: add db migration --- ...1111013836_CreateMediaSegments.Designer.cs | 715 ++++++++++++++++++ .../20231111013836_CreateMediaSegments.cs | 44 ++ .../Migrations/JellyfinDbModelSnapshot.cs | 34 +- 3 files changed, 791 insertions(+), 2 deletions(-) create mode 100644 Jellyfin.Server.Implementations/Migrations/20231111013836_CreateMediaSegments.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20231111013836_CreateMediaSegments.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20231111013836_CreateMediaSegments.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20231111013836_CreateMediaSegments.Designer.cs new file mode 100644 index 0000000000..b185790720 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20231111013836_CreateMediaSegments.Designer.cs @@ -0,0 +1,715 @@ +// +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("20231111013836_CreateMediaSegments")] + partial class CreateMediaSegments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("TypeIndex") + .HasColumnType("INTEGER"); + + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex", "Type", "TypeIndex"); + + b.HasIndex("ItemId"); + + b.ToTable("Segments"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.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/20231111013836_CreateMediaSegments.cs b/Jellyfin.Server.Implementations/Migrations/20231111013836_CreateMediaSegments.cs new file mode 100644 index 0000000000..432e54b827 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20231111013836_CreateMediaSegments.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class CreateMediaSegments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Segments", + columns: table => new + { + Type = table.Column(type: "INTEGER", nullable: false), + TypeIndex = table.Column(type: "INTEGER", nullable: false), + ItemId = table.Column(type: "TEXT", nullable: false), + StreamIndex = table.Column(type: "INTEGER", nullable: false), + StartTicks = table.Column(type: "INTEGER", nullable: false), + EndTicks = table.Column(type: "INTEGER", nullable: false), + Action = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Segments", x => new { x.ItemId, x.StreamIndex, x.Type, x.TypeIndex }); + }); + + migrationBuilder.CreateIndex( + name: "IX_Segments_ItemId", + table: "Segments", + column: "ItemId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Segments"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index f725ababe9..ef0602ec22 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.11"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -270,6 +270,36 @@ namespace Jellyfin.Server.Implementations.Migrations b.ToTable("ItemDisplayPreferences"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("TypeIndex") + .HasColumnType("INTEGER"); + + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex", "Type", "TypeIndex"); + + b.HasIndex("ItemId"); + + b.ToTable("Segments"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => { b.Property("Id") From 14dda8726b2b3683776fb6d136c737fbfb4eb548 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Sun, 12 Nov 2023 16:29:40 +0100 Subject: [PATCH 08/37] feat: Add api integration --- Emby.Server.Implementations/Dto/DtoService.cs | 19 ++++++++++++++++++- MediaBrowser.Model/Dto/BaseItemDto.cs | 6 ++++++ MediaBrowser.Model/Querying/ItemFields.cs | 5 +++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 7812687ea3..0b847603c0 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -24,6 +24,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.MediaSegments; using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; using Book = MediaBrowser.Controller.Entities.Book; @@ -53,6 +54,7 @@ namespace Emby.Server.Implementations.Dto private readonly Lazy _livetvManagerFactory; private readonly ITrickplayManager _trickplayManager; + private readonly IMediaSegmentsManager _mediaSegmentsManager; public DtoService( ILogger logger, @@ -65,7 +67,8 @@ namespace Emby.Server.Implementations.Dto IApplicationHost appHost, IMediaSourceManager mediaSourceManager, Lazy livetvManagerFactory, - ITrickplayManager trickplayManager) + ITrickplayManager trickplayManager, + IMediaSegmentsManager mediaSegmentsManager) { _logger = logger; _libraryManager = libraryManager; @@ -78,6 +81,7 @@ namespace Emby.Server.Implementations.Dto _mediaSourceManager = mediaSourceManager; _livetvManagerFactory = livetvManagerFactory; _trickplayManager = trickplayManager; + _mediaSegmentsManager = mediaSegmentsManager; } private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; @@ -1064,6 +1068,19 @@ namespace Emby.Server.Implementations.Dto } } + // Add MediaSegments for all media sources + if (dto.MediaSources is not null && options.ContainsField(ItemFields.MediaSegments)) + { + var allSegments = new List(); + + foreach (var source in dto.MediaSources) + { + allSegments.AddRange(_mediaSegmentsManager.GetAllMediaSegments(Guid.Parse(source.Id))); + } + + dto.MediaSegments = allSegments; + } + if (options.ContainsField(ItemFields.MediaStreams)) { // Add VideoInfo diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index cfff717db2..7547524d70 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -570,6 +570,12 @@ namespace MediaBrowser.Model.Dto /// The trickplay manifest. public Dictionary> Trickplay { get; set; } + /// + /// Gets or sets the media segments data. + /// + /// The media segments. + public List MediaSegments { get; set; } + /// /// Gets or sets the type of the location. /// diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index 49d7c0bcb0..b5ec12df0c 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -39,6 +39,11 @@ namespace MediaBrowser.Model.Querying /// Trickplay, + /// + /// The MediaSegments data. + /// + MediaSegments, + ChildCount, /// From 6dd21d43ade36f4a50eccb346d9b0afc61e4797f Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:28:14 +0100 Subject: [PATCH 09/37] fix: api elevation required --- Jellyfin.Api/Controllers/MediaSegmentController.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jellyfin.Api/Controllers/MediaSegmentController.cs b/Jellyfin.Api/Controllers/MediaSegmentController.cs index 7439b6ddae..ab1b9636ff 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentController.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Api.Constants; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Model.MediaSegments; @@ -68,6 +69,7 @@ public class MediaSegmentController : BaseJellyfinApiController /// Missing query parameter. /// An containing the queryresult of segment. [HttpPost("Segment")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task>> PostSegment( @@ -104,6 +106,7 @@ public class MediaSegmentController : BaseJellyfinApiController /// Invalid segments. /// An containing the queryresult of segment. [HttpPost] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task>> PostSegments( @@ -125,6 +128,7 @@ public class MediaSegmentController : BaseJellyfinApiController /// Missing query parameter. /// An containing the queryresult of segments. [HttpDelete] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task>> DeleteSegments( From f7774714ea15c71b700a2c5b126756531515055a Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:29:58 +0100 Subject: [PATCH 10/37] doc: more doc comments --- Jellyfin.Data/Entities/MediaSegment.cs | 2 +- Jellyfin.Data/Enums/MediaSegmentAction.cs | 2 +- Jellyfin.Data/Enums/MediaSegmentType.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Data/Entities/MediaSegment.cs b/Jellyfin.Data/Entities/MediaSegment.cs index 2582899753..173e7b6166 100644 --- a/Jellyfin.Data/Entities/MediaSegment.cs +++ b/Jellyfin.Data/Entities/MediaSegment.cs @@ -7,7 +7,7 @@ using Jellyfin.Data.Enums; namespace Jellyfin.Data.Entities { /// - /// Class MediaSegment. + /// A moment in time of a media stream (ItemId+StreamIndex) with Type and possible Action applicable between StartTicks/Endticks. /// public class MediaSegment { diff --git a/Jellyfin.Data/Enums/MediaSegmentAction.cs b/Jellyfin.Data/Enums/MediaSegmentAction.cs index c4cff97885..f585d5a5e3 100644 --- a/Jellyfin.Data/Enums/MediaSegmentAction.cs +++ b/Jellyfin.Data/Enums/MediaSegmentAction.cs @@ -1,7 +1,7 @@ namespace Jellyfin.Data.Enums { /// - /// Enum MediaSegmentAction. + /// An enum representing the Action of MediaSegment. /// public enum MediaSegmentAction { diff --git a/Jellyfin.Data/Enums/MediaSegmentType.cs b/Jellyfin.Data/Enums/MediaSegmentType.cs index b6ac2bede6..d14fbb2519 100644 --- a/Jellyfin.Data/Enums/MediaSegmentType.cs +++ b/Jellyfin.Data/Enums/MediaSegmentType.cs @@ -1,7 +1,7 @@ namespace Jellyfin.Data.Enums { /// - /// Enum MediaSegmentType. + /// An enum representing the Type of MediaSegment. /// public enum MediaSegmentType { From b8f84b5607466ce4853b252316a1cfe1abddd039 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Wed, 21 Feb 2024 00:44:57 +0100 Subject: [PATCH 11/37] fix: better async impl --- .../MediaSegments/MediaSegmentsManager.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs index 6680c01072..57765a8af1 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs @@ -163,14 +163,12 @@ namespace Jellyfin.Server.Implementations.MediaSegments /// /// Delete all segments when itemid is deleted from library. - /// TODO: Do not block. /// /// The sending entity. /// The . - private void LibraryManagerItemRemoved(object? sender, ItemChangeEventArgs itemChangeEventArgs) + private async void LibraryManagerItemRemoved(object? sender, ItemChangeEventArgs itemChangeEventArgs) { - var task = Task.Run(async () => { await DeleteSegmentsAsync(itemChangeEventArgs.Item.Id).ConfigureAwait(false); }); - task.Wait(); + await DeleteSegmentsAsync(itemChangeEventArgs.Item.Id).ConfigureAwait(false); } /// From ac42b50c2cf1d0fa6ad39ef5cebdbed02252ed56 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Sat, 24 Feb 2024 01:14:27 +0100 Subject: [PATCH 12/37] Revert "feat: add db migration" This reverts commit 3c0fb17cb42783a3c5e56ecca8463032768e5e65. --- ...1111013836_CreateMediaSegments.Designer.cs | 715 ------------------ .../20231111013836_CreateMediaSegments.cs | 44 -- .../Migrations/JellyfinDbModelSnapshot.cs | 34 +- 3 files changed, 2 insertions(+), 791 deletions(-) delete mode 100644 Jellyfin.Server.Implementations/Migrations/20231111013836_CreateMediaSegments.Designer.cs delete mode 100644 Jellyfin.Server.Implementations/Migrations/20231111013836_CreateMediaSegments.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20231111013836_CreateMediaSegments.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20231111013836_CreateMediaSegments.Designer.cs deleted file mode 100644 index b185790720..0000000000 --- a/Jellyfin.Server.Implementations/Migrations/20231111013836_CreateMediaSegments.Designer.cs +++ /dev/null @@ -1,715 +0,0 @@ -// -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("20231111013836_CreateMediaSegments")] - partial class CreateMediaSegments - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); - - modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DayOfWeek") - .HasColumnType("INTEGER"); - - b.Property("EndHour") - .HasColumnType("REAL"); - - b.Property("StartHour") - .HasColumnType("REAL"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AccessSchedules"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("LogSeverity") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("Overview") - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ShortOverview") - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DateCreated"); - - b.ToTable("ActivityLogs"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("Key") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client", "Key") - .IsUnique(); - - b.ToTable("CustomItemDisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChromecastVersion") - .HasColumnType("INTEGER"); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("DashboardTheme") - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("EnableNextVideoInfoOverlay") - .HasColumnType("INTEGER"); - - b.Property("IndexBy") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("ScrollDirection") - .HasColumnType("INTEGER"); - - b.Property("ShowBackdrop") - .HasColumnType("INTEGER"); - - b.Property("ShowSidebar") - .HasColumnType("INTEGER"); - - b.Property("SkipBackwardLength") - .HasColumnType("INTEGER"); - - b.Property("SkipForwardLength") - .HasColumnType("INTEGER"); - - b.Property("TvHome") - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client") - .IsUnique(); - - b.ToTable("DisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DisplayPreferencesId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("DisplayPreferencesId"); - - b.ToTable("HomeSection"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("Path") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("ImageInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("IndexBy") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("RememberIndexing") - .HasColumnType("INTEGER"); - - b.Property("RememberSorting") - .HasColumnType("INTEGER"); - - b.Property("SortBy") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("ViewType") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("ItemDisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("StreamIndex") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.Property("TypeIndex") - .HasColumnType("INTEGER"); - - b.Property("Action") - .HasColumnType("INTEGER"); - - b.Property("EndTicks") - .HasColumnType("INTEGER"); - - b.Property("StartTicks") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId", "StreamIndex", "Type", "TypeIndex"); - - b.HasIndex("ItemId"); - - b.ToTable("Segments"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("Permission_Permissions_Guid") - .HasColumnType("TEXT"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Permissions"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("Preference_Preferences_Guid") - .HasColumnType("TEXT"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(65535) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Preferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("DateLastActivity") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AccessToken") - .IsUnique(); - - b.ToTable("ApiKeys"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("AppName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property("AppVersion") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("DateLastActivity") - .HasColumnType("TEXT"); - - b.Property("DateModified") - .HasColumnType("TEXT"); - - b.Property("DeviceId") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("DeviceName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property("IsActive") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId"); - - b.HasIndex("AccessToken", "DateLastActivity"); - - b.HasIndex("DeviceId", "DateLastActivity"); - - b.HasIndex("UserId", "DeviceId"); - - b.ToTable("Devices"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CustomName") - .HasColumnType("TEXT"); - - b.Property("DeviceId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId") - .IsUnique(); - - b.ToTable("DeviceOptions"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("Width") - .HasColumnType("INTEGER"); - - b.Property("Bandwidth") - .HasColumnType("INTEGER"); - - b.Property("Height") - .HasColumnType("INTEGER"); - - b.Property("Interval") - .HasColumnType("INTEGER"); - - b.Property("ThumbnailCount") - .HasColumnType("INTEGER"); - - b.Property("TileHeight") - .HasColumnType("INTEGER"); - - b.Property("TileWidth") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId", "Width"); - - b.ToTable("TrickplayInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("AudioLanguagePreference") - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("AuthenticationProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("CastReceiverId") - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("DisplayCollectionsView") - .HasColumnType("INTEGER"); - - b.Property("DisplayMissingEpisodes") - .HasColumnType("INTEGER"); - - b.Property("EnableAutoLogin") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalPassword") - .HasColumnType("INTEGER"); - - b.Property("EnableNextEpisodeAutoPlay") - .HasColumnType("INTEGER"); - - b.Property("EnableUserPreferenceAccess") - .HasColumnType("INTEGER"); - - b.Property("HidePlayedInLatest") - .HasColumnType("INTEGER"); - - b.Property("InternalId") - .HasColumnType("INTEGER"); - - b.Property("InvalidLoginAttemptCount") - .HasColumnType("INTEGER"); - - b.Property("LastActivityDate") - .HasColumnType("TEXT"); - - b.Property("LastLoginDate") - .HasColumnType("TEXT"); - - b.Property("LoginAttemptsBeforeLockout") - .HasColumnType("INTEGER"); - - b.Property("MaxActiveSessions") - .HasColumnType("INTEGER"); - - b.Property("MaxParentalAgeRating") - .HasColumnType("INTEGER"); - - b.Property("MustUpdatePassword") - .HasColumnType("INTEGER"); - - b.Property("Password") - .HasMaxLength(65535) - .HasColumnType("TEXT"); - - b.Property("PasswordResetProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("PlayDefaultAudioTrack") - .HasColumnType("INTEGER"); - - b.Property("RememberAudioSelections") - .HasColumnType("INTEGER"); - - b.Property("RememberSubtitleSelections") - .HasColumnType("INTEGER"); - - b.Property("RemoteClientBitrateLimit") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SubtitleLanguagePreference") - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("SubtitleMode") - .HasColumnType("INTEGER"); - - b.Property("SyncPlayAccess") - .HasColumnType("INTEGER"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT") - .UseCollation("NOCASE"); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.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/20231111013836_CreateMediaSegments.cs b/Jellyfin.Server.Implementations/Migrations/20231111013836_CreateMediaSegments.cs deleted file mode 100644 index 432e54b827..0000000000 --- a/Jellyfin.Server.Implementations/Migrations/20231111013836_CreateMediaSegments.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Jellyfin.Server.Implementations.Migrations -{ - /// - public partial class CreateMediaSegments : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Segments", - columns: table => new - { - Type = table.Column(type: "INTEGER", nullable: false), - TypeIndex = table.Column(type: "INTEGER", nullable: false), - ItemId = table.Column(type: "TEXT", nullable: false), - StreamIndex = table.Column(type: "INTEGER", nullable: false), - StartTicks = table.Column(type: "INTEGER", nullable: false), - EndTicks = table.Column(type: "INTEGER", nullable: false), - Action = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Segments", x => new { x.ItemId, x.StreamIndex, x.Type, x.TypeIndex }); - }); - - migrationBuilder.CreateIndex( - name: "IX_Segments_ItemId", - table: "Segments", - column: "ItemId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Segments"); - } - } -} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index ef0602ec22..f725ababe9 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.13"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.11"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -270,36 +270,6 @@ namespace Jellyfin.Server.Implementations.Migrations b.ToTable("ItemDisplayPreferences"); }); - modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("StreamIndex") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.Property("TypeIndex") - .HasColumnType("INTEGER"); - - b.Property("Action") - .HasColumnType("INTEGER"); - - b.Property("EndTicks") - .HasColumnType("INTEGER"); - - b.Property("StartTicks") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId", "StreamIndex", "Type", "TypeIndex"); - - b.HasIndex("ItemId"); - - b.ToTable("Segments"); - }); - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => { b.Property("Id") From c6486d05921ed3ce8d081be2a610c0f8fdbf551b Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Sat, 24 Feb 2024 01:26:45 +0100 Subject: [PATCH 13/37] feat: add db migration --- .../20240224002503_MediaSegments.Designer.cs | 718 ++++++++++++++++++ .../20240224002503_MediaSegments.cs | 45 ++ .../Migrations/JellyfinDbModelSnapshot.cs | 37 +- 3 files changed, 798 insertions(+), 2 deletions(-) create mode 100644 Jellyfin.Server.Implementations/Migrations/20240224002503_MediaSegments.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20240224002503_MediaSegments.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20240224002503_MediaSegments.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20240224002503_MediaSegments.Designer.cs new file mode 100644 index 0000000000..cbac379489 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20240224002503_MediaSegments.Designer.cs @@ -0,0 +1,718 @@ +// +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("20240224002503_MediaSegments")] + partial class MediaSegments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.2"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("TypeIndex") + .HasColumnType("INTEGER"); + + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex", "Type", "TypeIndex"); + + b.HasIndex("ItemId"); + + b.ToTable("Segments"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.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/20240224002503_MediaSegments.cs b/Jellyfin.Server.Implementations/Migrations/20240224002503_MediaSegments.cs new file mode 100644 index 0000000000..dc4867f617 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20240224002503_MediaSegments.cs @@ -0,0 +1,45 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class MediaSegments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Segments", + columns: table => new + { + Type = table.Column(type: "INTEGER", nullable: false), + TypeIndex = table.Column(type: "INTEGER", nullable: false), + ItemId = table.Column(type: "TEXT", nullable: false), + StreamIndex = table.Column(type: "INTEGER", nullable: false), + StartTicks = table.Column(type: "INTEGER", nullable: false), + EndTicks = table.Column(type: "INTEGER", nullable: false), + Action = table.Column(type: "INTEGER", nullable: false), + Comment = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Segments", x => new { x.ItemId, x.StreamIndex, x.Type, x.TypeIndex }); + }); + + migrationBuilder.CreateIndex( + name: "IX_Segments_ItemId", + table: "Segments", + column: "ItemId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Segments"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index f725ababe9..08f3e34a74 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.11"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.2"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -270,6 +270,39 @@ namespace Jellyfin.Server.Implementations.Migrations b.ToTable("ItemDisplayPreferences"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("TypeIndex") + .HasColumnType("INTEGER"); + + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex", "Type", "TypeIndex"); + + b.HasIndex("ItemId"); + + b.ToTable("Segments"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => { b.Property("Id") From e6d0003b19e09b8fcf34c6e3937996b0284bd33e Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Sat, 24 Feb 2024 01:51:06 +0100 Subject: [PATCH 14/37] feat: add Type 'Annotation' and 'Comment' field --- Jellyfin.Api/Controllers/MediaSegmentController.cs | 14 ++++++++++---- Jellyfin.Data/Entities/MediaSegment.cs | 6 ++++++ Jellyfin.Data/Enums/MediaSegmentType.cs | 5 +++++ .../MediaSegments/MediaSegmentsManager.cs | 1 + 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentController.cs b/Jellyfin.Api/Controllers/MediaSegmentController.cs index ab1b9636ff..fca3cfea6e 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentController.cs @@ -4,9 +4,9 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Api; using MediaBrowser.Model.MediaSegments; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; @@ -65,6 +65,7 @@ public class MediaSegmentController : BaseJellyfinApiController /// Segment type. /// Optional: If you want to add a type multiple times to the same itemId increment it. /// Optional: Creator recommends an action. + /// Optional: A comment. /// Segments returned. /// Missing query parameter. /// An containing the queryresult of segment. @@ -79,7 +80,8 @@ public class MediaSegmentController : BaseJellyfinApiController [FromQuery, Required] int streamIndex, [FromQuery, Required] MediaSegmentType type, [FromQuery, DefaultValue(0)] int typeIndex, - [FromQuery, DefaultValue(MediaSegmentAction.Auto)] MediaSegmentAction action) + [FromQuery, DefaultValue(MediaSegmentAction.Auto)] MediaSegmentAction action, + [FromQuery] string comment) { var newMediaSegment = new MediaSegment() { @@ -89,13 +91,17 @@ public class MediaSegmentController : BaseJellyfinApiController StreamIndex = streamIndex, Type = type, TypeIndex = typeIndex, - Action = action + Action = action, + Comment = comment }; var segment = await _mediaSegmentManager.CreateMediaSegmentAsync(newMediaSegment).ConfigureAwait(false); return new QueryResult( - new List { segment }); + new List + { + segment + }); } /// diff --git a/Jellyfin.Data/Entities/MediaSegment.cs b/Jellyfin.Data/Entities/MediaSegment.cs index 173e7b6166..a4445e924f 100644 --- a/Jellyfin.Data/Entities/MediaSegment.cs +++ b/Jellyfin.Data/Entities/MediaSegment.cs @@ -52,5 +52,11 @@ namespace Jellyfin.Data.Entities /// /// The media segment action. public MediaSegmentAction Action { get; set; } + + /// + /// Gets or sets a comment. + /// + /// The media segment action. + public string Comment { get; set; } } } diff --git a/Jellyfin.Data/Enums/MediaSegmentType.cs b/Jellyfin.Data/Enums/MediaSegmentType.cs index d14fbb2519..5c73d6845f 100644 --- a/Jellyfin.Data/Enums/MediaSegmentType.cs +++ b/Jellyfin.Data/Enums/MediaSegmentType.cs @@ -29,5 +29,10 @@ namespace Jellyfin.Data.Enums /// Commercial that interrupt the viewer. /// Commercial = 4, + + /// + /// A Comment or additional info. + /// + Annotation = 5, } } diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs index 57765a8af1..922e3a7f38 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs @@ -137,6 +137,7 @@ namespace Jellyfin.Server.Implementations.MediaSegments found.StartTicks = segment.StartTicks; found.EndTicks = segment.EndTicks; found.Action = segment.Action; + found.Comment = segment.Comment; } else { From 84abd51d099f84ea64fa9324252d5e8c41f3b059 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Sat, 24 Feb 2024 15:51:45 +0100 Subject: [PATCH 15/37] fix: remove QueryResult --- .../Controllers/MediaSegmentController.cs | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentController.cs b/Jellyfin.Api/Controllers/MediaSegmentController.cs index fca3cfea6e..074dd46d44 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentController.cs @@ -8,7 +8,6 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Api; using MediaBrowser.Model.MediaSegments; -using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -44,7 +43,7 @@ public class MediaSegmentController : BaseJellyfinApiController /// An containing the queryresult of segments. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetSegments( + public ActionResult> GetSegments( [FromQuery] Guid itemId, [FromQuery, DefaultValue(-1)] int streamIndex, [FromQuery] MediaSegmentType? type, @@ -52,7 +51,7 @@ public class MediaSegmentController : BaseJellyfinApiController { var list = _mediaSegmentManager.GetAllMediaSegments(itemId, streamIndex, typeIndex, type); - return new QueryResult(list); + return list; } /// @@ -73,7 +72,7 @@ public class MediaSegmentController : BaseJellyfinApiController [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task>> PostSegment( + public async Task> PostSegment( [FromQuery, Required] long startTicks, [FromQuery, Required] long endTicks, [FromQuery, Required] Guid itemId, @@ -97,11 +96,7 @@ public class MediaSegmentController : BaseJellyfinApiController var segment = await _mediaSegmentManager.CreateMediaSegmentAsync(newMediaSegment).ConfigureAwait(false); - return new QueryResult( - new List - { - segment - }); + return segment; } /// @@ -115,12 +110,12 @@ public class MediaSegmentController : BaseJellyfinApiController [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task>> PostSegments( + public async Task>> PostSegments( [FromBody, Required] IEnumerable segments) { var nsegments = await _mediaSegmentManager.CreateMediaSegmentsAsync(segments).ConfigureAwait(false); - return new QueryResult(nsegments.ToList()); + return nsegments.ToList(); } /// @@ -137,7 +132,7 @@ public class MediaSegmentController : BaseJellyfinApiController [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task>> DeleteSegments( + public async Task>> DeleteSegments( [FromQuery] Guid itemId, [FromQuery, DefaultValue(-1)] int streamIndex, [FromQuery] MediaSegmentType? type, @@ -145,6 +140,6 @@ public class MediaSegmentController : BaseJellyfinApiController { var list = await _mediaSegmentManager.DeleteSegmentsAsync(itemId, streamIndex, typeIndex, type).ConfigureAwait(false); - return new QueryResult(list); + return list; } } From a0f427995d80d42fba3193b41c0f9d88b23e7530 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Sat, 24 Feb 2024 16:40:44 +0100 Subject: [PATCH 16/37] update DeleteSegments controller/impl --- Jellyfin.Api/Controllers/MediaSegmentController.cs | 8 ++++---- .../MediaSegments/MediaSegmentsManager.cs | 6 +++--- MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentController.cs b/Jellyfin.Api/Controllers/MediaSegmentController.cs index 074dd46d44..b0ea4e31ca 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentController.cs @@ -131,12 +131,12 @@ public class MediaSegmentController : BaseJellyfinApiController [HttpDelete] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> DeleteSegments( - [FromQuery] Guid itemId, - [FromQuery, DefaultValue(-1)] int streamIndex, + [FromQuery, Required] Guid itemId, + [FromQuery] int? streamIndex, [FromQuery] MediaSegmentType? type, - [FromQuery, DefaultValue(-1)] int typeIndex) + [FromQuery] int? typeIndex) { var list = await _mediaSegmentManager.DeleteSegmentsAsync(itemId, streamIndex, typeIndex, type).ConfigureAwait(false); diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs index 922e3a7f38..3382f11fc1 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs @@ -173,7 +173,7 @@ namespace Jellyfin.Server.Implementations.MediaSegments } /// - public async Task> DeleteSegmentsAsync(Guid itemId, int streamIndex = -1, int typeIndex = -1, MediaSegmentType? type = null) + public async Task> DeleteSegmentsAsync(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null) { var allSegments = new List(); @@ -187,7 +187,7 @@ namespace Jellyfin.Server.Implementations.MediaSegments { IQueryable queryable = dbContext.Segments.Where(s => s.ItemId.Equals(itemId)); - if (!streamIndex.Equals(-1)) + if (!streamIndex.Equals(null)) { queryable = queryable.Where(s => s.StreamIndex.Equals(streamIndex)); } @@ -197,7 +197,7 @@ namespace Jellyfin.Server.Implementations.MediaSegments queryable = queryable.Where(s => s.Type.Equals(type)); } - if (typeIndex > -1) + if (!typeIndex.Equals(null)) { queryable = queryable.Where(s => s.TypeIndex.Equals(typeIndex)); } diff --git a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs index b338e2c994..af385fff33 100644 --- a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs +++ b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs @@ -44,6 +44,6 @@ namespace MediaBrowser.Model.MediaSegments /// Optional: The typeIndex. /// Optional: The segment type. /// Deleted segments. - Task> DeleteSegmentsAsync(Guid itemId, int streamIndex = -1, int typeIndex = -1, MediaSegmentType? type = null); + Task> DeleteSegmentsAsync(Guid itemId, int? streamIndex, int? typeIndex, MediaSegmentType? type); } } From 07f5d79bae9669b67f2847505806c0a23bbe39bc Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Sat, 24 Feb 2024 16:51:52 +0100 Subject: [PATCH 17/37] update DeleteSegmentsControler --- Jellyfin.Api/Controllers/MediaSegmentController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentController.cs b/Jellyfin.Api/Controllers/MediaSegmentController.cs index b0ea4e31ca..b893c36825 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentController.cs @@ -126,7 +126,7 @@ public class MediaSegmentController : BaseJellyfinApiController /// Optional: All segments of type. /// Optional: All segments with typeIndex. /// Segments returned. - /// Missing query parameter. + /// Segments not found. /// An containing the queryresult of segments. [HttpDelete] [Authorize(Policy = Policies.RequiresElevation)] From 48a6be2b847c76d66943b4b0f2b970646366d65a Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Sat, 24 Feb 2024 16:56:20 +0100 Subject: [PATCH 18/37] update controller docs --- Jellyfin.Api/Controllers/MediaSegmentController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentController.cs b/Jellyfin.Api/Controllers/MediaSegmentController.cs index b893c36825..0e87a0cf03 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentController.cs @@ -40,7 +40,7 @@ public class MediaSegmentController : BaseJellyfinApiController /// Optional: All segments of type. /// Optional: All segments with typeIndex. /// Segments returned. - /// An containing the queryresult of segments. + /// An containing the found segments. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSegments( @@ -67,7 +67,7 @@ public class MediaSegmentController : BaseJellyfinApiController /// Optional: A comment. /// Segments returned. /// Missing query parameter. - /// An containing the queryresult of segment. + /// An containing the segment. [HttpPost("Segment")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] @@ -105,7 +105,7 @@ public class MediaSegmentController : BaseJellyfinApiController /// All segments that should be added. /// Segments returned. /// Invalid segments. - /// An containing the queryresult of segment. + /// An containing the created/updated segments. [HttpPost] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] @@ -127,7 +127,7 @@ public class MediaSegmentController : BaseJellyfinApiController /// Optional: All segments with typeIndex. /// Segments returned. /// Segments not found. - /// An containing the queryresult of segments. + /// An containing the deleted segments. [HttpDelete] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] From c1b0259ae42083a99dd9f1ce89ef164e6c63c2a2 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Sat, 24 Feb 2024 17:24:14 +0100 Subject: [PATCH 19/37] add more nullable --- Jellyfin.Api/Controllers/MediaSegmentController.cs | 4 ++-- .../MediaSegments/MediaSegmentsManager.cs | 6 +++--- MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentController.cs b/Jellyfin.Api/Controllers/MediaSegmentController.cs index 0e87a0cf03..15f3edbdf7 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentController.cs @@ -45,9 +45,9 @@ public class MediaSegmentController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSegments( [FromQuery] Guid itemId, - [FromQuery, DefaultValue(-1)] int streamIndex, + [FromQuery] int? streamIndex, [FromQuery] MediaSegmentType? type, - [FromQuery, DefaultValue(-1)] int typeIndex) + [FromQuery] int? typeIndex) { var list = _mediaSegmentManager.GetAllMediaSegments(itemId, streamIndex, typeIndex, type); diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs index 3382f11fc1..af1655d825 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs @@ -89,7 +89,7 @@ namespace Jellyfin.Server.Implementations.MediaSegments } /// - public List GetAllMediaSegments(Guid itemId = default, int streamIndex = -1, int typeIndex = -1, MediaSegmentType? type = null) + public List GetAllMediaSegments(Guid itemId = default, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null) { var allSegments = new List(); @@ -103,7 +103,7 @@ namespace Jellyfin.Server.Implementations.MediaSegments queryable = queryable.Where(s => s.ItemId.Equals(itemId)); } - if (!streamIndex.Equals(-1)) + if (!streamIndex.Equals(null)) { queryable = queryable.Where(s => s.StreamIndex.Equals(streamIndex)); } @@ -113,7 +113,7 @@ namespace Jellyfin.Server.Implementations.MediaSegments queryable = queryable.Where(s => s.Type.Equals(type)); } - if (typeIndex > -1) + if (!typeIndex.Equals(null)) { queryable = queryable.Where(s => s.TypeIndex.Equals(typeIndex)); } diff --git a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs index af385fff33..55cec5f634 100644 --- a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs +++ b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs @@ -34,7 +34,7 @@ namespace MediaBrowser.Model.MediaSegments /// Optional: The typeIndex. /// Optional: The segment type. /// List of MediaSegment. - List GetAllMediaSegments(Guid itemId = default, int streamIndex = -1, int typeIndex = -1, MediaSegmentType? type = null); + List GetAllMediaSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null); /// /// Delete Media Segments. @@ -44,6 +44,6 @@ namespace MediaBrowser.Model.MediaSegments /// Optional: The typeIndex. /// Optional: The segment type. /// Deleted segments. - Task> DeleteSegmentsAsync(Guid itemId, int? streamIndex, int? typeIndex, MediaSegmentType? type); + Task> DeleteSegmentsAsync(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null); } } From 54ca9e3e52491c6e60918e5a1108e50ef01c6092 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Sun, 25 Feb 2024 16:00:48 +0100 Subject: [PATCH 20/37] apply review feedback --- Emby.Server.Implementations/Dto/DtoService.cs | 2 +- .../Controllers/MediaSegmentController.cs | 14 +-- .../MediaSegments/MediaSegmentsManager.cs | 104 ++++++++++-------- .../MediaSegments/IMediaSegmentsManager.cs | 12 +- 4 files changed, 71 insertions(+), 61 deletions(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 0b847603c0..5ebefc3c52 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1075,7 +1075,7 @@ namespace Emby.Server.Implementations.Dto foreach (var source in dto.MediaSources) { - allSegments.AddRange(_mediaSegmentsManager.GetAllMediaSegments(Guid.Parse(source.Id))); + allSegments.AddRange(_mediaSegmentsManager.GetAllMediaSegments(Guid.Parse(source.Id)).GetAwaiter().GetResult()); } dto.MediaSegments = allSegments; diff --git a/Jellyfin.Api/Controllers/MediaSegmentController.cs b/Jellyfin.Api/Controllers/MediaSegmentController.cs index 15f3edbdf7..fbbbc1ab8e 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentController.cs @@ -43,13 +43,13 @@ public class MediaSegmentController : BaseJellyfinApiController /// An containing the found segments. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetSegments( + public async Task>> GetSegments( [FromQuery] Guid itemId, [FromQuery] int? streamIndex, [FromQuery] MediaSegmentType? type, [FromQuery] int? typeIndex) { - var list = _mediaSegmentManager.GetAllMediaSegments(itemId, streamIndex, typeIndex, type); + var list = await _mediaSegmentManager.GetAllMediaSegments(itemId, streamIndex, typeIndex, type).ConfigureAwait(false); return list; } @@ -94,7 +94,7 @@ public class MediaSegmentController : BaseJellyfinApiController Comment = comment }; - var segment = await _mediaSegmentManager.CreateMediaSegmentAsync(newMediaSegment).ConfigureAwait(false); + var segment = await _mediaSegmentManager.CreateMediaSegment(newMediaSegment).ConfigureAwait(false); return segment; } @@ -111,9 +111,9 @@ public class MediaSegmentController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task>> PostSegments( - [FromBody, Required] IEnumerable segments) + [FromBody, Required] IReadOnlyList segments) { - var nsegments = await _mediaSegmentManager.CreateMediaSegmentsAsync(segments).ConfigureAwait(false); + var nsegments = await _mediaSegmentManager.CreateMediaSegments(segments).ConfigureAwait(false); return nsegments.ToList(); } @@ -122,7 +122,7 @@ public class MediaSegmentController : BaseJellyfinApiController /// Delete media segments. All query parameters can be freely defined. /// /// Optional: All segments with MediaSourceId. - /// Segment is associated with MediaStreamIndex. + /// Optional: Segment is associated with MediaStreamIndex. /// Optional: All segments of type. /// Optional: All segments with typeIndex. /// Segments returned. @@ -138,7 +138,7 @@ public class MediaSegmentController : BaseJellyfinApiController [FromQuery] MediaSegmentType? type, [FromQuery] int? typeIndex) { - var list = await _mediaSegmentManager.DeleteSegmentsAsync(itemId, streamIndex, typeIndex, type).ConfigureAwait(false); + var list = await _mediaSegmentManager.DeleteSegments(itemId, streamIndex, typeIndex, type).ConfigureAwait(false); return list; } diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs index af1655d825..7daa310b43 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs @@ -1,61 +1,48 @@ -#pragma warning disable CA1307 - using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Model.MediaSegments; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Implementations.MediaSegments { /// /// Manages the creation and retrieval of instances. /// - public class MediaSegmentsManager : IMediaSegmentsManager + public class MediaSegmentsManager : IMediaSegmentsManager, IDisposable { private readonly ILibraryManager _libraryManager; private readonly IDbContextFactory _dbProvider; - private readonly IUserManager _userManager; - private readonly ILogger _logger; + private bool _disposed; /// /// Initializes a new instance of the class. /// /// The library manager. /// The database provider. - /// The user manager. - /// The logger. public MediaSegmentsManager( - ILibraryManager libraryManager, - IDbContextFactory dbProvider, - IUserManager userManager, - ILogger logger) + ILibraryManager libraryManager, + IDbContextFactory dbProvider) { _libraryManager = libraryManager; _libraryManager.ItemRemoved += LibraryManagerItemRemoved; - _dbProvider = dbProvider; - _userManager = userManager; - _logger = logger; } - // - // public event EventHandler>? OnUserUpdated; - /// - public async Task CreateMediaSegmentAsync(MediaSegment segment) + public async Task CreateMediaSegment(MediaSegment segment) { var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { ValidateSegment(segment); - var found = dbContext.Segments.Where(s => s.ItemId.Equals(segment.ItemId) && s.Type.Equals(segment.Type) && s.TypeIndex.Equals(segment.TypeIndex)).FirstOrDefault(); + var found = await dbContext.Segments.FirstOrDefaultAsync(s => s.ItemId.Equals(segment.ItemId) && s.Type.Equals(segment.Type) && s.TypeIndex.Equals(segment.TypeIndex)).ConfigureAwait(false); AddOrUpdateSegment(dbContext, segment, found); @@ -66,18 +53,17 @@ namespace Jellyfin.Server.Implementations.MediaSegments } /// - public async Task> CreateMediaSegmentsAsync(IEnumerable segments) + public async Task> CreateMediaSegments(IReadOnlyList segments) { var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - var foundSegments = dbContext.Segments.Select(s => s); - foreach (var segment in segments) { ValidateSegment(segment); - var found = foundSegments.Where(s => s.ItemId.Equals(segment.ItemId) && s.StreamIndex.Equals(segment.StreamIndex) && s.Type.Equals(segment.Type) && s.TypeIndex.Equals(segment.TypeIndex)).FirstOrDefault(); + var found = await dbContext.Segments.FirstOrDefaultAsync(s => s.ItemId.Equals(segment.ItemId) && s.StreamIndex == segment.StreamIndex && s.Type == segment.Type && s.TypeIndex == segment.TypeIndex) + .ConfigureAwait(false); AddOrUpdateSegment(dbContext, segment, found); } @@ -89,36 +75,36 @@ namespace Jellyfin.Server.Implementations.MediaSegments } /// - public List GetAllMediaSegments(Guid itemId = default, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null) + public async Task> GetAllMediaSegments(Guid itemId = default, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null) { - var allSegments = new List(); + List allSegments; - var dbContext = _dbProvider.CreateDbContext(); - using (dbContext) + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { IQueryable queryable = dbContext.Segments.Select(s => s); - if (!itemId.Equals(default)) + if (!itemId.IsEmpty()) { queryable = queryable.Where(s => s.ItemId.Equals(itemId)); } - if (!streamIndex.Equals(null)) + if (streamIndex is not null) { - queryable = queryable.Where(s => s.StreamIndex.Equals(streamIndex)); + queryable = queryable.Where(s => s.StreamIndex == streamIndex.Value); } - if (!type.Equals(null)) + if (type is not null) { - queryable = queryable.Where(s => s.Type.Equals(type)); + queryable = queryable.Where(s => s.Type == type.Value); } if (!typeIndex.Equals(null)) { - queryable = queryable.Where(s => s.TypeIndex.Equals(typeIndex)); + queryable = queryable.Where(s => s.TypeIndex == typeIndex.Value); } - allSegments = queryable.AsNoTracking().ToList(); + allSegments = await queryable.AsNoTracking().ToListAsync().ConfigureAwait(false); } return allSegments; @@ -169,17 +155,17 @@ namespace Jellyfin.Server.Implementations.MediaSegments /// The . private async void LibraryManagerItemRemoved(object? sender, ItemChangeEventArgs itemChangeEventArgs) { - await DeleteSegmentsAsync(itemChangeEventArgs.Item.Id).ConfigureAwait(false); + await DeleteSegments(itemChangeEventArgs.Item.Id).ConfigureAwait(false); } /// - public async Task> DeleteSegmentsAsync(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null) + public async Task> DeleteSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null) { - var allSegments = new List(); + List allSegments; - if (itemId.Equals(default)) + if (itemId.IsEmpty()) { - throw new ArgumentException($"itemId is not set. Please provide one."); + throw new ArgumentException("Default value provided", nameof(itemId)); } var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); @@ -187,22 +173,22 @@ namespace Jellyfin.Server.Implementations.MediaSegments { IQueryable queryable = dbContext.Segments.Where(s => s.ItemId.Equals(itemId)); - if (!streamIndex.Equals(null)) + if (streamIndex is not null) { - queryable = queryable.Where(s => s.StreamIndex.Equals(streamIndex)); + queryable = queryable.Where(s => s.StreamIndex == streamIndex); } - if (!type.Equals(null)) + if (type is not null) { - queryable = queryable.Where(s => s.Type.Equals(type)); + queryable = queryable.Where(s => s.Type == type); } - if (!typeIndex.Equals(null)) + if (typeIndex is not null) { - queryable = queryable.Where(s => s.TypeIndex.Equals(typeIndex)); + queryable = queryable.Where(s => s.TypeIndex == typeIndex); } - allSegments = queryable.AsNoTracking().ToList(); + allSegments = await queryable.ToListAsync().ConfigureAwait(false); dbContext.Segments.RemoveRange(allSegments); await dbContext.SaveChangesAsync().ConfigureAwait(false); @@ -210,5 +196,29 @@ namespace Jellyfin.Server.Implementations.MediaSegments return allSegments; } + + /// + /// Dispose event. + /// + /// dispose. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _libraryManager.ItemRemoved -= LibraryManagerItemRemoved; + } + + _disposed = true; + } + } + + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs index 55cec5f634..a2d5964dcf 100644 --- a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs +++ b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs @@ -17,14 +17,14 @@ namespace MediaBrowser.Model.MediaSegments /// /// The segment. /// New MediaSegment. - Task CreateMediaSegmentAsync(MediaSegment segment); + Task CreateMediaSegment(MediaSegment segment); /// - /// Create multiple new media segment. + /// Create or update multiple media segments. /// /// List of segments. - /// New MediaSegment. - Task> CreateMediaSegmentsAsync(IEnumerable segments); + /// New or updated MediaSegments. + Task> CreateMediaSegments(IReadOnlyList segments); /// /// Get all media segments. @@ -34,7 +34,7 @@ namespace MediaBrowser.Model.MediaSegments /// Optional: The typeIndex. /// Optional: The segment type. /// List of MediaSegment. - List GetAllMediaSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null); + public Task> GetAllMediaSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null); /// /// Delete Media Segments. @@ -44,6 +44,6 @@ namespace MediaBrowser.Model.MediaSegments /// Optional: The typeIndex. /// Optional: The segment type. /// Deleted segments. - Task> DeleteSegmentsAsync(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null); + Task> DeleteSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null); } } From 273c58adb7c5b59c90c4c0fb6439103bbdfb8193 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Mon, 26 Feb 2024 18:23:46 +0100 Subject: [PATCH 21/37] replace mediaStreamId with itemId comments --- Jellyfin.Api/Controllers/MediaSegmentController.cs | 6 +++--- MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentController.cs b/Jellyfin.Api/Controllers/MediaSegmentController.cs index fbbbc1ab8e..2a01ccd05f 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentController.cs @@ -35,7 +35,7 @@ public class MediaSegmentController : BaseJellyfinApiController /// /// Get all media segments. /// - /// Optional: Just segments with MediaSourceId. + /// Optional: Just segments with itemId. /// Optional: Just segments with MediaStreamIndex. /// Optional: All segments of type. /// Optional: All segments with typeIndex. @@ -59,7 +59,7 @@ public class MediaSegmentController : BaseJellyfinApiController /// /// Start position of segment in Ticks. /// End position of segment in Ticks. - /// Segment is associated with MediaSourceId. + /// Segment is associated with itemId. /// Segment is associated with MediaStreamIndex. /// Segment type. /// Optional: If you want to add a type multiple times to the same itemId increment it. @@ -121,7 +121,7 @@ public class MediaSegmentController : BaseJellyfinApiController /// /// Delete media segments. All query parameters can be freely defined. /// - /// Optional: All segments with MediaSourceId. + /// Optional: All segments with itemId. /// Optional: Segment is associated with MediaStreamIndex. /// Optional: All segments of type. /// Optional: All segments with typeIndex. diff --git a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs index a2d5964dcf..8f8c33b124 100644 --- a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs +++ b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs @@ -29,7 +29,7 @@ namespace MediaBrowser.Model.MediaSegments /// /// Get all media segments. /// - /// Optional: Just segments with MediaSourceId. + /// Optional: Just segments with itemId. /// Optional: Just segments with MediaStreamIndex. /// Optional: The typeIndex. /// Optional: The segment type. @@ -39,7 +39,7 @@ namespace MediaBrowser.Model.MediaSegments /// /// Delete Media Segments. /// - /// Required: The MediaSourceId. + /// Required: The itemId. /// Optional: Just segments with MediaStreamIndex. /// Optional: The typeIndex. /// Optional: The segment type. From 71f9f64c12baf40e87be634eeae04f424c7a9d87 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Mon, 26 Feb 2024 18:25:07 +0100 Subject: [PATCH 22/37] remove single segment creator code --- .../Controllers/MediaSegmentController.cs | 45 ------------------- .../MediaSegments/MediaSegmentsManager.cs | 18 -------- .../MediaSegments/IMediaSegmentsManager.cs | 7 --- 3 files changed, 70 deletions(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentController.cs b/Jellyfin.Api/Controllers/MediaSegmentController.cs index 2a01ccd05f..306602041f 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentController.cs @@ -54,51 +54,6 @@ public class MediaSegmentController : BaseJellyfinApiController return list; } - /// - /// Create or update a media segment. You can update start/end/action. - /// - /// Start position of segment in Ticks. - /// End position of segment in Ticks. - /// Segment is associated with itemId. - /// Segment is associated with MediaStreamIndex. - /// Segment type. - /// Optional: If you want to add a type multiple times to the same itemId increment it. - /// Optional: Creator recommends an action. - /// Optional: A comment. - /// Segments returned. - /// Missing query parameter. - /// An containing the segment. - [HttpPost("Segment")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> PostSegment( - [FromQuery, Required] long startTicks, - [FromQuery, Required] long endTicks, - [FromQuery, Required] Guid itemId, - [FromQuery, Required] int streamIndex, - [FromQuery, Required] MediaSegmentType type, - [FromQuery, DefaultValue(0)] int typeIndex, - [FromQuery, DefaultValue(MediaSegmentAction.Auto)] MediaSegmentAction action, - [FromQuery] string comment) - { - var newMediaSegment = new MediaSegment() - { - StartTicks = startTicks, - EndTicks = endTicks, - ItemId = itemId, - StreamIndex = streamIndex, - Type = type, - TypeIndex = typeIndex, - Action = action, - Comment = comment - }; - - var segment = await _mediaSegmentManager.CreateMediaSegment(newMediaSegment).ConfigureAwait(false); - - return segment; - } - /// /// Create or update multiple media segments. See /MediaSegment/Segment for required properties. /// diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs index 7daa310b43..d3a76d17b7 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs @@ -34,24 +34,6 @@ namespace Jellyfin.Server.Implementations.MediaSegments _dbProvider = dbProvider; } - /// - public async Task CreateMediaSegment(MediaSegment segment) - { - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) - { - ValidateSegment(segment); - - var found = await dbContext.Segments.FirstOrDefaultAsync(s => s.ItemId.Equals(segment.ItemId) && s.Type.Equals(segment.Type) && s.TypeIndex.Equals(segment.TypeIndex)).ConfigureAwait(false); - - AddOrUpdateSegment(dbContext, segment, found); - - await dbContext.SaveChangesAsync().ConfigureAwait(false); - } - - return segment; - } - /// public async Task> CreateMediaSegments(IReadOnlyList segments) { diff --git a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs index 8f8c33b124..e778a5c586 100644 --- a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs +++ b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs @@ -12,13 +12,6 @@ namespace MediaBrowser.Model.MediaSegments { public interface IMediaSegmentsManager { - /// - /// Create or update a media segment. - /// - /// The segment. - /// New MediaSegment. - Task CreateMediaSegment(MediaSegment segment); - /// /// Create or update multiple media segments. /// From a325911c6cbbb6171e76b735e327ef728c886b27 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Mon, 26 Feb 2024 18:51:32 +0100 Subject: [PATCH 23/37] feat: add MediaSegmentDto model --- .../Controllers/MediaSegmentController.cs | 23 +++-- .../MediaSegmentsDtos/MediaSegmentDto.cs | 96 +++++++++++++++++++ 2 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs diff --git a/Jellyfin.Api/Controllers/MediaSegmentController.cs b/Jellyfin.Api/Controllers/MediaSegmentController.cs index 306602041f..59d27b806e 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentController.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Api.Models.MediaSegmentsDtos; using Jellyfin.Data.Enums; using MediaBrowser.Common.Api; using MediaBrowser.Model.MediaSegments; @@ -43,19 +42,19 @@ public class MediaSegmentController : BaseJellyfinApiController /// An containing the found segments. [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>> GetSegments( - [FromQuery] Guid itemId, + public async Task>> GetSegments( + [FromQuery, Required] Guid itemId, [FromQuery] int? streamIndex, [FromQuery] MediaSegmentType? type, [FromQuery] int? typeIndex) { var list = await _mediaSegmentManager.GetAllMediaSegments(itemId, streamIndex, typeIndex, type).ConfigureAwait(false); - return list; + return list.Select(s => MediaSegmentDto.FromMediaSegment(s)).ToList(); } /// - /// Create or update multiple media segments. See /MediaSegment/Segment for required properties. + /// Create or update multiple media segments. /// /// All segments that should be added. /// Segments returned. @@ -65,12 +64,12 @@ public class MediaSegmentController : BaseJellyfinApiController [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task>> PostSegments( - [FromBody, Required] IReadOnlyList segments) + public async Task>> PostSegments( + [FromBody, Required] IReadOnlyList segments) { - var nsegments = await _mediaSegmentManager.CreateMediaSegments(segments).ConfigureAwait(false); + var nsegments = await _mediaSegmentManager.CreateMediaSegments(segments.Select(s => s.ToMediaSegment()).ToList()).ConfigureAwait(false); - return nsegments.ToList(); + return nsegments.Select(s => MediaSegmentDto.FromMediaSegment(s)).ToList(); } /// @@ -87,7 +86,7 @@ public class MediaSegmentController : BaseJellyfinApiController [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> DeleteSegments( + public async Task>> DeleteSegments( [FromQuery, Required] Guid itemId, [FromQuery] int? streamIndex, [FromQuery] MediaSegmentType? type, @@ -95,6 +94,6 @@ public class MediaSegmentController : BaseJellyfinApiController { var list = await _mediaSegmentManager.DeleteSegments(itemId, streamIndex, typeIndex, type).ConfigureAwait(false); - return list; + return list.Select(s => MediaSegmentDto.FromMediaSegment(s)).ToList(); } } diff --git a/Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs b/Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs new file mode 100644 index 0000000000..a5a560f82e --- /dev/null +++ b/Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs @@ -0,0 +1,96 @@ +using System; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; + +namespace Jellyfin.Api.Models.MediaSegmentsDtos; + +/// +/// Media Segment dto. +/// +public class MediaSegmentDto +{ + /// + /// Gets or sets the start position in Ticks. + /// + /// The start position. + public long StartTicks { get; set; } + + /// + /// Gets or sets the end position in Ticks. + /// + /// The end position. + public long EndTicks { get; set; } + + /// + /// Gets or sets the Type. + /// + /// The media segment type. + public MediaSegmentType Type { get; set; } + + /// + /// Gets or sets the TypeIndex which relates to the type. + /// + /// The type index. + public int TypeIndex { get; set; } + + /// + /// Gets or sets the associated MediaSourceId. + /// + /// The id. + public Guid ItemId { get; set; } + + /// + /// Gets or sets the associated MediaStreamIndex. + /// + /// The id. + public int StreamIndex { get; set; } + + /// + /// Gets or sets the creator recommended action. Can be overwritten with user defined action. + /// + /// The media segment action. + public MediaSegmentAction Action { get; set; } + + /// + /// Gets or sets a comment. + /// + /// The media segment action. + public string? Comment { get; set; } + + /// + /// Convert the dto to the model. + /// + /// The converted model. + public MediaSegment ToMediaSegment() + { + return new MediaSegment + { + StartTicks = StartTicks, + EndTicks = EndTicks, + Type = Type, + TypeIndex = TypeIndex, + ItemId = ItemId, + StreamIndex = StreamIndex, + Action = Action + }; + } + + /// + /// Convert the to dto model. + /// + /// segment to convert. + /// The converted model. + public static MediaSegmentDto FromMediaSegment(MediaSegment seg) + { + return new MediaSegmentDto + { + StartTicks = seg.StartTicks, + EndTicks = seg.EndTicks, + Type = seg.Type, + TypeIndex = seg.TypeIndex, + ItemId = seg.ItemId, + StreamIndex = seg.StreamIndex, + Action = seg.Action + }; + } +} From 374b6b384e1697c00d8f30047572a0fc55bf5631 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Mon, 26 Feb 2024 18:54:41 +0100 Subject: [PATCH 24/37] fix: MediaSegmentDto Comment field --- Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs b/Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs index a5a560f82e..20c1b8ec3a 100644 --- a/Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs +++ b/Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs @@ -71,7 +71,8 @@ public class MediaSegmentDto TypeIndex = TypeIndex, ItemId = ItemId, StreamIndex = StreamIndex, - Action = Action + Action = Action, + Comment = Comment }; } @@ -90,7 +91,8 @@ public class MediaSegmentDto TypeIndex = seg.TypeIndex, ItemId = seg.ItemId, StreamIndex = seg.StreamIndex, - Action = seg.Action + Action = seg.Action, + Comment = seg.Comment }; } } From 6091301e3e034bef4cc50f8668460e1bdaa6c45e Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Tue, 27 Feb 2024 01:55:35 +0100 Subject: [PATCH 25/37] try fix 'windows' --- windows | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows b/windows index ffc1ffb7c8..8cf2455a4b 160000 --- a/windows +++ b/windows @@ -1 +1 @@ -Subproject commit ffc1ffb7c869beb409b8c52a5fc3d3b5edec6f1e +Subproject commit 8cf2455a4b2804a6225061212a79797efd49015e From 1f4591c63ed1cc690f55091b7498fec7ecf75e6a Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Tue, 27 Feb 2024 02:11:32 +0100 Subject: [PATCH 26/37] remove windows submodule --- windows | 1 - 1 file changed, 1 deletion(-) delete mode 160000 windows diff --git a/windows b/windows deleted file mode 160000 index 8cf2455a4b..0000000000 --- a/windows +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8cf2455a4b2804a6225061212a79797efd49015e From e7d855beafea37dd2cefac2418e35b867058a1d6 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Tue, 27 Feb 2024 11:58:57 +0100 Subject: [PATCH 27/37] apply patchset --- ...ntroller.cs => MediaSegmentsController.cs} | 53 ++-- .../MediaSegments/MediaSegmentsManager.cs | 289 +++++++++--------- MediaBrowser.Model/Dto/BaseItemDto.cs | 2 +- .../MediaSegments/IMediaSegmentsManager.cs | 63 ++-- .../ReadOnlyListExtension.cs | 24 ++ 5 files changed, 231 insertions(+), 200 deletions(-) rename Jellyfin.Api/Controllers/{MediaSegmentController.cs => MediaSegmentsController.cs} (63%) diff --git a/Jellyfin.Api/Controllers/MediaSegmentController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs similarity index 63% rename from Jellyfin.Api/Controllers/MediaSegmentController.cs rename to Jellyfin.Api/Controllers/MediaSegmentsController.cs index 59d27b806e..ac553bff86 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Models.MediaSegmentsDtos; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Api; using MediaBrowser.Model.MediaSegments; using Microsoft.AspNetCore.Authorization; @@ -17,15 +17,15 @@ namespace Jellyfin.Api.Controllers; /// Media Segments controller. /// [Authorize] -public class MediaSegmentController : BaseJellyfinApiController +public class MediaSegmentsController : BaseJellyfinApiController { private readonly IMediaSegmentsManager _mediaSegmentManager; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Instance of the interface. - public MediaSegmentController( + public MediaSegmentsController( IMediaSegmentsManager mediaSegmentManager) { _mediaSegmentManager = mediaSegmentManager; @@ -34,66 +34,67 @@ public class MediaSegmentController : BaseJellyfinApiController /// /// Get all media segments. /// - /// Optional: Just segments with itemId. - /// Optional: Just segments with MediaStreamIndex. - /// Optional: All segments of type. - /// Optional: All segments with typeIndex. + /// The item id. + /// Just segments with MediaStreamIndex. + /// All segments of type. + /// All segments with typeIndex. /// Segments returned. /// An containing the found segments. - [HttpGet] + [HttpGet("{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>> GetSegments( - [FromQuery, Required] Guid itemId, + public async Task>> GetSegments( + [FromRoute, Required] Guid itemId, [FromQuery] int? streamIndex, [FromQuery] MediaSegmentType? type, [FromQuery] int? typeIndex) { var list = await _mediaSegmentManager.GetAllMediaSegments(itemId, streamIndex, typeIndex, type).ConfigureAwait(false); - - return list.Select(s => MediaSegmentDto.FromMediaSegment(s)).ToList(); + return Ok(list.ConvertAll(MediaSegmentDto.FromMediaSegment)); } /// /// Create or update multiple media segments. /// + /// The item the segments belong to. /// All segments that should be added. /// Segments returned. /// Invalid segments. /// An containing the created/updated segments. - [HttpPost] + [HttpPost("{itemId}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task>> PostSegments( + public async Task>> PostSegments( + [FromRoute, Required] Guid itemId, [FromBody, Required] IReadOnlyList segments) { - var nsegments = await _mediaSegmentManager.CreateMediaSegments(segments.Select(s => s.ToMediaSegment()).ToList()).ConfigureAwait(false); + var segmentsToAdd = segments.ConvertAll(s => s.ToMediaSegment()); + var addedSegments = await _mediaSegmentManager.CreateMediaSegments(itemId, segmentsToAdd).ConfigureAwait(false); - return nsegments.Select(s => MediaSegmentDto.FromMediaSegment(s)).ToList(); + return Ok(addedSegments.ConvertAll(MediaSegmentDto.FromMediaSegment)); } /// /// Delete media segments. All query parameters can be freely defined. /// - /// Optional: All segments with itemId. - /// Optional: Segment is associated with MediaStreamIndex. - /// Optional: All segments of type. - /// Optional: All segments with typeIndex. + /// The item id. + /// Segment is associated with MediaStreamIndex. + /// All segments of type. + /// All segments with typeIndex. /// Segments returned. /// Segments not found. /// An containing the deleted segments. - [HttpDelete] + [HttpDelete("{itemId}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> DeleteSegments( - [FromQuery, Required] Guid itemId, + public async Task>> DeleteSegments( + [FromRoute, Required] Guid itemId, [FromQuery] int? streamIndex, [FromQuery] MediaSegmentType? type, [FromQuery] int? typeIndex) { var list = await _mediaSegmentManager.DeleteSegments(itemId, streamIndex, typeIndex, type).ConfigureAwait(false); - - return list.Select(s => MediaSegmentDto.FromMediaSegment(s)).ToList(); + return Ok(list.ConvertAll(MediaSegmentDto.FromMediaSegment)); } } diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs index d3a76d17b7..5ca4f67946 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs @@ -9,198 +9,205 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.MediaSegments; using Microsoft.EntityFrameworkCore; -namespace Jellyfin.Server.Implementations.MediaSegments +namespace Jellyfin.Server.Implementations.MediaSegments; + +/// +/// Manages the creation and retrieval of instances. +/// +public class MediaSegmentsManager : IMediaSegmentsManager, IDisposable { + private readonly ILibraryManager _libraryManager; + private readonly IDbContextFactory _dbProvider; + private bool _disposed; + /// - /// Manages the creation and retrieval of instances. + /// Initializes a new instance of the class. /// - public class MediaSegmentsManager : IMediaSegmentsManager, IDisposable - { - private readonly ILibraryManager _libraryManager; - private readonly IDbContextFactory _dbProvider; - private bool _disposed; - - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The database provider. - public MediaSegmentsManager( + /// The library manager. + /// The database provider. + public MediaSegmentsManager( ILibraryManager libraryManager, IDbContextFactory dbProvider) + { + _libraryManager = libraryManager; + _libraryManager.ItemRemoved += LibraryManagerItemRemoved; + _dbProvider = dbProvider; + } + + /// + public async Task> CreateMediaSegments(Guid itemId, IReadOnlyList segments) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) { - _libraryManager = libraryManager; - _libraryManager.ItemRemoved += LibraryManagerItemRemoved; - _dbProvider = dbProvider; + throw new InvalidOperationException("Item not found"); } - /// - public async Task> CreateMediaSegments(IReadOnlyList segments) + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) + foreach (var segment in segments) { - foreach (var segment in segments) - { - ValidateSegment(segment); + segment.ItemId = itemId; + ValidateSegment(segment); - var found = await dbContext.Segments.FirstOrDefaultAsync(s => s.ItemId.Equals(segment.ItemId) && s.StreamIndex == segment.StreamIndex && s.Type == segment.Type && s.TypeIndex == segment.TypeIndex) - .ConfigureAwait(false); + var found = await dbContext.Segments.FirstOrDefaultAsync(s => s.ItemId.Equals(segment.ItemId) && s.StreamIndex == segment.StreamIndex && s.Type == segment.Type && s.TypeIndex == segment.TypeIndex) + .ConfigureAwait(false); - AddOrUpdateSegment(dbContext, segment, found); - } - - await dbContext.SaveChangesAsync().ConfigureAwait(false); + AddOrUpdateSegment(dbContext, segment, found); } - return segments; + await dbContext.SaveChangesAsync().ConfigureAwait(false); } - /// - public async Task> GetAllMediaSegments(Guid itemId = default, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null) - { - List allSegments; + return segments; + } - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) - { - IQueryable queryable = dbContext.Segments.Select(s => s); + /// + public async Task> GetAllMediaSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) + { + throw new InvalidOperationException("Item not found"); + } - if (!itemId.IsEmpty()) - { - queryable = queryable.Where(s => s.ItemId.Equals(itemId)); - } + List allSegments; - if (streamIndex is not null) - { - queryable = queryable.Where(s => s.StreamIndex == streamIndex.Value); - } + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + IQueryable queryable = dbContext.Segments.Where(s => s.ItemId.Equals(itemId)); - if (type is not null) - { - queryable = queryable.Where(s => s.Type == type.Value); - } + if (streamIndex is not null) + { + queryable = queryable.Where(s => s.StreamIndex == streamIndex.Value); + } - if (!typeIndex.Equals(null)) - { - queryable = queryable.Where(s => s.TypeIndex == typeIndex.Value); - } + if (type is not null) + { + queryable = queryable.Where(s => s.Type == type.Value); + } - allSegments = await queryable.AsNoTracking().ToListAsync().ConfigureAwait(false); + if (!typeIndex.Equals(null)) + { + queryable = queryable.Where(s => s.TypeIndex == typeIndex.Value); } - return allSegments; + allSegments = await queryable.AsNoTracking().ToListAsync().ConfigureAwait(false); } - /// - /// Add or Update a segment in db. - /// The db context. - /// The segment. - /// The found segment. - /// - private void AddOrUpdateSegment(JellyfinDbContext dbContext, MediaSegment segment, MediaSegment? found) + return allSegments; + } + + /// + /// Add or Update a segment in db. + /// The db context. + /// The segment. + /// The found segment. + /// + private void AddOrUpdateSegment(JellyfinDbContext dbContext, MediaSegment segment, MediaSegment? found) + { + if (found != null) { - if (found != null) - { - found.StartTicks = segment.StartTicks; - found.EndTicks = segment.EndTicks; - found.Action = segment.Action; - found.Comment = segment.Comment; - } - else - { - dbContext.Segments.Add(segment); - } + found.StartTicks = segment.StartTicks; + found.EndTicks = segment.EndTicks; + found.Action = segment.Action; + found.Comment = segment.Comment; + } + else + { + dbContext.Segments.Add(segment); } + } - /// - /// Validate a segment: itemId, start >= end and type. - /// - /// The segment to validate. - private void ValidateSegment(MediaSegment segment) + /// + /// Validate a segment: itemId, start >= end and type. + /// + /// The segment to validate. + private void ValidateSegment(MediaSegment segment) + { + if (segment.ItemId.Equals(default)) { - if (segment.ItemId.Equals(default)) - { - throw new ArgumentException($"itemId is default: itemId={segment.ItemId} for segment with type '{segment.Type}.{segment.TypeIndex}'"); - } + throw new ArgumentException($"itemId is default: itemId={segment.ItemId} for segment with type '{segment.Type}.{segment.TypeIndex}'"); + } - if (segment.StartTicks >= segment.EndTicks) - { - throw new ArgumentException($"start >= end: {segment.StartTicks}>={segment.EndTicks} for segment itemId '{segment.ItemId}' with type '{segment.Type}.{segment.TypeIndex}'"); - } + if (segment.StartTicks >= segment.EndTicks) + { + throw new ArgumentException($"start >= end: {segment.StartTicks}>={segment.EndTicks} for segment itemId '{segment.ItemId}' with type '{segment.Type}.{segment.TypeIndex}'"); } + } - /// - /// Delete all segments when itemid is deleted from library. - /// - /// The sending entity. - /// The . - private async void LibraryManagerItemRemoved(object? sender, ItemChangeEventArgs itemChangeEventArgs) + /// + /// Delete all segments when itemid is deleted from library. + /// + /// The sending entity. + /// The . + private async void LibraryManagerItemRemoved(object? sender, ItemChangeEventArgs itemChangeEventArgs) + { + await DeleteSegments(itemChangeEventArgs.Item.Id).ConfigureAwait(false); + } + + /// + public async Task> DeleteSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null) + { + List allSegments; + + if (itemId.IsEmpty()) { - await DeleteSegments(itemChangeEventArgs.Item.Id).ConfigureAwait(false); + throw new ArgumentException("Default value provided", nameof(itemId)); } - /// - public async Task> DeleteSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null) + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - List allSegments; + IQueryable queryable = dbContext.Segments.Where(s => s.ItemId.Equals(itemId)); - if (itemId.IsEmpty()) + if (streamIndex is not null) { - throw new ArgumentException("Default value provided", nameof(itemId)); + queryable = queryable.Where(s => s.StreamIndex == streamIndex); } - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) + if (type is not null) { - IQueryable queryable = dbContext.Segments.Where(s => s.ItemId.Equals(itemId)); - - if (streamIndex is not null) - { - queryable = queryable.Where(s => s.StreamIndex == streamIndex); - } - - if (type is not null) - { - queryable = queryable.Where(s => s.Type == type); - } - - if (typeIndex is not null) - { - queryable = queryable.Where(s => s.TypeIndex == typeIndex); - } - - allSegments = await queryable.ToListAsync().ConfigureAwait(false); + queryable = queryable.Where(s => s.Type == type); + } - dbContext.Segments.RemoveRange(allSegments); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + if (typeIndex is not null) + { + queryable = queryable.Where(s => s.TypeIndex == typeIndex); } - return allSegments; + allSegments = await queryable.ToListAsync().ConfigureAwait(false); + + dbContext.Segments.RemoveRange(allSegments); + await dbContext.SaveChangesAsync().ConfigureAwait(false); } - /// - /// Dispose event. - /// - /// dispose. - protected virtual void Dispose(bool disposing) + return allSegments; + } + + /// + /// Dispose event. + /// + /// dispose. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) { - if (!_disposed) + if (disposing) { - if (disposing) - { - _libraryManager.ItemRemoved -= LibraryManagerItemRemoved; - } - - _disposed = true; + _libraryManager.ItemRemoved -= LibraryManagerItemRemoved; } - } - /// - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + _disposed = true; } } + + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 7547524d70..19053a98ec 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -574,7 +574,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the media segments data. /// /// The media segments. - public List MediaSegments { get; set; } + public IReadOnlyList MediaSegments { get; set; } /// /// Gets or sets the type of the location. diff --git a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs index e778a5c586..33d55d70ad 100644 --- a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs +++ b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs @@ -1,42 +1,41 @@ -#nullable disable - -#pragma warning disable CA1002, CS1591 - using System; using System.Collections.Generic; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; -namespace MediaBrowser.Model.MediaSegments +namespace MediaBrowser.Model.MediaSegments; + +/// +/// Media segments manager definition. +/// +public interface IMediaSegmentsManager { - public interface IMediaSegmentsManager - { - /// - /// Create or update multiple media segments. - /// - /// List of segments. - /// New or updated MediaSegments. - Task> CreateMediaSegments(IReadOnlyList segments); + /// + /// Create or update multiple media segments. + /// + /// The item to create segments for. + /// List of segments. + /// New or updated MediaSegments. + Task> CreateMediaSegments(Guid itemId, IReadOnlyList segments); - /// - /// Get all media segments. - /// - /// Optional: Just segments with itemId. - /// Optional: Just segments with MediaStreamIndex. - /// Optional: The typeIndex. - /// Optional: The segment type. - /// List of MediaSegment. - public Task> GetAllMediaSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null); + /// + /// Get all media segments. + /// + /// Optional: Just segments with itemId. + /// Optional: Just segments with MediaStreamIndex. + /// Optional: The typeIndex. + /// Optional: The segment type. + /// List of MediaSegment. + public Task> GetAllMediaSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null); - /// - /// Delete Media Segments. - /// - /// Required: The itemId. - /// Optional: Just segments with MediaStreamIndex. - /// Optional: The typeIndex. - /// Optional: The segment type. - /// Deleted segments. - Task> DeleteSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null); - } + /// + /// Delete Media Segments. + /// + /// Required: The itemId. + /// Optional: Just segments with MediaStreamIndex. + /// Optional: The typeIndex. + /// Optional: The segment type. + /// Deleted segments. + Task> DeleteSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null); } diff --git a/src/Jellyfin.Extensions/ReadOnlyListExtension.cs b/src/Jellyfin.Extensions/ReadOnlyListExtension.cs index ba99bb534d..47457a59ba 100644 --- a/src/Jellyfin.Extensions/ReadOnlyListExtension.cs +++ b/src/Jellyfin.Extensions/ReadOnlyListExtension.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Jellyfin.Extensions { @@ -73,5 +74,28 @@ namespace Jellyfin.Extensions return source[0]; } + + /// + /// Converts a ReadOnlyList{TIn} to ReadOnlyList{TOut}. + /// + /// The source list. + /// The converter to use. + /// The input type. + /// The output type. + /// The converted list. + public static IReadOnlyList ConvertAll(this IReadOnlyList? source, Converter converter) + { + if (source is null || source.Count == 0) + { + return Array.Empty(); + } + + return source switch + { + List list => list.ConvertAll(converter), + TIn[] array => Array.ConvertAll(array, converter), + _ => source.Select(s => converter(s)).ToList() + }; + } } } From 753e23371879c3dfc2be54d4af6ec33d35c504ad Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Wed, 28 Feb 2024 17:47:29 +0100 Subject: [PATCH 28/37] feat: permission MediaSegmentsManagement --- Jellyfin.Api/Controllers/MediaSegmentsController.cs | 4 ++-- Jellyfin.Data/Entities/User.cs | 1 + Jellyfin.Data/Enums/PermissionKind.cs | 5 +++++ Jellyfin.Server.Implementations/Users/UserManager.cs | 1 + .../Extensions/ApiServiceCollectionExtensions.cs | 1 + MediaBrowser.Common/Api/Policies.cs | 5 +++++ MediaBrowser.Model/Users/UserPolicy.cs | 6 ++++++ 7 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs index ac553bff86..716dbd5bc5 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs @@ -61,7 +61,7 @@ public class MediaSegmentsController : BaseJellyfinApiController /// Invalid segments. /// An containing the created/updated segments. [HttpPost("{itemId}")] - [Authorize(Policy = Policies.RequiresElevation)] + [Authorize(Policy = Policies.MediaSegmentsManagement)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task>> PostSegments( @@ -85,7 +85,7 @@ public class MediaSegmentsController : BaseJellyfinApiController /// Segments not found. /// An containing the deleted segments. [HttpDelete("{itemId}")] - [Authorize(Policy = Policies.RequiresElevation)] + [Authorize(Policy = Policies.MediaSegmentsManagement)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> DeleteSegments( diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 2c9cc8d785..4789f2f9dc 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -507,6 +507,7 @@ namespace Jellyfin.Data.Entities Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false)); Permissions.Add(new Permission(PermissionKind.EnableSubtitleManagement, false)); Permissions.Add(new Permission(PermissionKind.EnableLyricManagement, false)); + Permissions.Add(new Permission(PermissionKind.EnableMediaSegmentsManagement, false)); } /// diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs index c3d6705c24..c4f00266eb 100644 --- a/Jellyfin.Data/Enums/PermissionKind.cs +++ b/Jellyfin.Data/Enums/PermissionKind.cs @@ -124,5 +124,10 @@ namespace Jellyfin.Data.Enums /// Whether the user can edit lyrics. /// EnableLyricManagement = 23, + + /// + /// Whether the user can media segments. + /// + EnableMediaSegmentsManagement = 24, } } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 41f1ac3519..f492926c2f 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -689,6 +689,7 @@ namespace Jellyfin.Server.Implementations.Users user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement); user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement); user.SetPermission(PermissionKind.EnableLyricManagement, policy.EnableLyricManagement); + user.SetPermission(PermissionKind.EnableMediaSegmentsManagement, policy.EnableMediaSegmentsManagement); user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 597643ed19..42fc37866e 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -83,6 +83,7 @@ namespace Jellyfin.Server.Extensions options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup)); options.AddPolicy(Policies.SubtitleManagement, new UserPermissionRequirement(PermissionKind.EnableSubtitleManagement)); options.AddPolicy(Policies.LyricManagement, new UserPermissionRequirement(PermissionKind.EnableLyricManagement)); + options.AddPolicy(Policies.MediaSegmentsManagement, new UserPermissionRequirement(PermissionKind.EnableMediaSegmentsManagement)); options.AddPolicy( Policies.RequiresElevation, policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication) diff --git a/MediaBrowser.Common/Api/Policies.cs b/MediaBrowser.Common/Api/Policies.cs index 435f4798ff..117a16beeb 100644 --- a/MediaBrowser.Common/Api/Policies.cs +++ b/MediaBrowser.Common/Api/Policies.cs @@ -94,4 +94,9 @@ public static class Policies /// Policy name for accessing lyric management. /// public const string LyricManagement = "LyricManagement"; + + /// + /// Policy name for accessing media segments management. + /// + public const string MediaSegmentsManagement = "MediaSegmentsManagement"; } diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 951e057632..c4a83f49d2 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -98,6 +98,12 @@ namespace MediaBrowser.Model.Users [DefaultValue(false)] public bool EnableLyricManagement { get; set; } + /// + /// Gets or sets a value indicating whether this user can manage media segments. + /// + [DefaultValue(false)] + public bool EnableMediaSegmentsManagement { get; set; } + /// /// Gets or sets a value indicating whether this instance is disabled. /// From 71b492e05d5c51a4118170489f4a5e939b15374a Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Sun, 3 Mar 2024 17:02:17 +0100 Subject: [PATCH 29/37] apply namespace changes --- Emby.Server.Implementations/Dto/DtoService.cs | 1 + Jellyfin.Api/Controllers/MediaSegmentsController.cs | 2 +- .../Models/MediaSegmentsDtos/MediaSegmentDto.cs | 5 +++-- Jellyfin.Data/Entities/MediaSegment.cs | 10 ++++------ Jellyfin.Data/Enums/MediaSegmentAction.cs | 2 +- Jellyfin.Data/Enums/MediaSegmentType.cs | 2 +- Jellyfin.Server.Implementations/JellyfinDbContext.cs | 1 + .../MediaSegments/MediaSegmentsManager.cs | 4 ++-- .../ModelConfiguration/MediaSegmentConfiguration.cs | 4 ++-- MediaBrowser.Model/Dto/BaseItemDto.cs | 1 + .../MediaSegments/IMediaSegmentsManager.cs | 4 ++-- 11 files changed, 19 insertions(+), 17 deletions(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 5ebefc3c52..a97738c8e4 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.IO; using System.Linq; using Jellyfin.Data.Entities; +using Jellyfin.Data.Entities.MediaSegment; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common; diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs index 716dbd5bc5..3be3286a05 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Jellyfin.Api.Models.MediaSegmentsDtos; -using Jellyfin.Data.Enums; +using Jellyfin.Data.Enums.MediaSegmentType; using Jellyfin.Extensions; using MediaBrowser.Common.Api; using MediaBrowser.Model.MediaSegments; diff --git a/Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs b/Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs index 20c1b8ec3a..71087c66f5 100644 --- a/Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs +++ b/Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs @@ -1,6 +1,7 @@ using System; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; +using Jellyfin.Data.Entities.MediaSegment; +using Jellyfin.Data.Enums.MediaSegmentAction; +using Jellyfin.Data.Enums.MediaSegmentType; namespace Jellyfin.Api.Models.MediaSegmentsDtos; diff --git a/Jellyfin.Data/Entities/MediaSegment.cs b/Jellyfin.Data/Entities/MediaSegment.cs index a4445e924f..ec75555c73 100644 --- a/Jellyfin.Data/Entities/MediaSegment.cs +++ b/Jellyfin.Data/Entities/MediaSegment.cs @@ -1,10 +1,8 @@ -#nullable disable -#pragma warning disable CS1591 - using System; -using Jellyfin.Data.Enums; +using Jellyfin.Data.Enums.MediaSegmentAction; +using Jellyfin.Data.Enums.MediaSegmentType; -namespace Jellyfin.Data.Entities +namespace Jellyfin.Data.Entities.MediaSegment { /// /// A moment in time of a media stream (ItemId+StreamIndex) with Type and possible Action applicable between StartTicks/Endticks. @@ -57,6 +55,6 @@ namespace Jellyfin.Data.Entities /// Gets or sets a comment. /// /// The media segment action. - public string Comment { get; set; } + public string? Comment { get; set; } } } diff --git a/Jellyfin.Data/Enums/MediaSegmentAction.cs b/Jellyfin.Data/Enums/MediaSegmentAction.cs index f585d5a5e3..d67c18a486 100644 --- a/Jellyfin.Data/Enums/MediaSegmentAction.cs +++ b/Jellyfin.Data/Enums/MediaSegmentAction.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Data.Enums.MediaSegmentAction { /// /// An enum representing the Action of MediaSegment. diff --git a/Jellyfin.Data/Enums/MediaSegmentType.cs b/Jellyfin.Data/Enums/MediaSegmentType.cs index 5c73d6845f..843bdbe9c9 100644 --- a/Jellyfin.Data/Enums/MediaSegmentType.cs +++ b/Jellyfin.Data/Enums/MediaSegmentType.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Data.Enums.MediaSegmentType { /// /// An enum representing the Type of MediaSegment. diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index 28f317956e..2ae6539c43 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Jellyfin.Data.Entities; +using Jellyfin.Data.Entities.MediaSegment; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Interfaces; using Microsoft.EntityFrameworkCore; diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs index 5ca4f67946..ba5d6b273e 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; +using Jellyfin.Data.Entities.MediaSegment; +using Jellyfin.Data.Enums.MediaSegmentType; using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Model.MediaSegments; diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs index 14803eb303..f9f7e9023e 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs +++ b/Jellyfin.Server.Implementations/ModelConfiguration/MediaSegmentConfiguration.cs @@ -1,8 +1,8 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Data.Entities.MediaSegment; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration +namespace Jellyfin.Server.Implementations.ModelConfiguration.MediaSegmentConfiguration { /// /// FluentAPI configuration for the MediaSegment entity. diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 19053a98ec..25937a1e37 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Jellyfin.Data.Entities; +using Jellyfin.Data.Entities.MediaSegment; using Jellyfin.Data.Enums; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs index 33d55d70ad..587c792c21 100644 --- a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs +++ b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; +using Jellyfin.Data.Entities.MediaSegment; +using Jellyfin.Data.Enums.MediaSegmentType; namespace MediaBrowser.Model.MediaSegments; From d780e8532160286f3322e44a3e685850841f78aa Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Sun, 3 Mar 2024 17:53:56 +0100 Subject: [PATCH 30/37] apply feedback MediaSegmentManager --- .../MediaSegments/MediaSegmentsManager.cs | 45 +++++-------------- 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs index ba5d6b273e..5516f5d803 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs @@ -14,11 +14,10 @@ namespace Jellyfin.Server.Implementations.MediaSegments; /// /// Manages the creation and retrieval of instances. /// -public class MediaSegmentsManager : IMediaSegmentsManager, IDisposable +public sealed class MediaSegmentsManager : IMediaSegmentsManager, IDisposable { private readonly ILibraryManager _libraryManager; private readonly IDbContextFactory _dbProvider; - private bool _disposed; /// /// Initializes a new instance of the class. @@ -51,8 +50,11 @@ public class MediaSegmentsManager : IMediaSegmentsManager, IDisposable segment.ItemId = itemId; ValidateSegment(segment); - var found = await dbContext.Segments.FirstOrDefaultAsync(s => s.ItemId.Equals(segment.ItemId) && s.StreamIndex == segment.StreamIndex && s.Type == segment.Type && s.TypeIndex == segment.TypeIndex) - .ConfigureAwait(false); + var found = await dbContext.Segments.FirstOrDefaultAsync(s => s.ItemId.Equals(segment.ItemId) + && s.StreamIndex == segment.StreamIndex + && s.Type == segment.Type + && s.TypeIndex == segment.TypeIndex) + .ConfigureAwait(false); AddOrUpdateSegment(dbContext, segment, found); } @@ -72,8 +74,6 @@ public class MediaSegmentsManager : IMediaSegmentsManager, IDisposable throw new InvalidOperationException("Item not found"); } - List allSegments; - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { @@ -94,10 +94,8 @@ public class MediaSegmentsManager : IMediaSegmentsManager, IDisposable queryable = queryable.Where(s => s.TypeIndex == typeIndex.Value); } - allSegments = await queryable.AsNoTracking().ToListAsync().ConfigureAwait(false); + return await queryable.AsNoTracking().ToListAsync().ConfigureAwait(false); } - - return allSegments; } /// @@ -108,7 +106,7 @@ public class MediaSegmentsManager : IMediaSegmentsManager, IDisposable /// private void AddOrUpdateSegment(JellyfinDbContext dbContext, MediaSegment segment, MediaSegment? found) { - if (found != null) + if (found is not null) { found.StartTicks = segment.StartTicks; found.EndTicks = segment.EndTicks; @@ -127,15 +125,12 @@ public class MediaSegmentsManager : IMediaSegmentsManager, IDisposable /// The segment to validate. private void ValidateSegment(MediaSegment segment) { - if (segment.ItemId.Equals(default)) + if (segment.ItemId.IsEmpty()) { throw new ArgumentException($"itemId is default: itemId={segment.ItemId} for segment with type '{segment.Type}.{segment.TypeIndex}'"); } - if (segment.StartTicks >= segment.EndTicks) - { - throw new ArgumentException($"start >= end: {segment.StartTicks}>={segment.EndTicks} for segment itemId '{segment.ItemId}' with type '{segment.Type}.{segment.TypeIndex}'"); - } + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(segment.StartTicks, segment.EndTicks, $"itemId '{segment.ItemId}' with type '{segment.Type}.{segment.TypeIndex}'"); } /// @@ -187,27 +182,9 @@ public class MediaSegmentsManager : IMediaSegmentsManager, IDisposable return allSegments; } - /// - /// Dispose event. - /// - /// dispose. - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - _libraryManager.ItemRemoved -= LibraryManagerItemRemoved; - } - - _disposed = true; - } - } - /// public void Dispose() { - Dispose(disposing: true); - GC.SuppressFinalize(this); + _libraryManager.ItemRemoved -= LibraryManagerItemRemoved; } } From b792ba63b712f8cd3789ee52d27045eae2876f43 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Thu, 14 Mar 2024 01:56:13 +0100 Subject: [PATCH 31/37] apply feedback --- Jellyfin.Api/Controllers/MediaSegmentsController.cs | 13 +++++++------ .../Models/MediaSegmentsDtos/MediaSegmentDto.cs | 2 +- Jellyfin.Data/Enums/MediaSegmentAction.cs | 2 +- .../MediaSegments/MediaSegmentsManager.cs | 11 ++--------- .../MediaSegments/IMediaSegmentsManager.cs | 4 ++-- 5 files changed, 13 insertions(+), 19 deletions(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs index 3be3286a05..2047fcd0c7 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs @@ -39,9 +39,11 @@ public class MediaSegmentsController : BaseJellyfinApiController /// All segments of type. /// All segments with typeIndex. /// Segments returned. + /// itemId doesn't exist. /// An containing the found segments. [HttpGet("{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetSegments( [FromRoute, Required] Guid itemId, [FromQuery] int? streamIndex, @@ -64,7 +66,7 @@ public class MediaSegmentsController : BaseJellyfinApiController [Authorize(Policy = Policies.MediaSegmentsManagement)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task>> PostSegments( + public async Task>> CreateSegments( [FromRoute, Required] Guid itemId, [FromBody, Required] IReadOnlyList segments) { @@ -81,20 +83,19 @@ public class MediaSegmentsController : BaseJellyfinApiController /// Segment is associated with MediaStreamIndex. /// All segments of type. /// All segments with typeIndex. - /// Segments returned. + /// Segments deleted. /// Segments not found. - /// An containing the deleted segments. + /// A representing the asynchronous operation. [HttpDelete("{itemId}")] [Authorize(Policy = Policies.MediaSegmentsManagement)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> DeleteSegments( + public async Task DeleteSegments( [FromRoute, Required] Guid itemId, [FromQuery] int? streamIndex, [FromQuery] MediaSegmentType? type, [FromQuery] int? typeIndex) { - var list = await _mediaSegmentManager.DeleteSegments(itemId, streamIndex, typeIndex, type).ConfigureAwait(false); - return Ok(list.ConvertAll(MediaSegmentDto.FromMediaSegment)); + await _mediaSegmentManager.DeleteSegments(itemId, streamIndex, typeIndex, type).ConfigureAwait(false); } } diff --git a/Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs b/Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs index 71087c66f5..fd14940867 100644 --- a/Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs +++ b/Jellyfin.Api/Models/MediaSegmentsDtos/MediaSegmentDto.cs @@ -55,7 +55,7 @@ public class MediaSegmentDto /// /// Gets or sets a comment. /// - /// The media segment action. + /// The user provided value to be displayed when the is a . public string? Comment { get; set; } /// diff --git a/Jellyfin.Data/Enums/MediaSegmentAction.cs b/Jellyfin.Data/Enums/MediaSegmentAction.cs index d67c18a486..f355af6e6b 100644 --- a/Jellyfin.Data/Enums/MediaSegmentAction.cs +++ b/Jellyfin.Data/Enums/MediaSegmentAction.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Data.Enums.MediaSegmentAction /// /// Prompt user to skip the MediaSegment. /// - Prompt = 3, + PromptToSkip = 3, /// /// Mute the MediaSegment. diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs index 5516f5d803..e001a4cf48 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs @@ -144,10 +144,8 @@ public sealed class MediaSegmentsManager : IMediaSegmentsManager, IDisposable } /// - public async Task> DeleteSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null) + public async Task DeleteSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null) { - List allSegments; - if (itemId.IsEmpty()) { throw new ArgumentException("Default value provided", nameof(itemId)); @@ -173,13 +171,8 @@ public sealed class MediaSegmentsManager : IMediaSegmentsManager, IDisposable queryable = queryable.Where(s => s.TypeIndex == typeIndex); } - allSegments = await queryable.ToListAsync().ConfigureAwait(false); - - dbContext.Segments.RemoveRange(allSegments); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + await queryable.ExecuteDeleteAsync().ConfigureAwait(false); } - - return allSegments; } /// diff --git a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs index 587c792c21..06546c0cb2 100644 --- a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs +++ b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs @@ -36,6 +36,6 @@ public interface IMediaSegmentsManager /// Optional: Just segments with MediaStreamIndex. /// Optional: The typeIndex. /// Optional: The segment type. - /// Deleted segments. - Task> DeleteSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null); + /// A representing the asynchronous operation. + public Task DeleteSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null); } From e97ae1ba0a8c9ee016f96594ff24a0b425c453a0 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Fri, 15 Mar 2024 15:23:13 +0100 Subject: [PATCH 32/37] update comment --- Jellyfin.Data/Entities/MediaSegment.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Data/Entities/MediaSegment.cs b/Jellyfin.Data/Entities/MediaSegment.cs index ec75555c73..efaf79bfb0 100644 --- a/Jellyfin.Data/Entities/MediaSegment.cs +++ b/Jellyfin.Data/Entities/MediaSegment.cs @@ -54,7 +54,7 @@ namespace Jellyfin.Data.Entities.MediaSegment /// /// Gets or sets a comment. /// - /// The media segment action. + /// The user provided value to be displayed when the is a . public string? Comment { get; set; } } } From cafbd4ed41c3831dfad5a10275cca0e5876fbfe9 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Fri, 15 Mar 2024 16:26:57 +0100 Subject: [PATCH 33/37] feat: add event for new&updated segments --- .../MediaSegments/MediaSegmentsManager.cs | 6 ++++++ MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs index e001a4cf48..0044a587fd 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentsManager.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities.MediaSegment; using Jellyfin.Data.Enums.MediaSegmentType; +using Jellyfin.Data.Events; using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Model.MediaSegments; @@ -33,6 +34,9 @@ public sealed class MediaSegmentsManager : IMediaSegmentsManager, IDisposable _dbProvider = dbProvider; } + /// + public event EventHandler>? SegmentsAddedOrUpdated; + /// public async Task> CreateMediaSegments(Guid itemId, IReadOnlyList segments) { @@ -62,6 +66,8 @@ public sealed class MediaSegmentsManager : IMediaSegmentsManager, IDisposable await dbContext.SaveChangesAsync().ConfigureAwait(false); } + SegmentsAddedOrUpdated?.Invoke(this, new GenericEventArgs(itemId)); + return segments; } diff --git a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs index 06546c0cb2..d2cc43fa3d 100644 --- a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs +++ b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Jellyfin.Data.Entities.MediaSegment; using Jellyfin.Data.Enums.MediaSegmentType; +using Jellyfin.Data.Events; namespace MediaBrowser.Model.MediaSegments; @@ -11,6 +12,11 @@ namespace MediaBrowser.Model.MediaSegments; /// public interface IMediaSegmentsManager { + /// + /// Occurs when new or updated segments are available for itemId. + /// + public event EventHandler>? SegmentsAddedOrUpdated; + /// /// Create or update multiple media segments. /// From 489e76d6caf91c10c905cf8bd72afa6100b1e8a4 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Fri, 15 Mar 2024 16:32:56 +0100 Subject: [PATCH 34/37] remove Action 'Auto' --- Jellyfin.Data/Enums/MediaSegmentAction.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Data/Enums/MediaSegmentAction.cs b/Jellyfin.Data/Enums/MediaSegmentAction.cs index f355af6e6b..3d38fdab75 100644 --- a/Jellyfin.Data/Enums/MediaSegmentAction.cs +++ b/Jellyfin.Data/Enums/MediaSegmentAction.cs @@ -5,29 +5,24 @@ namespace Jellyfin.Data.Enums.MediaSegmentAction /// public enum MediaSegmentAction { - /// - /// Auto, use default for type. - /// - Auto = 0, - /// /// None, do nothing with MediaSegment. /// - None = 1, + None = 0, /// /// Force skip the MediaSegment. /// - Skip = 2, + Skip = 1, /// /// Prompt user to skip the MediaSegment. /// - PromptToSkip = 3, + PromptToSkip = 2, /// /// Mute the MediaSegment. /// - Mute = 4, + Mute = 3, } } From 3052f10b4c1cdb6fec4369edaafaa47e1acb4888 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Sun, 17 Mar 2024 16:01:42 +0100 Subject: [PATCH 35/37] apply patch --- .../Controllers/MediaSegmentsController.cs | 74 ++++++++++++++++++- .../MediaSegments/IMediaSegmentsManager.cs | 3 + 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs index 2047fcd0c7..493a769de6 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs @@ -2,10 +2,13 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.MediaSegmentsDtos; using Jellyfin.Data.Enums.MediaSegmentType; using Jellyfin.Extensions; using MediaBrowser.Common.Api; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Library; using MediaBrowser.Model.MediaSegments; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -19,16 +22,24 @@ namespace Jellyfin.Api.Controllers; [Authorize] public class MediaSegmentsController : BaseJellyfinApiController { + private readonly IUserManager _userManager; private readonly IMediaSegmentsManager _mediaSegmentManager; + private readonly ILibraryManager _libraryManager; /// /// Initializes a new instance of the class. /// /// Instance of the interface. + /// Instance of the interface. + /// The library manager. public MediaSegmentsController( - IMediaSegmentsManager mediaSegmentManager) + IMediaSegmentsManager mediaSegmentManager, + IUserManager userManager, + ILibraryManager libraryManager) { + _userManager = userManager; _mediaSegmentManager = mediaSegmentManager; + _libraryManager = libraryManager; } /// @@ -40,16 +51,35 @@ public class MediaSegmentsController : BaseJellyfinApiController /// All segments with typeIndex. /// Segments returned. /// itemId doesn't exist. + /// User is not authorized to access the requested item. /// An containing the found segments. [HttpGet("{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task>> GetSegments( [FromRoute, Required] Guid itemId, [FromQuery] int? streamIndex, [FromQuery] MediaSegmentType? type, [FromQuery] int? typeIndex) { + var isApiKey = User.GetIsApiKey(); + var userId = User.GetUserId(); + var user = !isApiKey && !userId.IsEmpty() + ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException() + : null; + + var item = _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } + + if (!isApiKey && !item.IsVisible(user)) + { + return Unauthorized(); + } + var list = await _mediaSegmentManager.GetAllMediaSegments(itemId, streamIndex, typeIndex, type).ConfigureAwait(false); return Ok(list.ConvertAll(MediaSegmentDto.FromMediaSegment)); } @@ -61,18 +91,36 @@ public class MediaSegmentsController : BaseJellyfinApiController /// All segments that should be added. /// Segments returned. /// Invalid segments. + /// User is not authorized to access the requested item. /// An containing the created/updated segments. [HttpPost("{itemId}")] [Authorize(Policy = Policies.MediaSegmentsManagement)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task>> CreateSegments( [FromRoute, Required] Guid itemId, [FromBody, Required] IReadOnlyList segments) { + var isApiKey = User.GetIsApiKey(); + var userId = User.GetUserId(); + var user = !isApiKey && !userId.IsEmpty() + ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException() + : null; + + var item = _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } + + if (!isApiKey && !item.IsVisible(user)) + { + return Unauthorized(); + } + var segmentsToAdd = segments.ConvertAll(s => s.ToMediaSegment()); var addedSegments = await _mediaSegmentManager.CreateMediaSegments(itemId, segmentsToAdd).ConfigureAwait(false); - return Ok(addedSegments.ConvertAll(MediaSegmentDto.FromMediaSegment)); } @@ -85,17 +133,37 @@ public class MediaSegmentsController : BaseJellyfinApiController /// All segments with typeIndex. /// Segments deleted. /// Segments not found. + /// User is not authorized to access the requested item. /// A representing the asynchronous operation. [HttpDelete("{itemId}")] [Authorize(Policy = Policies.MediaSegmentsManagement)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DeleteSegments( + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task DeleteSegments( [FromRoute, Required] Guid itemId, [FromQuery] int? streamIndex, [FromQuery] MediaSegmentType? type, [FromQuery] int? typeIndex) { + var isApiKey = User.GetIsApiKey(); + var userId = User.GetUserId(); + var user = !isApiKey && !userId.IsEmpty() + ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException() + : null; + + var item = _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } + + if (!isApiKey && !item.IsVisible(user)) + { + return Unauthorized(); + } + await _mediaSegmentManager.DeleteSegments(itemId, streamIndex, typeIndex, type).ConfigureAwait(false); + return Ok(); } } diff --git a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs index d2cc43fa3d..dd3c2b967f 100644 --- a/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs +++ b/MediaBrowser.Model/MediaSegments/IMediaSegmentsManager.cs @@ -23,6 +23,7 @@ public interface IMediaSegmentsManager /// The item to create segments for. /// List of segments. /// New or updated MediaSegments. + /// Will be thrown when an non existing item is requested. Task> CreateMediaSegments(Guid itemId, IReadOnlyList segments); /// @@ -33,6 +34,7 @@ public interface IMediaSegmentsManager /// Optional: The typeIndex. /// Optional: The segment type. /// List of MediaSegment. + /// Will be thrown when an non existing item is requested. public Task> GetAllMediaSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null); /// @@ -43,5 +45,6 @@ public interface IMediaSegmentsManager /// Optional: The typeIndex. /// Optional: The segment type. /// A representing the asynchronous operation. + /// Will be thrown when a empty Guid is requested. public Task DeleteSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null); } From 5551f0cb5d88d0d5b4ef211c7c1a238ddb6d7924 Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Mon, 18 Mar 2024 16:45:46 +0100 Subject: [PATCH 36/37] fix renaming of PostSegments --- Jellyfin.Api/Controllers/MediaSegmentsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs index 493a769de6..5af625de11 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs @@ -57,7 +57,7 @@ public class MediaSegmentsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task>> GetSegments( + public async Task>> CreateSegments( [FromRoute, Required] Guid itemId, [FromQuery] int? streamIndex, [FromQuery] MediaSegmentType? type, From 9a1c744fb4544a7ab976d282e702a5acbef7f8ba Mon Sep 17 00:00:00 2001 From: endrl <119058008+endrl@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:59:15 +0100 Subject: [PATCH 37/37] Revert "fix renaming of PostSegments" This reverts commit 5551f0cb5d88d0d5b4ef211c7c1a238ddb6d7924. --- Jellyfin.Api/Controllers/MediaSegmentsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs index 5af625de11..493a769de6 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs @@ -57,7 +57,7 @@ public class MediaSegmentsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task>> CreateSegments( + public async Task>> GetSegments( [FromRoute, Required] Guid itemId, [FromQuery] int? streamIndex, [FromQuery] MediaSegmentType? type,