SqliteProviderInfoRepository.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. using System.IO;
  2. using MediaBrowser.Common.Configuration;
  3. using MediaBrowser.Controller.Providers;
  4. using MediaBrowser.Model.Logging;
  5. using System;
  6. using System.Collections.Generic;
  7. using System.Data;
  8. using System.Linq;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. namespace MediaBrowser.Server.Implementations.Persistence
  12. {
  13. public class SqliteProviderInfoRepository : IProviderRepository
  14. {
  15. private IDbConnection _connection;
  16. private readonly ILogger _logger;
  17. private IDbCommand _deleteInfosCommand;
  18. private IDbCommand _saveInfoCommand;
  19. private IDbCommand _saveStatusCommand;
  20. private readonly IApplicationPaths _appPaths;
  21. public SqliteProviderInfoRepository(IApplicationPaths appPaths, ILogManager logManager)
  22. {
  23. _appPaths = appPaths;
  24. _logger = logManager.GetLogger(GetType().Name);
  25. }
  26. private SqliteShrinkMemoryTimer _shrinkMemoryTimer;
  27. /// <summary>
  28. /// Gets the name of the repository
  29. /// </summary>
  30. /// <value>The name.</value>
  31. public string Name
  32. {
  33. get
  34. {
  35. return "SQLite";
  36. }
  37. }
  38. /// <summary>
  39. /// Opens the connection to the database
  40. /// </summary>
  41. /// <returns>Task.</returns>
  42. public async Task Initialize()
  43. {
  44. var dbFile = Path.Combine(_appPaths.DataPath, "providerinfo.db");
  45. _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false);
  46. string[] queries = {
  47. "create table if not exists providerinfos (ItemId GUID, ProviderId GUID, ProviderVersion TEXT, FileStamp GUID, LastRefreshStatus TEXT, LastRefreshed datetime, PRIMARY KEY (ItemId, ProviderId))",
  48. "create index if not exists idx_providerinfos on providerinfos(ItemId, ProviderId)",
  49. "create table if not exists MetadataStatus (ItemId GUID PRIMARY KEY, DateLastMetadataRefresh datetime, DateLastImagesRefresh datetime, LastStatus TEXT, LastErrorMessage TEXT, MetadataProvidersRefreshed TEXT, ImageProvidersRefreshed TEXT)",
  50. "create index if not exists idx_MetadataStatus on MetadataStatus(ItemId)",
  51. //pragmas
  52. "pragma temp_store = memory",
  53. "pragma shrink_memory"
  54. };
  55. _connection.RunQueries(queries, _logger);
  56. PrepareStatements();
  57. _shrinkMemoryTimer = new SqliteShrinkMemoryTimer(_connection, _writeLock, _logger);
  58. }
  59. private static readonly string[] SaveHistoryColumns =
  60. {
  61. "ItemId",
  62. "ProviderId",
  63. "ProviderVersion",
  64. "FileStamp",
  65. "LastRefreshStatus",
  66. "LastRefreshed"
  67. };
  68. private readonly string[] _historySelectColumns = SaveHistoryColumns.Skip(1).ToArray();
  69. private static readonly string[] StatusColumns =
  70. {
  71. "ItemId",
  72. "DateLastMetadataRefresh",
  73. "DateLastImagesRefresh",
  74. "LastStatus",
  75. "LastErrorMessage",
  76. "MetadataProvidersRefreshed",
  77. "ImageProvidersRefreshed"
  78. };
  79. /// <summary>
  80. /// The _write lock
  81. /// </summary>
  82. private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
  83. /// <summary>
  84. /// Prepares the statements.
  85. /// </summary>
  86. private void PrepareStatements()
  87. {
  88. _deleteInfosCommand = _connection.CreateCommand();
  89. _deleteInfosCommand.CommandText = "delete from providerinfos where ItemId=@ItemId";
  90. _deleteInfosCommand.Parameters.Add(_deleteInfosCommand, "@ItemId");
  91. _saveInfoCommand = _connection.CreateCommand();
  92. _saveInfoCommand.CommandText = string.Format("replace into providerinfos ({0}) values ({1})",
  93. string.Join(",", SaveHistoryColumns),
  94. string.Join(",", SaveHistoryColumns.Select(i => "@" + i).ToArray()));
  95. foreach (var col in SaveHistoryColumns)
  96. {
  97. _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@" + col);
  98. }
  99. _saveStatusCommand = _connection.CreateCommand();
  100. _saveStatusCommand.CommandText = string.Format("replace into MetadataStatus ({0}) values ({1})",
  101. string.Join(",", StatusColumns),
  102. string.Join(",", StatusColumns.Select(i => "@" + i).ToArray()));
  103. foreach (var col in StatusColumns)
  104. {
  105. _saveStatusCommand.Parameters.Add(_saveStatusCommand, "@" + col);
  106. }
  107. }
  108. public IEnumerable<BaseProviderInfo> GetProviderHistory(Guid itemId)
  109. {
  110. if (itemId == Guid.Empty)
  111. {
  112. throw new ArgumentNullException("itemId");
  113. }
  114. using (var cmd = _connection.CreateCommand())
  115. {
  116. var cmdText = "select " + string.Join(",", _historySelectColumns) + " from providerinfos where";
  117. cmdText += " ItemId=@ItemId";
  118. cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = itemId;
  119. cmd.CommandText = cmdText;
  120. using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult))
  121. {
  122. while (reader.Read())
  123. {
  124. yield return GetBaseProviderInfo(reader);
  125. }
  126. }
  127. }
  128. }
  129. /// <summary>
  130. /// Gets the base provider information.
  131. /// </summary>
  132. /// <param name="reader">The reader.</param>
  133. /// <returns>BaseProviderInfo.</returns>
  134. private BaseProviderInfo GetBaseProviderInfo(IDataReader reader)
  135. {
  136. var item = new BaseProviderInfo
  137. {
  138. ProviderId = reader.GetGuid(0)
  139. };
  140. if (!reader.IsDBNull(1))
  141. {
  142. item.ProviderVersion = reader.GetString(1);
  143. }
  144. item.FileStamp = reader.GetGuid(2);
  145. item.LastRefreshStatus = (ProviderRefreshStatus)Enum.Parse(typeof(ProviderRefreshStatus), reader.GetString(3), true);
  146. item.LastRefreshed = reader.GetDateTime(4).ToUniversalTime();
  147. return item;
  148. }
  149. public async Task SaveProviderHistory(Guid id, IEnumerable<BaseProviderInfo> infos, CancellationToken cancellationToken)
  150. {
  151. if (id == Guid.Empty)
  152. {
  153. throw new ArgumentNullException("id");
  154. }
  155. if (infos == null)
  156. {
  157. throw new ArgumentNullException("infos");
  158. }
  159. cancellationToken.ThrowIfCancellationRequested();
  160. await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
  161. IDbTransaction transaction = null;
  162. try
  163. {
  164. transaction = _connection.BeginTransaction();
  165. _deleteInfosCommand.GetParameter(0).Value = id;
  166. _deleteInfosCommand.Transaction = transaction;
  167. _deleteInfosCommand.ExecuteNonQuery();
  168. foreach (var stream in infos)
  169. {
  170. cancellationToken.ThrowIfCancellationRequested();
  171. _saveInfoCommand.GetParameter(0).Value = id;
  172. _saveInfoCommand.GetParameter(1).Value = stream.ProviderId;
  173. _saveInfoCommand.GetParameter(2).Value = stream.ProviderVersion;
  174. _saveInfoCommand.GetParameter(3).Value = stream.FileStamp;
  175. _saveInfoCommand.GetParameter(4).Value = stream.LastRefreshStatus.ToString();
  176. _saveInfoCommand.GetParameter(5).Value = stream.LastRefreshed;
  177. _saveInfoCommand.Transaction = transaction;
  178. _saveInfoCommand.ExecuteNonQuery();
  179. }
  180. transaction.Commit();
  181. }
  182. catch (OperationCanceledException)
  183. {
  184. if (transaction != null)
  185. {
  186. transaction.Rollback();
  187. }
  188. throw;
  189. }
  190. catch (Exception e)
  191. {
  192. _logger.ErrorException("Failed to save provider info:", e);
  193. if (transaction != null)
  194. {
  195. transaction.Rollback();
  196. }
  197. throw;
  198. }
  199. finally
  200. {
  201. if (transaction != null)
  202. {
  203. transaction.Dispose();
  204. }
  205. _writeLock.Release();
  206. }
  207. }
  208. public MetadataStatus GetMetadataStatus(Guid itemId)
  209. {
  210. if (itemId == Guid.Empty)
  211. {
  212. throw new ArgumentNullException("itemId");
  213. }
  214. using (var cmd = _connection.CreateCommand())
  215. {
  216. var cmdText = "select " + string.Join(",", StatusColumns) + " from MetadataStatus where";
  217. cmdText += " ItemId=@ItemId";
  218. cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = itemId;
  219. cmd.CommandText = cmdText;
  220. using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow))
  221. {
  222. while (reader.Read())
  223. {
  224. return GetStatus(reader);
  225. }
  226. return null;
  227. }
  228. }
  229. }
  230. private MetadataStatus GetStatus(IDataReader reader)
  231. {
  232. var result = new MetadataStatus
  233. {
  234. ItemId = reader.GetGuid(0)
  235. };
  236. if (!reader.IsDBNull(1))
  237. {
  238. result.DateLastMetadataRefresh = reader.GetDateTime(1).ToUniversalTime();
  239. }
  240. if (!reader.IsDBNull(2))
  241. {
  242. result.DateLastImagesRefresh = reader.GetDateTime(2).ToUniversalTime();
  243. }
  244. if (!reader.IsDBNull(3))
  245. {
  246. result.LastStatus = (ProviderRefreshStatus)Enum.Parse(typeof(ProviderRefreshStatus), reader.GetString(3), true);
  247. }
  248. if (!reader.IsDBNull(4))
  249. {
  250. result.LastErrorMessage = reader.GetString(4);
  251. }
  252. if (!reader.IsDBNull(5))
  253. {
  254. result.MetadataProvidersRefreshed = reader.GetString(5).Split('|').Where(i => !string.IsNullOrEmpty(i)).Select(i => new Guid(i)).ToList();
  255. }
  256. if (!reader.IsDBNull(6))
  257. {
  258. result.ImageProvidersRefreshed = reader.GetString(6).Split('|').Where(i => !string.IsNullOrEmpty(i)).Select(i => new Guid(i)).ToList();
  259. }
  260. return result;
  261. }
  262. public async Task SaveMetadataStatus(MetadataStatus status, CancellationToken cancellationToken)
  263. {
  264. if (status == null)
  265. {
  266. throw new ArgumentNullException("status");
  267. }
  268. cancellationToken.ThrowIfCancellationRequested();
  269. await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
  270. IDbTransaction transaction = null;
  271. try
  272. {
  273. transaction = _connection.BeginTransaction();
  274. _saveStatusCommand.GetParameter(0).Value = status.ItemId;
  275. _saveStatusCommand.GetParameter(1).Value = status.DateLastMetadataRefresh;
  276. _saveStatusCommand.GetParameter(2).Value = status.DateLastImagesRefresh;
  277. _saveStatusCommand.GetParameter(3).Value = status.LastStatus.ToString();
  278. _saveStatusCommand.GetParameter(4).Value = status.LastErrorMessage;
  279. _saveStatusCommand.GetParameter(5).Value = string.Join("|", status.MetadataProvidersRefreshed.ToArray());
  280. _saveStatusCommand.GetParameter(6).Value = string.Join("|", status.ImageProvidersRefreshed.ToArray());
  281. _saveStatusCommand.Transaction = transaction;
  282. _saveStatusCommand.ExecuteNonQuery();
  283. transaction.Commit();
  284. }
  285. catch (OperationCanceledException)
  286. {
  287. if (transaction != null)
  288. {
  289. transaction.Rollback();
  290. }
  291. throw;
  292. }
  293. catch (Exception e)
  294. {
  295. _logger.ErrorException("Failed to save provider info:", e);
  296. if (transaction != null)
  297. {
  298. transaction.Rollback();
  299. }
  300. throw;
  301. }
  302. finally
  303. {
  304. if (transaction != null)
  305. {
  306. transaction.Dispose();
  307. }
  308. _writeLock.Release();
  309. }
  310. }
  311. /// <summary>
  312. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
  313. /// </summary>
  314. public void Dispose()
  315. {
  316. Dispose(true);
  317. GC.SuppressFinalize(this);
  318. }
  319. private readonly object _disposeLock = new object();
  320. /// <summary>
  321. /// Releases unmanaged and - optionally - managed resources.
  322. /// </summary>
  323. /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  324. protected virtual void Dispose(bool dispose)
  325. {
  326. if (dispose)
  327. {
  328. try
  329. {
  330. lock (_disposeLock)
  331. {
  332. if (_shrinkMemoryTimer != null)
  333. {
  334. _shrinkMemoryTimer.Dispose();
  335. _shrinkMemoryTimer = null;
  336. }
  337. if (_connection != null)
  338. {
  339. if (_connection.IsOpen())
  340. {
  341. _connection.Close();
  342. }
  343. _connection.Dispose();
  344. _connection = null;
  345. }
  346. }
  347. }
  348. catch (Exception ex)
  349. {
  350. _logger.ErrorException("Error disposing database", ex);
  351. }
  352. }
  353. }
  354. }
  355. }