repo creation with initial code after cloning public repo
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
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;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Item;
|
||||
|
||||
/// <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 ChapterInfo? GetChapter(Guid baseItemId, int index)
|
||||
{
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
var chapter = context.Chapters.AsNoTracking()
|
||||
.Select(e => new
|
||||
{
|
||||
chapter = e,
|
||||
baseItemPath = e.Item.Path
|
||||
})
|
||||
.FirstOrDefault(e => e.chapter.ItemId.Equals(baseItemId) && e.chapter.ChapterIndex == index);
|
||||
if (chapter is not null)
|
||||
{
|
||||
return Map(chapter.chapter, chapter.baseItemPath!);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId)
|
||||
{
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
return context.Chapters.AsNoTracking().Where(e => e.ItemId.Equals(baseItemId))
|
||||
.Select(e => new
|
||||
{
|
||||
chapter = e,
|
||||
baseItemPath = e.Item.Path
|
||||
})
|
||||
.AsEnumerable()
|
||||
.Select(e => Map(e.chapter, e.baseItemPath!))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters)
|
||||
{
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
using (var transaction = context.Database.BeginTransaction())
|
||||
{
|
||||
context.Chapters.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete();
|
||||
for (var i = 0; i < chapters.Count; i++)
|
||||
{
|
||||
var chapter = chapters[i];
|
||||
context.Chapters.Add(Map(chapter, i, itemId));
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
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.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Item;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for obtaining Keyframe data.
|
||||
/// </summary>
|
||||
public class KeyframeRepository : IKeyframeRepository
|
||||
{
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="KeyframeRepository"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dbProvider">The EFCore db factory.</param>
|
||||
public KeyframeRepository(IDbContextFactory<JellyfinDbContext> dbProvider)
|
||||
{
|
||||
_dbProvider = dbProvider;
|
||||
}
|
||||
|
||||
private static MediaEncoding.Keyframes.KeyframeData Map(KeyframeData entity)
|
||||
{
|
||||
return new MediaEncoding.Keyframes.KeyframeData(
|
||||
entity.TotalDuration,
|
||||
(entity.KeyframeTicks ?? []).ToList());
|
||||
}
|
||||
|
||||
private KeyframeData Map(MediaEncoding.Keyframes.KeyframeData dto, Guid itemId)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ItemId = itemId,
|
||||
TotalDuration = dto.TotalDuration,
|
||||
KeyframeTicks = dto.KeyframeTicks.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<MediaEncoding.Keyframes.KeyframeData> GetKeyframeData(Guid itemId)
|
||||
{
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
|
||||
return context.KeyframeData.AsNoTracking().Where(e => e.ItemId.Equals(itemId)).Select(e => Map(e)).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SaveKeyframeDataAsync(Guid itemId, MediaEncoding.Keyframes.KeyframeData data, CancellationToken cancellationToken)
|
||||
{
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (transaction.ConfigureAwait(false))
|
||||
{
|
||||
await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
await context.KeyframeData.AddAsync(Map(data, itemId), cancellationToken).ConfigureAwait(false);
|
||||
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Item;
|
||||
|
||||
/// <summary>
|
||||
/// Manager for handling Media Attachments.
|
||||
/// </summary>
|
||||
/// <param name="dbProvider">Efcore Factory.</param>
|
||||
public class MediaAttachmentRepository(IDbContextFactory<JellyfinDbContext> dbProvider) : IMediaAttachmentRepository
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void SaveMediaAttachments(
|
||||
Guid id,
|
||||
IReadOnlyList<MediaAttachment> attachments,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var context = dbProvider.CreateDbContext();
|
||||
using var transaction = context.Database.BeginTransaction();
|
||||
|
||||
// Users may replace a media with a version that includes attachments to one without them.
|
||||
// So when saving attachments is triggered by a library scan, we always unconditionally
|
||||
// clear the old ones, and then add the new ones if given.
|
||||
context.AttachmentStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete();
|
||||
if (attachments.Any())
|
||||
{
|
||||
context.AttachmentStreamInfos.AddRange(attachments.Select(e => Map(e, id)));
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery filter)
|
||||
{
|
||||
using var context = dbProvider.CreateDbContext();
|
||||
var query = context.AttachmentStreamInfos.AsNoTracking().Where(e => e.ItemId.Equals(filter.ItemId));
|
||||
if (filter.Index.HasValue)
|
||||
{
|
||||
query = query.Where(e => e.Index == filter.Index);
|
||||
}
|
||||
|
||||
return query.AsEnumerable().Select(Map).ToArray();
|
||||
}
|
||||
|
||||
private MediaAttachment Map(AttachmentStreamInfo attachment)
|
||||
{
|
||||
return new MediaAttachment()
|
||||
{
|
||||
Codec = attachment.Codec,
|
||||
CodecTag = attachment.CodecTag,
|
||||
Comment = attachment.Comment,
|
||||
FileName = attachment.Filename,
|
||||
Index = attachment.Index,
|
||||
MimeType = attachment.MimeType,
|
||||
};
|
||||
}
|
||||
|
||||
private AttachmentStreamInfo Map(MediaAttachment attachment, Guid id)
|
||||
{
|
||||
return new AttachmentStreamInfo()
|
||||
{
|
||||
Codec = attachment.Codec,
|
||||
CodecTag = attachment.CodecTag,
|
||||
Comment = attachment.Comment,
|
||||
Filename = attachment.FileName,
|
||||
Index = attachment.Index,
|
||||
MimeType = attachment.MimeType,
|
||||
ItemId = id,
|
||||
Item = null!
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Item;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for obtaining MediaStreams.
|
||||
/// </summary>
|
||||
public class MediaStreamRepository : IMediaStreamRepository
|
||||
{
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
private readonly IServerApplicationHost _serverApplicationHost;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MediaStreamRepository"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dbProvider">The EFCore db factory.</param>
|
||||
/// <param name="serverApplicationHost">The Application host.</param>
|
||||
/// <param name="localization">The Localisation Provider.</param>
|
||||
public MediaStreamRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IServerApplicationHost serverApplicationHost, ILocalizationManager localization)
|
||||
{
|
||||
_dbProvider = dbProvider;
|
||||
_serverApplicationHost = serverApplicationHost;
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken)
|
||||
{
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
using var transaction = context.Database.BeginTransaction();
|
||||
|
||||
context.MediaStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete();
|
||||
context.MediaStreamInfos.AddRange(streams.Select(f => Map(f, id)));
|
||||
context.SaveChanges();
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery filter)
|
||||
{
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
return TranslateQuery(context.MediaStreamInfos.AsNoTracking(), filter).AsEnumerable().Select(Map).ToArray();
|
||||
}
|
||||
|
||||
private string? GetPathToSave(string? path)
|
||||
{
|
||||
if (path is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _serverApplicationHost.ReverseVirtualPath(path);
|
||||
}
|
||||
|
||||
private string? RestorePath(string? path)
|
||||
{
|
||||
if (path is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _serverApplicationHost.ExpandVirtualPath(path);
|
||||
}
|
||||
|
||||
private IQueryable<MediaStreamInfo> TranslateQuery(IQueryable<MediaStreamInfo> query, MediaStreamQuery filter)
|
||||
{
|
||||
query = query.Where(e => e.ItemId.Equals(filter.ItemId));
|
||||
if (filter.Index.HasValue)
|
||||
{
|
||||
query = query.Where(e => e.StreamIndex == filter.Index);
|
||||
}
|
||||
|
||||
if (filter.Type.HasValue)
|
||||
{
|
||||
var typeValue = (MediaStreamTypeEntity)filter.Type.Value;
|
||||
query = query.Where(e => e.StreamType == typeValue);
|
||||
}
|
||||
|
||||
return query.OrderBy(e => e.StreamIndex);
|
||||
}
|
||||
|
||||
private MediaStream Map(MediaStreamInfo entity)
|
||||
{
|
||||
var dto = new MediaStream();
|
||||
dto.Index = entity.StreamIndex;
|
||||
dto.Type = (MediaStreamType)entity.StreamType;
|
||||
|
||||
dto.IsAVC = entity.IsAvc;
|
||||
dto.Codec = entity.Codec;
|
||||
|
||||
var language = entity.Language;
|
||||
|
||||
// Check if the language has multiple three letter ISO codes
|
||||
// if yes choose the first as that is the ISO 639-2/T code we're needing
|
||||
if (language != null && _localization.TryGetISO6392TFromB(language, out string? isoT))
|
||||
{
|
||||
language = isoT;
|
||||
}
|
||||
|
||||
dto.Language = language;
|
||||
|
||||
dto.ChannelLayout = entity.ChannelLayout;
|
||||
dto.Profile = entity.Profile;
|
||||
dto.AspectRatio = entity.AspectRatio;
|
||||
dto.Path = RestorePath(entity.Path);
|
||||
dto.IsInterlaced = entity.IsInterlaced.GetValueOrDefault();
|
||||
dto.BitRate = entity.BitRate;
|
||||
dto.Channels = entity.Channels;
|
||||
dto.SampleRate = entity.SampleRate;
|
||||
dto.IsDefault = entity.IsDefault;
|
||||
dto.IsForced = entity.IsForced;
|
||||
dto.IsExternal = entity.IsExternal;
|
||||
dto.Height = entity.Height;
|
||||
dto.Width = entity.Width;
|
||||
dto.AverageFrameRate = entity.AverageFrameRate;
|
||||
dto.RealFrameRate = entity.RealFrameRate;
|
||||
dto.Level = entity.Level;
|
||||
dto.PixelFormat = entity.PixelFormat;
|
||||
dto.BitDepth = entity.BitDepth;
|
||||
dto.IsAnamorphic = entity.IsAnamorphic;
|
||||
dto.RefFrames = entity.RefFrames;
|
||||
dto.CodecTag = entity.CodecTag;
|
||||
dto.Comment = entity.Comment;
|
||||
dto.NalLengthSize = entity.NalLengthSize;
|
||||
dto.Title = entity.Title;
|
||||
dto.TimeBase = entity.TimeBase;
|
||||
dto.CodecTimeBase = entity.CodecTimeBase;
|
||||
dto.ColorPrimaries = entity.ColorPrimaries;
|
||||
dto.ColorSpace = entity.ColorSpace;
|
||||
dto.ColorTransfer = entity.ColorTransfer;
|
||||
dto.DvVersionMajor = entity.DvVersionMajor;
|
||||
dto.DvVersionMinor = entity.DvVersionMinor;
|
||||
dto.DvProfile = entity.DvProfile;
|
||||
dto.DvLevel = entity.DvLevel;
|
||||
dto.RpuPresentFlag = entity.RpuPresentFlag;
|
||||
dto.ElPresentFlag = entity.ElPresentFlag;
|
||||
dto.BlPresentFlag = entity.BlPresentFlag;
|
||||
dto.DvBlSignalCompatibilityId = entity.DvBlSignalCompatibilityId;
|
||||
dto.IsHearingImpaired = entity.IsHearingImpaired.GetValueOrDefault();
|
||||
dto.Rotation = entity.Rotation;
|
||||
dto.Hdr10PlusPresentFlag = entity.Hdr10PlusPresentFlag;
|
||||
|
||||
if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
|
||||
{
|
||||
dto.LocalizedDefault = _localization.GetLocalizedString("Default");
|
||||
dto.LocalizedExternal = _localization.GetLocalizedString("External");
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.Language))
|
||||
{
|
||||
var culture = _localization.FindLanguageInfo(dto.Language);
|
||||
dto.LocalizedLanguage = culture?.DisplayName;
|
||||
}
|
||||
|
||||
if (dto.Type is MediaStreamType.Subtitle)
|
||||
{
|
||||
dto.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
|
||||
dto.LocalizedForced = _localization.GetLocalizedString("Forced");
|
||||
dto.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
|
||||
}
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private MediaStreamInfo Map(MediaStream dto, Guid itemId)
|
||||
{
|
||||
var entity = new MediaStreamInfo
|
||||
{
|
||||
Item = null!,
|
||||
ItemId = itemId,
|
||||
StreamIndex = dto.Index,
|
||||
StreamType = (MediaStreamTypeEntity)dto.Type,
|
||||
IsAvc = dto.IsAVC,
|
||||
|
||||
Codec = dto.Codec,
|
||||
Language = dto.Language,
|
||||
ChannelLayout = dto.ChannelLayout,
|
||||
Profile = dto.Profile,
|
||||
AspectRatio = dto.AspectRatio,
|
||||
Path = GetPathToSave(dto.Path) ?? dto.Path,
|
||||
IsInterlaced = dto.IsInterlaced,
|
||||
BitRate = dto.BitRate,
|
||||
Channels = dto.Channels,
|
||||
SampleRate = dto.SampleRate,
|
||||
IsDefault = dto.IsDefault,
|
||||
IsForced = dto.IsForced,
|
||||
IsExternal = dto.IsExternal,
|
||||
Height = dto.Height,
|
||||
Width = dto.Width,
|
||||
AverageFrameRate = dto.AverageFrameRate,
|
||||
RealFrameRate = dto.RealFrameRate,
|
||||
Level = dto.Level.HasValue ? (float)dto.Level : null,
|
||||
PixelFormat = dto.PixelFormat,
|
||||
BitDepth = dto.BitDepth,
|
||||
IsAnamorphic = dto.IsAnamorphic,
|
||||
RefFrames = dto.RefFrames,
|
||||
CodecTag = dto.CodecTag,
|
||||
Comment = dto.Comment,
|
||||
NalLengthSize = dto.NalLengthSize,
|
||||
Title = dto.Title,
|
||||
TimeBase = dto.TimeBase,
|
||||
CodecTimeBase = dto.CodecTimeBase,
|
||||
ColorPrimaries = dto.ColorPrimaries,
|
||||
ColorSpace = dto.ColorSpace,
|
||||
ColorTransfer = dto.ColorTransfer,
|
||||
DvVersionMajor = dto.DvVersionMajor,
|
||||
DvVersionMinor = dto.DvVersionMinor,
|
||||
DvProfile = dto.DvProfile,
|
||||
DvLevel = dto.DvLevel,
|
||||
RpuPresentFlag = dto.RpuPresentFlag,
|
||||
ElPresentFlag = dto.ElPresentFlag,
|
||||
BlPresentFlag = dto.BlPresentFlag,
|
||||
DvBlSignalCompatibilityId = dto.DvBlSignalCompatibilityId,
|
||||
IsHearingImpaired = dto.IsHearingImpaired,
|
||||
Rotation = dto.Rotation,
|
||||
Hdr10PlusPresentFlag = dto.Hdr10PlusPresentFlag,
|
||||
};
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
#pragma warning disable RS0030 // Do not use banned APIs
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Item;
|
||||
|
||||
/// <summary>
|
||||
/// Static class for methods which maps types of ordering to their respecting ordering functions.
|
||||
/// </summary>
|
||||
public static class OrderMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates Func to be executed later with a given BaseItemEntity input for sorting items on query.
|
||||
/// </summary>
|
||||
/// <param name="sortBy">Item property to sort by.</param>
|
||||
/// <param name="query">Context Query.</param>
|
||||
/// <param name="jellyfinDbContext">Context.</param>
|
||||
/// <returns>Func to be executed later for sorting query.</returns>
|
||||
public static Expression<Func<BaseItemEntity, object?>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query, JellyfinDbContext jellyfinDbContext)
|
||||
{
|
||||
return (sortBy, query.User) switch
|
||||
{
|
||||
(ItemSortBy.AirTime, _) => e => e.SortName, // TODO
|
||||
(ItemSortBy.Runtime, _) => e => e.RunTimeTicks,
|
||||
(ItemSortBy.Random, _) => e => EF.Functions.Random(),
|
||||
(ItemSortBy.DatePlayed, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.LastPlayedDate,
|
||||
(ItemSortBy.PlayCount, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.PlayCount,
|
||||
(ItemSortBy.IsFavoriteOrLiked, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.IsFavorite,
|
||||
(ItemSortBy.IsFolder, _) => e => e.IsFolder,
|
||||
(ItemSortBy.IsPlayed, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played,
|
||||
(ItemSortBy.IsUnplayed, _) => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played,
|
||||
(ItemSortBy.DateLastContentAdded, _) => e => e.DateLastMediaAdded,
|
||||
(ItemSortBy.Artist, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
|
||||
(ItemSortBy.AlbumArtist, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
|
||||
(ItemSortBy.Studio, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
|
||||
(ItemSortBy.OfficialRating, _) => e => e.InheritedParentalRatingValue,
|
||||
(ItemSortBy.SeriesSortName, _) => e => e.SeriesName,
|
||||
(ItemSortBy.Album, _) => e => e.Album,
|
||||
(ItemSortBy.DateCreated, _) => e => e.DateCreated,
|
||||
(ItemSortBy.PremiereDate, _) => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)),
|
||||
(ItemSortBy.StartDate, _) => e => e.StartDate,
|
||||
(ItemSortBy.Name, _) => e => e.CleanName,
|
||||
(ItemSortBy.CommunityRating, _) => e => e.CommunityRating,
|
||||
(ItemSortBy.ProductionYear, _) => e => e.ProductionYear,
|
||||
(ItemSortBy.CriticRating, _) => e => e.CriticRating,
|
||||
(ItemSortBy.VideoBitRate, _) => e => e.TotalBitrate,
|
||||
(ItemSortBy.ParentIndexNumber, _) => e => e.ParentIndexNumber,
|
||||
(ItemSortBy.IndexNumber, _) => e => e.IndexNumber,
|
||||
(ItemSortBy.SeriesDatePlayed, not null) => e =>
|
||||
jellyfinDbContext.BaseItems
|
||||
.Where(w => w.SeriesPresentationUniqueKey == e.PresentationUniqueKey)
|
||||
.Join(jellyfinDbContext.UserData.Where(w => w.UserId == query.User.Id && w.Played), f => f.Id, f => f.ItemId, (item, userData) => userData.LastPlayedDate)
|
||||
.Max(f => f),
|
||||
(ItemSortBy.SeriesDatePlayed, null) => e => jellyfinDbContext.BaseItems.Where(w => w.SeriesPresentationUniqueKey == e.PresentationUniqueKey)
|
||||
.Join(jellyfinDbContext.UserData.Where(w => w.Played), f => f.Id, f => f.ItemId, (item, userData) => userData.LastPlayedDate)
|
||||
.Max(f => f),
|
||||
// ItemSortBy.SeriesDatePlayed => e => jellyfinDbContext.UserData
|
||||
// .Where(u => u.Item!.SeriesPresentationUniqueKey == e.PresentationUniqueKey && u.Played)
|
||||
// .Max(f => f.LastPlayedDate),
|
||||
// ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
|
||||
_ => e => e.SortName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an expression to order search results by match quality.
|
||||
/// Prioritizes: exact match (0) > prefix match with word boundary (1) > prefix match (2) > contains (3).
|
||||
/// </summary>
|
||||
/// <param name="searchTerm">The search term to match against.</param>
|
||||
/// <returns>An expression that returns an integer representing match quality (lower is better).</returns>
|
||||
public static Expression<Func<BaseItemEntity, int>> MapSearchRelevanceOrder(string searchTerm)
|
||||
{
|
||||
var cleanSearchTerm = GetCleanValue(searchTerm);
|
||||
var searchPrefix = cleanSearchTerm + " ";
|
||||
return e =>
|
||||
e.CleanName == cleanSearchTerm ? 0 :
|
||||
e.CleanName!.StartsWith(searchPrefix) ? 1 :
|
||||
e.CleanName!.StartsWith(cleanSearchTerm) ? 2 : 3;
|
||||
}
|
||||
|
||||
private static string GetCleanValue(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return value.RemoveDiacritics().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Entities.Libraries;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Item;
|
||||
#pragma warning disable RS0030 // Do not use banned APIs
|
||||
#pragma warning disable CA1304 // Specify CultureInfo
|
||||
#pragma warning disable CA1311 // Specify a culture or use an invariant version
|
||||
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
||||
|
||||
/// <summary>
|
||||
/// Manager for handling people.
|
||||
/// </summary>
|
||||
/// <param name="dbProvider">Efcore Factory.</param>
|
||||
/// <param name="itemTypeLookup">Items lookup service.</param>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="PeopleRepository"/> class.
|
||||
/// </remarks>
|
||||
public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IItemTypeLookup itemTypeLookup) : IPeopleRepository
|
||||
{
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider = dbProvider;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery filter)
|
||||
{
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter);
|
||||
|
||||
// Include PeopleBaseItemMap
|
||||
if (!filter.ItemId.IsEmpty())
|
||||
{
|
||||
dbQuery = dbQuery.Include(p => p.BaseItems!.Where(m => m.ItemId == filter.ItemId))
|
||||
.OrderBy(e => e.BaseItems!.First(e => e.ItemId == filter.ItemId).ListOrder)
|
||||
.ThenBy(e => e.PersonType)
|
||||
.ThenBy(e => e.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
dbQuery = dbQuery.OrderBy(e => e.Name);
|
||||
}
|
||||
|
||||
if (filter.Limit > 0)
|
||||
{
|
||||
dbQuery = dbQuery.Take(filter.Limit);
|
||||
}
|
||||
|
||||
return dbQuery.AsEnumerable().Select(Map).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery filter)
|
||||
{
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter).Select(e => e.Name).Distinct();
|
||||
|
||||
// dbQuery = dbQuery.OrderBy(e => e.ListOrder);
|
||||
if (filter.Limit > 0)
|
||||
{
|
||||
dbQuery = dbQuery.Take(filter.Limit);
|
||||
}
|
||||
|
||||
return dbQuery.ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people)
|
||||
{
|
||||
foreach (var person in people)
|
||||
{
|
||||
person.Name = person.Name.Trim();
|
||||
person.Role = person.Role?.Trim() ?? string.Empty;
|
||||
}
|
||||
|
||||
// multiple metadata providers can provide the _same_ person
|
||||
people = people.DistinctBy(e => e.Name + "-" + e.Type).ToArray();
|
||||
var personKeys = people.Select(e => e.Name + "-" + e.Type).ToArray();
|
||||
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
using var transaction = context.Database.BeginTransaction();
|
||||
var existingPersons = context.Peoples.Select(e => new
|
||||
{
|
||||
item = e,
|
||||
SelectionKey = e.Name + "-" + e.PersonType
|
||||
})
|
||||
.Where(p => personKeys.Contains(p.SelectionKey))
|
||||
.Select(f => f.item)
|
||||
.ToArray();
|
||||
|
||||
var toAdd = people
|
||||
.Where(e => e.Type is not PersonKind.Artist && e.Type is not PersonKind.AlbumArtist)
|
||||
.Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString()))
|
||||
.Select(Map);
|
||||
context.Peoples.AddRange(toAdd);
|
||||
context.SaveChanges();
|
||||
|
||||
var personsEntities = toAdd.Concat(existingPersons).ToArray();
|
||||
|
||||
var existingMaps = context.PeopleBaseItemMap.Include(e => e.People).Where(e => e.ItemId == itemId).ToList();
|
||||
|
||||
var listOrder = 0;
|
||||
|
||||
foreach (var person in people)
|
||||
{
|
||||
if (person.Type == PersonKind.Artist || person.Type == PersonKind.AlbumArtist)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var entityPerson = personsEntities.First(e => e.Name == person.Name && e.PersonType == person.Type.ToString());
|
||||
var existingMap = existingMaps.FirstOrDefault(e => e.People.Name == person.Name && e.People.PersonType == person.Type.ToString() && e.Role == person.Role);
|
||||
if (existingMap is null)
|
||||
{
|
||||
context.PeopleBaseItemMap.Add(new PeopleBaseItemMap()
|
||||
{
|
||||
Item = null!,
|
||||
ItemId = itemId,
|
||||
People = null!,
|
||||
PeopleId = entityPerson.Id,
|
||||
ListOrder = listOrder,
|
||||
SortOrder = person.SortOrder,
|
||||
Role = person.Role
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update the order for existing mappings
|
||||
existingMap.ListOrder = listOrder;
|
||||
existingMap.SortOrder = person.SortOrder;
|
||||
// person mapping already exists so remove from list
|
||||
existingMaps.Remove(existingMap);
|
||||
}
|
||||
|
||||
listOrder++;
|
||||
}
|
||||
|
||||
context.PeopleBaseItemMap.RemoveRange(existingMaps);
|
||||
|
||||
context.SaveChanges();
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
private PersonInfo Map(People people)
|
||||
{
|
||||
var mapping = people.BaseItems?.FirstOrDefault();
|
||||
var personInfo = new PersonInfo()
|
||||
{
|
||||
Id = people.Id,
|
||||
Name = people.Name,
|
||||
Role = mapping?.Role,
|
||||
SortOrder = mapping?.SortOrder
|
||||
};
|
||||
if (Enum.TryParse<PersonKind>(people.PersonType, out var kind))
|
||||
{
|
||||
personInfo.Type = kind;
|
||||
}
|
||||
|
||||
return personInfo;
|
||||
}
|
||||
|
||||
private People Map(PersonInfo people)
|
||||
{
|
||||
var personInfo = new People()
|
||||
{
|
||||
Name = people.Name,
|
||||
PersonType = people.Type.ToString(),
|
||||
Id = people.Id,
|
||||
};
|
||||
|
||||
return personInfo;
|
||||
}
|
||||
|
||||
private IQueryable<People> TranslateQuery(IQueryable<People> query, JellyfinDbContext context, InternalPeopleQuery filter)
|
||||
{
|
||||
if (filter.User is not null && filter.IsFavorite.HasValue)
|
||||
{
|
||||
var personType = itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
|
||||
var oldQuery = query;
|
||||
|
||||
query = context.UserData
|
||||
.Where(u => u.Item!.Type == personType && u.IsFavorite == filter.IsFavorite && u.UserId.Equals(filter.User.Id))
|
||||
.Join(oldQuery, e => e.Item!.Name, e => e.Name, (item, person) => person)
|
||||
.Distinct()
|
||||
.AsNoTracking();
|
||||
}
|
||||
|
||||
if (!filter.ItemId.IsEmpty())
|
||||
{
|
||||
query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.ItemId)));
|
||||
}
|
||||
|
||||
if (!filter.AppearsInItemId.IsEmpty())
|
||||
{
|
||||
query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.AppearsInItemId)));
|
||||
}
|
||||
|
||||
var queryPersonTypes = filter.PersonTypes.Where(IsValidPersonType).ToList();
|
||||
if (queryPersonTypes.Count > 0)
|
||||
{
|
||||
query = query.Where(e => queryPersonTypes.Contains(e.PersonType));
|
||||
}
|
||||
|
||||
var queryExcludePersonTypes = filter.ExcludePersonTypes.Where(IsValidPersonType).ToList();
|
||||
|
||||
if (queryExcludePersonTypes.Count > 0)
|
||||
{
|
||||
query = query.Where(e => !queryPersonTypes.Contains(e.PersonType));
|
||||
}
|
||||
|
||||
if (filter.MaxListOrder.HasValue && !filter.ItemId.IsEmpty())
|
||||
{
|
||||
query = query.Where(e => e.BaseItems!.First(w => w.ItemId == filter.ItemId).ListOrder <= filter.MaxListOrder.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.NameContains))
|
||||
{
|
||||
var nameContainsUpper = filter.NameContains.ToUpper();
|
||||
query = query.Where(e => e.Name.ToUpper().Contains(nameContainsUpper));
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
private bool IsAlphaNumeric(string str)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < str.Length; i++)
|
||||
{
|
||||
if (!char.IsLetter(str[i]) && !char.IsNumber(str[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsValidPersonType(string value)
|
||||
{
|
||||
return IsAlphaNumeric(value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user