pull/12880/head
commit
8c0b0d9102
@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "Development Jellyfin Server - FFmpeg",
|
||||
"image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy",
|
||||
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
|
||||
"postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust; sudo bash \"./.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh\"",
|
||||
// reads the extensions list and installs them
|
||||
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/dotnet:2": {
|
||||
"version": "none",
|
||||
"dotnetRuntimeVersions": "8.0",
|
||||
"aspNetCoreRuntimeVersions": "8.0"
|
||||
},
|
||||
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
|
||||
"preserve_apt_list": false,
|
||||
"packages": ["libfontconfig1"]
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"dockerDashComposeVersion": "v2"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {},
|
||||
"ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}
|
||||
},
|
||||
"hostRequirements": {
|
||||
"memory": "8gb",
|
||||
"cpus": 4
|
||||
}
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"recommendations": [
|
||||
"ms-dotnettools.csharp",
|
||||
"editorconfig.editorconfig",
|
||||
"github.vscode-github-actions",
|
||||
"ms-dotnettools.vscode-dotnet-runtime",
|
||||
"ms-dotnettools.csdevkit"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-dotnettools.csdevkit",
|
||||
"alexcvzz.vscode-sqlite"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1,269 +0,0 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
public abstract class BaseSqliteRepository : IDisposable
|
||||
{
|
||||
private bool _disposed = false;
|
||||
private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
|
||||
private SqliteConnection _writeConnection;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
protected BaseSqliteRepository(ILogger<BaseSqliteRepository> logger)
|
||||
{
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the DB file.
|
||||
/// </summary>
|
||||
protected string DbFilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger.
|
||||
/// </summary>
|
||||
/// <value>The logger.</value>
|
||||
protected ILogger<BaseSqliteRepository> Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cache size.
|
||||
/// </summary>
|
||||
/// <value>The cache size or null.</value>
|
||||
protected virtual int? CacheSize => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
|
||||
/// </summary>
|
||||
protected virtual string LockingMode => "NORMAL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
|
||||
/// </summary>
|
||||
/// <value>The journal mode.</value>
|
||||
protected virtual string JournalMode => "WAL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
|
||||
/// The default (-1) is overridden to prevent unconstrained WAL size, as reported by users.
|
||||
/// </summary>
|
||||
/// <value>The journal size limit.</value>
|
||||
protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
|
||||
|
||||
/// <summary>
|
||||
/// Gets the page size.
|
||||
/// </summary>
|
||||
/// <value>The page size or null.</value>
|
||||
protected virtual int? PageSize => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the temp store mode.
|
||||
/// </summary>
|
||||
/// <value>The temp store mode.</value>
|
||||
/// <see cref="TempStoreMode"/>
|
||||
protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the synchronous mode.
|
||||
/// </summary>
|
||||
/// <value>The synchronous mode or null.</value>
|
||||
/// <see cref="SynchronousMode"/>
|
||||
protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
|
||||
|
||||
public virtual void Initialize()
|
||||
{
|
||||
// Configuration and pragmas can affect VACUUM so it needs to be last.
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.Execute("VACUUM");
|
||||
}
|
||||
}
|
||||
|
||||
protected ManagedConnection GetConnection(bool readOnly = false)
|
||||
{
|
||||
if (!readOnly)
|
||||
{
|
||||
_writeLock.Wait();
|
||||
if (_writeConnection is not null)
|
||||
{
|
||||
return new ManagedConnection(_writeConnection, _writeLock);
|
||||
}
|
||||
|
||||
var writeConnection = new SqliteConnection($"Filename={DbFilePath};Pooling=False");
|
||||
writeConnection.Open();
|
||||
|
||||
if (CacheSize.HasValue)
|
||||
{
|
||||
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(LockingMode))
|
||||
{
|
||||
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(JournalMode))
|
||||
{
|
||||
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
|
||||
}
|
||||
|
||||
if (JournalSizeLimit.HasValue)
|
||||
{
|
||||
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
|
||||
}
|
||||
|
||||
if (Synchronous.HasValue)
|
||||
{
|
||||
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
||||
}
|
||||
|
||||
if (PageSize.HasValue)
|
||||
{
|
||||
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
|
||||
}
|
||||
|
||||
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||
|
||||
return new ManagedConnection(_writeConnection = writeConnection, _writeLock);
|
||||
}
|
||||
|
||||
var connection = new SqliteConnection($"Filename={DbFilePath};Mode=ReadOnly");
|
||||
connection.Open();
|
||||
|
||||
if (CacheSize.HasValue)
|
||||
{
|
||||
connection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(LockingMode))
|
||||
{
|
||||
connection.Execute("PRAGMA locking_mode=" + LockingMode);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(JournalMode))
|
||||
{
|
||||
connection.Execute("PRAGMA journal_mode=" + JournalMode);
|
||||
}
|
||||
|
||||
if (JournalSizeLimit.HasValue)
|
||||
{
|
||||
connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
|
||||
}
|
||||
|
||||
if (Synchronous.HasValue)
|
||||
{
|
||||
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
||||
}
|
||||
|
||||
if (PageSize.HasValue)
|
||||
{
|
||||
connection.Execute("PRAGMA page_size=" + PageSize.Value);
|
||||
}
|
||||
|
||||
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||
|
||||
return new ManagedConnection(connection, null);
|
||||
}
|
||||
|
||||
public SqliteCommand PrepareStatement(ManagedConnection connection, string sql)
|
||||
{
|
||||
var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
return command;
|
||||
}
|
||||
|
||||
protected bool TableExists(ManagedConnection connection, string name)
|
||||
{
|
||||
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected List<string> GetColumnNames(ManagedConnection connection, string table)
|
||||
{
|
||||
var columnNames = new List<string>();
|
||||
|
||||
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
|
||||
{
|
||||
if (row.TryGetString(1, out var columnName))
|
||||
{
|
||||
columnNames.Add(columnName);
|
||||
}
|
||||
}
|
||||
|
||||
return columnNames;
|
||||
}
|
||||
|
||||
protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
||||
{
|
||||
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
connection.Execute("alter table " + table + " add column " + columnName + " " + type + " NULL");
|
||||
}
|
||||
|
||||
protected void CheckDisposed()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (dispose)
|
||||
{
|
||||
_writeLock.Wait();
|
||||
try
|
||||
{
|
||||
_writeConnection.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeLock.Release();
|
||||
}
|
||||
|
||||
_writeLock.Dispose();
|
||||
}
|
||||
|
||||
_writeConnection = null;
|
||||
_writeLock = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Channels;
|
||||
using Emby.Server.Implementations.Playlists;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
|
||||
namespace Emby.Server.Implementations.Data;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class ItemTypeLookup : IItemTypeLookup
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> MusicGenreTypes { get; } = [
|
||||
typeof(Audio).FullName!,
|
||||
typeof(MusicVideo).FullName!,
|
||||
typeof(MusicAlbum).FullName!,
|
||||
typeof(MusicArtist).FullName!,
|
||||
];
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<BaseItemKind, string> BaseItemKindNames { get; } = new Dictionary<BaseItemKind, string>()
|
||||
{
|
||||
{ BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName! },
|
||||
{ BaseItemKind.Audio, typeof(Audio).FullName! },
|
||||
{ BaseItemKind.AudioBook, typeof(AudioBook).FullName! },
|
||||
{ BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName! },
|
||||
{ BaseItemKind.Book, typeof(Book).FullName! },
|
||||
{ BaseItemKind.BoxSet, typeof(BoxSet).FullName! },
|
||||
{ BaseItemKind.Channel, typeof(Channel).FullName! },
|
||||
{ BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName! },
|
||||
{ BaseItemKind.Episode, typeof(Episode).FullName! },
|
||||
{ BaseItemKind.Folder, typeof(Folder).FullName! },
|
||||
{ BaseItemKind.Genre, typeof(Genre).FullName! },
|
||||
{ BaseItemKind.Movie, typeof(Movie).FullName! },
|
||||
{ BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName! },
|
||||
{ BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName! },
|
||||
{ BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName! },
|
||||
{ BaseItemKind.MusicArtist, typeof(MusicArtist).FullName! },
|
||||
{ BaseItemKind.MusicGenre, typeof(MusicGenre).FullName! },
|
||||
{ BaseItemKind.MusicVideo, typeof(MusicVideo).FullName! },
|
||||
{ BaseItemKind.Person, typeof(Person).FullName! },
|
||||
{ BaseItemKind.Photo, typeof(Photo).FullName! },
|
||||
{ BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName! },
|
||||
{ BaseItemKind.Playlist, typeof(Playlist).FullName! },
|
||||
{ BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName! },
|
||||
{ BaseItemKind.Season, typeof(Season).FullName! },
|
||||
{ BaseItemKind.Series, typeof(Series).FullName! },
|
||||
{ BaseItemKind.Studio, typeof(Studio).FullName! },
|
||||
{ BaseItemKind.Trailer, typeof(Trailer).FullName! },
|
||||
{ BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName! },
|
||||
{ BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName! },
|
||||
{ BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName! },
|
||||
{ BaseItemKind.UserView, typeof(UserView).FullName! },
|
||||
{ BaseItemKind.Video, typeof(Video).FullName! },
|
||||
{ BaseItemKind.Year, typeof(Year).FullName! }
|
||||
}.ToFrozenDictionary();
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace Emby.Server.Implementations.Data;
|
||||
|
||||
public sealed class ManagedConnection : IDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim? _writeLock;
|
||||
|
||||
private SqliteConnection _db;
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
public ManagedConnection(SqliteConnection db, SemaphoreSlim? writeLock)
|
||||
{
|
||||
_db = db;
|
||||
_writeLock = writeLock;
|
||||
}
|
||||
|
||||
public SqliteTransaction BeginTransaction()
|
||||
=> _db.BeginTransaction();
|
||||
|
||||
public SqliteCommand CreateCommand()
|
||||
=> _db.CreateCommand();
|
||||
|
||||
public void Execute(string commandText)
|
||||
=> _db.Execute(commandText);
|
||||
|
||||
public SqliteCommand PrepareStatement(string sql)
|
||||
=> _db.PrepareStatement(sql);
|
||||
|
||||
public IEnumerable<SqliteDataReader> Query(string commandText)
|
||||
=> _db.Query(commandText);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_writeLock is null)
|
||||
{
|
||||
// Read connections are managed with an internal pool
|
||||
_db.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write lock is managed by BaseSqliteRepository
|
||||
// Don't dispose here
|
||||
_writeLock.Release();
|
||||
}
|
||||
|
||||
_db = null!;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,369 +0,0 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
public SqliteUserDataRepository(
|
||||
ILogger<SqliteUserDataRepository> logger,
|
||||
IServerConfigurationManager config,
|
||||
IUserManager userManager)
|
||||
: base(logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
|
||||
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the connection to the database.
|
||||
/// </summary>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
var userDatasTableExists = TableExists(connection, "UserDatas");
|
||||
var userDataTableExists = TableExists(connection, "userdata");
|
||||
|
||||
var users = userDatasTableExists ? null : _userManager.Users;
|
||||
using var transaction = connection.BeginTransaction();
|
||||
connection.Execute(string.Join(
|
||||
';',
|
||||
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
|
||||
"drop index if exists idx_userdata",
|
||||
"drop index if exists idx_userdata1",
|
||||
"drop index if exists idx_userdata2",
|
||||
"drop index if exists userdataindex1",
|
||||
"drop index if exists userdataindex",
|
||||
"drop index if exists userdataindex3",
|
||||
"drop index if exists userdataindex4",
|
||||
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
|
||||
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
|
||||
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
|
||||
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)",
|
||||
"create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)"));
|
||||
|
||||
if (!userDataTableExists)
|
||||
{
|
||||
transaction.Commit();
|
||||
return;
|
||||
}
|
||||
|
||||
var existingColumnNames = GetColumnNames(connection, "userdata");
|
||||
|
||||
AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
|
||||
AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
|
||||
AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
|
||||
|
||||
if (userDatasTableExists)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImportUserIds(connection, users);
|
||||
|
||||
connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
private void ImportUserIds(ManagedConnection db, IEnumerable<User> users)
|
||||
{
|
||||
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
|
||||
|
||||
using (var statement = db.PrepareStatement("update userdata set InternalUserId=@InternalUserId where UserId=@UserId"))
|
||||
{
|
||||
foreach (var user in users)
|
||||
{
|
||||
if (!userIdsWithUserData.Contains(user.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
statement.TryBind("@UserId", user.Id);
|
||||
statement.TryBind("@InternalUserId", user.InternalId);
|
||||
|
||||
statement.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Guid> GetAllUserIdsWithUserData(ManagedConnection db)
|
||||
{
|
||||
var list = new List<Guid>();
|
||||
|
||||
using (var statement = PrepareStatement(db, "select DISTINCT UserId from UserData where UserId not null"))
|
||||
{
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
try
|
||||
{
|
||||
list.Add(row.GetGuid(0));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error while getting user");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userData);
|
||||
|
||||
if (userId <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(userId));
|
||||
}
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
||||
|
||||
PersistUserData(userId, key, userData, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userData);
|
||||
|
||||
if (userId <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(userId));
|
||||
}
|
||||
|
||||
PersistAllUserData(userId, userData, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persists the user data.
|
||||
/// </summary>
|
||||
/// <param name="internalUserId">The user id.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="userData">The user data.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
using (var transaction = connection.BeginTransaction())
|
||||
{
|
||||
SaveUserData(connection, internalUserId, key, userData);
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData)
|
||||
{
|
||||
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
|
||||
{
|
||||
statement.TryBind("@userId", internalUserId);
|
||||
statement.TryBind("@key", key);
|
||||
|
||||
if (userData.Rating.HasValue)
|
||||
{
|
||||
statement.TryBind("@rating", userData.Rating.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
statement.TryBindNull("@rating");
|
||||
}
|
||||
|
||||
statement.TryBind("@played", userData.Played);
|
||||
statement.TryBind("@playCount", userData.PlayCount);
|
||||
statement.TryBind("@isFavorite", userData.IsFavorite);
|
||||
statement.TryBind("@playbackPositionTicks", userData.PlaybackPositionTicks);
|
||||
|
||||
if (userData.LastPlayedDate.HasValue)
|
||||
{
|
||||
statement.TryBind("@lastPlayedDate", userData.LastPlayedDate.Value.ToDateTimeParamValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
statement.TryBindNull("@lastPlayedDate");
|
||||
}
|
||||
|
||||
if (userData.AudioStreamIndex.HasValue)
|
||||
{
|
||||
statement.TryBind("@AudioStreamIndex", userData.AudioStreamIndex.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
statement.TryBindNull("@AudioStreamIndex");
|
||||
}
|
||||
|
||||
if (userData.SubtitleStreamIndex.HasValue)
|
||||
{
|
||||
statement.TryBind("@SubtitleStreamIndex", userData.SubtitleStreamIndex.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
statement.TryBindNull("@SubtitleStreamIndex");
|
||||
}
|
||||
|
||||
statement.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persist all user data for the specified user.
|
||||
/// </summary>
|
||||
private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
using (var transaction = connection.BeginTransaction())
|
||||
{
|
||||
foreach (var userItemData in userDataList)
|
||||
{
|
||||
SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user data.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>Task{UserItemData}.</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// userId
|
||||
/// or
|
||||
/// key.
|
||||
/// </exception>
|
||||
public UserItemData GetUserData(long userId, string key)
|
||||
{
|
||||
if (userId <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(userId));
|
||||
}
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
||||
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
||||
{
|
||||
statement.TryBind("@UserId", userId);
|
||||
statement.TryBind("@Key", key);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
return ReadRow(row);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public UserItemData GetUserData(long userId, List<string> keys)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(keys);
|
||||
|
||||
if (keys.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetUserData(userId, keys[0]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return all user-data associated with the given user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The internal user id.</param>
|
||||
/// <returns>The list of user item data.</returns>
|
||||
public List<UserItemData> GetAllUserData(long userId)
|
||||
{
|
||||
if (userId <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(userId));
|
||||
}
|
||||
|
||||
var list = new List<UserItemData>();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
|
||||
{
|
||||
statement.TryBind("@UserId", userId);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
list.Add(ReadRow(row));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a row from the specified reader into the provided userData object.
|
||||
/// </summary>
|
||||
/// <param name="reader">The list of result set values.</param>
|
||||
/// <returns>The user item data.</returns>
|
||||
private UserItemData ReadRow(SqliteDataReader reader)
|
||||
{
|
||||
var userData = new UserItemData
|
||||
{
|
||||
Key = reader.GetString(0)
|
||||
};
|
||||
|
||||
if (reader.TryGetDouble(2, out var rating))
|
||||
{
|
||||
userData.Rating = rating;
|
||||
}
|
||||
|
||||
userData.Played = reader.GetBoolean(3);
|
||||
userData.PlayCount = reader.GetInt32(4);
|
||||
userData.IsFavorite = reader.GetBoolean(5);
|
||||
userData.PlaybackPositionTicks = reader.GetInt64(6);
|
||||
|
||||
if (reader.TryReadDateTime(7, out var lastPlayedDate))
|
||||
{
|
||||
userData.LastPlayedDate = lastPlayedDate;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(8, out var audioStreamIndex))
|
||||
{
|
||||
userData.AudioStreamIndex = audioStreamIndex;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(9, out var subtitleStreamIndex))
|
||||
{
|
||||
userData.SubtitleStreamIndex = subtitleStreamIndex;
|
||||
}
|
||||
|
||||
return userData;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
namespace Emby.Server.Implementations.Data;
|
||||
|
||||
/// <summary>
|
||||
/// The disk synchronization mode, controls how aggressively SQLite will write data
|
||||
/// all the way out to physical storage.
|
||||
/// </summary>
|
||||
public enum SynchronousMode
|
||||
{
|
||||
/// <summary>
|
||||
/// SQLite continues without syncing as soon as it has handed data off to the operating system.
|
||||
/// </summary>
|
||||
Off = 0,
|
||||
|
||||
/// <summary>
|
||||
/// SQLite database engine will still sync at the most critical moments.
|
||||
/// </summary>
|
||||
Normal = 1,
|
||||
|
||||
/// <summary>
|
||||
/// SQLite database engine will use the xSync method of the VFS
|
||||
/// to ensure that all content is safely written to the disk surface prior to continuing.
|
||||
/// </summary>
|
||||
Full = 2,
|
||||
|
||||
/// <summary>
|
||||
/// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
|
||||
/// is synced after that journal is unlinked to commit a transaction in DELETE mode.
|
||||
/// </summary>
|
||||
Extra = 3
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
namespace Emby.Server.Implementations.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Storage mode used by temporary database files.
|
||||
/// </summary>
|
||||
public enum TempStoreMode
|
||||
{
|
||||
/// <summary>
|
||||
/// The compile-time C preprocessor macro SQLITE_TEMP_STORE
|
||||
/// is used to determine where temporary tables and indices are stored.
|
||||
/// </summary>
|
||||
Default = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Temporary tables and indices are stored in a file.
|
||||
/// </summary>
|
||||
File = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
|
||||
/// </summary>
|
||||
Memory = 2
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"Books": "liv"
|
||||
}
|
|
|
|
|
|
|
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue