using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using System; using System.Collections.Generic; using System.Data; using System.Data.SQLite; using System.IO; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Persistence { public class SqliteChapterRepository { private SQLiteConnection _connection; private readonly ILogger _logger; /// /// The _app paths /// private readonly IApplicationPaths _appPaths; private SQLiteCommand _deleteChaptersCommand; private SQLiteCommand _saveChapterCommand; /// /// Initializes a new instance of the class. /// /// The app paths. /// The log manager. /// appPaths /// or /// jsonSerializer public SqliteChapterRepository(IApplicationPaths appPaths, ILogManager logManager) { if (appPaths == null) { throw new ArgumentNullException("appPaths"); } _appPaths = appPaths; _logger = logManager.GetLogger(GetType().Name); } /// /// Opens the connection to the database /// /// Task. public async Task Initialize() { var dbFile = Path.Combine(_appPaths.DataPath, "chapters.db"); _connection = await SqliteExtensions.ConnectToDb(dbFile).ConfigureAwait(false); string[] queries = { "create table if not exists chapters (ItemId GUID, ChapterIndex INT, StartPositionTicks BIGINT, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", "create index if not exists idx_chapters on chapters(ItemId, ChapterIndex)", //pragmas "pragma temp_store = memory" }; _connection.RunQueries(queries, _logger); PrepareStatements(); } /// /// The _write lock /// private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); /// /// Prepares the statements. /// private void PrepareStatements() { _deleteChaptersCommand = new SQLiteCommand { CommandText = "delete from chapters where ItemId=@ItemId" }; _deleteChaptersCommand.Parameters.Add(new SQLiteParameter("@ItemId")); _saveChapterCommand = new SQLiteCommand { CommandText = "replace into chapters (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath) values (@ItemId, @ChapterIndex, @StartPositionTicks, @Name, @ImagePath)" }; _saveChapterCommand.Parameters.Add(new SQLiteParameter("@ItemId")); _saveChapterCommand.Parameters.Add(new SQLiteParameter("@ChapterIndex")); _saveChapterCommand.Parameters.Add(new SQLiteParameter("@StartPositionTicks")); _saveChapterCommand.Parameters.Add(new SQLiteParameter("@Name")); _saveChapterCommand.Parameters.Add(new SQLiteParameter("@ImagePath")); } /// /// Gets chapters for an item /// /// The id. /// IEnumerable{ChapterInfo}. /// id public IEnumerable GetChapters(Guid id) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select StartPositionTicks,Name,ImagePath from Chapters where ItemId = @ItemId order by ChapterIndex asc"; cmd.Parameters.Add("@ItemId", DbType.Guid).Value = id; using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) { while (reader.Read()) { var chapter = new ChapterInfo { StartPositionTicks = reader.GetInt64(0) }; if (!reader.IsDBNull(1)) { chapter.Name = reader.GetString(1); } if (!reader.IsDBNull(2)) { chapter.ImagePath = reader.GetString(2); } yield return chapter; } } } } /// /// Gets a single chapter for an item /// /// The id. /// The index. /// ChapterInfo. /// id public ChapterInfo GetChapter(Guid id, int index) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select StartPositionTicks,Name,ImagePath from Chapters where ItemId = @ItemId and ChapterIndex=@ChapterIndex"; cmd.Parameters.Add("@ItemId", DbType.Guid).Value = id; cmd.Parameters.Add("@ChapterIndex", DbType.Int32).Value = index; using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) { if (reader.Read()) { return new ChapterInfo { StartPositionTicks = reader.GetInt64(0), Name = reader.GetString(1), ImagePath = reader.GetString(2) }; } } return null; } } /// /// Saves the chapters. /// /// The id. /// The chapters. /// The cancellation token. /// Task. /// /// id /// or /// chapters /// or /// cancellationToken /// public async Task SaveChapters(Guid id, IEnumerable chapters, CancellationToken cancellationToken) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } if (chapters == null) { throw new ArgumentNullException("chapters"); } if (cancellationToken == null) { throw new ArgumentNullException("cancellationToken"); } cancellationToken.ThrowIfCancellationRequested(); await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); SQLiteTransaction transaction = null; try { transaction = _connection.BeginTransaction(); // First delete chapters _deleteChaptersCommand.Parameters[0].Value = id; _deleteChaptersCommand.Transaction = transaction; await _deleteChaptersCommand.ExecuteNonQueryAsync(cancellationToken); var index = 0; foreach (var chapter in chapters) { cancellationToken.ThrowIfCancellationRequested(); _saveChapterCommand.Parameters[0].Value = id; _saveChapterCommand.Parameters[1].Value = index; _saveChapterCommand.Parameters[2].Value = chapter.StartPositionTicks; _saveChapterCommand.Parameters[3].Value = chapter.Name; _saveChapterCommand.Parameters[4].Value = chapter.ImagePath; _saveChapterCommand.Transaction = transaction; await _saveChapterCommand.ExecuteNonQueryAsync(cancellationToken); index++; } transaction.Commit(); } catch (OperationCanceledException) { if (transaction != null) { transaction.Rollback(); } throw; } catch (Exception e) { _logger.ErrorException("Failed to save chapters:", e); if (transaction != null) { transaction.Rollback(); } throw; } finally { if (transaction != null) { transaction.Dispose(); } _writeLock.Release(); } } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private readonly object _disposeLock = new object(); /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool dispose) { if (dispose) { try { lock (_disposeLock) { if (_connection != null) { if (_connection.IsOpen()) { _connection.Close(); } _connection.Dispose(); _connection = null; } } } catch (Exception ex) { _logger.ErrorException("Error disposing database", ex); } } } } }