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