Merge pull request #10138 from cvium/sqlite_client_poc

This commit is contained in:
Bond-009 2023-08-28 11:54:35 +02:00 committed by GitHub
commit c7ca416206
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 595 additions and 1120 deletions

View File

@ -27,6 +27,7 @@
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.10" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.10" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.10" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.10" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.10" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.10" />
@ -72,8 +73,6 @@
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.5" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.5" />
<PackageVersion Include="SkiaSharp" Version="2.88.5" /> <PackageVersion Include="SkiaSharp" Version="2.88.5" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.6" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />

View File

@ -5,8 +5,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data namespace Emby.Server.Implementations.Data
{ {
@ -45,24 +45,6 @@ namespace Emby.Server.Implementations.Data
/// <value>The logger.</value> /// <value>The logger.</value>
protected ILogger<BaseSqliteRepository> Logger { get; } protected ILogger<BaseSqliteRepository> Logger { get; }
/// <summary>
/// Gets the default connection flags.
/// </summary>
/// <value>The default connection flags.</value>
protected virtual ConnectionFlags DefaultConnectionFlags => ConnectionFlags.NoMutex;
/// <summary>
/// Gets the transaction mode.
/// </summary>
/// <value>The transaction mode.</value>>
protected TransactionMode TransactionMode => TransactionMode.Deferred;
/// <summary>
/// Gets the transaction mode for read-only operations.
/// </summary>
/// <value>The transaction mode.</value>
protected TransactionMode ReadTransactionMode => TransactionMode.Deferred;
/// <summary> /// <summary>
/// Gets the cache size. /// Gets the cache size.
/// </summary> /// </summary>
@ -107,23 +89,8 @@ namespace Emby.Server.Implementations.Data
/// <see cref="SynchronousMode"/> /// <see cref="SynchronousMode"/>
protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal; protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
/// <summary>
/// Gets or sets the write lock.
/// </summary>
/// <value>The write lock.</value>
protected ConnectionPool WriteConnections { get; set; }
/// <summary>
/// Gets or sets the write connection.
/// </summary>
/// <value>The write connection.</value>
protected ConnectionPool ReadConnections { get; set; }
public virtual void Initialize() public virtual void Initialize()
{ {
WriteConnections = new ConnectionPool(WriteConnectionsCount, CreateWriteConnection);
ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection);
// Configuration and pragmas can affect VACUUM so it needs to be last. // Configuration and pragmas can affect VACUUM so it needs to be last.
using (var connection = GetConnection()) using (var connection = GetConnection())
{ {
@ -131,57 +98,10 @@ namespace Emby.Server.Implementations.Data
} }
} }
protected ManagedConnection GetConnection(bool readOnly = false) protected SqliteConnection GetConnection()
=> readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection();
protected SQLiteDatabaseConnection CreateWriteConnection()
{ {
var writeConnection = SQLite3.Open( var connection = new SqliteConnection($"Filename={DbFilePath}");
DbFilePath, connection.Open();
DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
null);
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 writeConnection;
}
protected SQLiteDatabaseConnection CreateReadConnection()
{
var connection = SQLite3.Open(
DbFilePath,
DefaultConnectionFlags | ConnectionFlags.ReadOnly,
null);
if (CacheSize.HasValue) if (CacheSize.HasValue)
{ {
@ -208,39 +128,38 @@ namespace Emby.Server.Implementations.Data
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
} }
if (PageSize.HasValue)
{
connection.Execute("PRAGMA page_size=" + PageSize.Value);
}
connection.Execute("PRAGMA temp_store=" + (int)TempStore); connection.Execute("PRAGMA temp_store=" + (int)TempStore);
return connection; return connection;
} }
public IStatement PrepareStatement(ManagedConnection connection, string sql) public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
=> connection.PrepareStatement(sql);
public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
=> connection.PrepareStatement(sql);
protected bool TableExists(ManagedConnection connection, string name)
{ {
return connection.RunInTransaction( var command = connection.CreateCommand();
db => command.CommandText = sql;
{ return command;
using (var statement = PrepareStatement(db, "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;
},
ReadTransactionMode);
} }
protected List<string> GetColumnNames(IDatabaseConnection connection, string table) protected bool TableExists(SqliteConnection 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(SqliteConnection connection, string table)
{ {
var columnNames = new List<string>(); var columnNames = new List<string>();
@ -255,7 +174,7 @@ namespace Emby.Server.Implementations.Data
return columnNames; return columnNames;
} }
protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames) protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
{ {
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase)) if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
{ {
@ -291,12 +210,6 @@ namespace Emby.Server.Implementations.Data
return; return;
} }
if (dispose)
{
WriteConnections.Dispose();
ReadConnections.Dispose();
}
_disposed = true; _disposed = true;
} }
} }

View File

@ -1,79 +0,0 @@
using System;
using System.Collections.Concurrent;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data;
/// <summary>
/// A pool of SQLite Database connections.
/// </summary>
public sealed class ConnectionPool : IDisposable
{
private readonly BlockingCollection<SQLiteDatabaseConnection> _connections = new();
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionPool" /> class.
/// </summary>
/// <param name="count">The number of database connection to create.</param>
/// <param name="factory">Factory function to create the database connections.</param>
public ConnectionPool(int count, Func<SQLiteDatabaseConnection> factory)
{
for (int i = 0; i < count; i++)
{
_connections.Add(factory.Invoke());
}
}
/// <summary>
/// Gets a database connection from the pool if one is available, otherwise blocks.
/// </summary>
/// <returns>A database connection.</returns>
public ManagedConnection GetConnection()
{
if (_disposed)
{
ThrowObjectDisposedException();
}
return new ManagedConnection(_connections.Take(), this);
static void ThrowObjectDisposedException()
{
throw new ObjectDisposedException(nameof(ConnectionPool));
}
}
/// <summary>
/// Return a database connection to the pool.
/// </summary>
/// <param name="connection">The database connection to return.</param>
public void Return(SQLiteDatabaseConnection connection)
{
if (_disposed)
{
connection.Dispose();
return;
}
_connections.Add(connection);
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
foreach (var connection in _connections)
{
connection.Dispose();
}
_connections.Dispose();
_disposed = true;
}
}

View File

@ -1,81 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
public sealed class ManagedConnection : IDisposable
{
private readonly ConnectionPool _pool;
private SQLiteDatabaseConnection _db;
private bool _disposed = false;
public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool)
{
_db = db;
_pool = pool;
}
public IStatement PrepareStatement(string sql)
{
return _db.PrepareStatement(sql);
}
public IEnumerable<IStatement> PrepareAll(string sql)
{
return _db.PrepareAll(sql);
}
public void ExecuteAll(string sql)
{
_db.ExecuteAll(sql);
}
public void Execute(string sql, params object[] values)
{
_db.Execute(sql, values);
}
public void RunQueries(string[] sql)
{
_db.RunQueries(sql);
}
public void RunInTransaction(Action<IDatabaseConnection> action, TransactionMode mode)
{
_db.RunInTransaction(action, mode);
}
public T RunInTransaction<T>(Func<IDatabaseConnection, T> action, TransactionMode mode)
{
return _db.RunInTransaction(action, mode);
}
public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql)
{
return _db.Query(sql);
}
public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql, params object[] values)
{
return _db.Query(sql, values);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_pool.Return(_db);
_db = null!; // Don't dispose it
_disposed = true;
}
}
}

View File

@ -1,11 +1,10 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Data;
using System.Globalization; using System.Globalization;
using SQLitePCL.pretty; using Microsoft.Data.Sqlite;
namespace Emby.Server.Implementations.Data namespace Emby.Server.Implementations.Data
{ {
@ -52,19 +51,29 @@ namespace Emby.Server.Implementations.Data
"yy-MM-dd" "yy-MM-dd"
}; };
public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries) public static IEnumerable<SqliteDataReader> Query(this SqliteConnection sqliteConnection, string commandText)
{ {
ArgumentNullException.ThrowIfNull(queries); if (sqliteConnection.State != ConnectionState.Open)
connection.RunInTransaction(conn =>
{ {
conn.ExecuteAll(string.Join(';', queries)); sqliteConnection.Open();
}); }
using var command = sqliteConnection.CreateCommand();
command.CommandText = commandText;
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
yield return reader;
}
}
} }
public static Guid ReadGuidFromBlob(this ResultSetValue result) public static void Execute(this SqliteConnection sqliteConnection, string commandText)
{ {
return new Guid(result.ToBlob()); using var command = sqliteConnection.CreateCommand();
command.CommandText = commandText;
command.ExecuteNonQuery();
} }
public static string ToDateTimeParamValue(this DateTime dateValue) public static string ToDateTimeParamValue(this DateTime dateValue)
@ -83,27 +92,15 @@ namespace Emby.Server.Implementations.Data
private static string GetDateTimeKindFormat(DateTimeKind kind) private static string GetDateTimeKindFormat(DateTimeKind kind)
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal; => (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
public static DateTime ReadDateTime(this ResultSetValue result) public static bool TryReadDateTime(this SqliteDataReader reader, int index, out DateTime result)
{ {
var dateText = result.ToString(); if (reader.IsDBNull(index))
return DateTime.ParseExact(
dateText,
_datetimeFormats,
DateTimeFormatInfo.InvariantInfo,
DateTimeStyles.AdjustToUniversal);
}
public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
{
var item = reader[index];
if (item.IsDbNull())
{ {
result = default; result = default;
return false; return false;
} }
var dateText = item.ToString(); var dateText = reader.GetString(index);
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult)) if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
{ {
@ -115,335 +112,145 @@ namespace Emby.Server.Implementations.Data
return false; return false;
} }
public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result) public static bool TryGetGuid(this SqliteDataReader reader, int index, out Guid result)
{ {
var item = reader[index]; if (reader.IsDBNull(index))
if (item.IsDbNull())
{ {
result = default; result = default;
return false; return false;
} }
result = item.ReadGuidFromBlob(); result = reader.GetGuid(index);
return true; return true;
} }
public static bool IsDbNull(this ResultSetValue result) public static bool TryGetString(this SqliteDataReader reader, int index, out string result)
{ {
return result.SQLiteType == SQLiteType.Null; result = string.Empty;
}
public static string GetString(this IReadOnlyList<ResultSetValue> result, int index) if (reader.IsDBNull(index))
{
return result[index].ToString();
}
public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result)
{
result = null;
var item = reader[index];
if (item.IsDbNull())
{ {
return false; return false;
} }
result = item.ToString(); result = reader.GetString(index);
return true; return true;
} }
public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index) public static bool TryGetBoolean(this SqliteDataReader reader, int index, out bool result)
{ {
return result[index].ToBool(); if (reader.IsDBNull(index))
}
public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result)
{
var item = reader[index];
if (item.IsDbNull())
{ {
result = default; result = default;
return false; return false;
} }
result = item.ToBool(); result = reader.GetBoolean(index);
return true; return true;
} }
public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result) public static bool TryGetInt32(this SqliteDataReader reader, int index, out int result)
{ {
var item = reader[index]; if (reader.IsDBNull(index))
if (item.IsDbNull())
{ {
result = default; result = default;
return false; return false;
} }
result = item.ToInt(); result = reader.GetInt32(index);
return true; return true;
} }
public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index) public static bool TryGetInt64(this SqliteDataReader reader, int index, out long result)
{ {
return result[index].ToInt64(); if (reader.IsDBNull(index))
}
public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result)
{
var item = reader[index];
if (item.IsDbNull())
{ {
result = default; result = default;
return false; return false;
} }
result = item.ToInt64(); result = reader.GetInt64(index);
return true; return true;
} }
public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result) public static bool TryGetSingle(this SqliteDataReader reader, int index, out float result)
{ {
var item = reader[index]; if (reader.IsDBNull(index))
if (item.IsDbNull())
{ {
result = default; result = default;
return false; return false;
} }
result = item.ToFloat(); result = reader.GetFloat(index);
return true; return true;
} }
public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result) public static bool TryGetDouble(this SqliteDataReader reader, int index, out double result)
{ {
var item = reader[index]; if (reader.IsDBNull(index))
if (item.IsDbNull())
{ {
result = default; result = default;
return false; return false;
} }
result = item.ToDouble(); result = reader.GetDouble(index);
return true; return true;
} }
public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index) public static void TryBind(this SqliteCommand statement, string name, Guid value)
{ {
return result[index].ReadGuidFromBlob(); statement.TryBind(name, value, true);
} }
[Conditional("DEBUG")] public static void TryBind(this SqliteCommand statement, string name, object? value, bool isBlob = false)
private static void CheckName(string name)
{ {
throw new ArgumentException("Invalid param name: " + name, nameof(name)); var preparedValue = value ?? DBNull.Value;
} if (statement.Parameters.Contains(name))
public static void TryBind(this IStatement statement, string name, double value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{ {
bindParam.Bind(value); statement.Parameters[name].Value = preparedValue;
} }
else else
{ {
CheckName(name); // Blobs aren't always detected automatically
} if (isBlob)
}
public static void TryBind(this IStatement statement, string name, string value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
if (value is null)
{ {
bindParam.BindNull(); statement.Parameters.Add(new SqliteParameter(name, SqliteType.Blob) { Value = value });
} }
else else
{ {
bindParam.Bind(value); statement.Parameters.AddWithValue(name, preparedValue);
} }
} }
else }
public static void TryBindNull(this SqliteCommand statement, string name)
{
statement.TryBind(name, DBNull.Value);
}
public static IEnumerable<SqliteDataReader> ExecuteQuery(this SqliteCommand command)
{
using (var reader = command.ExecuteReader())
{ {
CheckName(name); while (reader.Read())
{
yield return reader;
}
} }
} }
public static void TryBind(this IStatement statement, string name, bool value) public static int SelectScalarInt(this SqliteCommand command)
{ {
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) var result = command.ExecuteScalar();
{ // Can't be null since the method is used to retrieve Count
bindParam.Bind(value); return Convert.ToInt32(result!, CultureInfo.InvariantCulture);
}
else
{
CheckName(name);
}
} }
public static void TryBind(this IStatement statement, string name, float value) public static SqliteCommand PrepareStatement(this SqliteConnection sqliteConnection, string sql)
{ {
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) var command = sqliteConnection.CreateCommand();
{ command.CommandText = sql;
bindParam.Bind(value); return command;
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, int value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value);
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, Guid value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
Span<byte> byteValue = stackalloc byte[16];
value.TryWriteBytes(byteValue);
bindParam.Bind(byteValue);
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, DateTime value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value.ToDateTimeParamValue());
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, long value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value);
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, ReadOnlySpan<byte> value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value);
}
else
{
CheckName(name);
}
}
public static void TryBindNull(this IStatement statement, string name)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.BindNull();
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, DateTime? value)
{
if (value.HasValue)
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
}
public static void TryBind(this IStatement statement, string name, Guid? value)
{
if (value.HasValue)
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
}
public static void TryBind(this IStatement statement, string name, double? value)
{
if (value.HasValue)
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
}
public static void TryBind(this IStatement statement, string name, int? value)
{
if (value.HasValue)
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
}
public static void TryBind(this IStatement statement, string name, float? value)
{
if (value.HasValue)
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
}
public static void TryBind(this IStatement statement, string name, bool? value)
{
if (value.HasValue)
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
}
public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement)
{
while (statement.MoveNext())
{
yield return statement.Current;
}
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,8 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data namespace Emby.Server.Implementations.Data
{ {
@ -44,48 +44,48 @@ namespace Emby.Server.Implementations.Data
var userDataTableExists = TableExists(connection, "userdata"); var userDataTableExists = TableExists(connection, "userdata");
var users = userDatasTableExists ? null : _userManager.Users; 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)"));
connection.RunInTransaction( if (!userDataTableExists)
db => {
{ transaction.Commit();
db.ExecuteAll(string.Join(';', new[] return;
{ }
"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", var existingColumnNames = GetColumnNames(connection, "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)"
}));
if (userDataTableExists) AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
{ AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
var existingColumnNames = GetColumnNames(db, "userdata"); AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames); if (userDatasTableExists)
AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames); {
AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames); return;
}
if (!userDatasTableExists) ImportUserIds(connection, users);
{
ImportUserIds(db, users);
db.ExecuteAll("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"); 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();
},
TransactionMode);
} }
} }
private void ImportUserIds(IDatabaseConnection db, IEnumerable<User> users) private void ImportUserIds(SqliteConnection db, IEnumerable<User> users)
{ {
var userIdsWithUserData = GetAllUserIdsWithUserData(db); var userIdsWithUserData = GetAllUserIdsWithUserData(db);
@ -101,13 +101,12 @@ namespace Emby.Server.Implementations.Data
statement.TryBind("@UserId", user.Id); statement.TryBind("@UserId", user.Id);
statement.TryBind("@InternalUserId", user.InternalId); statement.TryBind("@InternalUserId", user.InternalId);
statement.MoveNext(); statement.ExecuteNonQuery();
statement.Reset();
} }
} }
} }
private List<Guid> GetAllUserIdsWithUserData(IDatabaseConnection db) private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db)
{ {
var list = new List<Guid>(); var list = new List<Guid>();
@ -117,7 +116,7 @@ namespace Emby.Server.Implementations.Data
{ {
try try
{ {
list.Add(row[0].ReadGuidFromBlob()); list.Add(row.GetGuid(0));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -169,17 +168,14 @@ namespace Emby.Server.Implementations.Data
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection()) using (var connection = GetConnection())
using (var transaction = connection.BeginTransaction())
{ {
connection.RunInTransaction( SaveUserData(connection, internalUserId, key, userData);
db => transaction.Commit();
{
SaveUserData(db, internalUserId, key, userData);
},
TransactionMode);
} }
} }
private static void SaveUserData(IDatabaseConnection db, long internalUserId, string key, UserItemData userData) private static void SaveUserData(SqliteConnection 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)")) 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)"))
{ {
@ -227,7 +223,7 @@ namespace Emby.Server.Implementations.Data
statement.TryBindNull("@SubtitleStreamIndex"); statement.TryBindNull("@SubtitleStreamIndex");
} }
statement.MoveNext(); statement.ExecuteNonQuery();
} }
} }
@ -239,16 +235,14 @@ namespace Emby.Server.Implementations.Data
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection()) using (var connection = GetConnection())
using (var transaction = connection.BeginTransaction())
{ {
connection.RunInTransaction( foreach (var userItemData in userDataList)
db => {
{ SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
foreach (var userItemData in userDataList) }
{
SaveUserData(db, internalUserId, userItemData.Key, userItemData); transaction.Commit();
}
},
TransactionMode);
} }
} }
@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.Data
ArgumentException.ThrowIfNullOrEmpty(key); ArgumentException.ThrowIfNullOrEmpty(key);
using (var connection = GetConnection(true)) using (var connection = GetConnection())
{ {
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId")) using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{ {
@ -336,7 +330,7 @@ namespace Emby.Server.Implementations.Data
/// </summary> /// </summary>
/// <param name="reader">The list of result set values.</param> /// <param name="reader">The list of result set values.</param>
/// <returns>The user item data.</returns> /// <returns>The user item data.</returns>
private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader) private UserItemData ReadRow(SqliteDataReader reader)
{ {
var userData = new UserItemData(); var userData = new UserItemData();
@ -348,10 +342,10 @@ namespace Emby.Server.Implementations.Data
userData.Rating = rating; userData.Rating = rating;
} }
userData.Played = reader[3].ToBool(); userData.Played = reader.GetBoolean(3);
userData.PlayCount = reader[4].ToInt(); userData.PlayCount = reader.GetInt32(4);
userData.IsFavorite = reader[5].ToBool(); userData.IsFavorite = reader.GetBoolean(5);
userData.PlaybackPositionTicks = reader[6].ToInt64(); userData.PlaybackPositionTicks = reader.GetInt64(6);
if (reader.TryReadDateTime(7, out var lastPlayedDate)) if (reader.TryReadDateTime(7, out var lastPlayedDate))
{ {

View File

@ -24,6 +24,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="DiscUtils.Udf" /> <PackageReference Include="DiscUtils.Udf" />
<PackageReference Include="Jellyfin.XmlTv" /> <PackageReference Include="Jellyfin.XmlTv" />
<PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
@ -31,7 +32,6 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="Mono.Nat" /> <PackageReference Include="Mono.Nat" />
<PackageReference Include="prometheus-net.DotNetRuntime" /> <PackageReference Include="prometheus-net.DotNetRuntime" />
<PackageReference Include="SQLitePCL.pretty.netstandard" />
<PackageReference Include="DotNet.Glob" /> <PackageReference Include="DotNet.Glob" />
</ItemGroup> </ItemGroup>

View File

@ -15,7 +15,6 @@ using MediaBrowser.Model.IO;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Serilog; using Serilog;
using SQLitePCL;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Jellyfin.Server.Helpers; namespace Jellyfin.Server.Helpers;
@ -297,7 +296,5 @@ public static class StartupHelpers
// Disable the "Expect: 100-Continue" header by default // Disable the "Expect: 100-Continue" header by default
// http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
ServicePointManager.Expect100Continue = false; ServicePointManager.Expect100Continue = false;
Batteries_V2.Init();
} }
} }

View File

@ -48,7 +48,6 @@
<PackageReference Include="Serilog.Sinks.Console" /> <PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.File" /> <PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="Serilog.Sinks.Graylog" /> <PackageReference Include="Serilog.Sinks.Graylog" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -5,9 +5,9 @@ using Emby.Server.Implementations.Data;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Jellyfin.Server.Migrations.Routines namespace Jellyfin.Server.Migrations.Routines
{ {
@ -61,17 +61,12 @@ namespace Jellyfin.Server.Migrations.Routines
}; };
var dataPath = _paths.DataPath; var dataPath = _paths.DataPath;
using (var connection = SQLite3.Open( using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
Path.Combine(dataPath, DbFilename),
ConnectionFlags.ReadOnly,
null))
{ {
using var userDbConnection = SQLite3.Open(Path.Combine(dataPath, "users.db"), ConnectionFlags.ReadOnly, null); using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}");
_logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin."); _logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin.");
using var dbContext = _provider.CreateDbContext(); using var dbContext = _provider.CreateDbContext();
var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id");
// Make sure that the database is empty in case of failed migration due to power outages, etc. // Make sure that the database is empty in case of failed migration due to power outages, etc.
dbContext.ActivityLogs.RemoveRange(dbContext.ActivityLogs); dbContext.ActivityLogs.RemoveRange(dbContext.ActivityLogs);
dbContext.SaveChanges(); dbContext.SaveChanges();
@ -81,51 +76,52 @@ namespace Jellyfin.Server.Migrations.Routines
var newEntries = new List<ActivityLog>(); var newEntries = new List<ActivityLog>();
var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id");
foreach (var entry in queryResult) foreach (var entry in queryResult)
{ {
if (!logLevelDictionary.TryGetValue(entry[8].ToString(), out var severity)) if (!logLevelDictionary.TryGetValue(entry.GetString(8), out var severity))
{ {
severity = LogLevel.Trace; severity = LogLevel.Trace;
} }
var guid = Guid.Empty; var guid = Guid.Empty;
if (entry[6].SQLiteType != SQLiteType.Null && !Guid.TryParse(entry[6].ToString(), out guid)) if (!entry.IsDBNull(6) && !entry.TryGetGuid(6, out guid))
{ {
var id = entry.GetString(6);
// This is not a valid Guid, see if it is an internal ID from an old Emby schema // This is not a valid Guid, see if it is an internal ID from an old Emby schema
_logger.LogWarning("Invalid Guid in UserId column: {Guid}", entry[6].ToString()); _logger.LogWarning("Invalid Guid in UserId column: {Guid}", id);
using var statement = userDbConnection.PrepareStatement("SELECT guid FROM LocalUsersv2 WHERE Id=@Id"); using var statement = userDbConnection.PrepareStatement("SELECT guid FROM LocalUsersv2 WHERE Id=@Id");
statement.TryBind("@Id", entry[6].ToString()); statement.TryBind("@Id", id);
foreach (var row in statement.Query()) using var reader = statement.ExecuteReader();
if (reader.HasRows && reader.Read() && reader.TryGetGuid(0, out guid))
{ {
if (row.Count > 0 && Guid.TryParse(row[0].ToString(), out guid)) // Successfully parsed a Guid from the user table.
{ break;
// Successfully parsed a Guid from the user table.
break;
}
} }
} }
var newEntry = new ActivityLog(entry[1].ToString(), entry[4].ToString(), guid) var newEntry = new ActivityLog(entry.GetString(1), entry.GetString(4), guid)
{ {
DateCreated = entry[7].ReadDateTime(), DateCreated = entry.GetDateTime(7),
LogSeverity = severity LogSeverity = severity
}; };
if (entry[2].SQLiteType != SQLiteType.Null) if (entry.TryGetString(2, out var result))
{ {
newEntry.Overview = entry[2].ToString(); newEntry.Overview = result;
} }
if (entry[3].SQLiteType != SQLiteType.Null) if (entry.TryGetString(3, out result))
{ {
newEntry.ShortOverview = entry[3].ToString(); newEntry.ShortOverview = result;
} }
if (entry[5].SQLiteType != SQLiteType.Null) if (entry.TryGetString(5, out result))
{ {
newEntry.ItemId = entry[5].ToString(); newEntry.ItemId = result;
} }
newEntries.Add(newEntry); newEntries.Add(newEntry);

View File

@ -6,9 +6,9 @@ using Jellyfin.Data.Entities.Security;
using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Jellyfin.Server.Migrations.Routines namespace Jellyfin.Server.Migrations.Routines
{ {
@ -56,10 +56,7 @@ namespace Jellyfin.Server.Migrations.Routines
public void Perform() public void Perform()
{ {
var dataPath = _appPaths.DataPath; var dataPath = _appPaths.DataPath;
using (var connection = SQLite3.Open( using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
Path.Combine(dataPath, DbFilename),
ConnectionFlags.ReadOnly,
null))
{ {
using var dbContext = _dbProvider.CreateDbContext(); using var dbContext = _dbProvider.CreateDbContext();
@ -67,23 +64,23 @@ namespace Jellyfin.Server.Migrations.Routines
foreach (var row in authenticatedDevices) foreach (var row in authenticatedDevices)
{ {
var dateCreatedStr = row[9].ToString(); var dateCreatedStr = row.GetString(9);
_ = DateTime.TryParse(dateCreatedStr, out var dateCreated); _ = DateTime.TryParse(dateCreatedStr, out var dateCreated);
var dateLastActivityStr = row[10].ToString(); var dateLastActivityStr = row.GetString(10);
_ = DateTime.TryParse(dateLastActivityStr, out var dateLastActivity); _ = DateTime.TryParse(dateLastActivityStr, out var dateLastActivity);
if (row[6].IsDbNull()) if (row.IsDBNull(6))
{ {
dbContext.ApiKeys.Add(new ApiKey(row[3].ToString()) dbContext.ApiKeys.Add(new ApiKey(row.GetString(3))
{ {
AccessToken = row[1].ToString(), AccessToken = row.GetString(1),
DateCreated = dateCreated, DateCreated = dateCreated,
DateLastActivity = dateLastActivity DateLastActivity = dateLastActivity
}); });
} }
else else
{ {
var userId = new Guid(row[6].ToString()); var userId = row.GetGuid(6);
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
if (user is null) if (user is null)
{ {
@ -92,14 +89,14 @@ namespace Jellyfin.Server.Migrations.Routines
} }
dbContext.Devices.Add(new Device( dbContext.Devices.Add(new Device(
new Guid(row[6].ToString()), userId,
row[3].ToString(), row.GetString(3),
row[4].ToString(), row.GetString(4),
row[5].ToString(), row.GetString(5),
row[2].ToString()) row.GetString(2))
{ {
AccessToken = row[1].ToString(), AccessToken = row.GetString(1),
IsActive = row[8].ToBool(), IsActive = row.GetBoolean(8),
DateCreated = dateCreated, DateCreated = dateCreated,
DateLastActivity = dateLastActivity DateLastActivity = dateLastActivity
}); });
@ -110,12 +107,12 @@ namespace Jellyfin.Server.Migrations.Routines
var deviceIds = new HashSet<string>(); var deviceIds = new HashSet<string>();
foreach (var row in deviceOptions) foreach (var row in deviceOptions)
{ {
if (row[2].IsDbNull()) if (row.IsDBNull(2))
{ {
continue; continue;
} }
var deviceId = row[2].ToString(); var deviceId = row.GetString(2);
if (deviceIds.Contains(deviceId)) if (deviceIds.Contains(deviceId))
{ {
continue; continue;
@ -125,7 +122,7 @@ namespace Jellyfin.Server.Migrations.Routines
dbContext.DeviceOptions.Add(new DeviceOptions(deviceId) dbContext.DeviceOptions.Add(new DeviceOptions(deviceId)
{ {
CustomName = row[1].IsDbNull() ? null : row[1].ToString() CustomName = row.IsDBNull(1) ? null : row.GetString(1)
}); });
} }

View File

@ -4,15 +4,16 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Emby.Server.Implementations.Data;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Jellyfin.Server.Migrations.Routines namespace Jellyfin.Server.Migrations.Routines
{ {
@ -83,22 +84,22 @@ namespace Jellyfin.Server.Migrations.Routines
var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename); var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null)) using (var connection = new SqliteConnection($"Filename={dbFilePath}"))
{ {
using var dbContext = _provider.CreateDbContext(); using var dbContext = _provider.CreateDbContext();
var results = connection.Query("SELECT * FROM userdisplaypreferences"); var results = connection.Query("SELECT * FROM userdisplaypreferences");
foreach (var result in results) foreach (var result in results)
{ {
var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result[3].ToBlob(), _jsonOptions); var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result.GetStream(3), _jsonOptions);
if (dto is null) if (dto is null)
{ {
continue; continue;
} }
var itemId = new Guid(result[1].ToBlob()); var itemId = result.GetGuid(1);
var dtoUserId = new Guid(result[1].ToBlob()); var dtoUserId = itemId;
var client = result[2].ToString(); var client = result.GetString(2);
var displayPreferencesKey = $"{dtoUserId}|{itemId}|{client}"; var displayPreferencesKey = $"{dtoUserId}|{itemId}|{client}";
if (displayPrefs.Contains(displayPreferencesKey)) if (displayPrefs.Contains(displayPreferencesKey))
{ {

View File

@ -1,13 +1,12 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using Emby.Server.Implementations.Data; using Emby.Server.Implementations.Data;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Jellyfin.Server.Migrations.Routines namespace Jellyfin.Server.Migrations.Routines
{ {
@ -20,17 +19,14 @@ namespace Jellyfin.Server.Migrations.Routines
private readonly ILogger<MigrateRatingLevels> _logger; private readonly ILogger<MigrateRatingLevels> _logger;
private readonly IServerApplicationPaths _applicationPaths; private readonly IServerApplicationPaths _applicationPaths;
private readonly ILocalizationManager _localizationManager; private readonly ILocalizationManager _localizationManager;
private readonly IItemRepository _repository;
public MigrateRatingLevels( public MigrateRatingLevels(
IServerApplicationPaths applicationPaths, IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
ILocalizationManager localizationManager, ILocalizationManager localizationManager)
IItemRepository repository)
{ {
_applicationPaths = applicationPaths; _applicationPaths = applicationPaths;
_localizationManager = localizationManager; _localizationManager = localizationManager;
_repository = repository;
_logger = loggerFactory.CreateLogger<MigrateRatingLevels>(); _logger = loggerFactory.CreateLogger<MigrateRatingLevels>();
} }
@ -70,15 +66,13 @@ namespace Jellyfin.Server.Migrations.Routines
// Migrate parental rating strings to new levels // Migrate parental rating strings to new levels
_logger.LogInformation("Recalculating parental rating levels based on rating string."); _logger.LogInformation("Recalculating parental rating levels based on rating string.");
using (var connection = SQLite3.Open( using (var connection = new SqliteConnection($"Filename={dbPath}"))
dbPath, using (var transaction = connection.BeginTransaction())
ConnectionFlags.ReadWrite,
null))
{ {
var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems"); var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems");
foreach (var entry in queryResult) foreach (var entry in queryResult)
{ {
var ratingString = entry[0].ToString(); var ratingString = entry.GetString(0);
if (string.IsNullOrEmpty(ratingString)) if (string.IsNullOrEmpty(ratingString))
{ {
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';"); connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';");
@ -91,12 +85,14 @@ namespace Jellyfin.Server.Migrations.Routines
ratingValue = "NULL"; ratingValue = "NULL";
} }
var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;"); using var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;");
statement.TryBind("@Value", ratingValue); statement.TryBind("@Value", ratingValue);
statement.TryBind("@Rating", ratingString); statement.TryBind("@Rating", ratingString);
statement.ExecuteQuery(); statement.ExecuteNonQuery();
} }
} }
transaction.Commit();
} }
} }
} }

View File

@ -11,9 +11,9 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users; using MediaBrowser.Model.Users;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
using JsonSerializer = System.Text.Json.JsonSerializer; using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Jellyfin.Server.Migrations.Routines namespace Jellyfin.Server.Migrations.Routines
@ -64,7 +64,7 @@ namespace Jellyfin.Server.Migrations.Routines
var dataPath = _paths.DataPath; var dataPath = _paths.DataPath;
_logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin."); _logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
using (var connection = SQLite3.Open(Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null)) using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
{ {
var dbContext = _provider.CreateDbContext(); var dbContext = _provider.CreateDbContext();
@ -75,7 +75,7 @@ namespace Jellyfin.Server.Migrations.Routines
foreach (var entry in queryResult) foreach (var entry in queryResult)
{ {
UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.Options); UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry.GetStream(2), JsonDefaults.Options);
if (mockup is null) if (mockup is null)
{ {
continue; continue;
@ -108,8 +108,8 @@ namespace Jellyfin.Server.Migrations.Routines
var user = new User(mockup.Name, policy.AuthenticationProviderId!, policy.PasswordResetProviderId!) var user = new User(mockup.Name, policy.AuthenticationProviderId!, policy.PasswordResetProviderId!)
{ {
Id = entry[1].ReadGuidFromBlob(), Id = entry.GetGuid(1),
InternalId = entry[0].ToInt64(), InternalId = entry.GetInt64(0),
MaxParentalAgeRating = policy.MaxParentalRating, MaxParentalAgeRating = policy.MaxParentalRating,
EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess, EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit, RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,

View File

@ -1,10 +1,11 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using Emby.Server.Implementations.Data;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Jellyfin.Server.Migrations.Routines namespace Jellyfin.Server.Migrations.Routines
{ {
@ -37,14 +38,12 @@ namespace Jellyfin.Server.Migrations.Routines
{ {
var dataPath = _paths.DataPath; var dataPath = _paths.DataPath;
var dbPath = Path.Combine(dataPath, DbFilename); var dbPath = Path.Combine(dataPath, DbFilename);
using (var connection = SQLite3.Open( using (var connection = new SqliteConnection($"Filename={dbPath}"))
dbPath, using (var transaction = connection.BeginTransaction())
ConnectionFlags.ReadWrite,
null))
{ {
// Query the database for the ids of duplicate extras // Query the database for the ids of duplicate extras
var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'"); var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'");
var bads = string.Join(", ", queryResult.SelectScalarString()); var bads = string.Join(", ", queryResult.Select(x => x.GetString(0)));
// Do nothing if no duplicate extras were detected // Do nothing if no duplicate extras were detected
if (bads.Length == 0) if (bads.Length == 0)
@ -76,6 +75,7 @@ namespace Jellyfin.Server.Migrations.Routines
// Delete all duplicate extras // Delete all duplicate extras
_logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads); _logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads);
connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')"); connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')");
transaction.Commit();
} }
} }
} }

View File

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@ -213,11 +214,10 @@ namespace MediaBrowser.Providers.TV
{ {
// Null season numbers will have a 'dummy' season created because seasons are always required. // Null season numbers will have a 'dummy' season created because seasons are always required.
var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber); var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
string? seasonName = null;
if (seasonNumber.HasValue && seasonNames.TryGetValue(seasonNumber.Value, out var tmp)) if (!seasonNumber.HasValue || !seasonNames.TryGetValue(seasonNumber.Value, out var seasonName))
{ {
seasonName = tmp; seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
} }
if (existingSeason is null) if (existingSeason is null)
@ -225,9 +225,9 @@ namespace MediaBrowser.Providers.TV
var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false); var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
series.AddChild(season); series.AddChild(season);
} }
else else if (!string.Equals(existingSeason.Name, seasonName, StringComparison.Ordinal))
{ {
existingSeason.Name = GetValidSeasonNameForSeries(series, seasonName, seasonNumber); existingSeason.Name = seasonName;
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
} }
} }
@ -247,7 +247,6 @@ namespace MediaBrowser.Providers.TV
int? seasonNumber, int? seasonNumber,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
seasonName = GetValidSeasonNameForSeries(series, seasonName, seasonNumber);
Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name); Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name);
var season = new Season var season = new Season