diff options
Diffstat (limited to 'MediaBrowser.Server.Implementations')
10 files changed, 725 insertions, 351 deletions
diff --git a/MediaBrowser.Server.Implementations/Library/DisplayPreferencesManager.cs b/MediaBrowser.Server.Implementations/Library/DisplayPreferencesManager.cs deleted file mode 100644 index 57a9c9d78..000000000 --- a/MediaBrowser.Server.Implementations/Library/DisplayPreferencesManager.cs +++ /dev/null @@ -1,99 +0,0 @@ -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.Library -{ - /// <summary> - /// Class DisplayPreferencesManager - /// </summary> - public class DisplayPreferencesManager : IDisplayPreferencesManager - { - /// <summary> - /// The _logger - /// </summary> - private readonly ILogger _logger; - - /// <summary> - /// The _display preferences - /// </summary> - private readonly ConcurrentDictionary<Guid, Task<DisplayPreferences>> _displayPreferences = new ConcurrentDictionary<Guid, Task<DisplayPreferences>>(); - - /// <summary> - /// Gets the active user repository - /// </summary> - /// <value>The display preferences repository.</value> - public IDisplayPreferencesRepository Repository { get; set; } - - /// <summary> - /// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class. - /// </summary> - /// <param name="logger">The logger.</param> - public DisplayPreferencesManager(ILogger logger) - { - _logger = logger; - } - - /// <summary> - /// Gets the display preferences. - /// </summary> - /// <param name="displayPreferencesId">The display preferences id.</param> - /// <returns>DisplayPreferences.</returns> - public Task<DisplayPreferences> GetDisplayPreferences(Guid displayPreferencesId) - { - return _displayPreferences.GetOrAdd(displayPreferencesId, keyName => RetrieveDisplayPreferences(displayPreferencesId)); - } - - /// <summary> - /// Retrieves the display preferences. - /// </summary> - /// <param name="displayPreferencesId">The display preferences id.</param> - /// <returns>DisplayPreferences.</returns> - private async Task<DisplayPreferences> RetrieveDisplayPreferences(Guid displayPreferencesId) - { - var displayPreferences = await Repository.GetDisplayPreferences(displayPreferencesId).ConfigureAwait(false); - - return displayPreferences ?? new DisplayPreferences { Id = displayPreferencesId }; - } - - /// <summary> - /// Saves display preferences for an item - /// </summary> - /// <param name="displayPreferences">The display preferences.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public async Task SaveDisplayPreferences(DisplayPreferences displayPreferences, CancellationToken cancellationToken) - { - if (displayPreferences == null) - { - throw new ArgumentNullException("displayPreferences"); - } - if (displayPreferences.Id == Guid.Empty) - { - throw new ArgumentNullException("displayPreferences.Id"); - } - - try - { - await Repository.SaveDisplayPreferences(displayPreferences, - cancellationToken).ConfigureAwait(false); - - var newValue = Task.FromResult(displayPreferences); - - // Once it succeeds, put it into the dictionary to make it available to everyone else - _displayPreferences.AddOrUpdate(displayPreferences.Id, newValue, delegate { return newValue; }); - } - catch (Exception ex) - { - _logger.ErrorException("Error saving display preferences", ex); - - throw; - } - } - } -} diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index fc7e1e6ba..34930b34e 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -119,7 +119,6 @@ <Compile Include="HttpServer\SwaggerService.cs" /> <Compile Include="IO\DirectoryWatchers.cs" /> <Compile Include="Library\CoreResolutionIgnoreRule.cs" /> - <Compile Include="Library\DisplayPreferencesManager.cs" /> <Compile Include="Library\LibraryManager.cs" /> <Compile Include="Library\LuceneSearchEngine.cs" /> <Compile Include="Library\ResolverHelper.cs" /> @@ -139,8 +138,8 @@ <Compile Include="Library\UserManager.cs" /> <Compile Include="Localization\LocalizationManager.cs" /> <Compile Include="MediaEncoder\MediaEncoder.cs" /> + <Compile Include="Persistence\SqliteChapterRepository.cs" /> <Compile Include="Persistence\SqliteExtensions.cs" /> - <Compile Include="Persistence\SqliteRepository.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Providers\ProviderManager.cs" /> <Compile Include="ScheduledTasks\ArtistValidationTask.cs" /> diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteChapterRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteChapterRepository.cs new file mode 100644 index 000000000..dd6343a67 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteChapterRepository.cs @@ -0,0 +1,326 @@ +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; + + /// <summary> + /// The _app paths + /// </summary> + private readonly IApplicationPaths _appPaths; + + private SQLiteCommand _deleteChaptersCommand; + private SQLiteCommand _saveChapterCommand; + + /// <summary> + /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class. + /// </summary> + /// <param name="appPaths">The app paths.</param> + /// <param name="jsonSerializer">The json serializer.</param> + /// <param name="logManager">The log manager.</param> + /// <exception cref="System.ArgumentNullException"> + /// appPaths + /// or + /// jsonSerializer + /// </exception> + public SqliteChapterRepository(IApplicationPaths appPaths, ILogManager logManager) + { + if (appPaths == null) + { + throw new ArgumentNullException("appPaths"); + } + + _appPaths = appPaths; + + _logger = logManager.GetLogger(GetType().Name); + } + + /// <summary> + /// Opens the connection to the database + /// </summary> + /// <returns>Task.</returns> + 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(); + } + + /// <summary> + /// The _write lock + /// </summary> + private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); + + /// <summary> + /// Prepares the statements. + /// </summary> + 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")); + } + + /// <summary> + /// Gets chapters for an item + /// </summary> + /// <param name="id">The id.</param> + /// <returns>IEnumerable{ChapterInfo}.</returns> + /// <exception cref="System.ArgumentNullException">id</exception> + public IEnumerable<ChapterInfo> 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; + } + } + } + } + + /// <summary> + /// Gets a single chapter for an item + /// </summary> + /// <param name="id">The id.</param> + /// <param name="index">The index.</param> + /// <returns>ChapterInfo.</returns> + /// <exception cref="System.ArgumentNullException">id</exception> + 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; + } + } + + /// <summary> + /// Saves the chapters. + /// </summary> + /// <param name="id">The id.</param> + /// <param name="chapters">The chapters.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException"> + /// id + /// or + /// chapters + /// or + /// cancellationToken + /// </exception> + public async Task SaveChapters(Guid id, IEnumerable<ChapterInfo> 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(); + } + } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private readonly object _disposeLock = new object(); + + /// <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 (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); + } + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs index f4d341c34..cb965c3f9 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs @@ -15,13 +15,12 @@ namespace MediaBrowser.Server.Implementations.Persistence /// <summary> /// Class SQLiteDisplayPreferencesRepository /// </summary> - public class SqliteDisplayPreferencesRepository : SqliteRepository, IDisplayPreferencesRepository + public class SqliteDisplayPreferencesRepository : IDisplayPreferencesRepository { - /// <summary> - /// The repository name - /// </summary> - public const string RepositoryName = "SQLite"; + private SQLiteConnection _connection; + private readonly ILogger _logger; + /// <summary> /// Gets the name of the repository /// </summary> @@ -30,7 +29,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { get { - return RepositoryName; + return "SQLite"; } } @@ -58,7 +57,6 @@ namespace MediaBrowser.Server.Implementations.Persistence /// appPaths /// </exception> public SqliteDisplayPreferencesRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager) - : base(logManager) { if (jsonSerializer == null) { @@ -71,6 +69,8 @@ namespace MediaBrowser.Server.Implementations.Persistence _jsonSerializer = jsonSerializer; _appPaths = appPaths; + + _logger = logManager.GetLogger(GetType().Name); } /// <summary> @@ -81,7 +81,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { var dbFile = Path.Combine(_appPaths.DataPath, "displaypreferences.db"); - await ConnectToDb(dbFile).ConfigureAwait(false); + _connection = await SqliteExtensions.ConnectToDb(dbFile).ConfigureAwait(false); string[] queries = { @@ -92,7 +92,7 @@ namespace MediaBrowser.Server.Implementations.Persistence "pragma temp_store = memory" }; - RunQueries(queries); + _connection.RunQueries(queries, _logger); } /// <summary> @@ -127,9 +127,9 @@ namespace MediaBrowser.Server.Implementations.Persistence try { - transaction = Connection.BeginTransaction(); + transaction = _connection.BeginTransaction(); - using (var cmd = Connection.CreateCommand()) + using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "replace into displaypreferences (id, data) values (@1, @2)"; cmd.AddParam("@1", displayPreferences.Id); @@ -153,7 +153,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } catch (Exception e) { - Logger.ErrorException("Failed to save display preferences:", e); + _logger.ErrorException("Failed to save display preferences:", e); if (transaction != null) { @@ -179,24 +179,24 @@ namespace MediaBrowser.Server.Implementations.Persistence /// <param name="displayPreferencesId">The display preferences id.</param> /// <returns>Task{DisplayPreferences}.</returns> /// <exception cref="System.ArgumentNullException">item</exception> - public async Task<DisplayPreferences> GetDisplayPreferences(Guid displayPreferencesId) + public DisplayPreferences GetDisplayPreferences(Guid displayPreferencesId) { if (displayPreferencesId == Guid.Empty) { throw new ArgumentNullException("displayPreferencesId"); } - var cmd = Connection.CreateCommand(); + var cmd = _connection.CreateCommand(); cmd.CommandText = "select data from displaypreferences where id = @id"; var idParam = cmd.Parameters.Add("@id", DbType.Guid); idParam.Value = displayPreferencesId; - using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow).ConfigureAwait(false)) + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) { if (reader.Read()) { - using (var stream = GetStream(reader, 0)) + using (var stream = reader.GetMemoryStream(0)) { return _jsonSerializer.DeserializeFromStream<DisplayPreferences>(stream); } @@ -205,5 +205,47 @@ namespace MediaBrowser.Server.Implementations.Persistence return null; } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private readonly object _disposeLock = new object(); + + /// <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 (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); + } + } + } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs index 00dbbe513..2b14e9b24 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs @@ -1,6 +1,9 @@ using System; using System.Data; using System.Data.SQLite; +using System.IO; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; namespace MediaBrowser.Server.Implementations.Persistence { @@ -57,5 +60,103 @@ namespace MediaBrowser.Server.Implementations.Persistence { return conn.State == ConnectionState.Open; } + + /// <summary> + /// Gets a stream from a DataReader at a given ordinal + /// </summary> + /// <param name="reader">The reader.</param> + /// <param name="ordinal">The ordinal.</param> + /// <returns>Stream.</returns> + /// <exception cref="System.ArgumentNullException">reader</exception> + public static Stream GetMemoryStream(this IDataReader reader, int ordinal) + { + if (reader == null) + { + throw new ArgumentNullException("reader"); + } + + var memoryStream = new MemoryStream(); + var num = 0L; + var array = new byte[4096]; + long bytes; + do + { + bytes = reader.GetBytes(ordinal, num, array, 0, array.Length); + memoryStream.Write(array, 0, (int)bytes); + num += bytes; + } + while (bytes > 0L); + memoryStream.Position = 0; + return memoryStream; + } + + /// <summary> + /// Runs the queries. + /// </summary> + /// <param name="connection">The connection.</param> + /// <param name="queries">The queries.</param> + /// <param name="logger">The logger.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + /// <exception cref="System.ArgumentNullException">queries</exception> + public static void RunQueries(this IDbConnection connection, string[] queries, ILogger logger) + { + if (queries == null) + { + throw new ArgumentNullException("queries"); + } + + using (var tran = connection.BeginTransaction()) + { + try + { + using (var cmd = connection.CreateCommand()) + { + foreach (var query in queries) + { + cmd.Transaction = tran; + cmd.CommandText = query; + cmd.ExecuteNonQuery(); + } + } + + tran.Commit(); + } + catch (Exception e) + { + logger.ErrorException("Error running queries", e); + tran.Rollback(); + throw; + } + } + } + + /// <summary> + /// Connects to db. + /// </summary> + /// <param name="dbPath">The db path.</param> + /// <returns>Task{IDbConnection}.</returns> + /// <exception cref="System.ArgumentNullException">dbPath</exception> + public static async Task<SQLiteConnection> ConnectToDb(string dbPath) + { + if (string.IsNullOrEmpty(dbPath)) + { + throw new ArgumentNullException("dbPath"); + } + + var connectionstr = new SQLiteConnectionStringBuilder + { + PageSize = 4096, + CacheSize = 4096, + SyncMode = SynchronizationModes.Off, + DataSource = dbPath, + JournalMode = SQLiteJournalModeEnum.Wal + }; + + var connection = new SQLiteConnection(connectionstr.ConnectionString); + + await connection.OpenAsync().ConfigureAwait(false); + + return connection; + } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs index a9cd3d1eb..b3251ddb9 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs @@ -18,13 +18,12 @@ namespace MediaBrowser.Server.Implementations.Persistence /// <summary> /// Class SQLiteItemRepository /// </summary> - public class SqliteItemRepository : SqliteRepository, IItemRepository + public class SqliteItemRepository : IItemRepository { - /// <summary> - /// The repository name - /// </summary> - public const string RepositoryName = "SQLite"; + private SQLiteConnection _connection; + private readonly ILogger _logger; + /// <summary> /// Gets the name of the repository /// </summary> @@ -33,7 +32,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { get { - return RepositoryName; + return "SQLite"; } } @@ -55,6 +54,8 @@ namespace MediaBrowser.Server.Implementations.Persistence private readonly string _criticReviewsPath; + private SqliteChapterRepository _chapterRepository; + /// <summary> /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class. /// </summary> @@ -67,7 +68,6 @@ namespace MediaBrowser.Server.Implementations.Persistence /// jsonSerializer /// </exception> public SqliteItemRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager) - : base(logManager) { if (appPaths == null) { @@ -82,6 +82,10 @@ namespace MediaBrowser.Server.Implementations.Persistence _jsonSerializer = jsonSerializer; _criticReviewsPath = Path.Combine(_appPaths.DataPath, "critic-reviews"); + + _logger = logManager.GetLogger(GetType().Name); + + _chapterRepository = new SqliteChapterRepository(appPaths, logManager); } /// <summary> @@ -92,20 +96,22 @@ namespace MediaBrowser.Server.Implementations.Persistence { var dbFile = Path.Combine(_appPaths.DataPath, "library.db"); - await ConnectToDb(dbFile).ConfigureAwait(false); + _connection = await SqliteExtensions.ConnectToDb(dbFile).ConfigureAwait(false); string[] queries = { "create table if not exists baseitems (guid GUID primary key, data BLOB)", "create index if not exists idx_baseitems on baseitems(guid)", - "create table if not exists schema_version (table_name primary key, version)", + //pragmas "pragma temp_store = memory" }; - RunQueries(queries); + _connection.RunQueries(queries, _logger); PrepareStatements(); + + await _chapterRepository.Initialize().ConfigureAwait(false); } /// <summary> @@ -175,7 +181,7 @@ namespace MediaBrowser.Server.Implementations.Persistence try { - transaction = Connection.BeginTransaction(); + transaction = _connection.BeginTransaction(); foreach (var item in items) { @@ -202,7 +208,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } catch (Exception e) { - Logger.ErrorException("Failed to save items:", e); + _logger.ErrorException("Failed to save items:", e); if (transaction != null) { @@ -237,7 +243,7 @@ namespace MediaBrowser.Server.Implementations.Persistence throw new ArgumentNullException("id"); } - using (var cmd = Connection.CreateCommand()) + using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select data from baseitems where guid = @guid"; var guidParam = cmd.Parameters.Add("@guid", DbType.Guid); @@ -247,7 +253,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { if (reader.Read()) { - using (var stream = GetStream(reader, 0)) + using (var stream = reader.GetMemoryStream(0)) { return _jsonSerializer.DeserializeFromStream(stream, type) as BaseItem; } @@ -305,5 +311,95 @@ namespace MediaBrowser.Server.Implementations.Persistence _jsonSerializer.SerializeToFile(criticReviews.ToList(), path); }); } + + /// <summary> + /// Gets chapters for an item + /// </summary> + /// <param name="id">The id.</param> + /// <returns>IEnumerable{ChapterInfo}.</returns> + /// <exception cref="System.ArgumentNullException">id</exception> + public IEnumerable<ChapterInfo> GetChapters(Guid id) + { + return _chapterRepository.GetChapters(id); + } + + /// <summary> + /// Gets a single chapter for an item + /// </summary> + /// <param name="id">The id.</param> + /// <param name="index">The index.</param> + /// <returns>ChapterInfo.</returns> + /// <exception cref="System.ArgumentNullException">id</exception> + public ChapterInfo GetChapter(Guid id, int index) + { + return _chapterRepository.GetChapter(id, index); + } + + /// <summary> + /// Saves the chapters. + /// </summary> + /// <param name="id">The id.</param> + /// <param name="chapters">The chapters.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException"> + /// id + /// or + /// chapters + /// or + /// cancellationToken + /// </exception> + public Task SaveChapters(Guid id, IEnumerable<ChapterInfo> chapters, CancellationToken cancellationToken) + { + return _chapterRepository.SaveChapters(id, chapters, cancellationToken); + } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private readonly object _disposeLock = new object(); + + /// <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 (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); + } + + if (_chapterRepository != null) + { + _chapterRepository.Dispose(); + _chapterRepository = null; + } + } + } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteRepository.cs deleted file mode 100644 index cfdc9b5fb..000000000 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteRepository.cs +++ /dev/null @@ -1,182 +0,0 @@ -using MediaBrowser.Model.Logging; -using System; -using System.Data; -using System.Data.SQLite; -using System.IO; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.Persistence -{ - /// <summary> - /// Class SqliteRepository - /// </summary> - public abstract class SqliteRepository : IDisposable - { - /// <summary> - /// The db file name - /// </summary> - protected string DbFileName; - /// <summary> - /// The connection - /// </summary> - protected SQLiteConnection Connection; - - /// <summary> - /// Gets the logger. - /// </summary> - /// <value>The logger.</value> - protected ILogger Logger { get; private set; } - - /// <summary> - /// Initializes a new instance of the <see cref="SqliteRepository" /> class. - /// </summary> - /// <param name="logManager">The log manager.</param> - /// <exception cref="System.ArgumentNullException">logger</exception> - protected SqliteRepository(ILogManager logManager) - { - if (logManager == null) - { - throw new ArgumentNullException("logManager"); - } - - Logger = logManager.GetLogger(GetType().Name); - } - - /// <summary> - /// Connects to DB. - /// </summary> - /// <param name="dbPath">The db path.</param> - /// <returns>Task{System.Boolean}.</returns> - /// <exception cref="System.ArgumentNullException">dbPath</exception> - protected Task ConnectToDb(string dbPath) - { - if (string.IsNullOrEmpty(dbPath)) - { - throw new ArgumentNullException("dbPath"); - } - - DbFileName = dbPath; - var connectionstr = new SQLiteConnectionStringBuilder - { - PageSize = 4096, - CacheSize = 40960, - SyncMode = SynchronizationModes.Off, - DataSource = dbPath, - JournalMode = SQLiteJournalModeEnum.Wal - }; - - Connection = new SQLiteConnection(connectionstr.ConnectionString); - - return Connection.OpenAsync(); - } - - /// <summary> - /// Runs the queries. - /// </summary> - /// <param name="queries">The queries.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - /// <exception cref="System.ArgumentNullException">queries</exception> - protected void RunQueries(string[] queries) - { - if (queries == null) - { - throw new ArgumentNullException("queries"); - } - - using (var tran = Connection.BeginTransaction()) - { - try - { - using (var cmd = Connection.CreateCommand()) - { - foreach (var query in queries) - { - cmd.Transaction = tran; - cmd.CommandText = query; - cmd.ExecuteNonQuery(); - } - } - - tran.Commit(); - } - catch (Exception e) - { - Logger.ErrorException("Error running queries", e); - tran.Rollback(); - throw; - } - } - } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private readonly object _disposeLock = new object(); - - /// <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 (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); - } - } - } - - /// <summary> - /// Gets a stream from a DataReader at a given ordinal - /// </summary> - /// <param name="reader">The reader.</param> - /// <param name="ordinal">The ordinal.</param> - /// <returns>Stream.</returns> - /// <exception cref="System.ArgumentNullException">reader</exception> - protected static Stream GetStream(IDataReader reader, int ordinal) - { - if (reader == null) - { - throw new ArgumentNullException("reader"); - } - - var memoryStream = new MemoryStream(); - var num = 0L; - var array = new byte[4096]; - long bytes; - do - { - bytes = reader.GetBytes(ordinal, num, array, 0, array.Length); - memoryStream.Write(array, 0, (int)bytes); - num += bytes; - } - while (bytes > 0L); - memoryStream.Position = 0; - return memoryStream; - } - } -}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs index 05829e007..1d127ae96 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs @@ -13,17 +13,16 @@ using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Persistence { - public class SqliteUserDataRepository : SqliteRepository, IUserDataRepository + public class SqliteUserDataRepository : IUserDataRepository { + private readonly ILogger _logger; + private readonly ConcurrentDictionary<string, UserItemData> _userData = new ConcurrentDictionary<string, UserItemData>(); private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); - /// <summary> - /// The repository name - /// </summary> - public const string RepositoryName = "SQLite"; - + private SQLiteConnection _connection; + /// <summary> /// Gets the name of the repository /// </summary> @@ -32,7 +31,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { get { - return RepositoryName; + return "SQLite"; } } @@ -55,7 +54,6 @@ namespace MediaBrowser.Server.Implementations.Persistence /// appPaths /// </exception> public SqliteUserDataRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager) - : base(logManager) { if (jsonSerializer == null) { @@ -68,6 +66,7 @@ namespace MediaBrowser.Server.Implementations.Persistence _jsonSerializer = jsonSerializer; _appPaths = appPaths; + _logger = logManager.GetLogger(GetType().Name); } /// <summary> @@ -78,7 +77,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { var dbFile = Path.Combine(_appPaths.DataPath, "userdata.db"); - await ConnectToDb(dbFile).ConfigureAwait(false); + _connection = await SqliteExtensions.ConnectToDb(dbFile).ConfigureAwait(false); string[] queries = { @@ -89,7 +88,7 @@ namespace MediaBrowser.Server.Implementations.Persistence "pragma temp_store = memory" }; - RunQueries(queries); + _connection.RunQueries(queries, _logger); } /// <summary> @@ -139,7 +138,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } catch (Exception ex) { - Logger.ErrorException("Error saving user data", ex); + _logger.ErrorException("Error saving user data", ex); throw; } @@ -178,9 +177,9 @@ namespace MediaBrowser.Server.Implementations.Persistence try { - transaction = Connection.BeginTransaction(); + transaction = _connection.BeginTransaction(); - using (var cmd = Connection.CreateCommand()) + using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "replace into userdata (key, userId, data) values (@1, @2, @3)"; cmd.AddParam("@1", key); @@ -205,7 +204,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } catch (Exception e) { - Logger.ErrorException("Failed to save user data:", e); + _logger.ErrorException("Failed to save user data:", e); if (transaction != null) { @@ -258,7 +257,7 @@ namespace MediaBrowser.Server.Implementations.Persistence /// <returns>Task{UserItemData}.</returns> private UserItemData RetrieveUserData(Guid userId, string key) { - using (var cmd = Connection.CreateCommand()) + using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select data from userdata where key = @key and userId=@userId"; @@ -272,7 +271,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { if (reader.Read()) { - using (var stream = GetStream(reader, 0)) + using (var stream = reader.GetMemoryStream(0)) { return _jsonSerializer.DeserializeFromStream<UserItemData>(stream); } @@ -282,5 +281,47 @@ namespace MediaBrowser.Server.Implementations.Persistence return new UserItemData(); } } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private readonly object _disposeLock = new object(); + + /// <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 (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); + } + } + } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs index efd39529a..09e34cf08 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs @@ -16,15 +16,14 @@ namespace MediaBrowser.Server.Implementations.Persistence /// <summary> /// Class SQLiteUserRepository /// </summary> - public class SqliteUserRepository : SqliteRepository, IUserRepository + public class SqliteUserRepository : IUserRepository { - /// <summary> - /// The repository name - /// </summary> - public const string RepositoryName = "SQLite"; - + private readonly ILogger _logger; + private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); + private SQLiteConnection _connection; + /// <summary> /// Gets the name of the repository /// </summary> @@ -33,7 +32,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { get { - return RepositoryName; + return "SQLite"; } } @@ -56,7 +55,6 @@ namespace MediaBrowser.Server.Implementations.Persistence /// <param name="logManager">The log manager.</param> /// <exception cref="System.ArgumentNullException">appPaths</exception> public SqliteUserRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager) - : base(logManager) { if (appPaths == null) { @@ -69,6 +67,8 @@ namespace MediaBrowser.Server.Implementations.Persistence _appPaths = appPaths; _jsonSerializer = jsonSerializer; + + _logger = logManager.GetLogger(GetType().Name); } /// <summary> @@ -79,7 +79,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { var dbFile = Path.Combine(_appPaths.DataPath, "users.db"); - await ConnectToDb(dbFile).ConfigureAwait(false); + _connection = await SqliteExtensions.ConnectToDb(dbFile).ConfigureAwait(false); string[] queries = { @@ -90,7 +90,7 @@ namespace MediaBrowser.Server.Implementations.Persistence "pragma temp_store = memory" }; - RunQueries(queries); + _connection.RunQueries(queries, _logger); } /// <summary> @@ -124,9 +124,9 @@ namespace MediaBrowser.Server.Implementations.Persistence try { - transaction = Connection.BeginTransaction(); + transaction = _connection.BeginTransaction(); - using (var cmd = Connection.CreateCommand()) + using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "replace into users (guid, data) values (@1, @2)"; cmd.AddParam("@1", user.Id); @@ -150,7 +150,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } catch (Exception e) { - Logger.ErrorException("Failed to save user:", e); + _logger.ErrorException("Failed to save user:", e); if (transaction != null) { @@ -176,7 +176,7 @@ namespace MediaBrowser.Server.Implementations.Persistence /// <returns>IEnumerable{User}.</returns> public IEnumerable<User> RetrieveAllUsers() { - using (var cmd = Connection.CreateCommand()) + using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select data from users"; @@ -184,7 +184,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { while (reader.Read()) { - using (var stream = GetStream(reader, 0)) + using (var stream = reader.GetMemoryStream(0)) { var user = _jsonSerializer.DeserializeFromStream<User>(stream); yield return user; @@ -221,9 +221,9 @@ namespace MediaBrowser.Server.Implementations.Persistence try { - transaction = Connection.BeginTransaction(); + transaction = _connection.BeginTransaction(); - using (var cmd = Connection.CreateCommand()) + using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "delete from users where guid=@guid"; @@ -248,7 +248,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } catch (Exception e) { - Logger.ErrorException("Failed to delete user:", e); + _logger.ErrorException("Failed to delete user:", e); if (transaction != null) { @@ -267,5 +267,47 @@ namespace MediaBrowser.Server.Implementations.Persistence _writeLock.Release(); } } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private readonly object _disposeLock = new object(); + + /// <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 (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); + } + } + } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs b/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs index 2d9d5abfe..79ea89ac6 100644 --- a/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs +++ b/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs @@ -2,6 +2,7 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; @@ -45,6 +46,8 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks /// <value>The new item timer.</value> private Timer NewItemTimer { get; set; } + private readonly IItemRepository _itemRepo; + /// <summary> /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class. /// </summary> @@ -52,12 +55,14 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks /// <param name="logManager">The log manager.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="jsonSerializer">The json serializer.</param> - public ChapterImagesTask(Kernel kernel, ILogManager logManager, ILibraryManager libraryManager, IJsonSerializer jsonSerializer) + /// <param name="itemRepo">The item repo.</param> + public ChapterImagesTask(Kernel kernel, ILogManager logManager, ILibraryManager libraryManager, IJsonSerializer jsonSerializer, IItemRepository itemRepo) { _kernel = kernel; _logger = logManager.GetLogger(GetType().Name); _libraryManager = libraryManager; _jsonSerializer = jsonSerializer; + _itemRepo = itemRepo; libraryManager.ItemAdded += libraryManager_ItemAdded; libraryManager.ItemUpdated += libraryManager_ItemAdded; @@ -106,7 +111,9 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks { try { - await _kernel.FFMpegManager.PopulateChapterImages(item, CancellationToken.None, true, true); + var chapters = _itemRepo.GetChapters(item.Id).ToList(); + + await _kernel.FFMpegManager.PopulateChapterImages(item, chapters, true, true, CancellationToken.None); } catch (Exception ex) { @@ -137,7 +144,6 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks { var videos = _libraryManager.RootFolder.RecursiveChildren .OfType<Video>() - .Where(v => v.Chapters != null && v.Chapters.Count != 0) .ToList(); var numComplete = 0; @@ -163,7 +169,9 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks var extract = !previouslyFailedImages.Contains(key, StringComparer.OrdinalIgnoreCase); - var success = await _kernel.FFMpegManager.PopulateChapterImages(video, cancellationToken, extract, true); + var chapters = _itemRepo.GetChapters(video.Id).ToList(); + + var success = await _kernel.FFMpegManager.PopulateChapterImages(video, chapters, extract, true, cancellationToken); if (!success) { |
