Merge pull request #12798 from JPVenson/feature/EFUserData
Refactor library.db into jellyfin.db and EFCorepull/12932/head
commit
93b8eade61
@ -1,269 +0,0 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
public abstract class BaseSqliteRepository : IDisposable
|
||||
{
|
||||
private bool _disposed = false;
|
||||
private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
|
||||
private SqliteConnection _writeConnection;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
protected BaseSqliteRepository(ILogger<BaseSqliteRepository> logger)
|
||||
{
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the DB file.
|
||||
/// </summary>
|
||||
protected string DbFilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger.
|
||||
/// </summary>
|
||||
/// <value>The logger.</value>
|
||||
protected ILogger<BaseSqliteRepository> Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cache size.
|
||||
/// </summary>
|
||||
/// <value>The cache size or null.</value>
|
||||
protected virtual int? CacheSize => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
|
||||
/// </summary>
|
||||
protected virtual string LockingMode => "NORMAL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
|
||||
/// </summary>
|
||||
/// <value>The journal mode.</value>
|
||||
protected virtual string JournalMode => "WAL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
|
||||
/// The default (-1) is overridden to prevent unconstrained WAL size, as reported by users.
|
||||
/// </summary>
|
||||
/// <value>The journal size limit.</value>
|
||||
protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
|
||||
|
||||
/// <summary>
|
||||
/// Gets the page size.
|
||||
/// </summary>
|
||||
/// <value>The page size or null.</value>
|
||||
protected virtual int? PageSize => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the temp store mode.
|
||||
/// </summary>
|
||||
/// <value>The temp store mode.</value>
|
||||
/// <see cref="TempStoreMode"/>
|
||||
protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the synchronous mode.
|
||||
/// </summary>
|
||||
/// <value>The synchronous mode or null.</value>
|
||||
/// <see cref="SynchronousMode"/>
|
||||
protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
|
||||
|
||||
public virtual void Initialize()
|
||||
{
|
||||
// Configuration and pragmas can affect VACUUM so it needs to be last.
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.Execute("VACUUM");
|
||||
}
|
||||
}
|
||||
|
||||
protected ManagedConnection GetConnection(bool readOnly = false)
|
||||
{
|
||||
if (!readOnly)
|
||||
{
|
||||
_writeLock.Wait();
|
||||
if (_writeConnection is not null)
|
||||
{
|
||||
return new ManagedConnection(_writeConnection, _writeLock);
|
||||
}
|
||||
|
||||
var writeConnection = new SqliteConnection($"Filename={DbFilePath};Pooling=False");
|
||||
writeConnection.Open();
|
||||
|
||||
if (CacheSize.HasValue)
|
||||
{
|
||||
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(LockingMode))
|
||||
{
|
||||
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(JournalMode))
|
||||
{
|
||||
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
|
||||
}
|
||||
|
||||
if (JournalSizeLimit.HasValue)
|
||||
{
|
||||
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
|
||||
}
|
||||
|
||||
if (Synchronous.HasValue)
|
||||
{
|
||||
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
||||
}
|
||||
|
||||
if (PageSize.HasValue)
|
||||
{
|
||||
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
|
||||
}
|
||||
|
||||
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||
|
||||
return new ManagedConnection(_writeConnection = writeConnection, _writeLock);
|
||||
}
|
||||
|
||||
var connection = new SqliteConnection($"Filename={DbFilePath};Mode=ReadOnly");
|
||||
connection.Open();
|
||||
|
||||
if (CacheSize.HasValue)
|
||||
{
|
||||
connection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(LockingMode))
|
||||
{
|
||||
connection.Execute("PRAGMA locking_mode=" + LockingMode);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(JournalMode))
|
||||
{
|
||||
connection.Execute("PRAGMA journal_mode=" + JournalMode);
|
||||
}
|
||||
|
||||
if (JournalSizeLimit.HasValue)
|
||||
{
|
||||
connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
|
||||
}
|
||||
|
||||
if (Synchronous.HasValue)
|
||||
{
|
||||
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
||||
}
|
||||
|
||||
if (PageSize.HasValue)
|
||||
{
|
||||
connection.Execute("PRAGMA page_size=" + PageSize.Value);
|
||||
}
|
||||
|
||||
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||
|
||||
return new ManagedConnection(connection, null);
|
||||
}
|
||||
|
||||
public SqliteCommand PrepareStatement(ManagedConnection connection, string sql)
|
||||
{
|
||||
var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
return command;
|
||||
}
|
||||
|
||||
protected bool TableExists(ManagedConnection connection, string name)
|
||||
{
|
||||
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected List<string> GetColumnNames(ManagedConnection connection, string table)
|
||||
{
|
||||
var columnNames = new List<string>();
|
||||
|
||||
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
|
||||
{
|
||||
if (row.TryGetString(1, out var columnName))
|
||||
{
|
||||
columnNames.Add(columnName);
|
||||
}
|
||||
}
|
||||
|
||||
return columnNames;
|
||||
}
|
||||
|
||||
protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
||||
{
|
||||
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
connection.Execute("alter table " + table + " add column " + columnName + " " + type + " NULL");
|
||||
}
|
||||
|
||||
protected void CheckDisposed()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (dispose)
|
||||
{
|
||||
_writeLock.Wait();
|
||||
try
|
||||
{
|
||||
_writeConnection.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeLock.Release();
|
||||
}
|
||||
|
||||
_writeLock.Dispose();
|
||||
}
|
||||
|
||||
_writeConnection = null;
|
||||
_writeLock = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Channels;
|
||||
using Emby.Server.Implementations.Playlists;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
|
||||
namespace Emby.Server.Implementations.Data;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class ItemTypeLookup : IItemTypeLookup
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> MusicGenreTypes { get; } = [
|
||||
typeof(Audio).FullName!,
|
||||
typeof(MusicVideo).FullName!,
|
||||
typeof(MusicAlbum).FullName!,
|
||||
typeof(MusicArtist).FullName!,
|
||||
];
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<BaseItemKind, string> BaseItemKindNames { get; } = new Dictionary<BaseItemKind, string>()
|
||||
{
|
||||
{ BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName! },
|
||||
{ BaseItemKind.Audio, typeof(Audio).FullName! },
|
||||
{ BaseItemKind.AudioBook, typeof(AudioBook).FullName! },
|
||||
{ BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName! },
|
||||
{ BaseItemKind.Book, typeof(Book).FullName! },
|
||||
{ BaseItemKind.BoxSet, typeof(BoxSet).FullName! },
|
||||
{ BaseItemKind.Channel, typeof(Channel).FullName! },
|
||||
{ BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName! },
|
||||
{ BaseItemKind.Episode, typeof(Episode).FullName! },
|
||||
{ BaseItemKind.Folder, typeof(Folder).FullName! },
|
||||
{ BaseItemKind.Genre, typeof(Genre).FullName! },
|
||||
{ BaseItemKind.Movie, typeof(Movie).FullName! },
|
||||
{ BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName! },
|
||||
{ BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName! },
|
||||
{ BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName! },
|
||||
{ BaseItemKind.MusicArtist, typeof(MusicArtist).FullName! },
|
||||
{ BaseItemKind.MusicGenre, typeof(MusicGenre).FullName! },
|
||||
{ BaseItemKind.MusicVideo, typeof(MusicVideo).FullName! },
|
||||
{ BaseItemKind.Person, typeof(Person).FullName! },
|
||||
{ BaseItemKind.Photo, typeof(Photo).FullName! },
|
||||
{ BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName! },
|
||||
{ BaseItemKind.Playlist, typeof(Playlist).FullName! },
|
||||
{ BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName! },
|
||||
{ BaseItemKind.Season, typeof(Season).FullName! },
|
||||
{ BaseItemKind.Series, typeof(Series).FullName! },
|
||||
{ BaseItemKind.Studio, typeof(Studio).FullName! },
|
||||
{ BaseItemKind.Trailer, typeof(Trailer).FullName! },
|
||||
{ BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName! },
|
||||
{ BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName! },
|
||||
{ BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName! },
|
||||
{ BaseItemKind.UserView, typeof(UserView).FullName! },
|
||||
{ BaseItemKind.Video, typeof(Video).FullName! },
|
||||
{ BaseItemKind.Year, typeof(Year).FullName! }
|
||||
}.ToFrozenDictionary();
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace Emby.Server.Implementations.Data;
|
||||
|
||||
public sealed class ManagedConnection : IDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim? _writeLock;
|
||||
|
||||
private SqliteConnection _db;
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
public ManagedConnection(SqliteConnection db, SemaphoreSlim? writeLock)
|
||||
{
|
||||
_db = db;
|
||||
_writeLock = writeLock;
|
||||
}
|
||||
|
||||
public SqliteTransaction BeginTransaction()
|
||||
=> _db.BeginTransaction();
|
||||
|
||||
public SqliteCommand CreateCommand()
|
||||
=> _db.CreateCommand();
|
||||
|
||||
public void Execute(string commandText)
|
||||
=> _db.Execute(commandText);
|
||||
|
||||
public SqliteCommand PrepareStatement(string sql)
|
||||
=> _db.PrepareStatement(sql);
|
||||
|
||||
public IEnumerable<SqliteDataReader> Query(string commandText)
|
||||
=> _db.Query(commandText);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_writeLock is null)
|
||||
{
|
||||
// Read connections are managed with an internal pool
|
||||
_db.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write lock is managed by BaseSqliteRepository
|
||||
// Don't dispose here
|
||||
_writeLock.Release();
|
||||
}
|
||||
|
||||
_db = null!;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,369 +0,0 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
public SqliteUserDataRepository(
|
||||
ILogger<SqliteUserDataRepository> logger,
|
||||
IServerConfigurationManager config,
|
||||
IUserManager userManager)
|
||||
: base(logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
|
||||
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the connection to the database.
|
||||
/// </summary>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
var userDatasTableExists = TableExists(connection, "UserDatas");
|
||||
var userDataTableExists = TableExists(connection, "userdata");
|
||||
|
||||
var users = userDatasTableExists ? null : _userManager.Users;
|
||||
using var transaction = connection.BeginTransaction();
|
||||
connection.Execute(string.Join(
|
||||
';',
|
||||
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
|
||||
"drop index if exists idx_userdata",
|
||||
"drop index if exists idx_userdata1",
|
||||
"drop index if exists idx_userdata2",
|
||||
"drop index if exists userdataindex1",
|
||||
"drop index if exists userdataindex",
|
||||
"drop index if exists userdataindex3",
|
||||
"drop index if exists userdataindex4",
|
||||
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
|
||||
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
|
||||
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
|
||||
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)",
|
||||
"create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)"));
|
||||
|
||||
if (!userDataTableExists)
|
||||
{
|
||||
transaction.Commit();
|
||||
return;
|
||||
}
|
||||
|
||||
var existingColumnNames = GetColumnNames(connection, "userdata");
|
||||
|
||||
AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
|
||||
AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
|
||||
AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
|
||||
|
||||
if (userDatasTableExists)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImportUserIds(connection, users);
|
||||
|
||||
connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
private void ImportUserIds(ManagedConnection db, IEnumerable<User> users)
|
||||
{
|
||||
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
|
||||
|
||||
using (var statement = db.PrepareStatement("update userdata set InternalUserId=@InternalUserId where UserId=@UserId"))
|
||||
{
|
||||
foreach (var user in users)
|
||||
{
|
||||
if (!userIdsWithUserData.Contains(user.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
statement.TryBind("@UserId", user.Id);
|
||||
statement.TryBind("@InternalUserId", user.InternalId);
|
||||
|
||||
statement.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Guid> GetAllUserIdsWithUserData(ManagedConnection db)
|
||||
{
|
||||
var list = new List<Guid>();
|
||||
|
||||
using (var statement = PrepareStatement(db, "select DISTINCT UserId from UserData where UserId not null"))
|
||||
{
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
try
|
||||
{
|
||||
list.Add(row.GetGuid(0));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error while getting user");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userData);
|
||||
|
||||
if (userId <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(userId));
|
||||
}
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
||||
|
||||
PersistUserData(userId, key, userData, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userData);
|
||||
|
||||
if (userId <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(userId));
|
||||
}
|
||||
|
||||
PersistAllUserData(userId, userData, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persists the user data.
|
||||
/// </summary>
|
||||
/// <param name="internalUserId">The user id.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="userData">The user data.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
using (var transaction = connection.BeginTransaction())
|
||||
{
|
||||
SaveUserData(connection, internalUserId, key, userData);
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData)
|
||||
{
|
||||
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
|
||||
{
|
||||
statement.TryBind("@userId", internalUserId);
|
||||
statement.TryBind("@key", key);
|
||||
|
||||
if (userData.Rating.HasValue)
|
||||
{
|
||||
statement.TryBind("@rating", userData.Rating.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
statement.TryBindNull("@rating");
|
||||
}
|
||||
|
||||
statement.TryBind("@played", userData.Played);
|
||||
statement.TryBind("@playCount", userData.PlayCount);
|
||||
statement.TryBind("@isFavorite", userData.IsFavorite);
|
||||
statement.TryBind("@playbackPositionTicks", userData.PlaybackPositionTicks);
|
||||
|
||||
if (userData.LastPlayedDate.HasValue)
|
||||
{
|
||||
statement.TryBind("@lastPlayedDate", userData.LastPlayedDate.Value.ToDateTimeParamValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
statement.TryBindNull("@lastPlayedDate");
|
||||
}
|
||||
|
||||
if (userData.AudioStreamIndex.HasValue)
|
||||
{
|
||||
statement.TryBind("@AudioStreamIndex", userData.AudioStreamIndex.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
statement.TryBindNull("@AudioStreamIndex");
|
||||
}
|
||||
|
||||
if (userData.SubtitleStreamIndex.HasValue)
|
||||
{
|
||||
statement.TryBind("@SubtitleStreamIndex", userData.SubtitleStreamIndex.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
statement.TryBindNull("@SubtitleStreamIndex");
|
||||
}
|
||||
|
||||
statement.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persist all user data for the specified user.
|
||||
/// </summary>
|
||||
private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
using (var transaction = connection.BeginTransaction())
|
||||
{
|
||||
foreach (var userItemData in userDataList)
|
||||
{
|
||||
SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user data.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>Task{UserItemData}.</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// userId
|
||||
/// or
|
||||
/// key.
|
||||
/// </exception>
|
||||
public UserItemData GetUserData(long userId, string key)
|
||||
{
|
||||
if (userId <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(userId));
|
||||
}
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
||||
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
||||
{
|
||||
statement.TryBind("@UserId", userId);
|
||||
statement.TryBind("@Key", key);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
return ReadRow(row);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public UserItemData GetUserData(long userId, List<string> keys)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(keys);
|
||||
|
||||
if (keys.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetUserData(userId, keys[0]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return all user-data associated with the given user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The internal user id.</param>
|
||||
/// <returns>The list of user item data.</returns>
|
||||
public List<UserItemData> GetAllUserData(long userId)
|
||||
{
|
||||
if (userId <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(userId));
|
||||
}
|
||||
|
||||
var list = new List<UserItemData>();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
|
||||
{
|
||||
statement.TryBind("@UserId", userId);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
list.Add(ReadRow(row));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a row from the specified reader into the provided userData object.
|
||||
/// </summary>
|
||||
/// <param name="reader">The list of result set values.</param>
|
||||
/// <returns>The user item data.</returns>
|
||||
private UserItemData ReadRow(SqliteDataReader reader)
|
||||
{
|
||||
var userData = new UserItemData
|
||||
{
|
||||
Key = reader.GetString(0)
|
||||
};
|
||||
|
||||
if (reader.TryGetDouble(2, out var rating))
|
||||
{
|
||||
userData.Rating = rating;
|
||||
}
|
||||
|
||||
userData.Played = reader.GetBoolean(3);
|
||||
userData.PlayCount = reader.GetInt32(4);
|
||||
userData.IsFavorite = reader.GetBoolean(5);
|
||||
userData.PlaybackPositionTicks = reader.GetInt64(6);
|
||||
|
||||
if (reader.TryReadDateTime(7, out var lastPlayedDate))
|
||||
{
|
||||
userData.LastPlayedDate = lastPlayedDate;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(8, out var audioStreamIndex))
|
||||
{
|
||||
userData.AudioStreamIndex = audioStreamIndex;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(9, out var subtitleStreamIndex))
|
||||
{
|
||||
userData.SubtitleStreamIndex = subtitleStreamIndex;
|
||||
}
|
||||
|
||||
return userData;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
namespace Emby.Server.Implementations.Data;
|
||||
|
||||
/// <summary>
|
||||
/// The disk synchronization mode, controls how aggressively SQLite will write data
|
||||
/// all the way out to physical storage.
|
||||
/// </summary>
|
||||
public enum SynchronousMode
|
||||
{
|
||||
/// <summary>
|
||||
/// SQLite continues without syncing as soon as it has handed data off to the operating system.
|
||||
/// </summary>
|
||||
Off = 0,
|
||||
|
||||
/// <summary>
|
||||
/// SQLite database engine will still sync at the most critical moments.
|
||||
/// </summary>
|
||||
Normal = 1,
|
||||
|
||||
/// <summary>
|
||||
/// SQLite database engine will use the xSync method of the VFS
|
||||
/// to ensure that all content is safely written to the disk surface prior to continuing.
|
||||
/// </summary>
|
||||
Full = 2,
|
||||
|
||||
/// <summary>
|
||||
/// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
|
||||
/// is synced after that journal is unlinked to commit a transaction in DELETE mode.
|
||||
/// </summary>
|
||||
Extra = 3
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
namespace Emby.Server.Implementations.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Storage mode used by temporary database files.
|
||||
/// </summary>
|
||||
public enum TempStoreMode
|
||||
{
|
||||
/// <summary>
|
||||
/// The compile-time C preprocessor macro SQLITE_TEMP_STORE
|
||||
/// is used to determine where temporary tables and indices are stored.
|
||||
/// </summary>
|
||||
Default = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Temporary tables and indices are stored in a file.
|
||||
/// </summary>
|
||||
File = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
|
||||
/// </summary>
|
||||
Memory = 2
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the relational informations for an <see cref="BaseItemEntity"/>.
|
||||
/// </summary>
|
||||
public class AncestorId
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or Sets the AncestorId.
|
||||
/// </summary>
|
||||
public required Guid ParentItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the related BaseItem.
|
||||
/// </summary>
|
||||
public required Guid ItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the ParentItem.
|
||||
/// </summary>
|
||||
public required BaseItemEntity ParentItem { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the Child item.
|
||||
/// </summary>
|
||||
public required BaseItemEntity Item { get; set; }
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Provides informations about an Attachment to an <see cref="BaseItemEntity"/>.
|
||||
/// </summary>
|
||||
public class AttachmentStreamInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or Sets the <see cref="BaseItemEntity"/> reference.
|
||||
/// </summary>
|
||||
public required Guid ItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the <see cref="BaseItemEntity"/> reference.
|
||||
/// </summary>
|
||||
public required BaseItemEntity Item { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets The index within the source file.
|
||||
/// </summary>
|
||||
public required int Index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the codec of the attachment.
|
||||
/// </summary>
|
||||
public required string Codec { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the codec tag of the attachment.
|
||||
/// </summary>
|
||||
public string? CodecTag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the comment of the attachment.
|
||||
/// </summary>
|
||||
public string? Comment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the filename of the attachment.
|
||||
/// </summary>
|
||||
public string? Filename { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the attachments mimetype.
|
||||
/// </summary>
|
||||
public string? MimeType { get; set; }
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
#pragma warning disable CA2227 // Collection properties should be read only
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
public class BaseItemEntity
|
||||
{
|
||||
public required Guid Id { get; set; }
|
||||
|
||||
public required string Type { get; set; }
|
||||
|
||||
public string? Data { get; set; }
|
||||
|
||||
public string? Path { get; set; }
|
||||
|
||||
public DateTime StartDate { get; set; }
|
||||
|
||||
public DateTime EndDate { get; set; }
|
||||
|
||||
public string? ChannelId { get; set; }
|
||||
|
||||
public bool IsMovie { get; set; }
|
||||
|
||||
public float? CommunityRating { get; set; }
|
||||
|
||||
public string? CustomRating { get; set; }
|
||||
|
||||
public int? IndexNumber { get; set; }
|
||||
|
||||
public bool IsLocked { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public string? OfficialRating { get; set; }
|
||||
|
||||
public string? MediaType { get; set; }
|
||||
|
||||
public string? Overview { get; set; }
|
||||
|
||||
public int? ParentIndexNumber { get; set; }
|
||||
|
||||
public DateTime? PremiereDate { get; set; }
|
||||
|
||||
public int? ProductionYear { get; set; }
|
||||
|
||||
public string? Genres { get; set; }
|
||||
|
||||
public string? SortName { get; set; }
|
||||
|
||||
public string? ForcedSortName { get; set; }
|
||||
|
||||
public long? RunTimeTicks { get; set; }
|
||||
|
||||
public DateTime? DateCreated { get; set; }
|
||||
|
||||
public DateTime? DateModified { get; set; }
|
||||
|
||||
public bool IsSeries { get; set; }
|
||||
|
||||
public string? EpisodeTitle { get; set; }
|
||||
|
||||
public bool IsRepeat { get; set; }
|
||||
|
||||
public string? PreferredMetadataLanguage { get; set; }
|
||||
|
||||
public string? PreferredMetadataCountryCode { get; set; }
|
||||
|
||||
public DateTime? DateLastRefreshed { get; set; }
|
||||
|
||||
public DateTime? DateLastSaved { get; set; }
|
||||
|
||||
public bool IsInMixedFolder { get; set; }
|
||||
|
||||
public string? Studios { get; set; }
|
||||
|
||||
public string? ExternalServiceId { get; set; }
|
||||
|
||||
public string? Tags { get; set; }
|
||||
|
||||
public bool IsFolder { get; set; }
|
||||
|
||||
public int? InheritedParentalRatingValue { get; set; }
|
||||
|
||||
public string? UnratedType { get; set; }
|
||||
|
||||
public float? CriticRating { get; set; }
|
||||
|
||||
public string? CleanName { get; set; }
|
||||
|
||||
public string? PresentationUniqueKey { get; set; }
|
||||
|
||||
public string? OriginalTitle { get; set; }
|
||||
|
||||
public string? PrimaryVersionId { get; set; }
|
||||
|
||||
public DateTime? DateLastMediaAdded { get; set; }
|
||||
|
||||
public string? Album { get; set; }
|
||||
|
||||
public float? LUFS { get; set; }
|
||||
|
||||
public float? NormalizationGain { get; set; }
|
||||
|
||||
public bool IsVirtualItem { get; set; }
|
||||
|
||||
public string? SeriesName { get; set; }
|
||||
|
||||
public string? SeasonName { get; set; }
|
||||
|
||||
public string? ExternalSeriesId { get; set; }
|
||||
|
||||
public string? Tagline { get; set; }
|
||||
|
||||
public string? ProductionLocations { get; set; }
|
||||
|
||||
public string? ExtraIds { get; set; }
|
||||
|
||||
public int? TotalBitrate { get; set; }
|
||||
|
||||
public BaseItemExtraType? ExtraType { get; set; }
|
||||
|
||||
public string? Artists { get; set; }
|
||||
|
||||
public string? AlbumArtists { get; set; }
|
||||
|
||||
public string? ExternalId { get; set; }
|
||||
|
||||
public string? SeriesPresentationUniqueKey { get; set; }
|
||||
|
||||
public string? ShowId { get; set; }
|
||||
|
||||
public string? OwnerId { get; set; }
|
||||
|
||||
public int? Width { get; set; }
|
||||
|
||||
public int? Height { get; set; }
|
||||
|
||||
public long? Size { get; set; }
|
||||
|
||||
public ProgramAudioEntity? Audio { get; set; }
|
||||
|
||||
public Guid? ParentId { get; set; }
|
||||
|
||||
public Guid? TopParentId { get; set; }
|
||||
|
||||
public Guid? SeasonId { get; set; }
|
||||
|
||||
public Guid? SeriesId { get; set; }
|
||||
|
||||
public ICollection<PeopleBaseItemMap>? Peoples { get; set; }
|
||||
|
||||
public ICollection<UserData>? UserData { get; set; }
|
||||
|
||||
public ICollection<ItemValueMap>? ItemValues { get; set; }
|
||||
|
||||
public ICollection<MediaStreamInfo>? MediaStreams { get; set; }
|
||||
|
||||
public ICollection<Chapter>? Chapters { get; set; }
|
||||
|
||||
public ICollection<BaseItemProvider>? Provider { get; set; }
|
||||
|
||||
public ICollection<AncestorId>? ParentAncestors { get; set; }
|
||||
|
||||
public ICollection<AncestorId>? Children { get; set; }
|
||||
|
||||
public ICollection<BaseItemMetadataField>? LockedFields { get; set; }
|
||||
|
||||
public ICollection<BaseItemTrailerType>? TrailerTypes { get; set; }
|
||||
|
||||
public ICollection<BaseItemImageInfo>? Images { get; set; }
|
||||
|
||||
// those are references to __LOCAL__ ids not DB ids ... TODO: Bring the whole folder structure into the DB
|
||||
// public ICollection<BaseItemEntity>? SeriesEpisodes { get; set; }
|
||||
// public BaseItemEntity? Series { get; set; }
|
||||
// public BaseItemEntity? Season { get; set; }
|
||||
// public BaseItemEntity? Parent { get; set; }
|
||||
// public ICollection<BaseItemEntity>? DirectChildren { get; set; }
|
||||
// public BaseItemEntity? TopParent { get; set; }
|
||||
// public ICollection<BaseItemEntity>? AllChildren { get; set; }
|
||||
// public ICollection<BaseItemEntity>? SeasonEpisodes { get; set; }
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
#pragma warning disable CS1591
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
public enum BaseItemExtraType
|
||||
{
|
||||
Unknown = 0,
|
||||
Clip = 1,
|
||||
Trailer = 2,
|
||||
BehindTheScenes = 3,
|
||||
DeletedScene = 4,
|
||||
Interview = 5,
|
||||
Scene = 6,
|
||||
Sample = 7,
|
||||
ThemeSong = 8,
|
||||
ThemeVideo = 9,
|
||||
Featurette = 10,
|
||||
Short = 11
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
#pragma warning disable CA2227
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Enum TrailerTypes.
|
||||
/// </summary>
|
||||
public class BaseItemImageInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or Sets.
|
||||
/// </summary>
|
||||
public required Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the path to the original image.
|
||||
/// </summary>
|
||||
public required string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the time the image was last modified.
|
||||
/// </summary>
|
||||
public DateTime DateModified { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the imagetype.
|
||||
/// </summary>
|
||||
public ImageInfoImageType ImageType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the width of the original image.
|
||||
/// </summary>
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the height of the original image.
|
||||
/// </summary>
|
||||
public int Height { get; set; }
|
||||
|
||||
#pragma warning disable CA1819 // Properties should not return arrays
|
||||
/// <summary>
|
||||
/// Gets or Sets the blurhash.
|
||||
/// </summary>
|
||||
public byte[]? Blurhash { get; set; }
|
||||
#pragma warning restore CA1819
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the reference id to the BaseItem.
|
||||
/// </summary>
|
||||
public required Guid ItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the referenced Item.
|
||||
/// </summary>
|
||||
public required BaseItemEntity Item { get; set; }
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Enum MetadataFields.
|
||||
/// </summary>
|
||||
public class BaseItemMetadataField
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or Sets Numerical ID of this enumeratable.
|
||||
/// </summary>
|
||||
public required int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
|
||||
/// </summary>
|
||||
public required Guid ItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
|
||||
/// </summary>
|
||||
public required BaseItemEntity Item { get; set; }
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Key-Value relation of an BaseItem's provider.
|
||||
/// </summary>
|
||||
public class BaseItemProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or Sets the reference ItemId.
|
||||
/// </summary>
|
||||
public Guid ItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the reference BaseItem.
|
||||
/// </summary>
|
||||
public required BaseItemEntity Item { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the ProvidersId.
|
||||
/// </summary>
|
||||
public required string ProviderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the Providers Value.
|
||||
/// </summary>
|
||||
public required string ProviderValue { get; set; }
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Enum TrailerTypes.
|
||||
/// </summary>
|
||||
public class BaseItemTrailerType
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or Sets Numerical ID of this enumeratable.
|
||||
/// </summary>
|
||||
public required int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
|
||||
/// </summary>
|
||||
public required Guid ItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
|
||||
/// </summary>
|
||||
public required BaseItemEntity Item { get; set; }
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// The Chapter entity.
|
||||
/// </summary>
|
||||
public class Chapter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or Sets the <see cref="BaseItemEntity"/> reference id.
|
||||
/// </summary>
|
||||
public required Guid ItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the <see cref="BaseItemEntity"/> reference.
|
||||
/// </summary>
|
||||
public required BaseItemEntity Item { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the chapters index in Item.
|
||||
/// </summary>
|
||||
public required int ChapterIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the position within the source file.
|
||||
/// </summary>
|
||||
public required long StartPositionTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the common name.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the image path.
|
||||
/// </summary>
|
||||
public string? ImagePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the time the image was last modified.
|
||||
/// </summary>
|
||||
public DateTime? ImageDateModified { get; set; }
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Enum ImageType.
|
||||
/// </summary>
|
||||
public enum ImageInfoImageType
|
||||
{
|
||||
/// <summary>
|
||||
/// The primary.
|
||||
/// </summary>
|
||||
Primary = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The art.
|
||||
/// </summary>
|
||||
Art = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The backdrop.
|
||||
/// </summary>
|
||||
Backdrop = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The banner.
|
||||
/// </summary>
|
||||
Banner = 3,
|
||||
|
||||
/// <summary>
|
||||
/// The logo.
|
||||
/// </summary>
|
||||
Logo = 4,
|
||||
|
||||
/// <summary>
|
||||
/// The thumb.
|
||||
/// </summary>
|
||||
Thumb = 5,
|
||||
|
||||
/// <summary>
|
||||
/// The disc.
|
||||
/// </summary>
|
||||
Disc = 6,
|
||||
|
||||
/// <summary>
|
||||
/// The box.
|
||||
/// </summary>
|
||||
Box = 7,
|
||||
|
||||
/// <summary>
|
||||
/// The screenshot.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This enum value is obsolete.
|
||||
/// XmlSerializer does not serialize/deserialize objects that are marked as [Obsolete].
|
||||
/// </remarks>
|
||||
Screenshot = 8,
|
||||
|
||||
/// <summary>
|
||||
/// The menu.
|
||||
/// </summary>
|
||||
Menu = 9,
|
||||
|
||||
/// <summary>
|
||||
/// The chapter image.
|
||||
/// </summary>
|
||||
Chapter = 10,
|
||||
|
||||
/// <summary>
|
||||
/// The box rear.
|
||||
/// </summary>
|
||||
BoxRear = 11,
|
||||
|
||||
/// <summary>
|
||||
/// The user profile image.
|
||||
/// </summary>
|
||||
Profile = 12
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an ItemValue for a BaseItem.
|
||||
/// </summary>
|
||||
public class ItemValue
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or Sets the ItemValueId.
|
||||
/// </summary>
|
||||
public required Guid ItemValueId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the Type.
|
||||
/// </summary>
|
||||
public required ItemValueType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the Value.
|
||||
/// </summary>
|
||||
public required string Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the sanatised Value.
|
||||
/// </summary>
|
||||
public required string CleanValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets all associated BaseItems.
|
||||
/// </summary>
|
||||
#pragma warning disable CA2227 // Collection properties should be read only
|
||||
public ICollection<ItemValueMap>? BaseItemsMap { get; set; }
|
||||
#pragma warning restore CA2227 // Collection properties should be read only
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Mapping table for the ItemValue BaseItem relation.
|
||||
/// </summary>
|
||||
public class ItemValueMap
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or Sets the ItemId.
|
||||
/// </summary>
|
||||
public required Guid ItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the ItemValueId.
|
||||
/// </summary>
|
||||
public required Guid ItemValueId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the referenced <see cref="BaseItemEntity"/>.
|
||||
/// </summary>
|
||||
public required BaseItemEntity Item { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the referenced <see cref="ItemValue"/>.
|
||||
/// </summary>
|
||||
public required ItemValue ItemValue { get; set; }
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
#pragma warning disable CA1027 // Mark enums with FlagsAttribute
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the Value types for an <see cref="ItemValue"/>.
|
||||
/// </summary>
|
||||
public enum ItemValueType
|
||||
{
|
||||
/// <summary>
|
||||
/// Artists.
|
||||
/// </summary>
|
||||
Artist = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Album.
|
||||
/// </summary>
|
||||
AlbumArtist = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Genre.
|
||||
/// </summary>
|
||||
Genre = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Studios.
|
||||
/// </summary>
|
||||
Studios = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Tags.
|
||||
/// </summary>
|
||||
Tags = 4,
|
||||
|
||||
/// <summary>
|
||||
/// InheritedTags.
|
||||
/// </summary>
|
||||
InheritedTags = 6,
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
public class MediaStreamInfo
|
||||
{
|
||||
public required Guid ItemId { get; set; }
|
||||
|
||||
public required BaseItemEntity Item { get; set; }
|
||||
|
||||
public int StreamIndex { get; set; }
|
||||
|
||||
public required MediaStreamTypeEntity StreamType { get; set; }
|
||||
|
||||
public string? Codec { get; set; }
|
||||
|
||||
public string? Language { get; set; }
|
||||
|
||||
public string? ChannelLayout { get; set; }
|
||||
|
||||
public string? Profile { get; set; }
|
||||
|
||||
public string? AspectRatio { get; set; }
|
||||
|
||||
public string? Path { get; set; }
|
||||
|
||||
public bool? IsInterlaced { get; set; }
|
||||
|
||||
public int? BitRate { get; set; }
|
||||
|
||||
public int? Channels { get; set; }
|
||||
|
||||
public int? SampleRate { get; set; }
|
||||
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
public bool IsForced { get; set; }
|
||||
|
||||
public bool IsExternal { get; set; }
|
||||
|
||||
public int? Height { get; set; }
|
||||
|
||||
public int? Width { get; set; }
|
||||
|
||||
public float? AverageFrameRate { get; set; }
|
||||
|
||||
public float? RealFrameRate { get; set; }
|
||||
|
||||
public float? Level { get; set; }
|
||||
|
||||
public string? PixelFormat { get; set; }
|
||||
|
||||
public int? BitDepth { get; set; }
|
||||
|
||||
public bool? IsAnamorphic { get; set; }
|
||||
|
||||
public int? RefFrames { get; set; }
|
||||
|
||||
public string? CodecTag { get; set; }
|
||||
|
||||
public string? Comment { get; set; }
|
||||
|
||||
public string? NalLengthSize { get; set; }
|
||||
|
||||
public bool? IsAvc { get; set; }
|
||||
|
||||
public string? Title { get; set; }
|
||||
|
||||
public string? TimeBase { get; set; }
|
||||
|
||||
public string? CodecTimeBase { get; set; }
|
||||
|
||||
public string? ColorPrimaries { get; set; }
|
||||
|
||||
public string? ColorSpace { get; set; }
|
||||
|
||||
public string? ColorTransfer { get; set; }
|
||||
|
||||
public int? DvVersionMajor { get; set; }
|
||||
|
||||
public int? DvVersionMinor { get; set; }
|
||||
|
||||
public int? DvProfile { get; set; }
|
||||
|
||||
public int? DvLevel { get; set; }
|
||||
|
||||
public int? RpuPresentFlag { get; set; }
|
||||
|
||||
public int? ElPresentFlag { get; set; }
|
||||
|
||||
public int? BlPresentFlag { get; set; }
|
||||
|
||||
public int? DvBlSignalCompatibilityId { get; set; }
|
||||
|
||||
public bool? IsHearingImpaired { get; set; }
|
||||
|
||||
public int? Rotation { get; set; }
|
||||
|
||||
public string? KeyFrames { get; set; }
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Enum MediaStreamType.
|
||||
/// </summary>
|
||||
public enum MediaStreamTypeEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// The audio.
|
||||
/// </summary>
|
||||
Audio = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The video.
|
||||
/// </summary>
|
||||
Video = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The subtitle.
|
||||
/// </summary>
|
||||
Subtitle = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The embedded image.
|
||||
/// </summary>
|
||||
EmbeddedImage = 3,
|
||||
|
||||
/// <summary>
|
||||
/// The data.
|
||||
/// </summary>
|
||||
Data = 4,
|
||||
|
||||
/// <summary>
|
||||
/// The lyric.
|
||||
/// </summary>
|
||||
Lyric = 5
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
#pragma warning disable CA2227 // Collection properties should be read only
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// People entity.
|
||||
/// </summary>
|
||||
public class People
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or Sets the PeopleId.
|
||||
/// </summary>
|
||||
public required Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the Persons Name.
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the Type.
|
||||
/// </summary>
|
||||
public string? PersonType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the mapping of People to BaseItems.
|
||||
/// </summary>
|
||||
public ICollection<PeopleBaseItemMap>? BaseItems { get; set; }
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Mapping table for People to BaseItems.
|
||||
/// </summary>
|
||||
public class PeopleBaseItemMap
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or Sets the SortOrder.
|
||||
/// </summary>
|
||||
public int? SortOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the ListOrder.
|
||||
/// </summary>
|
||||
public int? ListOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the Role name the assosiated actor played in the <see cref="BaseItemEntity"/>.
|
||||
/// </summary>
|
||||
public string? Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets The ItemId.
|
||||
/// </summary>
|
||||
public required Guid ItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets Reference Item.
|
||||
/// </summary>
|
||||
public required BaseItemEntity Item { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets The PeopleId.
|
||||
/// </summary>
|
||||
public required Guid PeopleId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets Reference People.
|
||||
/// </summary>
|
||||
public required People People { get; set; }
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Lists types of Audio.
|
||||
/// </summary>
|
||||
public enum ProgramAudioEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Mono.
|
||||
/// </summary>
|
||||
Mono = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Sterio.
|
||||
/// </summary>
|
||||
Stereo = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Dolby.
|
||||
/// </summary>
|
||||
Dolby = 2,
|
||||
|
||||
/// <summary>
|
||||
/// DolbyDigital.
|
||||
/// </summary>
|
||||
DolbyDigital = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Thx.
|
||||
/// </summary>
|
||||
Thx = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Atmos.
|
||||
/// </summary>
|
||||
Atmos = 5
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Jellyfin.Data.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Provides <see cref="BaseItemEntity"/> and <see cref="User"/> related data.
|
||||
/// </summary>
|
||||
public class UserData
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the custom data key.
|
||||
/// </summary>
|
||||
/// <value>The rating.</value>
|
||||
public required string CustomDataKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the users 0-10 rating.
|
||||
/// </summary>
|
||||
/// <value>The rating.</value>
|
||||
public double? Rating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playback position ticks.
|
||||
/// </summary>
|
||||
/// <value>The playback position ticks.</value>
|
||||
public long PlaybackPositionTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the play count.
|
||||
/// </summary>
|
||||
/// <value>The play count.</value>
|
||||
public int PlayCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is favorite.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is favorite; otherwise, <c>false</c>.</value>
|
||||
public bool IsFavorite { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last played date.
|
||||
/// </summary>
|
||||
/// <value>The last played date.</value>
|
||||
public DateTime? LastPlayedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this <see cref="UserData" /> is played.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if played; otherwise, <c>false</c>.</value>
|
||||
public bool Played { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the audio stream.
|
||||
/// </summary>
|
||||
/// <value>The index of the audio stream.</value>
|
||||
public int? AudioStreamIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the subtitle stream.
|
||||
/// </summary>
|
||||
/// <value>The index of the subtitle stream.</value>
|
||||
public int? SubtitleStreamIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the item is liked or not.
|
||||
/// This should never be serialized.
|
||||
/// </summary>
|
||||
/// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value>
|
||||
public bool? Likes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the key.
|
||||
/// </summary>
|
||||
/// <value>The key.</value>
|
||||
public required Guid ItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the BaseItem.
|
||||
/// </summary>
|
||||
public required BaseItemEntity? Item { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the UserId.
|
||||
/// </summary>
|
||||
public required Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the User.
|
||||
/// </summary>
|
||||
public required User? User { get; set; }
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.Dto;
|
||||
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 cref="IChapterRepository"/>
|
||||
public ChapterInfo? GetChapter(BaseItemDto baseItem, int index)
|
||||
{
|
||||
return GetChapter(baseItem.Id, index);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IChapterRepository"/>
|
||||
public IReadOnlyList<ChapterInfo> GetChapters(BaseItemDto baseItem)
|
||||
{
|
||||
return GetChapters(baseItem.Id);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IChapterRepository"/>
|
||||
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 cref="IChapterRepository"/>
|
||||
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 cref="IChapterRepository"/>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
chapterEntity.ImageTag = _imageProcessor.GetImageCacheTag(baseItemPath, chapterEntity.ImageDateModified);
|
||||
return chapterEntity;
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Jellyfin.Data.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();
|
||||
context.AttachmentStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete();
|
||||
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,213 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Jellyfin.Data.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;
|
||||
}
|
||||
|
||||
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;
|
||||
dto.Language = entity.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;
|
||||
dto.Rotation = entity.Rotation;
|
||||
|
||||
if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
|
||||
{
|
||||
dto.LocalizedDefault = _localization.GetLocalizedString("Default");
|
||||
dto.LocalizedExternal = _localization.GetLocalizedString("External");
|
||||
|
||||
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
|
||||
};
|
||||
return entity;
|
||||
}
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
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
|
||||
|
||||
/// <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);
|
||||
|
||||
// dbQuery = dbQuery.OrderBy(e => e.ListOrder);
|
||||
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);
|
||||
|
||||
// dbQuery = dbQuery.OrderBy(e => e.ListOrder);
|
||||
if (filter.Limit > 0)
|
||||
{
|
||||
dbQuery = dbQuery.Take(filter.Limit);
|
||||
}
|
||||
|
||||
return dbQuery.Select(e => e.Name).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people)
|
||||
{
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
using var transaction = context.Database.BeginTransaction();
|
||||
|
||||
context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).ExecuteDelete();
|
||||
// TODO: yes for __SOME__ reason there can be duplicates.
|
||||
foreach (var item in people.DistinctBy(e => e.Id))
|
||||
{
|
||||
var personEntity = Map(item);
|
||||
var existingEntity = context.Peoples.FirstOrDefault(e => e.Id == personEntity.Id);
|
||||
if (existingEntity is null)
|
||||
{
|
||||
context.Peoples.Add(personEntity);
|
||||
existingEntity = personEntity;
|
||||
}
|
||||
|
||||
context.PeopleBaseItemMap.Add(new PeopleBaseItemMap()
|
||||
{
|
||||
Item = null!,
|
||||
ItemId = itemId,
|
||||
People = existingEntity,
|
||||
PeopleId = existingEntity.Id,
|
||||
ListOrder = item.SortOrder,
|
||||
SortOrder = item.SortOrder,
|
||||
Role = item.Role
|
||||
});
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
private PersonInfo Map(People people)
|
||||
{
|
||||
var personInfo = new PersonInfo()
|
||||
{
|
||||
Id = people.Id,
|
||||
Name = people.Name,
|
||||
};
|
||||
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];
|
||||
query = query.Where(e => e.PersonType == personType)
|
||||
.Where(e => context.BaseItems.Where(d => d.UserData!.Any(w => w.IsFavorite == filter.IsFavorite && w.UserId.Equals(filter.User.Id)))
|
||||
.Select(f => f.Name).Contains(e.Name));
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
query = query.Where(e => e.Name.Contains(filter.NameContains));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// AncestorId configuration.
|
||||
/// </summary>
|
||||
public class AncestorIdConfiguration : IEntityTypeConfiguration<AncestorId>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<AncestorId> builder)
|
||||
{
|
||||
builder.HasKey(e => new { e.ItemId, e.ParentItemId });
|
||||
builder.HasIndex(e => e.ParentItemId);
|
||||
builder.HasOne(e => e.ParentItem).WithMany(e => e.ParentAncestors).HasForeignKey(f => f.ParentItemId);
|
||||
builder.HasOne(e => e.Item).WithMany(e => e.Children).HasForeignKey(f => f.ItemId);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
using Jellyfin.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// FluentAPI configuration for the AttachmentStreamInfo entity.
|
||||
/// </summary>
|
||||
public class AttachmentStreamInfoConfiguration : IEntityTypeConfiguration<AttachmentStreamInfo>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<AttachmentStreamInfo> builder)
|
||||
{
|
||||
builder.HasKey(e => new { e.ItemId, e.Index });
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using SQLitePCL;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for BaseItem.
|
||||
/// </summary>
|
||||
public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<BaseItemEntity> builder)
|
||||
{
|
||||
builder.HasKey(e => e.Id);
|
||||
// TODO: See rant in entity file.
|
||||
// builder.HasOne(e => e.Parent).WithMany(e => e.DirectChildren).HasForeignKey(e => e.ParentId);
|
||||
// builder.HasOne(e => e.TopParent).WithMany(e => e.AllChildren).HasForeignKey(e => e.TopParentId);
|
||||
// builder.HasOne(e => e.Season).WithMany(e => e.SeasonEpisodes).HasForeignKey(e => e.SeasonId);
|
||||
// builder.HasOne(e => e.Series).WithMany(e => e.SeriesEpisodes).HasForeignKey(e => e.SeriesId);
|
||||
builder.HasMany(e => e.Peoples);
|
||||
builder.HasMany(e => e.UserData);
|
||||
builder.HasMany(e => e.ItemValues);
|
||||
builder.HasMany(e => e.MediaStreams);
|
||||
builder.HasMany(e => e.Chapters);
|
||||
builder.HasMany(e => e.Provider);
|
||||
builder.HasMany(e => e.ParentAncestors);
|
||||
builder.HasMany(e => e.Children);
|
||||
builder.HasMany(e => e.LockedFields);
|
||||
builder.HasMany(e => e.TrailerTypes);
|
||||
builder.HasMany(e => e.Images);
|
||||
|
||||
builder.HasIndex(e => e.Path);
|
||||
builder.HasIndex(e => e.ParentId);
|
||||
builder.HasIndex(e => e.PresentationUniqueKey);
|
||||
builder.HasIndex(e => new { e.Id, e.Type, e.IsFolder, e.IsVirtualItem });
|
||||
|
||||
// covering index
|
||||
builder.HasIndex(e => new { e.TopParentId, e.Id });
|
||||
// series
|
||||
builder.HasIndex(e => new { e.Type, e.SeriesPresentationUniqueKey, e.PresentationUniqueKey, e.SortName });
|
||||
// series counts
|
||||
// seriesdateplayed sort order
|
||||
builder.HasIndex(e => new { e.Type, e.SeriesPresentationUniqueKey, e.IsFolder, e.IsVirtualItem });
|
||||
// live tv programs
|
||||
builder.HasIndex(e => new { e.Type, e.TopParentId, e.StartDate });
|
||||
// covering index for getitemvalues
|
||||
builder.HasIndex(e => new { e.Type, e.TopParentId, e.Id });
|
||||
// used by movie suggestions
|
||||
builder.HasIndex(e => new { e.Type, e.TopParentId, e.PresentationUniqueKey });
|
||||
// latest items
|
||||
builder.HasIndex(e => new { e.Type, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey, e.DateCreated });
|
||||
builder.HasIndex(e => new { e.IsFolder, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey, e.DateCreated });
|
||||
// resume
|
||||
builder.HasIndex(e => new { e.MediaType, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey });
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using SQLitePCL;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Provides configuration for the BaseItemMetadataField entity.
|
||||
/// </summary>
|
||||
public class BaseItemMetadataFieldConfiguration : IEntityTypeConfiguration<BaseItemMetadataField>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<BaseItemMetadataField> builder)
|
||||
{
|
||||
builder.HasKey(e => new { e.Id, e.ItemId });
|
||||
builder.HasOne(e => e.Item);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// BaseItemProvider configuration.
|
||||
/// </summary>
|
||||
public class BaseItemProviderConfiguration : IEntityTypeConfiguration<BaseItemProvider>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<BaseItemProvider> builder)
|
||||
{
|
||||
builder.HasKey(e => new { e.ItemId, e.ProviderId });
|
||||
builder.HasOne(e => e.Item);
|
||||
builder.HasIndex(e => new { e.ProviderId, e.ProviderValue, e.ItemId });
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using SQLitePCL;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Provides configuration for the BaseItemMetadataField entity.
|
||||
/// </summary>
|
||||
public class BaseItemTrailerTypeConfiguration : IEntityTypeConfiguration<BaseItemTrailerType>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<BaseItemTrailerType> builder)
|
||||
{
|
||||
builder.HasKey(e => new { e.Id, e.ItemId });
|
||||
builder.HasOne(e => e.Item);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Chapter configuration.
|
||||
/// </summary>
|
||||
public class ChapterConfiguration : IEntityTypeConfiguration<Chapter>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<Chapter> builder)
|
||||
{
|
||||
builder.HasKey(e => new { e.ItemId, e.ChapterIndex });
|
||||
builder.HasOne(e => e.Item);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// itemvalues Configuration.
|
||||
/// </summary>
|
||||
public class ItemValuesConfiguration : IEntityTypeConfiguration<ItemValue>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<ItemValue> builder)
|
||||
{
|
||||
builder.HasKey(e => e.ItemValueId);
|
||||
builder.HasIndex(e => new { e.Type, e.CleanValue }).IsUnique();
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// itemvalues Configuration.
|
||||
/// </summary>
|
||||
public class ItemValuesMapConfiguration : IEntityTypeConfiguration<ItemValueMap>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<ItemValueMap> builder)
|
||||
{
|
||||
builder.HasKey(e => new { e.ItemValueId, e.ItemId });
|
||||
builder.HasOne(e => e.Item);
|
||||
builder.HasOne(e => e.ItemValue);
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// People configuration.
|
||||
/// </summary>
|
||||
public class MediaStreamInfoConfiguration : IEntityTypeConfiguration<MediaStreamInfo>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<MediaStreamInfo> builder)
|
||||
{
|
||||
builder.HasKey(e => new { e.ItemId, e.StreamIndex });
|
||||
builder.HasIndex(e => e.StreamIndex);
|
||||
builder.HasIndex(e => e.StreamType);
|
||||
builder.HasIndex(e => new { e.StreamIndex, e.StreamType });
|
||||
builder.HasIndex(e => new { e.StreamIndex, e.StreamType, e.Language });
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// People configuration.
|
||||
/// </summary>
|
||||
public class PeopleBaseItemMapConfiguration : IEntityTypeConfiguration<PeopleBaseItemMap>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<PeopleBaseItemMap> builder)
|
||||
{
|
||||
builder.HasKey(e => new { e.ItemId, e.PeopleId });
|
||||
builder.HasIndex(e => new { e.ItemId, e.SortOrder });
|
||||
builder.HasIndex(e => new { e.ItemId, e.ListOrder });
|
||||
builder.HasOne(e => e.Item);
|
||||
builder.HasOne(e => e.People);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// People configuration.
|
||||
/// </summary>
|
||||
public class PeopleConfiguration : IEntityTypeConfiguration<People>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<People> builder)
|
||||
{
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.HasIndex(e => e.Name);
|
||||
builder.HasMany(e => e.BaseItems);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// FluentAPI configuration for the UserData entity.
|
||||
/// </summary>
|
||||
public class UserDataConfiguration : IEntityTypeConfiguration<UserData>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<UserData> builder)
|
||||
{
|
||||
builder.HasKey(d => new { d.ItemId, d.UserId, d.CustomDataKey });
|
||||
builder.HasIndex(d => new { d.ItemId, d.UserId, d.Played });
|
||||
builder.HasIndex(d => new { d.ItemId, d.UserId, d.PlaybackPositionTicks });
|
||||
builder.HasIndex(d => new { d.ItemId, d.UserId, d.IsFavorite });
|
||||
builder.HasIndex(d => new { d.ItemId, d.UserId, d.LastPlayedDate });
|
||||
builder.HasOne(e => e.Item);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Marks a BaseItem as needing custom serialisation from the Data field of the db.
|
||||
/// </summary>
|
||||
[System.AttributeUsage(System.AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
|
||||
public sealed class RequiresSourceSerialisationAttribute : System.Attribute
|
||||
{
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Chapters
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IChapterManager.
|
||||
/// </summary>
|
||||
public interface IChapterManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves the chapters.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item.</param>
|
||||
/// <param name="chapters">The set of chapters.</param>
|
||||
void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Chapters;
|
||||
|
||||
/// <summary>
|
||||
/// Interface IChapterManager.
|
||||
/// </summary>
|
||||
public interface IChapterRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves the chapters.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item.</param>
|
||||
/// <param name="chapters">The set of chapters.</param>
|
||||
void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all chapters associated with the baseItem.
|
||||
/// </summary>
|
||||
/// <param name="baseItem">The baseitem.</param>
|
||||
/// <returns>A readonly list of chapter instances.</returns>
|
||||
IReadOnlyList<ChapterInfo> GetChapters(BaseItemDto baseItem);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a single chapter of a BaseItem on a specific index.
|
||||
/// </summary>
|
||||
/// <param name="baseItem">The baseitem.</param>
|
||||
/// <param name="index">The index of that chapter.</param>
|
||||
/// <returns>A chapter instance.</returns>
|
||||
ChapterInfo? GetChapter(BaseItemDto baseItem, int index);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all chapters associated with the baseItem.
|
||||
/// </summary>
|
||||
/// <param name="baseItemId">The BaseItems id.</param>
|
||||
/// <returns>A readonly list of chapter instances.</returns>
|
||||
IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a single chapter of a BaseItem on a specific index.
|
||||
/// </summary>
|
||||
/// <param name="baseItemId">The BaseItems id.</param>
|
||||
/// <param name="index">The index of that chapter.</param>
|
||||
/// <returns>A chapter instance.</returns>
|
||||
ChapterInfo? GetChapter(Guid baseItemId, int index);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue