e81c127514
- Add JSON-based config loading with XML fallback for DB and library options - Implement LibraryOptionsRepository with EF Core, migrations, and entity - Update CollectionFolder to use DB-backed options with XML fallback/backfill - Register repository in DI and initialize at startup - Use EF execution strategy for transactional DB operations - Suppress code analysis warnings in .csproj and test files - Add DATABASE_MIGRATION.md, LIBRARY_OPTIONS_DB_DESIGN.md, and WEBSOCKET_AUTHENTICATION.md - Add database.json.example and improve migration docs - Add tests for JSON config loader and update test naming warnings
144 lines
5.1 KiB
C#
144 lines
5.1 KiB
C#
// <copyright file="ChapterRepository.cs" company="PlaceholderCompany">
|
|
// Copyright (c) PlaceholderCompany. All rights reserved.
|
|
// </copyright>
|
|
|
|
namespace Jellyfin.Server.Implementations.Item;
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Jellyfin.Database.Implementations;
|
|
using Jellyfin.Database.Implementations.Entities;
|
|
using MediaBrowser.Controller.Drawing;
|
|
using MediaBrowser.Controller.Persistence;
|
|
using MediaBrowser.Model.Entities;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
/// <summary>
|
|
/// The Chapter manager.
|
|
/// </summary>
|
|
public class ChapterRepository : IChapterRepository
|
|
{
|
|
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
|
private readonly IImageProcessor _imageProcessor;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ChapterRepository"/> class.
|
|
/// </summary>
|
|
/// <param name="dbProvider">The EFCore provider.</param>
|
|
/// <param name="imageProcessor">The Image Processor.</param>
|
|
public ChapterRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IImageProcessor imageProcessor)
|
|
{
|
|
_dbProvider = dbProvider;
|
|
_imageProcessor = imageProcessor;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<ChapterInfo?> GetChapterAsync(Guid baseItemId, int index, CancellationToken cancellationToken = default)
|
|
{
|
|
await using var context = _dbProvider.CreateDbContext();
|
|
var chapter = await context.Chapters.AsNoTracking()
|
|
.Select(e => new
|
|
{
|
|
chapter = e,
|
|
baseItemPath = e.Item.Path
|
|
})
|
|
.FirstOrDefaultAsync(e => e.chapter.ItemId.Equals(baseItemId) && e.chapter.ChapterIndex == index, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (chapter is not null)
|
|
{
|
|
return Map(chapter.chapter, chapter.baseItemPath!);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<ChapterInfo>> GetChaptersAsync(Guid baseItemId, CancellationToken cancellationToken = default)
|
|
{
|
|
await using var context = _dbProvider.CreateDbContext();
|
|
var chapters = await context.Chapters.AsNoTracking()
|
|
.Where(e => e.ItemId.Equals(baseItemId))
|
|
.Select(e => new
|
|
{
|
|
chapter = e,
|
|
baseItemPath = e.Item.Path
|
|
})
|
|
.ToArrayAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
return chapters.Select(e => Map(e.chapter, e.baseItemPath!)).ToArray();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task SaveChaptersAsync(Guid itemId, IReadOnlyList<ChapterInfo> chapters, CancellationToken cancellationToken = default)
|
|
{
|
|
await using var context = _dbProvider.CreateDbContext();
|
|
|
|
// Use the execution strategy to handle transactional consistency and retries
|
|
// This is required for PostgreSQL NpgsqlRetryingExecutionStrategy
|
|
var executionStrategy = context.Database.CreateExecutionStrategy();
|
|
await executionStrategy.ExecuteAsync(async () =>
|
|
{
|
|
await context.Chapters
|
|
.Where(e => e.ItemId.Equals(itemId))
|
|
.ExecuteDeleteAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
for (var i = 0; i < chapters.Count; i++)
|
|
{
|
|
var chapter = chapters[i];
|
|
await context.Chapters.AddAsync(Map(chapter, i, itemId), cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task DeleteChaptersAsync(Guid itemId, CancellationToken cancellationToken)
|
|
{
|
|
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
|
await using (dbContext.ConfigureAwait(false))
|
|
{
|
|
await dbContext.Chapters.Where(c => c.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
|
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private Chapter Map(ChapterInfo chapterInfo, int index, Guid itemId)
|
|
{
|
|
return new Chapter()
|
|
{
|
|
ChapterIndex = index,
|
|
StartPositionTicks = chapterInfo.StartPositionTicks,
|
|
ImageDateModified = chapterInfo.ImageDateModified,
|
|
ImagePath = chapterInfo.ImagePath,
|
|
ItemId = itemId,
|
|
Name = chapterInfo.Name,
|
|
Item = null!
|
|
};
|
|
}
|
|
|
|
private ChapterInfo Map(Chapter chapterInfo, string baseItemPath)
|
|
{
|
|
var chapterEntity = new ChapterInfo()
|
|
{
|
|
StartPositionTicks = chapterInfo.StartPositionTicks,
|
|
ImageDateModified = chapterInfo.ImageDateModified.GetValueOrDefault(),
|
|
ImagePath = chapterInfo.ImagePath,
|
|
Name = chapterInfo.Name,
|
|
};
|
|
|
|
if (!string.IsNullOrEmpty(chapterInfo.ImagePath))
|
|
{
|
|
chapterEntity.ImageTag = _imageProcessor.GetImageCacheTag(baseItemPath, chapterEntity.ImageDateModified);
|
|
}
|
|
|
|
return chapterEntity;
|
|
}
|
|
}
|