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