SqliteChapterRepository.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Model.Entities;
  3. using MediaBrowser.Model.Logging;
  4. using MediaBrowser.Model.Serialization;
  5. using System;
  6. using System.Collections.Generic;
  7. using System.Data;
  8. using System.Data.SQLite;
  9. using System.IO;
  10. using System.Threading;
  11. using System.Threading.Tasks;
  12. namespace MediaBrowser.Server.Implementations.Persistence
  13. {
  14. public class SqliteChapterRepository
  15. {
  16. private SQLiteConnection _connection;
  17. private readonly ILogger _logger;
  18. /// <summary>
  19. /// The _app paths
  20. /// </summary>
  21. private readonly IApplicationPaths _appPaths;
  22. private SQLiteCommand _deleteChaptersCommand;
  23. private SQLiteCommand _saveChapterCommand;
  24. /// <summary>
  25. /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
  26. /// </summary>
  27. /// <param name="appPaths">The app paths.</param>
  28. /// <param name="jsonSerializer">The json serializer.</param>
  29. /// <param name="logManager">The log manager.</param>
  30. /// <exception cref="System.ArgumentNullException">
  31. /// appPaths
  32. /// or
  33. /// jsonSerializer
  34. /// </exception>
  35. public SqliteChapterRepository(IApplicationPaths appPaths, ILogManager logManager)
  36. {
  37. if (appPaths == null)
  38. {
  39. throw new ArgumentNullException("appPaths");
  40. }
  41. _appPaths = appPaths;
  42. _logger = logManager.GetLogger(GetType().Name);
  43. }
  44. /// <summary>
  45. /// Opens the connection to the database
  46. /// </summary>
  47. /// <returns>Task.</returns>
  48. public async Task Initialize()
  49. {
  50. var dbFile = Path.Combine(_appPaths.DataPath, "chapters.db");
  51. _connection = await SqliteExtensions.ConnectToDb(dbFile).ConfigureAwait(false);
  52. string[] queries = {
  53. "create table if not exists chapters (ItemId GUID, ChapterIndex INT, StartPositionTicks BIGINT, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))",
  54. "create index if not exists idx_chapters on chapters(ItemId, ChapterIndex)",
  55. //pragmas
  56. "pragma temp_store = memory"
  57. };
  58. _connection.RunQueries(queries, _logger);
  59. PrepareStatements();
  60. }
  61. /// <summary>
  62. /// The _write lock
  63. /// </summary>
  64. private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
  65. /// <summary>
  66. /// Prepares the statements.
  67. /// </summary>
  68. private void PrepareStatements()
  69. {
  70. _deleteChaptersCommand = new SQLiteCommand
  71. {
  72. CommandText = "delete from chapters where ItemId=@ItemId"
  73. };
  74. _deleteChaptersCommand.Parameters.Add(new SQLiteParameter("@ItemId"));
  75. _saveChapterCommand = new SQLiteCommand
  76. {
  77. CommandText = "replace into chapters (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath) values (@ItemId, @ChapterIndex, @StartPositionTicks, @Name, @ImagePath)"
  78. };
  79. _saveChapterCommand.Parameters.Add(new SQLiteParameter("@ItemId"));
  80. _saveChapterCommand.Parameters.Add(new SQLiteParameter("@ChapterIndex"));
  81. _saveChapterCommand.Parameters.Add(new SQLiteParameter("@StartPositionTicks"));
  82. _saveChapterCommand.Parameters.Add(new SQLiteParameter("@Name"));
  83. _saveChapterCommand.Parameters.Add(new SQLiteParameter("@ImagePath"));
  84. }
  85. /// <summary>
  86. /// Gets chapters for an item
  87. /// </summary>
  88. /// <param name="id">The id.</param>
  89. /// <returns>IEnumerable{ChapterInfo}.</returns>
  90. /// <exception cref="System.ArgumentNullException">id</exception>
  91. public IEnumerable<ChapterInfo> GetChapters(Guid id)
  92. {
  93. if (id == Guid.Empty)
  94. {
  95. throw new ArgumentNullException("id");
  96. }
  97. using (var cmd = _connection.CreateCommand())
  98. {
  99. cmd.CommandText = "select StartPositionTicks,Name,ImagePath from Chapters where ItemId = @ItemId order by ChapterIndex asc";
  100. cmd.Parameters.Add("@ItemId", DbType.Guid).Value = id;
  101. using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult))
  102. {
  103. while (reader.Read())
  104. {
  105. var chapter = new ChapterInfo
  106. {
  107. StartPositionTicks = reader.GetInt64(0)
  108. };
  109. if (!reader.IsDBNull(1))
  110. {
  111. chapter.Name = reader.GetString(1);
  112. }
  113. if (!reader.IsDBNull(2))
  114. {
  115. chapter.ImagePath = reader.GetString(2);
  116. }
  117. yield return chapter;
  118. }
  119. }
  120. }
  121. }
  122. /// <summary>
  123. /// Gets a single chapter for an item
  124. /// </summary>
  125. /// <param name="id">The id.</param>
  126. /// <param name="index">The index.</param>
  127. /// <returns>ChapterInfo.</returns>
  128. /// <exception cref="System.ArgumentNullException">id</exception>
  129. public ChapterInfo GetChapter(Guid id, int index)
  130. {
  131. if (id == Guid.Empty)
  132. {
  133. throw new ArgumentNullException("id");
  134. }
  135. using (var cmd = _connection.CreateCommand())
  136. {
  137. cmd.CommandText = "select StartPositionTicks,Name,ImagePath from Chapters where ItemId = @ItemId and ChapterIndex=@ChapterIndex";
  138. cmd.Parameters.Add("@ItemId", DbType.Guid).Value = id;
  139. cmd.Parameters.Add("@ChapterIndex", DbType.Int32).Value = index;
  140. using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow))
  141. {
  142. if (reader.Read())
  143. {
  144. return new ChapterInfo
  145. {
  146. StartPositionTicks = reader.GetInt64(0),
  147. Name = reader.GetString(1),
  148. ImagePath = reader.GetString(2)
  149. };
  150. }
  151. }
  152. return null;
  153. }
  154. }
  155. /// <summary>
  156. /// Saves the chapters.
  157. /// </summary>
  158. /// <param name="id">The id.</param>
  159. /// <param name="chapters">The chapters.</param>
  160. /// <param name="cancellationToken">The cancellation token.</param>
  161. /// <returns>Task.</returns>
  162. /// <exception cref="System.ArgumentNullException">
  163. /// id
  164. /// or
  165. /// chapters
  166. /// or
  167. /// cancellationToken
  168. /// </exception>
  169. public async Task SaveChapters(Guid id, IEnumerable<ChapterInfo> chapters, CancellationToken cancellationToken)
  170. {
  171. if (id == Guid.Empty)
  172. {
  173. throw new ArgumentNullException("id");
  174. }
  175. if (chapters == null)
  176. {
  177. throw new ArgumentNullException("chapters");
  178. }
  179. if (cancellationToken == null)
  180. {
  181. throw new ArgumentNullException("cancellationToken");
  182. }
  183. cancellationToken.ThrowIfCancellationRequested();
  184. await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
  185. SQLiteTransaction transaction = null;
  186. try
  187. {
  188. transaction = _connection.BeginTransaction();
  189. // First delete chapters
  190. _deleteChaptersCommand.Parameters[0].Value = id;
  191. _deleteChaptersCommand.Transaction = transaction;
  192. await _deleteChaptersCommand.ExecuteNonQueryAsync(cancellationToken);
  193. var index = 0;
  194. foreach (var chapter in chapters)
  195. {
  196. cancellationToken.ThrowIfCancellationRequested();
  197. _saveChapterCommand.Parameters[0].Value = id;
  198. _saveChapterCommand.Parameters[1].Value = index;
  199. _saveChapterCommand.Parameters[2].Value = chapter.StartPositionTicks;
  200. _saveChapterCommand.Parameters[3].Value = chapter.Name;
  201. _saveChapterCommand.Parameters[4].Value = chapter.ImagePath;
  202. _saveChapterCommand.Transaction = transaction;
  203. await _saveChapterCommand.ExecuteNonQueryAsync(cancellationToken);
  204. index++;
  205. }
  206. transaction.Commit();
  207. }
  208. catch (OperationCanceledException)
  209. {
  210. if (transaction != null)
  211. {
  212. transaction.Rollback();
  213. }
  214. throw;
  215. }
  216. catch (Exception e)
  217. {
  218. _logger.ErrorException("Failed to save chapters:", e);
  219. if (transaction != null)
  220. {
  221. transaction.Rollback();
  222. }
  223. throw;
  224. }
  225. finally
  226. {
  227. if (transaction != null)
  228. {
  229. transaction.Dispose();
  230. }
  231. _writeLock.Release();
  232. }
  233. }
  234. /// <summary>
  235. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
  236. /// </summary>
  237. public void Dispose()
  238. {
  239. Dispose(true);
  240. GC.SuppressFinalize(this);
  241. }
  242. private readonly object _disposeLock = new object();
  243. /// <summary>
  244. /// Releases unmanaged and - optionally - managed resources.
  245. /// </summary>
  246. /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  247. protected virtual void Dispose(bool dispose)
  248. {
  249. if (dispose)
  250. {
  251. try
  252. {
  253. lock (_disposeLock)
  254. {
  255. if (_connection != null)
  256. {
  257. if (_connection.IsOpen())
  258. {
  259. _connection.Close();
  260. }
  261. _connection.Dispose();
  262. _connection = null;
  263. }
  264. }
  265. }
  266. catch (Exception ex)
  267. {
  268. _logger.ErrorException("Error disposing database", ex);
  269. }
  270. }
  271. }
  272. }
  273. }