endrl 2 weeks ago committed by GitHub
commit 1116a6b6bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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;
@ -24,6 +25,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 +55,7 @@ namespace Emby.Server.Implementations.Dto
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
private readonly ITrickplayManager _trickplayManager;
private readonly IMediaSegmentsManager _mediaSegmentsManager;
public DtoService(
ILogger<DtoService> logger,
@ -65,7 +68,8 @@ namespace Emby.Server.Implementations.Dto
IApplicationHost appHost,
IMediaSourceManager mediaSourceManager,
Lazy<ILiveTvManager> livetvManagerFactory,
ITrickplayManager trickplayManager)
ITrickplayManager trickplayManager,
IMediaSegmentsManager mediaSegmentsManager)
{
_logger = logger;
_libraryManager = libraryManager;
@ -78,6 +82,7 @@ namespace Emby.Server.Implementations.Dto
_mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory;
_trickplayManager = trickplayManager;
_mediaSegmentsManager = mediaSegmentsManager;
}
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@ -1067,6 +1072,19 @@ namespace Emby.Server.Implementations.Dto
dto.ExtraType = video.ExtraType;
}
// Add MediaSegments for all media sources
if (dto.MediaSources is not null && options.ContainsField(ItemFields.MediaSegments))
{
var allSegments = new List<MediaSegment>();
foreach (var source in dto.MediaSources)
{
allSegments.AddRange(_mediaSegmentsManager.GetAllMediaSegments(Guid.Parse(source.Id)).GetAwaiter().GetResult());
}
dto.MediaSegments = allSegments;
}
if (options.ContainsField(ItemFields.MediaStreams))
{
// Add VideoInfo

@ -0,0 +1,169 @@
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;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers;
/// <summary>
/// Media Segments controller.
/// </summary>
[Authorize]
public class MediaSegmentsController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
private readonly IMediaSegmentsManager _mediaSegmentManager;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="MediaSegmentsController"/> class.
/// </summary>
/// <param name="mediaSegmentManager">Instance of the <see cref="IMediaSegmentsManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="libraryManager">The library manager.</param>
public MediaSegmentsController(
IMediaSegmentsManager mediaSegmentManager,
IUserManager userManager,
ILibraryManager libraryManager)
{
_userManager = userManager;
_mediaSegmentManager = mediaSegmentManager;
_libraryManager = libraryManager;
}
/// <summary>
/// Get all media segments.
/// </summary>
/// <param name="itemId">The item id.</param>
/// <param name="streamIndex">Just segments with MediaStreamIndex.</param>
/// <param name="type">All segments of type.</param>
/// <param name="typeIndex">All segments with typeIndex.</param>
/// <response code="200">Segments returned.</response>
/// <response code="404">itemId doesn't exist.</response>
/// <response code="401">User is not authorized to access the requested item.</response>
/// <returns>An <see cref="OkResult"/>containing the found segments.</returns>
[HttpGet("{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<IReadOnlyList<MediaSegmentDto>>> 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));
}
/// <summary>
/// Create or update multiple media segments.
/// </summary>
/// <param name="itemId">The item the segments belong to.</param>
/// <param name="segments">All segments that should be added.</param>
/// <response code="200">Segments returned.</response>
/// <response code="400">Invalid segments.</response>
/// <response code="401">User is not authorized to access the requested item.</response>
/// <returns>An <see cref="OkResult"/>containing the created/updated segments.</returns>
[HttpPost("{itemId}")]
[Authorize(Policy = Policies.MediaSegmentsManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<IReadOnlyList<MediaSegmentDto>>> CreateSegments(
[FromRoute, Required] Guid itemId,
[FromBody, Required] IReadOnlyList<MediaSegmentDto> 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));
}
/// <summary>
/// Delete media segments. All query parameters can be freely defined.
/// </summary>
/// <param name="itemId">The item id.</param>
/// <param name="streamIndex">Segment is associated with MediaStreamIndex.</param>
/// <param name="type">All segments of type.</param>
/// <param name="typeIndex">All segments with typeIndex.</param>
/// <response code="200">Segments deleted.</response>
/// <response code="404">Segments not found.</response>
/// <response code="401">User is not authorized to access the requested item.</response>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
[HttpDelete("{itemId}")]
[Authorize(Policy = Policies.MediaSegmentsManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> 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();
}
}

@ -0,0 +1,99 @@
using System;
using Jellyfin.Data.Entities.MediaSegment;
using Jellyfin.Data.Enums.MediaSegmentAction;
using Jellyfin.Data.Enums.MediaSegmentType;
namespace Jellyfin.Api.Models.MediaSegmentsDtos;
/// <summary>
/// Media Segment dto.
/// </summary>
public class MediaSegmentDto
{
/// <summary>
/// Gets or sets the start position in Ticks.
/// </summary>
/// <value>The start position.</value>
public long StartTicks { get; set; }
/// <summary>
/// Gets or sets the end position in Ticks.
/// </summary>
/// <value>The end position.</value>
public long EndTicks { get; set; }
/// <summary>
/// Gets or sets the Type.
/// </summary>
/// <value>The media segment type.</value>
public MediaSegmentType Type { get; set; }
/// <summary>
/// Gets or sets the TypeIndex which relates to the type.
/// </summary>
/// <value>The type index.</value>
public int TypeIndex { get; set; }
/// <summary>
/// Gets or sets the associated MediaSourceId.
/// </summary>
/// <value>The id.</value>
public Guid ItemId { get; set; }
/// <summary>
/// Gets or sets the associated MediaStreamIndex.
/// </summary>
/// <value>The id.</value>
public int StreamIndex { get; set; }
/// <summary>
/// Gets or sets the creator recommended action. Can be overwritten with user defined action.
/// </summary>
/// <value>The media segment action.</value>
public MediaSegmentAction Action { get; set; }
/// <summary>
/// Gets or sets a comment.
/// </summary>
/// <value>The user provided value to be displayed when the <see cref="MediaSegmentDto.Type"/> is a <see cref="MediaSegmentType.Annotation" />.</value>
public string? Comment { get; set; }
/// <summary>
/// Convert the dto to the <see cref="MediaSegment"/> model.
/// </summary>
/// <returns>The converted <see cref="MediaSegment"/> model.</returns>
public MediaSegment ToMediaSegment()
{
return new MediaSegment
{
StartTicks = StartTicks,
EndTicks = EndTicks,
Type = Type,
TypeIndex = TypeIndex,
ItemId = ItemId,
StreamIndex = StreamIndex,
Action = Action,
Comment = Comment
};
}
/// <summary>
/// Convert the <see cref="MediaSegment"/> to dto model.
/// </summary>
/// <param name="seg">segment to convert.</param>
/// <returns>The converted <see cref="MediaSegmentDto"/> model.</returns>
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,
Comment = seg.Comment
};
}
}

@ -0,0 +1,60 @@
using System;
using Jellyfin.Data.Enums.MediaSegmentAction;
using Jellyfin.Data.Enums.MediaSegmentType;
namespace Jellyfin.Data.Entities.MediaSegment
{
/// <summary>
/// A moment in time of a media stream (ItemId+StreamIndex) with Type and possible Action applicable between StartTicks/Endticks.
/// </summary>
public class MediaSegment
{
/// <summary>
/// Gets or sets the start position in Ticks.
/// </summary>
/// <value>The start position.</value>
public long StartTicks { get; set; }
/// <summary>
/// Gets or sets the end position in Ticks.
/// </summary>
/// <value>The end position.</value>
public long EndTicks { get; set; }
/// <summary>
/// Gets or sets the Type.
/// </summary>
/// <value>The media segment type.</value>
public MediaSegmentType Type { get; set; }
/// <summary>
/// Gets or sets the TypeIndex which relates to the type.
/// </summary>
/// <value>The type index.</value>
public int TypeIndex { get; set; }
/// <summary>
/// Gets or sets the associated MediaSourceId.
/// </summary>
/// <value>The id.</value>
public Guid ItemId { get; set; }
/// <summary>
/// Gets or sets the associated MediaStreamIndex.
/// </summary>
/// <value>The id.</value>
public int StreamIndex { get; set; }
/// <summary>
/// Gets or sets the creator recommended action. Can be overwritten with user defined action.
/// </summary>
/// <value>The media segment action.</value>
public MediaSegmentAction Action { get; set; }
/// <summary>
/// Gets or sets a comment.
/// </summary>
/// <value>The user provided value to be displayed when the <see cref="MediaSegment.Type"/> is a <see cref="MediaSegmentType.Annotation" />.</value>
public string? Comment { get; set; }
}
}

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

@ -0,0 +1,28 @@
namespace Jellyfin.Data.Enums.MediaSegmentAction
{
/// <summary>
/// An enum representing the Action of MediaSegment.
/// </summary>
public enum MediaSegmentAction
{
/// <summary>
/// None, do nothing with MediaSegment.
/// </summary>
None = 0,
/// <summary>
/// Force skip the MediaSegment.
/// </summary>
Skip = 1,
/// <summary>
/// Prompt user to skip the MediaSegment.
/// </summary>
PromptToSkip = 2,
/// <summary>
/// Mute the MediaSegment.
/// </summary>
Mute = 3,
}
}

@ -0,0 +1,38 @@
namespace Jellyfin.Data.Enums.MediaSegmentType
{
/// <summary>
/// An enum representing the Type of MediaSegment.
/// </summary>
public enum MediaSegmentType
{
/// <summary>
/// The Intro.
/// </summary>
Intro = 0,
/// <summary>
/// The Outro.
/// </summary>
Outro = 1,
/// <summary>
/// Recap of last tv show episode(s).
/// </summary>
Recap = 2,
/// <summary>
/// The preview for the next tv show episode.
/// </summary>
Preview = 3,
/// <summary>
/// Commercial that interrupt the viewer.
/// </summary>
Commercial = 4,
/// <summary>
/// A Comment or additional info.
/// </summary>
Annotation = 5,
}
}

@ -124,5 +124,10 @@ namespace Jellyfin.Data.Enums
/// Whether the user can edit lyrics.
/// </summary>
EnableLyricManagement = 23,
/// <summary>
/// Whether the user can media segments.
/// </summary>
EnableMediaSegmentsManagement = 24,
}
}

@ -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;
@ -83,6 +84,11 @@ public class JellyfinDbContext : DbContext
/// </summary>
public DbSet<TrickplayInfo> TrickplayInfos => Set<TrickplayInfo>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the media segments.
/// </summary>
public DbSet<MediaSegment> Segments => Set<MediaSegment>();
/*public DbSet<Artwork> Artwork => Set<Artwork>();
public DbSet<Book> Books => Set<Book>();

@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
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;
using Microsoft.EntityFrameworkCore;
namespace Jellyfin.Server.Implementations.MediaSegments;
/// <summary>
/// Manages the creation and retrieval of <see cref="MediaSegment"/> instances.
/// </summary>
public sealed class MediaSegmentsManager : IMediaSegmentsManager, IDisposable
{
private readonly ILibraryManager _libraryManager;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
/// <summary>
/// Initializes a new instance of the <see cref="MediaSegmentsManager"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="dbProvider">The database provider.</param>
public MediaSegmentsManager(
ILibraryManager libraryManager,
IDbContextFactory<JellyfinDbContext> dbProvider)
{
_libraryManager = libraryManager;
_libraryManager.ItemRemoved += LibraryManagerItemRemoved;
_dbProvider = dbProvider;
}
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<Guid>>? SegmentsAddedOrUpdated;
/// <inheritdoc/>
public async Task<IReadOnlyList<MediaSegment>> CreateMediaSegments(Guid itemId, IReadOnlyList<MediaSegment> segments)
{
var item = _libraryManager.GetItemById(itemId);
if (item is null)
{
throw new InvalidOperationException("Item not found");
}
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
foreach (var segment in segments)
{
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);
AddOrUpdateSegment(dbContext, segment, found);
}
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
SegmentsAddedOrUpdated?.Invoke(this, new GenericEventArgs<Guid>(itemId));
return segments;
}
/// <inheritdoc/>
public async Task<IReadOnlyList<MediaSegment>> 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");
}
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
IQueryable<MediaSegment> queryable = dbContext.Segments.Where(s => s.ItemId.Equals(itemId));
if (streamIndex is not null)
{
queryable = queryable.Where(s => s.StreamIndex == streamIndex.Value);
}
if (type is not null)
{
queryable = queryable.Where(s => s.Type == type.Value);
}
if (!typeIndex.Equals(null))
{
queryable = queryable.Where(s => s.TypeIndex == typeIndex.Value);
}
return await queryable.AsNoTracking().ToListAsync().ConfigureAwait(false);
}
}
/// <summary>
/// Add or Update a segment in db.
/// <param name="dbContext">The db context.</param>
/// <param name="segment">The segment.</param>
/// <param name="found">The found segment.</param>
/// </summary>
private void AddOrUpdateSegment(JellyfinDbContext dbContext, MediaSegment segment, MediaSegment? found)
{
if (found is not null)
{
found.StartTicks = segment.StartTicks;
found.EndTicks = segment.EndTicks;
found.Action = segment.Action;
found.Comment = segment.Comment;
}
else
{
dbContext.Segments.Add(segment);
}
}
/// <summary>
/// Validate a segment: itemId, start >= end and type.
/// </summary>
/// <param name="segment">The segment to validate.</param>
private void ValidateSegment(MediaSegment segment)
{
if (segment.ItemId.IsEmpty())
{
throw new ArgumentException($"itemId is default: itemId={segment.ItemId} for segment with type '{segment.Type}.{segment.TypeIndex}'");
}
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(segment.StartTicks, segment.EndTicks, $"itemId '{segment.ItemId}' with type '{segment.Type}.{segment.TypeIndex}'");
}
/// <summary>
/// Delete all segments when itemid is deleted from library.
/// </summary>
/// <param name="sender">The sending entity.</param>
/// <param name="itemChangeEventArgs">The <see cref="ItemChangeEventArgs"/>.</param>
private async void LibraryManagerItemRemoved(object? sender, ItemChangeEventArgs itemChangeEventArgs)
{
await DeleteSegments(itemChangeEventArgs.Item.Id).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task DeleteSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null)
{
if (itemId.IsEmpty())
{
throw new ArgumentException("Default value provided", nameof(itemId));
}
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
IQueryable<MediaSegment> 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);
}
await queryable.ExecuteDeleteAsync().ConfigureAwait(false);
}
}
/// <inheritdoc/>
public void Dispose()
{
_libraryManager.ItemRemoved -= LibraryManagerItemRemoved;
}
}

@ -0,0 +1,718 @@
// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
[DbContext(typeof(JellyfinDbContext))]
[Migration("20240224002503_MediaSegments")]
partial class MediaSegments
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DayOfWeek")
.HasColumnType("INTEGER");
b.Property<double>("EndHour")
.HasColumnType("REAL");
b.Property<double>("StartHour")
.HasColumnType("REAL");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AccessSchedules");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<int>("LogSeverity")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Overview")
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("ShortOverview")
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DateCreated");
b.ToTable("ActivityLogs");
});
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "ItemId", "Client", "Key")
.IsUnique();
b.ToTable("CustomItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChromecastVersion")
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<string>("DashboardTheme")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<bool>("EnableNextVideoInfoOverlay")
.HasColumnType("INTEGER");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<int>("ScrollDirection")
.HasColumnType("INTEGER");
b.Property<bool>("ShowBackdrop")
.HasColumnType("INTEGER");
b.Property<bool>("ShowSidebar")
.HasColumnType("INTEGER");
b.Property<int>("SkipBackwardLength")
.HasColumnType("INTEGER");
b.Property<int>("SkipForwardLength")
.HasColumnType("INTEGER");
b.Property<string>("TvHome")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "ItemId", "Client")
.IsUnique();
b.ToTable("DisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DisplayPreferencesId")
.HasColumnType("INTEGER");
b.Property<int>("Order")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("DisplayPreferencesId");
b.ToTable("HomeSection");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("ImageInfos");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<bool>("RememberIndexing")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSorting")
.HasColumnType("INTEGER");
b.Property<string>("SortBy")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<int>("ViewType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
{
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<int>("StreamIndex")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<int>("TypeIndex")
.HasColumnType("INTEGER");
b.Property<int>("Action")
.HasColumnType("INTEGER");
b.Property<string>("Comment")
.HasColumnType("TEXT");
b.Property<long>("EndTicks")
.HasColumnType("INTEGER");
b.Property<long>("StartTicks")
.HasColumnType("INTEGER");
b.HasKey("ItemId", "StreamIndex", "Type", "TypeIndex");
b.HasIndex("ItemId");
b.ToTable("Segments");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Permission_Permissions_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.Property<bool>("Value")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId", "Kind")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Permissions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Preference_Preferences_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "Kind")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Preferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateLastActivity")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccessToken")
.IsUnique();
b.ToTable("ApiKeys");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("AppName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<string>("AppVersion")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateLastActivity")
.HasColumnType("TEXT");
b.Property<DateTime>("DateModified")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("DeviceName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DeviceId");
b.HasIndex("AccessToken", "DateLastActivity");
b.HasIndex("DeviceId", "DateLastActivity");
b.HasIndex("UserId", "DeviceId");
b.ToTable("Devices");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CustomName")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DeviceId")
.IsUnique();
b.ToTable("DeviceOptions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
{
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<int>("Width")
.HasColumnType("INTEGER");
b.Property<int>("Bandwidth")
.HasColumnType("INTEGER");
b.Property<int>("Height")
.HasColumnType("INTEGER");
b.Property<int>("Interval")
.HasColumnType("INTEGER");
b.Property<int>("ThumbnailCount")
.HasColumnType("INTEGER");
b.Property<int>("TileHeight")
.HasColumnType("INTEGER");
b.Property<int>("TileWidth")
.HasColumnType("INTEGER");
b.HasKey("ItemId", "Width");
b.ToTable("TrickplayInfos");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AudioLanguagePreference")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("AuthenticationProviderId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("CastReceiverId")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER");
b.Property<bool>("DisplayMissingEpisodes")
.HasColumnType("INTEGER");
b.Property<bool>("EnableAutoLogin")
.HasColumnType("INTEGER");
b.Property<bool>("EnableLocalPassword")
.HasColumnType("INTEGER");
b.Property<bool>("EnableNextEpisodeAutoPlay")
.HasColumnType("INTEGER");
b.Property<bool>("EnableUserPreferenceAccess")
.HasColumnType("INTEGER");
b.Property<bool>("HidePlayedInLatest")
.HasColumnType("INTEGER");
b.Property<long>("InternalId")
.HasColumnType("INTEGER");
b.Property<int>("InvalidLoginAttemptCount")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastActivityDate")
.HasColumnType("TEXT");
b.Property<DateTime?>("LastLoginDate")
.HasColumnType("TEXT");
b.Property<int?>("LoginAttemptsBeforeLockout")
.HasColumnType("INTEGER");
b.Property<int>("MaxActiveSessions")
.HasColumnType("INTEGER");
b.Property<int?>("MaxParentalAgeRating")
.HasColumnType("INTEGER");
b.Property<bool>("MustUpdatePassword")
.HasColumnType("INTEGER");
b.Property<string>("Password")
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.Property<string>("PasswordResetProviderId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("PlayDefaultAudioTrack")
.HasColumnType("INTEGER");
b.Property<bool>("RememberAudioSelections")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSubtitleSelections")
.HasColumnType("INTEGER");
b.Property<int?>("RemoteClientBitrateLimit")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("SubtitleLanguagePreference")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<int>("SubtitleMode")
.HasColumnType("INTEGER");
b.Property<int>("SyncPlayAccess")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT")
.UseCollation("NOCASE");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("AccessSchedules")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("DisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
.WithMany("HomeSections")
.HasForeignKey("DisplayPreferencesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithOne("ProfileImage")
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("ItemDisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Permissions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Preferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Navigation("HomeSections");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Navigation("AccessSchedules");
b.Navigation("DisplayPreferences");
b.Navigation("ItemDisplayPreferences");
b.Navigation("Permissions");
b.Navigation("Preferences");
b.Navigation("ProfileImage");
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,45 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class MediaSegments : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Segments",
columns: table => new
{
Type = table.Column<int>(type: "INTEGER", nullable: false),
TypeIndex = table.Column<int>(type: "INTEGER", nullable: false),
ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
StreamIndex = table.Column<int>(type: "INTEGER", nullable: false),
StartTicks = table.Column<long>(type: "INTEGER", nullable: false),
EndTicks = table.Column<long>(type: "INTEGER", nullable: false),
Action = table.Column<int>(type: "INTEGER", nullable: false),
Comment = table.Column<string>(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");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Segments");
}
}
}

@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.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<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<int>("StreamIndex")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<int>("TypeIndex")
.HasColumnType("INTEGER");
b.Property<int>("Action")
.HasColumnType("INTEGER");
b.Property<string>("Comment")
.HasColumnType("TEXT");
b.Property<long>("EndTicks")
.HasColumnType("INTEGER");
b.Property<long>("StartTicks")
.HasColumnType("INTEGER");
b.HasKey("ItemId", "StreamIndex", "Type", "TypeIndex");
b.HasIndex("ItemId");
b.ToTable("Segments");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.Property<int>("Id")

@ -0,0 +1,42 @@
using Jellyfin.Data.Entities.MediaSegment;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Jellyfin.Server.Implementations.ModelConfiguration.MediaSegmentConfiguration
{
/// <summary>
/// FluentAPI configuration for the MediaSegment entity.
/// </summary>
public class MediaSegmentConfiguration : IEntityTypeConfiguration<MediaSegment>
{
/// <inheritdoc/>
public void Configure(EntityTypeBuilder<MediaSegment> builder)
{
builder
.Property(s => s.StartTicks)
.IsRequired();
builder
.Property(s => s.EndTicks)
.IsRequired();
builder
.Property(s => s.Type)
.IsRequired();
builder
.Property(s => s.TypeIndex)
.IsRequired();
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.StreamIndex, s.Type, s.TypeIndex });
builder
.HasIndex(s => s.ItemId);
}
}
}

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

@ -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<IDisplayPreferencesManager, DisplayPreferencesManager>();
serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>();
serviceCollection.AddSingleton<IMediaSegmentsManager, MediaSegmentsManager>();
// TODO search the assemblies instead of adding them manually?
serviceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>();

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

@ -94,4 +94,9 @@ public static class Policies
/// Policy name for accessing lyric management.
/// </summary>
public const string LyricManagement = "LyricManagement";
/// <summary>
/// Policy name for accessing media segments management.
/// </summary>
public const string MediaSegmentsManagement = "MediaSegmentsManagement";
}

@ -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;
@ -570,6 +571,12 @@ namespace MediaBrowser.Model.Dto
/// <value>The trickplay manifest.</value>
public Dictionary<string, Dictionary<int, TrickplayInfo>> Trickplay { get; set; }
/// <summary>
/// Gets or sets the media segments data.
/// </summary>
/// <value>The media segments.</value>
public IReadOnlyList<MediaSegment> MediaSegments { get; set; }
/// <summary>
/// Gets or sets the type of the location.
/// </summary>

@ -0,0 +1,50 @@
using System;
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;
/// <summary>
/// Media segments manager definition.
/// </summary>
public interface IMediaSegmentsManager
{
/// <summary>
/// Occurs when new or updated segments are available for itemId.
/// </summary>
public event EventHandler<GenericEventArgs<Guid>>? SegmentsAddedOrUpdated;
/// <summary>
/// Create or update multiple media segments.
/// </summary>
/// <param name="itemId">The item to create segments for.</param>
/// <param name="segments">List of segments.</param>
/// <returns>New or updated MediaSegments.</returns>
/// <exception cref="InvalidOperationException">Will be thrown when an non existing item is requested.</exception>
Task<IReadOnlyList<MediaSegment>> CreateMediaSegments(Guid itemId, IReadOnlyList<MediaSegment> segments);
/// <summary>
/// Get all media segments.
/// </summary>
/// <param name="itemId">Optional: Just segments with itemId.</param>
/// <param name="streamIndex">Optional: Just segments with MediaStreamIndex.</param>
/// <param name="typeIndex">Optional: The typeIndex.</param>
/// <param name="type">Optional: The segment type.</param>
/// <returns>List of MediaSegment.</returns>
/// <exception cref="InvalidOperationException">Will be thrown when an non existing item is requested.</exception>
public Task<IReadOnlyList<MediaSegment>> GetAllMediaSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null);
/// <summary>
/// Delete Media Segments.
/// </summary>
/// <param name="itemId">Required: The itemId.</param>
/// <param name="streamIndex">Optional: Just segments with MediaStreamIndex.</param>
/// <param name="typeIndex">Optional: The typeIndex.</param>
/// <param name="type">Optional: The segment type.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
/// <exception cref="ArgumentException">Will be thrown when a empty Guid is requested.</exception>
public Task DeleteSegments(Guid itemId, int? streamIndex = null, int? typeIndex = null, MediaSegmentType? type = null);
}

@ -39,6 +39,11 @@ namespace MediaBrowser.Model.Querying
/// </summary>
Trickplay,
/// <summary>
/// The MediaSegments data.
/// </summary>
MediaSegments,
ChildCount,
/// <summary>

@ -98,6 +98,12 @@ namespace MediaBrowser.Model.Users
[DefaultValue(false)]
public bool EnableLyricManagement { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this user can manage media segments.
/// </summary>
[DefaultValue(false)]
public bool EnableMediaSegmentsManagement { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is disabled.
/// </summary>

@ -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];
}
/// <summary>
/// Converts a ReadOnlyList{TIn} to ReadOnlyList{TOut}.
/// </summary>
/// <param name="source">The source list.</param>
/// <param name="converter">The converter to use.</param>
/// <typeparam name="TIn">The input type.</typeparam>
/// <typeparam name="TOut">The output type.</typeparam>
/// <returns>The converted list.</returns>
public static IReadOnlyList<TOut> ConvertAll<TIn, TOut>(this IReadOnlyList<TIn>? source, Converter<TIn, TOut> converter)
{
if (source is null || source.Count == 0)
{
return Array.Empty<TOut>();
}
return source switch
{
List<TIn> list => list.ConvertAll(converter),
TIn[] array => Array.ConvertAll(array, converter),
_ => source.Select(s => converter(s)).ToList()
};
}
}
}

Loading…
Cancel
Save