Merge 9a1c744fb4
into f20a9c9b2b
commit
1116a6b6bb
@ -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; }
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
@ -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,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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
Loading…
Reference in new issue