Merge pull request #12798 from JPVenson/feature/EFUserData

Refactor library.db into jellyfin.db and EFCore
pull/12932/head
Joshua M. Boniface 2 months ago committed by GitHub
commit 93b8eade61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -527,3 +527,6 @@ dotnet_diagnostic.CA2234.severity = suggestion
# disable warning xUnit1028: Test methods must have a supported return type.
dotnet_diagnostic.xUnit1028.severity = none
# CA1826: Do not use Enumerable methods on indexable collections
dotnet_diagnostic.CA1826.severity = suggestion

@ -40,6 +40,7 @@ using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Manager;
using Jellyfin.Networking.Udp;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Item;
using Jellyfin.Server.Implementations.MediaSegments;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
@ -83,7 +84,6 @@ using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Lyric;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.Tmdb;
@ -268,6 +268,11 @@ namespace Emby.Server.Implementations
public string ExpandVirtualPath(string path)
{
if (path is null)
{
return null;
}
var appPaths = ApplicationPaths;
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
@ -492,10 +497,14 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
serviceCollection.AddSingleton<IItemRepository, BaseItemRepository>();
serviceCollection.AddSingleton<IPeopleRepository, PeopleRepository>();
serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>();
serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>();
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
serviceCollection.AddSingleton<EncodingHelper>();
@ -540,8 +549,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
serviceCollection.AddSingleton<IAuthService, AuthService>();
@ -579,9 +586,6 @@ namespace Emby.Server.Implementations
}
}
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize();
((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize();
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
await localizationManager.LoadAll().ConfigureAwait(false);
@ -635,6 +639,7 @@ namespace Emby.Server.Implementations
BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.ItemRepository = Resolve<IItemRepository>();
BaseItem.ChapterRepository = Resolve<IChapterRepository>();
BaseItem.FileSystem = Resolve<IFileSystem>();
BaseItem.UserDataManager = Resolve<IUserDataManager>();
BaseItem.ChannelManager = Resolve<IChannelManager>();

@ -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;
}
}
}

@ -1,10 +1,13 @@
#pragma warning disable CS1591
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Server.Implementations;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Data
@ -13,20 +16,24 @@ namespace Emby.Server.Implementations.Data
{
private readonly ILibraryManager _libraryManager;
private readonly ILogger<CleanDatabaseScheduledTask> _logger;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger<CleanDatabaseScheduledTask> logger)
public CleanDatabaseScheduledTask(
ILibraryManager libraryManager,
ILogger<CleanDatabaseScheduledTask> logger,
IDbContextFactory<JellyfinDbContext> dbProvider)
{
_libraryManager = libraryManager;
_logger = logger;
_dbProvider = dbProvider;
}
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
CleanDeadItems(cancellationToken, progress);
return Task.CompletedTask;
await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
}
private void CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
{
var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
@ -34,7 +41,7 @@ namespace Emby.Server.Implementations.Data
});
var numComplete = 0;
var numItems = itemIds.Count;
var numItems = itemIds.Count + 1;
_logger.LogDebug("Cleaning {0} items with dead parent links", numItems);
@ -60,6 +67,17 @@ namespace Emby.Server.Implementations.Data
progress.Report(percent * 100);
}
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
await using (transaction.ConfigureAwait(false))
{
await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
}
progress.Report(100);
}
}

@ -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;
}
}

@ -127,8 +127,16 @@ namespace Emby.Server.Implementations.Data
return false;
}
result = reader.GetGuid(index);
return true;
try
{
result = reader.GetGuid(index);
return true;
}
catch
{
result = Guid.Empty;
return false;
}
}
public static bool TryGetString(this SqliteDataReader reader, int index, out string result)

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
}

@ -10,6 +10,7 @@ using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@ -51,6 +52,7 @@ namespace Emby.Server.Implementations.Dto
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
private readonly ITrickplayManager _trickplayManager;
private readonly IChapterRepository _chapterRepository;
public DtoService(
ILogger<DtoService> logger,
@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
IApplicationHost appHost,
IMediaSourceManager mediaSourceManager,
Lazy<ILiveTvManager> livetvManagerFactory,
ITrickplayManager trickplayManager)
ITrickplayManager trickplayManager,
IChapterRepository chapterRepository)
{
_logger = logger;
_libraryManager = libraryManager;
@ -76,6 +79,7 @@ namespace Emby.Server.Implementations.Dto
_mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory;
_trickplayManager = trickplayManager;
_chapterRepository = chapterRepository;
}
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@ -165,7 +169,7 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
private static IList<BaseItem> GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
private static IReadOnlyList<BaseItem> GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
{
return byName.GetTaggedItems(
new InternalItemsQuery(user)
@ -327,7 +331,7 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList<BaseItem> taggedItems)
private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList<BaseItem> taggedItems)
{
if (item is MusicArtist)
{
@ -1060,7 +1064,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.Chapters))
{
dto.Chapters = _itemRepo.GetChapters(item);
dto.Chapters = _chapterRepository.GetChapters(item.Id).ToList();
}
if (options.ContainsField(ItemFields.Trickplay))

@ -144,9 +144,15 @@ namespace Emby.Server.Implementations.EntryPoints
.Select(i =>
{
var dto = _userDataManager.GetUserDataDto(i, user);
if (dto is null)
{
return null!;
}
dto.ItemId = i.Id;
return dto;
})
.Where(e => e is not null)
.ToArray()
};
}

@ -76,6 +76,7 @@ namespace Emby.Server.Implementations.Library
private readonly IItemRepository _itemRepository;
private readonly IImageProcessor _imageProcessor;
private readonly NamingOptions _namingOptions;
private readonly IPeopleRepository _peopleRepository;
private readonly ExtraResolver _extraResolver;
/// <summary>
@ -112,6 +113,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="imageProcessor">The image processor.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
/// <param name="peopleRepository">The People Repository.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILoggerFactory loggerFactory,
@ -127,7 +129,8 @@ namespace Emby.Server.Implementations.Library
IItemRepository itemRepository,
IImageProcessor imageProcessor,
NamingOptions namingOptions,
IDirectoryService directoryService)
IDirectoryService directoryService,
IPeopleRepository peopleRepository)
{
_appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>();
@ -144,7 +147,7 @@ namespace Emby.Server.Implementations.Library
_imageProcessor = imageProcessor;
_cache = new ConcurrentDictionary<Guid, BaseItem>();
_namingOptions = namingOptions;
_peopleRepository = peopleRepository;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@ -1274,7 +1277,7 @@ namespace Emby.Server.Implementations.Library
return ItemIsVisible(item, user) ? item : null;
}
public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
{
if (query.Recursive && !query.ParentId.IsEmpty())
{
@ -1300,7 +1303,7 @@ namespace Emby.Server.Implementations.Library
return itemList;
}
public List<BaseItem> GetItemList(InternalItemsQuery query)
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
{
return GetItemList(query, true);
}
@ -1324,7 +1327,7 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetCount(query);
}
public List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
{
SetTopParentIdsOrAncestors(query, parents);
@ -1357,7 +1360,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository.GetItemList(query));
}
public List<Guid> GetItemIds(InternalItemsQuery query)
public IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query)
{
if (query.User is not null)
{
@ -1807,11 +1810,11 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public void CreateItem(BaseItem item, BaseItem? parent)
{
CreateItems(new[] { item }, parent, CancellationToken.None);
CreateOrUpdateItems(new[] { item }, parent, CancellationToken.None);
}
/// <inheritdoc />
public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
public void CreateOrUpdateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
{
_itemRepository.SaveItems(items, cancellationToken);
@ -1955,13 +1958,13 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{
_itemRepository.SaveItems(items, cancellationToken);
foreach (var item in items)
{
await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
}
_itemRepository.SaveItems(items, cancellationToken);
if (ItemUpdated is not null)
{
foreach (var item in items)
@ -2736,12 +2739,12 @@ namespace Emby.Server.Implementations.Library
return path;
}
public List<PersonInfo> GetPeople(InternalPeopleQuery query)
public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
{
return _itemRepository.GetPeople(query);
return _peopleRepository.GetPeople(query);
}
public List<PersonInfo> GetPeople(BaseItem item)
public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
{
if (item.SupportsPeople)
{
@ -2756,12 +2759,12 @@ namespace Emby.Server.Implementations.Library
}
}
return new List<PersonInfo>();
return [];
}
public List<Person> GetPeopleItems(InternalPeopleQuery query)
public IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query)
{
return _itemRepository.GetPeopleNames(query)
return _peopleRepository.GetPeopleNames(query)
.Select(i =>
{
try
@ -2779,9 +2782,9 @@ namespace Emby.Server.Implementations.Library
.ToList()!; // null values are filtered out
}
public List<string> GetPeopleNames(InternalPeopleQuery query)
public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
{
return _itemRepository.GetPeopleNames(query);
return _peopleRepository.GetPeopleNames(query);
}
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
@ -2790,16 +2793,17 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken)
public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellationToken)
{
if (!item.SupportsPeople)
{
return;
}
_itemRepository.UpdatePeople(item.Id, people);
if (people is not null)
{
people = people.Where(e => e is not null).ToArray();
_peopleRepository.UpdatePeople(item.Id, people);
await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
}
}
@ -2914,14 +2918,13 @@ namespace Emby.Server.Implementations.Library
private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
{
List<BaseItem>? personsToSave = null;
foreach (var person in people)
{
cancellationToken.ThrowIfCancellationRequested();
var itemUpdateType = ItemUpdateType.MetadataDownload;
var saveEntity = false;
var createEntity = false;
var personEntity = GetPerson(person.Name);
if (personEntity is null)
@ -2938,6 +2941,7 @@ namespace Emby.Server.Implementations.Library
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
saveEntity = true;
createEntity = true;
}
foreach (var id in person.ProviderIds)
@ -2965,15 +2969,15 @@ namespace Emby.Server.Implementations.Library
if (saveEntity)
{
(personsToSave ??= new()).Add(personEntity);
if (createEntity)
{
CreateOrUpdateItems([personEntity], null, CancellationToken.None);
}
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
CreateOrUpdateItems([personEntity], null, CancellationToken.None);
}
}
if (personsToSave is not null)
{
CreateItems(personsToSave, null, CancellationToken.None);
}
}
private void StartScanInBackground()
@ -3027,7 +3031,7 @@ namespace Emby.Server.Implementations.Library
{
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
libraryOptions.PathInfos = [..libraryOptions.PathInfos, pathInfo];
libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo];
SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);

@ -5,6 +5,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
@ -51,7 +52,8 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
private readonly IDirectoryService _directoryService;
private readonly IMediaStreamRepository _mediaStreamRepository;
private readonly IMediaAttachmentRepository _mediaAttachmentRepository;
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@ -69,7 +71,9 @@ namespace Emby.Server.Implementations.Library
IFileSystem fileSystem,
IUserDataManager userDataManager,
IMediaEncoder mediaEncoder,
IDirectoryService directoryService)
IDirectoryService directoryService,
IMediaStreamRepository mediaStreamRepository,
IMediaAttachmentRepository mediaAttachmentRepository)
{
_appHost = appHost;
_itemRepo = itemRepo;
@ -82,6 +86,8 @@ namespace Emby.Server.Implementations.Library
_localizationManager = localizationManager;
_appPaths = applicationPaths;
_directoryService = directoryService;
_mediaStreamRepository = mediaStreamRepository;
_mediaAttachmentRepository = mediaAttachmentRepository;
}
public void AddParts(IEnumerable<IMediaSourceProvider> providers)
@ -89,9 +95,9 @@ namespace Emby.Server.Implementations.Library
_providers = providers.ToArray();
}
public List<MediaStream> GetMediaStreams(MediaStreamQuery query)
public IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery query)
{
var list = _itemRepo.GetMediaStreams(query);
var list = _mediaStreamRepository.GetMediaStreams(query);
foreach (var stream in list)
{
@ -121,7 +127,7 @@ namespace Emby.Server.Implementations.Library
return false;
}
public List<MediaStream> GetMediaStreams(Guid itemId)
public IReadOnlyList<MediaStream> GetMediaStreams(Guid itemId)
{
var list = GetMediaStreams(new MediaStreamQuery
{
@ -131,7 +137,7 @@ namespace Emby.Server.Implementations.Library
return GetMediaStreamsForItem(list);
}
private List<MediaStream> GetMediaStreamsForItem(List<MediaStream> streams)
private IReadOnlyList<MediaStream> GetMediaStreamsForItem(IReadOnlyList<MediaStream> streams)
{
foreach (var stream in streams)
{
@ -145,13 +151,13 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
public IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
{
return _itemRepo.GetMediaAttachments(query);
return _mediaAttachmentRepository.GetMediaAttachments(query);
}
/// <inheritdoc />
public List<MediaAttachment> GetMediaAttachments(Guid itemId)
public IReadOnlyList<MediaAttachment> GetMediaAttachments(Guid itemId)
{
return GetMediaAttachments(new MediaAttachmentQuery
{
@ -159,7 +165,7 @@ namespace Emby.Server.Implementations.Library
});
}
public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
public async Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
{
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
@ -212,7 +218,7 @@ namespace Emby.Server.Implementations.Library
list.Add(source);
}
return SortMediaSources(list);
return SortMediaSources(list).ToArray();
}
/// <inheritdoc />>
@ -332,7 +338,7 @@ namespace Emby.Server.Implementations.Library
return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
}
public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
{
ArgumentNullException.ThrowIfNull(item);
@ -453,7 +459,7 @@ namespace Emby.Server.Implementations.Library
}
}
private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
{
return sources.OrderBy(i =>
{
@ -470,8 +476,7 @@ namespace Emby.Server.Implementations.Library
return stream?.Width ?? 0;
})
.Where(i => i.Type != MediaSourceType.Placeholder)
.ToList();
.Where(i => i.Type != MediaSourceType.Placeholder);
}
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
@ -806,7 +811,7 @@ namespace Emby.Server.Implementations.Library
return result.Item1;
}
public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
public async Task<IReadOnlyList<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
{
var stream = new MediaSourceInfo
{
@ -829,10 +834,7 @@ namespace Emby.Server.Implementations.Library
await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
.AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
return new List<MediaSourceInfo>
{
stream
};
return [stream];
}
public async Task CloseLiveStream(string id)

@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
@ -24,30 +25,23 @@ namespace Emby.Server.Implementations.Library
_libraryManager = libraryManager;
}
public List<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
{
var list = new List<BaseItem>
{
item
};
list.AddRange(GetInstantMixFromGenres(item.Genres, user, dtoOptions));
return list;
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
/// <inheritdoc />
public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
public IReadOnlyList<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
}
public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
public IReadOnlyList<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
public List<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
public IReadOnlyList<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
{
var genres = item
.GetRecursiveChildren(user, new InternalItemsQuery(user)
@ -63,12 +57,12 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenres(genres, user, dtoOptions);
}
public List<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
public IReadOnlyList<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
public List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
public IReadOnlyList<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
{
var genreIds = genres.DistinctNames().Select(i =>
{
@ -85,7 +79,7 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
}
public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
public IReadOnlyList<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
{
return _libraryManager.GetItemList(new InternalItemsQuery(user)
{
@ -97,7 +91,7 @@ namespace Emby.Server.Implementations.Library
});
}
public List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
public IReadOnlyList<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
{
if (item is MusicGenre)
{

@ -171,7 +171,7 @@ namespace Emby.Server.Implementations.Library
}
};
List<BaseItem> mediaItems;
IReadOnlyList<BaseItem> mediaItems;
if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist)
{

@ -1,17 +1,21 @@
#pragma warning disable RS0030 // Do not use banned APIs
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;
using Jellyfin.Extensions;
using Jellyfin.Server.Implementations;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using Microsoft.EntityFrameworkCore;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
using Book = MediaBrowser.Controller.Entities.Book;
@ -26,22 +30,18 @@ namespace Emby.Server.Implementations.Library
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
private readonly IServerConfigurationManager _config;
private readonly IUserManager _userManager;
private readonly IUserDataRepository _repository;
private readonly IDbContextFactory<JellyfinDbContext> _repository;
/// <summary>
/// Initializes a new instance of the <see cref="UserDataManager"/> class.
/// </summary>
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="repository">Instance of the <see cref="IUserDataRepository"/> interface.</param>
/// <param name="repository">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
public UserDataManager(
IServerConfigurationManager config,
IUserManager userManager,
IUserDataRepository repository)
IDbContextFactory<JellyfinDbContext> repository)
{
_config = config;
_userManager = userManager;
_repository = repository;
}
@ -59,13 +59,27 @@ namespace Emby.Server.Implementations.Library
var keys = item.GetUserDataKeys();
var userId = user.InternalId;
using var dbContext = _repository.CreateDbContext();
using var transaction = dbContext.Database.BeginTransaction();
foreach (var key in keys)
{
_repository.SaveUserData(userId, key, userData, cancellationToken);
userData.Key = key;
var userDataEntry = Map(userData, user.Id, item.Id);
if (dbContext.UserData.Any(f => f.ItemId == userDataEntry.ItemId && f.UserId == userDataEntry.UserId && f.CustomDataKey == userDataEntry.CustomDataKey))
{
dbContext.UserData.Attach(userDataEntry).State = EntityState.Modified;
}
else
{
dbContext.UserData.Add(userDataEntry);
}
}
dbContext.SaveChanges();
transaction.Commit();
var userId = user.InternalId;
var cacheKey = GetCacheKey(userId, item.Id);
_userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
@ -86,7 +100,7 @@ namespace Emby.Server.Implementations.Library
ArgumentNullException.ThrowIfNull(item);
ArgumentNullException.ThrowIfNull(userDataDto);
var userData = GetUserData(user, item);
var userData = GetUserData(user, item) ?? throw new InvalidOperationException("UserData should not be null.");
if (userDataDto.PlaybackPositionTicks.HasValue)
{
@ -126,33 +140,91 @@ namespace Emby.Server.Implementations.Library
SaveUserData(user, item, userData, reason, CancellationToken.None);
}
private UserItemData GetUserData(User user, Guid itemId, List<string> keys)
private UserData Map(UserItemData dto, Guid userId, Guid itemId)
{
var userId = user.InternalId;
var cacheKey = GetCacheKey(userId, itemId);
return new UserData()
{
ItemId = itemId,
CustomDataKey = dto.Key,
Item = null,
User = null,
AudioStreamIndex = dto.AudioStreamIndex,
IsFavorite = dto.IsFavorite,
LastPlayedDate = dto.LastPlayedDate,
Likes = dto.Likes,
PlaybackPositionTicks = dto.PlaybackPositionTicks,
PlayCount = dto.PlayCount,
Played = dto.Played,
Rating = dto.Rating,
UserId = userId,
SubtitleStreamIndex = dto.SubtitleStreamIndex,
};
}
return _userData.GetOrAdd(cacheKey, _ => GetUserDataInternal(userId, keys));
private UserItemData Map(UserData dto)
{
return new UserItemData()
{
Key = dto.CustomDataKey!,
AudioStreamIndex = dto.AudioStreamIndex,
IsFavorite = dto.IsFavorite,
LastPlayedDate = dto.LastPlayedDate,
Likes = dto.Likes,
PlaybackPositionTicks = dto.PlaybackPositionTicks,
PlayCount = dto.PlayCount,
Played = dto.Played,
Rating = dto.Rating,
SubtitleStreamIndex = dto.SubtitleStreamIndex,
};
}
private UserItemData GetUserDataInternal(long internalUserId, List<string> keys)
private UserItemData? GetUserData(User user, Guid itemId, List<string> keys)
{
var userData = _repository.GetUserData(internalUserId, keys);
var cacheKey = GetCacheKey(user.InternalId, itemId);
if (userData is not null)
if (_userData.TryGetValue(cacheKey, out var data))
{
return userData;
return data;
}
if (keys.Count > 0)
data = GetUserDataInternal(user.Id, itemId, keys);
if (data is null)
{
return new UserItemData
return new UserItemData()
{
Key = keys[0]
Key = keys[0],
};
}
throw new UnreachableException();
return _userData.GetOrAdd(cacheKey, data);
}
private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
{
if (keys.Count == 0)
{
return null;
}
using var context = _repository.CreateDbContext();
var userData = context.UserData.AsNoTracking().Where(e => e.ItemId == itemId && keys.Contains(e.CustomDataKey) && e.UserId.Equals(userId)).ToArray();
if (userData.Length > 0)
{
var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N"));
if (directDataReference is not null)
{
return Map(directDataReference);
}
return Map(userData.First());
}
return new UserItemData
{
Key = keys.Last()!
};
}
/// <summary>
@ -165,20 +237,25 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
public UserItemData GetUserData(User user, BaseItem item)
public UserItemData? GetUserData(User user, BaseItem item)
{
return GetUserData(user, item.Id, item.GetUserDataKeys());
}
/// <inheritdoc />
public UserItemDataDto GetUserDataDto(BaseItem item, User user)
public UserItemDataDto? GetUserDataDto(BaseItem item, User user)
=> GetUserDataDto(item, null, user, new DtoOptions());
/// <inheritdoc />
public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
public UserItemDataDto? GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
{
var userData = GetUserData(user, item);
var dto = GetUserItemDataDto(userData);
if (userData is null)
{
return null;
}
var dto = GetUserItemDataDto(userData, item.Id);
item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
return dto;
@ -188,9 +265,10 @@ namespace Emby.Server.Implementations.Library
/// Converts a UserItemData to a DTOUserItemData.
/// </summary>
/// <param name="data">The data.</param>
/// <param name="itemId">The reference key to an Item.</param>
/// <returns>DtoUserItemData.</returns>
/// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
private UserItemDataDto GetUserItemDataDto(UserItemData data)
private UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
{
ArgumentNullException.ThrowIfNull(data);
@ -203,6 +281,7 @@ namespace Emby.Server.Implementations.Library
Rating = data.Rating,
Played = data.Played,
LastPlayedDate = data.LastPlayedDate,
ItemId = itemId,
Key = data.Key
};
}

@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.MediaEncoder
private readonly IFileSystem _fileSystem;
private readonly ILogger<EncodingManager> _logger;
private readonly IMediaEncoder _encoder;
private readonly IChapterManager _chapterManager;
private readonly IChapterRepository _chapterManager;
private readonly ILibraryManager _libraryManager;
/// <summary>
@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.MediaEncoder
ILogger<EncodingManager> logger,
IFileSystem fileSystem,
IMediaEncoder encoder,
IChapterManager chapterManager,
IChapterRepository chapterManager,
ILibraryManager libraryManager)
{
_logger = logger;

@ -5,12 +5,14 @@ using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Playlists
{
[RequiresSourceSerialisation]
public class PlaylistsFolder : BasePluginFolder
{
public PlaylistsFolder()

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@ -32,6 +33,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
private readonly IEncodingManager _encodingManager;
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
private readonly IChapterRepository _chapterRepository;
/// <summary>
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
@ -43,6 +45,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
/// <param name="chapterRepository">Instance of the <see cref="IChapterRepository"/> interface.</param>
public ChapterImagesTask(
ILogger<ChapterImagesTask> logger,
ILibraryManager libraryManager,
@ -50,7 +53,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
IApplicationPaths appPaths,
IEncodingManager encodingManager,
IFileSystem fileSystem,
ILocalizationManager localization)
ILocalizationManager localization,
IChapterRepository chapterRepository)
{
_logger = logger;
_libraryManager = libraryManager;
@ -59,6 +63,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
_encodingManager = encodingManager;
_fileSystem = fileSystem;
_localization = localization;
_chapterRepository = chapterRepository;
}
/// <inheritdoc />
@ -141,7 +146,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
try
{
var chapters = _itemRepo.GetChapters(video);
var chapters = _chapterRepository.GetChapters(video.Id);
var success = await _encodingManager.RefreshChapterImages(video, directoryService, chapters, extract, true, cancellationToken).ConfigureAwait(false);

@ -117,7 +117,7 @@ namespace Emby.Server.Implementations.TV
.ToList();
// Avoid implicitly captured closure
var episodes = GetNextUpEpisodes(request, user, items, options);
var episodes = GetNextUpEpisodes(request, user, items.Distinct().ToArray(), options);
return GetResult(episodes, request);
}
@ -262,7 +262,7 @@ namespace Emby.Server.Implementations.TV
{
var userData = _userDataManager.GetUserData(user, nextEpisode);
if (userData.PlaybackPositionTicks > 0)
if (userData?.PlaybackPositionTicks > 0)
{
return null;
}
@ -275,6 +275,11 @@ namespace Emby.Server.Implementations.TV
{
var userData = _userDataManager.GetUserData(user, lastWatchedEpisode);
if (userData is null)
{
return (DateTime.MinValue, GetEpisode);
}
var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
return (lastWatchedDate, GetEpisode);

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@ -389,23 +391,19 @@ public class InstantMixController : BaseJellyfinApiController
return GetResult(items, user, limit, dtoOptions);
}
private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
private QueryResult<BaseItemDto> GetResult(IReadOnlyList<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
{
var list = items;
var totalCount = items.Count;
var totalCount = list.Count;
if (limit.HasValue && limit < list.Count)
if (limit.HasValue && limit < items.Count)
{
list = list.GetRange(0, limit.Value);
items = items.Take(limit.Value).ToArray();
}
var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
var result = new QueryResult<BaseItemDto>(
0,
totalCount,
returnList);
_dtoService.GetBaseItemDtos(items, dtoOptions, user));
return result;
}

@ -967,7 +967,7 @@ public class ItemsController : BaseJellyfinApiController
[HttpGet("UserItems/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<UserItemDataDto> GetItemUserData(
public ActionResult<UserItemDataDto?> GetItemUserData(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
@ -1005,7 +1005,7 @@ public class ItemsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<UserItemDataDto> GetItemUserDataLegacy(
public ActionResult<UserItemDataDto?> GetItemUserDataLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> GetItemUserData(userId, itemId);
@ -1022,7 +1022,7 @@ public class ItemsController : BaseJellyfinApiController
[HttpPost("UserItems/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<UserItemDataDto> UpdateItemUserData(
public ActionResult<UserItemDataDto?> UpdateItemUserData(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,
[FromBody, Required] UpdateUserItemDataDto userDataDto)
@ -1064,7 +1064,7 @@ public class ItemsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<UserItemDataDto> UpdateItemUserDataLegacy(
public ActionResult<UserItemDataDto?> UpdateItemUserDataLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromBody, Required] UpdateUserItemDataDto userDataDto)

@ -780,11 +780,9 @@ public class LibraryController : BaseJellyfinApiController
Genres = item.Genres,
Limit = limit,
IncludeItemTypes = includeItemTypes.ToArray(),
SimilarTo = item,
DtoOptions = dtoOptions,
EnableTotalRecordCount = !isMovie ?? true,
EnableGroupByMetadataKey = isMovie ?? false,
MinSimilarityScore = 2 // A remnant from album/artist scoring
};
// ExcludeArtistIds
@ -793,7 +791,7 @@ public class LibraryController : BaseJellyfinApiController
query.ExcludeArtistIds = excludeArtistIds;
}
List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
var itemsResult = _libraryManager.GetItemList(query);
var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);

@ -99,6 +99,7 @@ public class LibraryStructureController : BaseJellyfinApiController
/// <param name="name">The name of the folder.</param>
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <response code="204">Folder removed.</response>
/// <response code="404">Folder not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
@ -106,7 +107,9 @@ public class LibraryStructureController : BaseJellyfinApiController
[FromQuery] string name,
[FromQuery] bool refreshLibrary = false)
{
// TODO: refactor! this relies on an FileNotFound exception to return NotFound when attempting to remove a library that does not exist.
await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
return NoContent();
}

@ -120,7 +120,7 @@ public class MoviesController : BaseJellyfinApiController
DtoOptions = dtoOptions
});
var mostRecentMovies = recentlyPlayedMovies.GetRange(0, Math.Min(recentlyPlayedMovies.Count, 6));
var mostRecentMovies = recentlyPlayedMovies.Take(Math.Min(recentlyPlayedMovies.Count, 6)).ToList();
// Get recently played directors
var recentDirectors = GetDirectors(mostRecentMovies)
.ToList();
@ -276,7 +276,6 @@ public class MoviesController : BaseJellyfinApiController
Limit = itemLimit,
IncludeItemTypes = itemTypes.ToArray(),
IsMovie = true,
SimilarTo = item,
EnableGroupByMetadataKey = true,
DtoOptions = dtoOptions
});

@ -72,7 +72,7 @@ public class PlaystateController : BaseJellyfinApiController
[HttpPost("UserPlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem(
public async Task<ActionResult<UserItemDataDto?>> MarkPlayedItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
@ -121,7 +121,7 @@ public class PlaystateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public Task<ActionResult<UserItemDataDto>> MarkPlayedItemLegacy(
public Task<ActionResult<UserItemDataDto?>> MarkPlayedItemLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
@ -138,7 +138,7 @@ public class PlaystateController : BaseJellyfinApiController
[HttpDelete("UserPlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem(
public async Task<ActionResult<UserItemDataDto?>> MarkUnplayedItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
@ -185,7 +185,7 @@ public class PlaystateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public Task<ActionResult<UserItemDataDto>> MarkUnplayedItemLegacy(
public Task<ActionResult<UserItemDataDto?>> MarkUnplayedItemLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> MarkUnplayedItem(userId, itemId);
@ -502,7 +502,7 @@ public class PlaystateController : BaseJellyfinApiController
/// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
/// <param name="datePlayed">The date played.</param>
/// <returns>Task.</returns>
private UserItemDataDto UpdatePlayedStatus(User user, BaseItem item, bool wasPlayed, DateTime? datePlayed)
private UserItemDataDto? UpdatePlayedStatus(User user, BaseItem item, bool wasPlayed, DateTime? datePlayed)
{
if (wasPlayed)
{

@ -305,7 +305,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("UserItems/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> DeleteUserItemRating(
public ActionResult<UserItemDataDto?> DeleteUserItemRating(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
@ -338,7 +338,7 @@ public class UserLibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<UserItemDataDto> DeleteUserItemRatingLegacy(
public ActionResult<UserItemDataDto?> DeleteUserItemRatingLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> DeleteUserItemRating(userId, itemId);
@ -353,7 +353,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("UserItems/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> UpdateUserItemRating(
public ActionResult<UserItemDataDto?> UpdateUserItemRating(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,
[FromQuery] bool? likes)
@ -388,7 +388,7 @@ public class UserLibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<UserItemDataDto> UpdateUserItemRatingLegacy(
public ActionResult<UserItemDataDto?> UpdateUserItemRatingLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery] bool? likes)
@ -662,12 +662,15 @@ public class UserLibraryController : BaseJellyfinApiController
// Get the user data for this item
var data = _userDataRepository.GetUserData(user, item);
// Set favorite status
data.IsFavorite = isFavorite;
if (data is not null)
{
// Set favorite status
data.IsFavorite = isFavorite;
_userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
_userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
}
return _userDataRepository.GetUserDataDto(item, user);
return _userDataRepository.GetUserDataDto(item, user)!;
}
/// <summary>
@ -676,14 +679,17 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="user">The user.</param>
/// <param name="item">The item.</param>
/// <param name="likes">if set to <c>true</c> [likes].</param>
private UserItemDataDto UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)
private UserItemDataDto? UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)
{
// Get the user data for this item
var data = _userDataRepository.GetUserData(user, item);
data.Likes = likes;
if (data is not null)
{
data.Likes = likes;
_userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
_userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
}
return _userDataRepository.GetUserDataDto(item, user);
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Extensions;
@ -105,18 +106,18 @@ public class YearsController : BaseJellyfinApiController
bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
IList<BaseItem> items;
IReadOnlyList<BaseItem> items;
if (parentItem.IsFolder)
{
var folder = (Folder)parentItem;
if (userId.IsNullOrEmpty())
{
items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList();
items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToArray();
}
else
{
items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList();
items = recursive ? folder.GetRecursiveChildren(user, query) : folder.GetChildren(user, true).Where(Filter).ToArray();
}
}
else

@ -132,7 +132,7 @@ public static class StreamingHelpers
mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
? mediaSources[0]
: mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal));
: mediaSources.FirstOrDefault(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal));
if (mediaSource is null && Guid.Parse(streamingRequest.MediaSourceId).Equals(streamingRequest.Id))
{

@ -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; }
}

@ -154,14 +154,4 @@ public enum ItemSortBy
/// The index number.
/// </summary>
IndexNumber = 29,
/// <summary>
/// The similarity score.
/// </summary>
SimilarityScore = 30,
/// <summary>
/// The search score.
/// </summary>
SearchScore = 31,
}

@ -21,7 +21,7 @@ public static class ServiceCollectionExtensions
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
{
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")};Pooling=false");
});
return serviceCollection;

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);
}
}

@ -4,20 +4,18 @@ using Jellyfin.Data.Entities;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Interfaces;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations;
/// <inheritdoc/>
public class JellyfinDbContext : DbContext
/// <summary>
/// Initializes a new instance of the <see cref="JellyfinDbContext"/> class.
/// </summary>
/// <param name="options">The database context options.</param>
/// <param name="logger">Logger.</param>
public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILogger<JellyfinDbContext> logger) : DbContext(options)
{
/// <summary>
/// Initializes a new instance of the <see cref="JellyfinDbContext"/> class.
/// </summary>
/// <param name="options">The database context options.</param>
public JellyfinDbContext(DbContextOptions<JellyfinDbContext> options) : base(options)
{
}
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the access schedules.
/// </summary>
@ -88,6 +86,76 @@ public class JellyfinDbContext : DbContext
/// </summary>
public DbSet<MediaSegment> MediaSegments => Set<MediaSegment>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
/// </summary>
public DbSet<UserData> UserData => Set<UserData>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
/// </summary>
public DbSet<AncestorId> AncestorIds => Set<AncestorId>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
/// </summary>
public DbSet<AttachmentStreamInfo> AttachmentStreamInfos => Set<AttachmentStreamInfo>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
/// </summary>
public DbSet<BaseItemEntity> BaseItems => Set<BaseItemEntity>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
/// </summary>
public DbSet<Chapter> Chapters => Set<Chapter>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<ItemValue> ItemValues => Set<ItemValue>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<ItemValueMap> ItemValuesMap => Set<ItemValueMap>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<MediaStreamInfo> MediaStreamInfos => Set<MediaStreamInfo>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<People> Peoples => Set<People>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<PeopleBaseItemMap> PeopleBaseItemMap => Set<PeopleBaseItemMap>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the referenced Providers with ids.
/// </summary>
public DbSet<BaseItemProvider> BaseItemProviders => Set<BaseItemProvider>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<BaseItemImageInfo> BaseItemImageInfos => Set<BaseItemImageInfo>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<BaseItemMetadataField> BaseItemMetadataFields => Set<BaseItemMetadataField>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<BaseItemTrailerType> BaseItemTrailerTypes => Set<BaseItemTrailerType>();
/*public DbSet<Artwork> Artwork => Set<Artwork>();
public DbSet<Book> Books => Set<Book>();
@ -183,7 +251,15 @@ public class JellyfinDbContext : DbContext
saveEntity.OnSavingChanges();
}
return base.SaveChanges();
try
{
return base.SaveChanges();
}
catch (Exception e)
{
logger.LogError(e, "Error trying to save changes.");
throw;
}
}
/// <inheritdoc />

@ -0,0 +1,639 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class LibraryDbMigration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "BaseItems",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
Type = table.Column<string>(type: "TEXT", nullable: false),
Data = table.Column<string>(type: "TEXT", nullable: true),
Path = table.Column<string>(type: "TEXT", nullable: true),
StartDate = table.Column<DateTime>(type: "TEXT", nullable: false),
EndDate = table.Column<DateTime>(type: "TEXT", nullable: false),
ChannelId = table.Column<string>(type: "TEXT", nullable: true),
IsMovie = table.Column<bool>(type: "INTEGER", nullable: false),
CommunityRating = table.Column<float>(type: "REAL", nullable: true),
CustomRating = table.Column<string>(type: "TEXT", nullable: true),
IndexNumber = table.Column<int>(type: "INTEGER", nullable: true),
IsLocked = table.Column<bool>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: true),
OfficialRating = table.Column<string>(type: "TEXT", nullable: true),
MediaType = table.Column<string>(type: "TEXT", nullable: true),
Overview = table.Column<string>(type: "TEXT", nullable: true),
ParentIndexNumber = table.Column<int>(type: "INTEGER", nullable: true),
PremiereDate = table.Column<DateTime>(type: "TEXT", nullable: true),
ProductionYear = table.Column<int>(type: "INTEGER", nullable: true),
Genres = table.Column<string>(type: "TEXT", nullable: true),
SortName = table.Column<string>(type: "TEXT", nullable: true),
ForcedSortName = table.Column<string>(type: "TEXT", nullable: true),
RunTimeTicks = table.Column<long>(type: "INTEGER", nullable: true),
DateCreated = table.Column<DateTime>(type: "TEXT", nullable: true),
DateModified = table.Column<DateTime>(type: "TEXT", nullable: true),
IsSeries = table.Column<bool>(type: "INTEGER", nullable: false),
EpisodeTitle = table.Column<string>(type: "TEXT", nullable: true),
IsRepeat = table.Column<bool>(type: "INTEGER", nullable: false),
PreferredMetadataLanguage = table.Column<string>(type: "TEXT", nullable: true),
PreferredMetadataCountryCode = table.Column<string>(type: "TEXT", nullable: true),
DateLastRefreshed = table.Column<DateTime>(type: "TEXT", nullable: true),
DateLastSaved = table.Column<DateTime>(type: "TEXT", nullable: true),
IsInMixedFolder = table.Column<bool>(type: "INTEGER", nullable: false),
Studios = table.Column<string>(type: "TEXT", nullable: true),
ExternalServiceId = table.Column<string>(type: "TEXT", nullable: true),
Tags = table.Column<string>(type: "TEXT", nullable: true),
IsFolder = table.Column<bool>(type: "INTEGER", nullable: false),
InheritedParentalRatingValue = table.Column<int>(type: "INTEGER", nullable: true),
UnratedType = table.Column<string>(type: "TEXT", nullable: true),
CriticRating = table.Column<float>(type: "REAL", nullable: true),
CleanName = table.Column<string>(type: "TEXT", nullable: true),
PresentationUniqueKey = table.Column<string>(type: "TEXT", nullable: true),
OriginalTitle = table.Column<string>(type: "TEXT", nullable: true),
PrimaryVersionId = table.Column<string>(type: "TEXT", nullable: true),
DateLastMediaAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
Album = table.Column<string>(type: "TEXT", nullable: true),
LUFS = table.Column<float>(type: "REAL", nullable: true),
NormalizationGain = table.Column<float>(type: "REAL", nullable: true),
IsVirtualItem = table.Column<bool>(type: "INTEGER", nullable: false),
SeriesName = table.Column<string>(type: "TEXT", nullable: true),
SeasonName = table.Column<string>(type: "TEXT", nullable: true),
ExternalSeriesId = table.Column<string>(type: "TEXT", nullable: true),
Tagline = table.Column<string>(type: "TEXT", nullable: true),
ProductionLocations = table.Column<string>(type: "TEXT", nullable: true),
ExtraIds = table.Column<string>(type: "TEXT", nullable: true),
TotalBitrate = table.Column<int>(type: "INTEGER", nullable: true),
ExtraType = table.Column<int>(type: "INTEGER", nullable: true),
Artists = table.Column<string>(type: "TEXT", nullable: true),
AlbumArtists = table.Column<string>(type: "TEXT", nullable: true),
ExternalId = table.Column<string>(type: "TEXT", nullable: true),
SeriesPresentationUniqueKey = table.Column<string>(type: "TEXT", nullable: true),
ShowId = table.Column<string>(type: "TEXT", nullable: true),
OwnerId = table.Column<string>(type: "TEXT", nullable: true),
Width = table.Column<int>(type: "INTEGER", nullable: true),
Height = table.Column<int>(type: "INTEGER", nullable: true),
Size = table.Column<long>(type: "INTEGER", nullable: true),
Audio = table.Column<int>(type: "INTEGER", nullable: true),
ParentId = table.Column<Guid>(type: "TEXT", nullable: true),
TopParentId = table.Column<Guid>(type: "TEXT", nullable: true),
SeasonId = table.Column<Guid>(type: "TEXT", nullable: true),
SeriesId = table.Column<Guid>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_BaseItems", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ItemValues",
columns: table => new
{
ItemValueId = table.Column<Guid>(type: "TEXT", nullable: false),
Type = table.Column<int>(type: "INTEGER", nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: false),
CleanValue = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ItemValues", x => x.ItemValueId);
});
migrationBuilder.CreateTable(
name: "Peoples",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
PersonType = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Peoples", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AncestorIds",
columns: table => new
{
ParentItemId = table.Column<Guid>(type: "TEXT", nullable: false),
ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
BaseItemEntityId = table.Column<Guid>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AncestorIds", x => new { x.ItemId, x.ParentItemId });
table.ForeignKey(
name: "FK_AncestorIds_BaseItems_BaseItemEntityId",
column: x => x.BaseItemEntityId,
principalTable: "BaseItems",
principalColumn: "Id");
table.ForeignKey(
name: "FK_AncestorIds_BaseItems_ItemId",
column: x => x.ItemId,
principalTable: "BaseItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AncestorIds_BaseItems_ParentItemId",
column: x => x.ParentItemId,
principalTable: "BaseItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AttachmentStreamInfos",
columns: table => new
{
ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
Index = table.Column<int>(type: "INTEGER", nullable: false),
Codec = table.Column<string>(type: "TEXT", nullable: false),
CodecTag = table.Column<string>(type: "TEXT", nullable: true),
Comment = table.Column<string>(type: "TEXT", nullable: true),
Filename = table.Column<string>(type: "TEXT", nullable: true),
MimeType = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AttachmentStreamInfos", x => new { x.ItemId, x.Index });
table.ForeignKey(
name: "FK_AttachmentStreamInfos_BaseItems_ItemId",
column: x => x.ItemId,
principalTable: "BaseItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "BaseItemImageInfos",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
Path = table.Column<string>(type: "TEXT", nullable: false),
DateModified = table.Column<DateTime>(type: "TEXT", nullable: false),
ImageType = table.Column<int>(type: "INTEGER", nullable: false),
Width = table.Column<int>(type: "INTEGER", nullable: false),
Height = table.Column<int>(type: "INTEGER", nullable: false),
Blurhash = table.Column<byte[]>(type: "BLOB", nullable: true),
ItemId = table.Column<Guid>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BaseItemImageInfos", x => x.Id);
table.ForeignKey(
name: "FK_BaseItemImageInfos_BaseItems_ItemId",
column: x => x.ItemId,
principalTable: "BaseItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "BaseItemMetadataFields",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false),
ItemId = table.Column<Guid>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BaseItemMetadataFields", x => new { x.Id, x.ItemId });
table.ForeignKey(
name: "FK_BaseItemMetadataFields_BaseItems_ItemId",
column: x => x.ItemId,
principalTable: "BaseItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "BaseItemProviders",
columns: table => new
{
ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
ProviderId = table.Column<string>(type: "TEXT", nullable: false),
ProviderValue = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BaseItemProviders", x => new { x.ItemId, x.ProviderId });
table.ForeignKey(
name: "FK_BaseItemProviders_BaseItems_ItemId",
column: x => x.ItemId,
principalTable: "BaseItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "BaseItemTrailerTypes",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false),
ItemId = table.Column<Guid>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BaseItemTrailerTypes", x => new { x.Id, x.ItemId });
table.ForeignKey(
name: "FK_BaseItemTrailerTypes_BaseItems_ItemId",
column: x => x.ItemId,
principalTable: "BaseItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Chapters",
columns: table => new
{
ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
ChapterIndex = table.Column<int>(type: "INTEGER", nullable: false),
StartPositionTicks = table.Column<long>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: true),
ImagePath = table.Column<string>(type: "TEXT", nullable: true),
ImageDateModified = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Chapters", x => new { x.ItemId, x.ChapterIndex });
table.ForeignKey(
name: "FK_Chapters_BaseItems_ItemId",
column: x => x.ItemId,
principalTable: "BaseItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MediaStreamInfos",
columns: table => new
{
ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
StreamIndex = table.Column<int>(type: "INTEGER", nullable: false),
StreamType = table.Column<int>(type: "INTEGER", nullable: true),
Codec = table.Column<string>(type: "TEXT", nullable: true),
Language = table.Column<string>(type: "TEXT", nullable: true),
ChannelLayout = table.Column<string>(type: "TEXT", nullable: true),
Profile = table.Column<string>(type: "TEXT", nullable: true),
AspectRatio = table.Column<string>(type: "TEXT", nullable: true),
Path = table.Column<string>(type: "TEXT", nullable: true),
IsInterlaced = table.Column<bool>(type: "INTEGER", nullable: false),
BitRate = table.Column<int>(type: "INTEGER", nullable: false),
Channels = table.Column<int>(type: "INTEGER", nullable: false),
SampleRate = table.Column<int>(type: "INTEGER", nullable: false),
IsDefault = table.Column<bool>(type: "INTEGER", nullable: false),
IsForced = table.Column<bool>(type: "INTEGER", nullable: false),
IsExternal = table.Column<bool>(type: "INTEGER", nullable: false),
Height = table.Column<int>(type: "INTEGER", nullable: false),
Width = table.Column<int>(type: "INTEGER", nullable: false),
AverageFrameRate = table.Column<float>(type: "REAL", nullable: false),
RealFrameRate = table.Column<float>(type: "REAL", nullable: false),
Level = table.Column<float>(type: "REAL", nullable: false),
PixelFormat = table.Column<string>(type: "TEXT", nullable: true),
BitDepth = table.Column<int>(type: "INTEGER", nullable: false),
IsAnamorphic = table.Column<bool>(type: "INTEGER", nullable: false),
RefFrames = table.Column<int>(type: "INTEGER", nullable: false),
CodecTag = table.Column<string>(type: "TEXT", nullable: false),
Comment = table.Column<string>(type: "TEXT", nullable: false),
NalLengthSize = table.Column<string>(type: "TEXT", nullable: false),
IsAvc = table.Column<bool>(type: "INTEGER", nullable: false),
Title = table.Column<string>(type: "TEXT", nullable: false),
TimeBase = table.Column<string>(type: "TEXT", nullable: false),
CodecTimeBase = table.Column<string>(type: "TEXT", nullable: false),
ColorPrimaries = table.Column<string>(type: "TEXT", nullable: false),
ColorSpace = table.Column<string>(type: "TEXT", nullable: false),
ColorTransfer = table.Column<string>(type: "TEXT", nullable: false),
DvVersionMajor = table.Column<int>(type: "INTEGER", nullable: false),
DvVersionMinor = table.Column<int>(type: "INTEGER", nullable: false),
DvProfile = table.Column<int>(type: "INTEGER", nullable: false),
DvLevel = table.Column<int>(type: "INTEGER", nullable: false),
RpuPresentFlag = table.Column<int>(type: "INTEGER", nullable: false),
ElPresentFlag = table.Column<int>(type: "INTEGER", nullable: false),
BlPresentFlag = table.Column<int>(type: "INTEGER", nullable: false),
DvBlSignalCompatibilityId = table.Column<int>(type: "INTEGER", nullable: false),
IsHearingImpaired = table.Column<bool>(type: "INTEGER", nullable: false),
Rotation = table.Column<int>(type: "INTEGER", nullable: false),
KeyFrames = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MediaStreamInfos", x => new { x.ItemId, x.StreamIndex });
table.ForeignKey(
name: "FK_MediaStreamInfos_BaseItems_ItemId",
column: x => x.ItemId,
principalTable: "BaseItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UserData",
columns: table => new
{
ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
Rating = table.Column<double>(type: "REAL", nullable: true),
PlaybackPositionTicks = table.Column<long>(type: "INTEGER", nullable: false),
PlayCount = table.Column<int>(type: "INTEGER", nullable: false),
IsFavorite = table.Column<bool>(type: "INTEGER", nullable: false),
LastPlayedDate = table.Column<DateTime>(type: "TEXT", nullable: true),
Played = table.Column<bool>(type: "INTEGER", nullable: false),
AudioStreamIndex = table.Column<int>(type: "INTEGER", nullable: true),
SubtitleStreamIndex = table.Column<int>(type: "INTEGER", nullable: true),
Likes = table.Column<bool>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_UserData", x => new { x.ItemId, x.UserId });
table.ForeignKey(
name: "FK_UserData_BaseItems_ItemId",
column: x => x.ItemId,
principalTable: "BaseItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UserData_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ItemValuesMap",
columns: table => new
{
ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
ItemValueId = table.Column<Guid>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ItemValuesMap", x => new { x.ItemValueId, x.ItemId });
table.ForeignKey(
name: "FK_ItemValuesMap_BaseItems_ItemId",
column: x => x.ItemId,
principalTable: "BaseItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ItemValuesMap_ItemValues_ItemValueId",
column: x => x.ItemValueId,
principalTable: "ItemValues",
principalColumn: "ItemValueId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PeopleBaseItemMap",
columns: table => new
{
ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
PeopleId = table.Column<Guid>(type: "TEXT", nullable: false),
SortOrder = table.Column<int>(type: "INTEGER", nullable: true),
ListOrder = table.Column<int>(type: "INTEGER", nullable: true),
Role = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PeopleBaseItemMap", x => new { x.ItemId, x.PeopleId });
table.ForeignKey(
name: "FK_PeopleBaseItemMap_BaseItems_ItemId",
column: x => x.ItemId,
principalTable: "BaseItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PeopleBaseItemMap_Peoples_PeopleId",
column: x => x.PeopleId,
principalTable: "Peoples",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AncestorIds_BaseItemEntityId",
table: "AncestorIds",
column: "BaseItemEntityId");
migrationBuilder.CreateIndex(
name: "IX_AncestorIds_ParentItemId",
table: "AncestorIds",
column: "ParentItemId");
migrationBuilder.CreateIndex(
name: "IX_BaseItemImageInfos_ItemId",
table: "BaseItemImageInfos",
column: "ItemId");
migrationBuilder.CreateIndex(
name: "IX_BaseItemMetadataFields_ItemId",
table: "BaseItemMetadataFields",
column: "ItemId");
migrationBuilder.CreateIndex(
name: "IX_BaseItemProviders_ProviderId_ProviderValue_ItemId",
table: "BaseItemProviders",
columns: new[] { "ProviderId", "ProviderValue", "ItemId" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_Id_Type_IsFolder_IsVirtualItem",
table: "BaseItems",
columns: new[] { "Id", "Type", "IsFolder", "IsVirtualItem" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_IsFolder_TopParentId_IsVirtualItem_PresentationUniqueKey_DateCreated",
table: "BaseItems",
columns: new[] { "IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_MediaType_TopParentId_IsVirtualItem_PresentationUniqueKey",
table: "BaseItems",
columns: new[] { "MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_ParentId",
table: "BaseItems",
column: "ParentId");
migrationBuilder.CreateIndex(
name: "IX_BaseItems_Path",
table: "BaseItems",
column: "Path");
migrationBuilder.CreateIndex(
name: "IX_BaseItems_PresentationUniqueKey",
table: "BaseItems",
column: "PresentationUniqueKey");
migrationBuilder.CreateIndex(
name: "IX_BaseItems_TopParentId_Id",
table: "BaseItems",
columns: new[] { "TopParentId", "Id" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_Type_SeriesPresentationUniqueKey_IsFolder_IsVirtualItem",
table: "BaseItems",
columns: new[] { "Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_Type_SeriesPresentationUniqueKey_PresentationUniqueKey_SortName",
table: "BaseItems",
columns: new[] { "Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_Type_TopParentId_Id",
table: "BaseItems",
columns: new[] { "Type", "TopParentId", "Id" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_Type_TopParentId_IsVirtualItem_PresentationUniqueKey_DateCreated",
table: "BaseItems",
columns: new[] { "Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_Type_TopParentId_PresentationUniqueKey",
table: "BaseItems",
columns: new[] { "Type", "TopParentId", "PresentationUniqueKey" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_Type_TopParentId_StartDate",
table: "BaseItems",
columns: new[] { "Type", "TopParentId", "StartDate" });
migrationBuilder.CreateIndex(
name: "IX_BaseItemTrailerTypes_ItemId",
table: "BaseItemTrailerTypes",
column: "ItemId");
migrationBuilder.CreateIndex(
name: "IX_ItemValues_Type_CleanValue",
table: "ItemValues",
columns: new[] { "Type", "CleanValue" });
migrationBuilder.CreateIndex(
name: "IX_ItemValuesMap_ItemId",
table: "ItemValuesMap",
column: "ItemId");
migrationBuilder.CreateIndex(
name: "IX_MediaStreamInfos_StreamIndex",
table: "MediaStreamInfos",
column: "StreamIndex");
migrationBuilder.CreateIndex(
name: "IX_MediaStreamInfos_StreamIndex_StreamType",
table: "MediaStreamInfos",
columns: new[] { "StreamIndex", "StreamType" });
migrationBuilder.CreateIndex(
name: "IX_MediaStreamInfos_StreamIndex_StreamType_Language",
table: "MediaStreamInfos",
columns: new[] { "StreamIndex", "StreamType", "Language" });
migrationBuilder.CreateIndex(
name: "IX_MediaStreamInfos_StreamType",
table: "MediaStreamInfos",
column: "StreamType");
migrationBuilder.CreateIndex(
name: "IX_PeopleBaseItemMap_ItemId_ListOrder",
table: "PeopleBaseItemMap",
columns: new[] { "ItemId", "ListOrder" });
migrationBuilder.CreateIndex(
name: "IX_PeopleBaseItemMap_ItemId_SortOrder",
table: "PeopleBaseItemMap",
columns: new[] { "ItemId", "SortOrder" });
migrationBuilder.CreateIndex(
name: "IX_PeopleBaseItemMap_PeopleId",
table: "PeopleBaseItemMap",
column: "PeopleId");
migrationBuilder.CreateIndex(
name: "IX_Peoples_Name",
table: "Peoples",
column: "Name");
migrationBuilder.CreateIndex(
name: "IX_UserData_ItemId_UserId_IsFavorite",
table: "UserData",
columns: new[] { "ItemId", "UserId", "IsFavorite" });
migrationBuilder.CreateIndex(
name: "IX_UserData_ItemId_UserId_LastPlayedDate",
table: "UserData",
columns: new[] { "ItemId", "UserId", "LastPlayedDate" });
migrationBuilder.CreateIndex(
name: "IX_UserData_ItemId_UserId_PlaybackPositionTicks",
table: "UserData",
columns: new[] { "ItemId", "UserId", "PlaybackPositionTicks" });
migrationBuilder.CreateIndex(
name: "IX_UserData_ItemId_UserId_Played",
table: "UserData",
columns: new[] { "ItemId", "UserId", "Played" });
migrationBuilder.CreateIndex(
name: "IX_UserData_UserId",
table: "UserData",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AncestorIds");
migrationBuilder.DropTable(
name: "AttachmentStreamInfos");
migrationBuilder.DropTable(
name: "BaseItemImageInfos");
migrationBuilder.DropTable(
name: "BaseItemMetadataFields");
migrationBuilder.DropTable(
name: "BaseItemProviders");
migrationBuilder.DropTable(
name: "BaseItemTrailerTypes");
migrationBuilder.DropTable(
name: "Chapters");
migrationBuilder.DropTable(
name: "ItemValuesMap");
migrationBuilder.DropTable(
name: "MediaStreamInfos");
migrationBuilder.DropTable(
name: "PeopleBaseItemMap");
migrationBuilder.DropTable(
name: "UserData");
migrationBuilder.DropTable(
name: "ItemValues");
migrationBuilder.DropTable(
name: "Peoples");
migrationBuilder.DropTable(
name: "BaseItems");
}
}
}

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class AddedCustomDataKey : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "CustomDataKey",
table: "UserData",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CustomDataKey",
table: "UserData");
}
}
}

@ -0,0 +1,54 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class AddedCustomDataKeyKey : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_UserData",
table: "UserData");
migrationBuilder.AlterColumn<string>(
name: "CustomDataKey",
table: "UserData",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AddPrimaryKey(
name: "PK_UserData",
table: "UserData",
columns: new[] { "ItemId", "UserId", "CustomDataKey" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_UserData",
table: "UserData");
migrationBuilder.AlterColumn<string>(
name: "CustomDataKey",
table: "UserData",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AddPrimaryKey(
name: "PK_UserData",
table: "UserData",
columns: new[] { "ItemId", "UserId" });
}
}
}

@ -0,0 +1,49 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class FixAncestorIdConfig : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_AncestorIds_BaseItems_BaseItemEntityId",
table: "AncestorIds");
migrationBuilder.DropIndex(
name: "IX_AncestorIds_BaseItemEntityId",
table: "AncestorIds");
migrationBuilder.DropColumn(
name: "BaseItemEntityId",
table: "AncestorIds");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "BaseItemEntityId",
table: "AncestorIds",
type: "TEXT",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_AncestorIds_BaseItemEntityId",
table: "AncestorIds",
column: "BaseItemEntityId");
migrationBuilder.AddForeignKey(
name: "FK_AncestorIds_BaseItems_BaseItemEntityId",
table: "AncestorIds",
column: "BaseItemEntityId",
principalTable: "BaseItems",
principalColumn: "Id");
}
}
}

@ -0,0 +1,702 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class FixMediaStreams : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "Width",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<string>(
name: "Title",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "TimeBase",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<int>(
name: "StreamType",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "SampleRate",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<int>(
name: "RpuPresentFlag",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<int>(
name: "Rotation",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<int>(
name: "RefFrames",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<float>(
name: "RealFrameRate",
table: "MediaStreamInfos",
type: "REAL",
nullable: true,
oldClrType: typeof(float),
oldType: "REAL");
migrationBuilder.AlterColumn<string>(
name: "Profile",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Path",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "NalLengthSize",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<float>(
name: "Level",
table: "MediaStreamInfos",
type: "REAL",
nullable: true,
oldClrType: typeof(float),
oldType: "REAL");
migrationBuilder.AlterColumn<string>(
name: "Language",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<bool>(
name: "IsHearingImpaired",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(bool),
oldType: "INTEGER");
migrationBuilder.AlterColumn<bool>(
name: "IsAvc",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(bool),
oldType: "INTEGER");
migrationBuilder.AlterColumn<bool>(
name: "IsAnamorphic",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(bool),
oldType: "INTEGER");
migrationBuilder.AlterColumn<int>(
name: "Height",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<int>(
name: "ElPresentFlag",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<int>(
name: "DvVersionMinor",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<int>(
name: "DvVersionMajor",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<int>(
name: "DvProfile",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<int>(
name: "DvLevel",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<int>(
name: "DvBlSignalCompatibilityId",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<string>(
name: "Comment",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "ColorTransfer",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "ColorSpace",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "ColorPrimaries",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "CodecTimeBase",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "CodecTag",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "Codec",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "Channels",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<string>(
name: "ChannelLayout",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "BlPresentFlag",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<int>(
name: "BitRate",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<int>(
name: "BitDepth",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<float>(
name: "AverageFrameRate",
table: "MediaStreamInfos",
type: "REAL",
nullable: true,
oldClrType: typeof(float),
oldType: "REAL");
migrationBuilder.AlterColumn<string>(
name: "AspectRatio",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "Width",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Title",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "TimeBase",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "StreamType",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AlterColumn<int>(
name: "SampleRate",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "RpuPresentFlag",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "Rotation",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "RefFrames",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<float>(
name: "RealFrameRate",
table: "MediaStreamInfos",
type: "REAL",
nullable: false,
defaultValue: 0f,
oldClrType: typeof(float),
oldType: "REAL",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Profile",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "Path",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "NalLengthSize",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<float>(
name: "Level",
table: "MediaStreamInfos",
type: "REAL",
nullable: false,
defaultValue: 0f,
oldClrType: typeof(float),
oldType: "REAL",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Language",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<bool>(
name: "IsHearingImpaired",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: false,
oldClrType: typeof(bool),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<bool>(
name: "IsAvc",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: false,
oldClrType: typeof(bool),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<bool>(
name: "IsAnamorphic",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: false,
oldClrType: typeof(bool),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "Height",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "ElPresentFlag",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "DvVersionMinor",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "DvVersionMajor",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "DvProfile",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "DvLevel",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "DvBlSignalCompatibilityId",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Comment",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ColorTransfer",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ColorSpace",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ColorPrimaries",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "CodecTimeBase",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "CodecTag",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Codec",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<int>(
name: "Channels",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ChannelLayout",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<int>(
name: "BlPresentFlag",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "BitRate",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "BitDepth",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<float>(
name: "AverageFrameRate",
table: "MediaStreamInfos",
type: "REAL",
nullable: false,
defaultValue: 0f,
oldClrType: typeof(float),
oldType: "REAL",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "AspectRatio",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
}
}
}

@ -0,0 +1,144 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class FixMediaStreams2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Profile",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "Path",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "Language",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<bool>(
name: "IsInterlaced",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: true,
oldClrType: typeof(bool),
oldType: "INTEGER");
migrationBuilder.AlterColumn<string>(
name: "Codec",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "ChannelLayout",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "AspectRatio",
table: "MediaStreamInfos",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Profile",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Path",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Language",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<bool>(
name: "IsInterlaced",
table: "MediaStreamInfos",
type: "INTEGER",
nullable: false,
defaultValue: false,
oldClrType: typeof(bool),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Codec",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "ChannelLayout",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "AspectRatio",
table: "MediaStreamInfos",
type: "TEXT",
nullable: false,
defaultValue: string.Empty,
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
}
}
}

@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class EnforceUniqueItemValue : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_ItemValues_Type_CleanValue",
table: "ItemValues");
migrationBuilder.CreateIndex(
name: "IX_ItemValues_Type_CleanValue",
table: "ItemValues",
columns: new[] { "Type", "CleanValue" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_ItemValues_Type_CleanValue",
table: "ItemValues");
migrationBuilder.CreateIndex(
name: "IX_ItemValues_Type_CleanValue",
table: "ItemValues",
columns: new[] { "Type", "CleanValue" });
}
}
}

@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging.Abstractions;
namespace Jellyfin.Server.Implementations.Migrations
{
@ -14,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
var optionsBuilder = new DbContextOptionsBuilder<JellyfinDbContext>();
optionsBuilder.UseSqlite("Data Source=jellyfin.db");
return new JellyfinDbContext(optionsBuilder.Options);
return new JellyfinDbContext(optionsBuilder.Options, NullLogger<JellyfinDbContext>.Instance);
}
}
}

@ -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);
}
}

@ -179,7 +179,7 @@ public class TrickplayManager : ITrickplayManager
{
// Extract images
// Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
var mediaSource = video.GetMediaSources(false).FirstOrDefault(source => Guid.Parse(source.Id).Equals(video.Id));
if (mediaSource is null)
{

@ -48,7 +48,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.UpdateDefaultPluginRepository),
typeof(Routines.FixAudioData),
typeof(Routines.MoveTrickplayFiles),
typeof(Routines.RemoveDuplicatePlaylistChildren)
typeof(Routines.RemoveDuplicatePlaylistChildren),
typeof(Routines.MigrateLibraryDb),
};
/// <summary>

File diff suppressed because it is too large Load Diff

@ -13,6 +13,7 @@ using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@ -193,6 +194,7 @@ namespace Jellyfin.Server
// Don't throw additional exception if startup failed.
if (appHost.ServiceProvider is not null)
{
var isSqlite = false;
_logger.LogInformation("Running query planner optimizations in the database... This might take a while");
// Run before disposing the application
var context = await appHost.ServiceProvider.GetRequiredService<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false);
@ -200,9 +202,15 @@ namespace Jellyfin.Server
{
if (context.Database.IsSqlite())
{
isSqlite = true;
await context.Database.ExecuteSqlRawAsync("PRAGMA optimize").ConfigureAwait(false);
}
}
if (isSqlite)
{
SqliteConnection.ClearAllPools();
}
}
host?.Dispose();

@ -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);
}

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Drawing
@ -57,6 +58,22 @@ namespace MediaBrowser.Controller.Drawing
/// <returns>BlurHash.</returns>
string GetImageBlurHash(string path, ImageDimensions imageDimensions);
/// <summary>
/// Gets the image cache tag.
/// </summary>
/// <param name="baseItemPath">The items basePath.</param>
/// <param name="imageDateModified">The image last modification date.</param>
/// <returns>Guid.</returns>
string? GetImageCacheTag(string baseItemPath, DateTime imageDateModified);
/// <summary>
/// Gets the image cache tag.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="image">The image.</param>
/// <returns>Guid.</returns>
string? GetImageCacheTag(BaseItemDto item, ChapterInfo image);
/// <summary>
/// Gets the image cache tag.
/// </summary>
@ -65,6 +82,14 @@ namespace MediaBrowser.Controller.Drawing
/// <returns>Guid.</returns>
string GetImageCacheTag(BaseItem item, ItemImageInfo image);
/// <summary>
/// Gets the image cache tag.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="image">The image.</param>
/// <returns>Guid.</returns>
string GetImageCacheTag(BaseItemDto item, ItemImageInfo image);
string? GetImageCacheTag(BaseItem item, ChapterInfo chapter);
string? GetImageCacheTag(User user);

@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Entities
return CreateResolveArgs(directoryService, true).FileSystemChildren;
}
protected override List<BaseItem> LoadChildren()
protected override IReadOnlyList<BaseItem> LoadChildren()
{
lock (_childIdsLock)
{

@ -21,6 +21,7 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <summary>
/// Class MusicAlbum.
/// </summary>
[Common.RequiresSourceSerialisation]
public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer
{
public MusicAlbum()

@ -21,6 +21,7 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <summary>
/// Class MusicArtist.
/// </summary>
[Common.RequiresSourceSerialisation]
public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo>
{
[JsonIgnore]
@ -84,7 +85,7 @@ namespace MediaBrowser.Controller.Entities.Audio
return !IsAccessedByName;
}
public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
if (query.IncludeItemTypes.Length == 0)
{

@ -14,6 +14,7 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <summary>
/// Class MusicGenre.
/// </summary>
[Common.RequiresSourceSerialisation]
public class MusicGenre : BaseItem, IItemByName
{
[JsonIgnore]
@ -64,7 +65,7 @@ namespace MediaBrowser.Controller.Entities.Audio
return true;
}
public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
{
query.GenreIds = new[] { Id };
query.IncludeItemTypes = new[] { BaseItemKind.MusicVideo, BaseItemKind.Audio, BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist };

@ -9,6 +9,7 @@ using MediaBrowser.Controller.Providers;
namespace MediaBrowser.Controller.Entities
{
[Common.RequiresSourceSerialisation]
public class AudioBook : Audio.Audio, IHasSeries, IHasLookupInfo<SongInfo>
{
[JsonIgnore]

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save