using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.Data; using System.Globalization; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Persistence { public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository { private IDbConnection _connection; private readonly IApplicationPaths _appPaths; public SqliteUserDataRepository(ILogManager logManager, IApplicationPaths appPaths) : base(logManager) { _appPaths = appPaths; } /// /// Gets the name of the repository /// /// The name. public string Name { get { return "SQLite"; } } /// /// Opens the connection to the database /// /// Task. public async Task Initialize(IDbConnector dbConnector) { var dbFile = Path.Combine(_appPaths.DataPath, "userdata_v2.db"); _connection = await dbConnector.Connect(dbFile).ConfigureAwait(false); string[] queries = { "create table if not exists userdata (key nvarchar, userId GUID, rating float null, played bit, playCount int, isFavorite bit, playbackPositionTicks bigint, lastPlayedDate datetime null)", "create index if not exists idx_userdata on userdata(key)", "create unique index if not exists userdataindex on userdata (key, userId)", //pragmas "pragma temp_store = memory", "pragma shrink_memory" }; _connection.RunQueries(queries, Logger); _connection.AddColumn(Logger, "userdata", "AudioStreamIndex", "int"); _connection.AddColumn(Logger, "userdata", "SubtitleStreamIndex", "int"); } /// /// Saves the user data. /// /// The user id. /// The key. /// The user data. /// The cancellation token. /// Task. /// userData /// or /// cancellationToken /// or /// userId /// or /// userDataId public Task SaveUserData(Guid userId, string key, UserItemData userData, CancellationToken cancellationToken) { if (userData == null) { throw new ArgumentNullException("userData"); } if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException("key"); } return PersistUserData(userId, key, userData, cancellationToken); } public Task SaveAllUserData(Guid userId, IEnumerable userData, CancellationToken cancellationToken) { if (userData == null) { throw new ArgumentNullException("userData"); } if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } return PersistAllUserData(userId, userData, cancellationToken); } /// /// Persists the user data. /// /// The user id. /// The key. /// The user data. /// The cancellation token. /// Task. public async Task PersistUserData(Guid userId, string key, UserItemData userData, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; try { transaction = _connection.BeginTransaction(); using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"; cmd.Parameters.Add(cmd, "@key", DbType.String).Value = key; cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; cmd.Parameters.Add(cmd, "@rating", DbType.Double).Value = userData.Rating; cmd.Parameters.Add(cmd, "@played", DbType.Boolean).Value = userData.Played; cmd.Parameters.Add(cmd, "@playCount", DbType.Int32).Value = userData.PlayCount; cmd.Parameters.Add(cmd, "@isFavorite", DbType.Boolean).Value = userData.IsFavorite; cmd.Parameters.Add(cmd, "@playbackPositionTicks", DbType.Int64).Value = userData.PlaybackPositionTicks; cmd.Parameters.Add(cmd, "@lastPlayedDate", DbType.DateTime).Value = userData.LastPlayedDate; cmd.Parameters.Add(cmd, "@AudioStreamIndex", DbType.Int32).Value = userData.AudioStreamIndex; cmd.Parameters.Add(cmd, "@SubtitleStreamIndex", DbType.Int32).Value = userData.SubtitleStreamIndex; cmd.Transaction = transaction; cmd.ExecuteNonQuery(); } transaction.Commit(); } catch (OperationCanceledException) { if (transaction != null) { transaction.Rollback(); } throw; } catch (Exception e) { Logger.ErrorException("Failed to save user data:", e); if (transaction != null) { transaction.Rollback(); } throw; } finally { if (transaction != null) { transaction.Dispose(); } WriteLock.Release(); } } /// /// Persist all user data for the specified user /// /// /// /// /// private async Task PersistAllUserData(Guid userId, IEnumerable userData, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; try { transaction = _connection.BeginTransaction(); foreach (var userItemData in userData) { using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"; cmd.Parameters.Add(cmd, "@key", DbType.String).Value = userItemData.Key; cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; cmd.Parameters.Add(cmd, "@rating", DbType.Double).Value = userItemData.Rating; cmd.Parameters.Add(cmd, "@played", DbType.Boolean).Value = userItemData.Played; cmd.Parameters.Add(cmd, "@playCount", DbType.Int32).Value = userItemData.PlayCount; cmd.Parameters.Add(cmd, "@isFavorite", DbType.Boolean).Value = userItemData.IsFavorite; cmd.Parameters.Add(cmd, "@playbackPositionTicks", DbType.Int64).Value = userItemData.PlaybackPositionTicks; cmd.Parameters.Add(cmd, "@lastPlayedDate", DbType.DateTime).Value = userItemData.LastPlayedDate; cmd.Parameters.Add(cmd, "@AudioStreamIndex", DbType.Int32).Value = userItemData.AudioStreamIndex; cmd.Parameters.Add(cmd, "@SubtitleStreamIndex", DbType.Int32).Value = userItemData.SubtitleStreamIndex; cmd.Transaction = transaction; cmd.ExecuteNonQuery(); } cancellationToken.ThrowIfCancellationRequested(); } transaction.Commit(); } catch (OperationCanceledException) { if (transaction != null) { transaction.Rollback(); } throw; } catch (Exception e) { Logger.ErrorException("Failed to save user data:", e); if (transaction != null) { transaction.Rollback(); } throw; } finally { if (transaction != null) { transaction.Dispose(); } WriteLock.Release(); } } /// /// Gets the user data. /// /// The user id. /// The key. /// Task{UserItemData}. /// /// userId /// or /// key /// public UserItemData GetUserData(Guid userId, string key) { if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException("key"); } using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where key = @key and userId=@userId"; cmd.Parameters.Add(cmd, "@key", DbType.String).Value = key; cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) { if (reader.Read()) { return ReadRow(reader); } } return null; } } public UserItemData GetUserData(Guid userId, List keys) { if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } if (keys == null) { throw new ArgumentNullException("keys"); } using (var cmd = _connection.CreateCommand()) { var index = 0; var excludeIds = new List(); var builder = new StringBuilder(); foreach (var key in keys) { var paramName = "@Key" + index; excludeIds.Add("Key =" + paramName); cmd.Parameters.Add(cmd, paramName, DbType.String).Value = key; builder.Append(" WHEN Key=" + paramName + " THEN " + index); index++; } var keyText = string.Join(" OR ", excludeIds.ToArray()); cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where userId=@userId AND (" + keyText + ") "; cmd.CommandText += " ORDER BY (Case " + builder + " Else " + keys.Count.ToString(CultureInfo.InvariantCulture) + " End )"; cmd.CommandText += " LIMIT 1"; cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) { if (reader.Read()) { return ReadRow(reader); } } return null; } } /// /// Return all user-data associated with the given user /// /// /// public IEnumerable GetAllUserData(Guid userId) { if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where userId=@userId"; cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) { while (reader.Read()) { yield return ReadRow(reader); } } } } /// /// Read a row from the specified reader into the provided userData object /// /// private UserItemData ReadRow(IDataReader reader) { var userData = new UserItemData(); userData.Key = reader.GetString(0); userData.UserId = reader.GetGuid(1); if (!reader.IsDBNull(2)) { userData.Rating = reader.GetDouble(2); } userData.Played = reader.GetBoolean(3); userData.PlayCount = reader.GetInt32(4); userData.IsFavorite = reader.GetBoolean(5); userData.PlaybackPositionTicks = reader.GetInt64(6); if (!reader.IsDBNull(7)) { userData.LastPlayedDate = reader.GetDateTime(7).ToUniversalTime(); } if (!reader.IsDBNull(8)) { userData.AudioStreamIndex = reader.GetInt32(8); } if (!reader.IsDBNull(9)) { userData.SubtitleStreamIndex = reader.GetInt32(9); } return userData; } protected override void CloseConnection() { if (_connection != null) { if (_connection.IsOpen()) { _connection.Close(); } _connection.Dispose(); _connection = null; } } } }