SqliteNotificationsRepository.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. using MediaBrowser.Controller.Notifications;
  2. using MediaBrowser.Model.Logging;
  3. using MediaBrowser.Model.Notifications;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Data;
  7. using System.Linq;
  8. using System.Threading;
  9. using System.Threading.Tasks;
  10. namespace MediaBrowser.Server.Implementations.Persistence
  11. {
  12. public class SqliteNotificationsRepository : INotificationsRepository
  13. {
  14. private readonly IDbConnection _connection;
  15. private readonly ILogger _logger;
  16. private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
  17. public SqliteNotificationsRepository(IDbConnection connection, ILogManager logManager)
  18. {
  19. _connection = connection;
  20. _logger = logManager.GetLogger(GetType().Name);
  21. }
  22. public event EventHandler<NotificationUpdateEventArgs> NotificationAdded;
  23. public event EventHandler<NotificationReadEventArgs> NotificationsMarkedRead;
  24. public event EventHandler<NotificationUpdateEventArgs> NotificationUpdated;
  25. private IDbCommand _replaceNotificationCommand;
  26. private IDbCommand _markReadCommand;
  27. public void Initialize()
  28. {
  29. string[] queries = {
  30. "create table if not exists Notifications (Id GUID NOT NULL, UserId GUID NOT NULL, Date DATETIME NOT NULL, Name TEXT NOT NULL, Description TEXT, Url TEXT, Level TEXT NOT NULL, IsRead BOOLEAN NOT NULL, Category TEXT NOT NULL, RelatedId TEXT, PRIMARY KEY (Id, UserId))",
  31. "create index if not exists idx_Notifications on Notifications(Id, UserId)",
  32. //pragmas
  33. "pragma temp_store = memory"
  34. };
  35. _connection.RunQueries(queries, _logger);
  36. PrepareStatements();
  37. }
  38. private void PrepareStatements()
  39. {
  40. _replaceNotificationCommand = _connection.CreateCommand();
  41. _replaceNotificationCommand.CommandText = "replace into Notifications (Id, UserId, Date, Name, Description, Url, Level, IsRead, Category, RelatedId) values (@Id, @UserId, @Date, @Name, @Description, @Url, @Level, @IsRead, @Category, @RelatedId)";
  42. _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@Id");
  43. _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@UserId");
  44. _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@Date");
  45. _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@Name");
  46. _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@Description");
  47. _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@Url");
  48. _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@Level");
  49. _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@IsRead");
  50. _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@Category");
  51. _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@RelatedId");
  52. _markReadCommand = _connection.CreateCommand();
  53. _markReadCommand.CommandText = "update Notifications set IsRead=@IsRead where Id=@Id and UserId=@UserId";
  54. _markReadCommand.Parameters.Add(_replaceNotificationCommand, "@UserId");
  55. _markReadCommand.Parameters.Add(_replaceNotificationCommand, "@IsRead");
  56. _markReadCommand.Parameters.Add(_replaceNotificationCommand, "@Id");
  57. }
  58. /// <summary>
  59. /// Gets the notifications.
  60. /// </summary>
  61. /// <param name="query">The query.</param>
  62. /// <returns>NotificationResult.</returns>
  63. public NotificationResult GetNotifications(NotificationQuery query)
  64. {
  65. var whereClause = string.Empty;
  66. var result = new NotificationResult();
  67. using (var cmd = _connection.CreateCommand())
  68. {
  69. if (query.IsRead.HasValue || query.UserId.HasValue)
  70. {
  71. var clauses = new List<string>();
  72. if (query.IsRead.HasValue)
  73. {
  74. clauses.Add("IsRead=@IsRead");
  75. cmd.Parameters.Add(cmd, "@IsRead", DbType.Boolean).Value = query.IsRead.Value;
  76. }
  77. if (query.UserId.HasValue)
  78. {
  79. clauses.Add("UserId=@UserId");
  80. cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.UserId.Value;
  81. }
  82. whereClause = " where " + string.Join(" And ", clauses.ToArray());
  83. }
  84. cmd.CommandText = string.Format("select count(Id) from Notifications{0};select Id,UserId,Date,Name,Description,Url,Level,IsRead,Category,RelatedId from Notifications{0} order by IsRead asc, Date desc", whereClause);
  85. using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess))
  86. {
  87. if (reader.Read())
  88. {
  89. result.TotalRecordCount = reader.GetInt32(0);
  90. }
  91. if (reader.NextResult())
  92. {
  93. var notifications = GetNotifications(reader);
  94. if (query.StartIndex.HasValue)
  95. {
  96. notifications = notifications.Skip(query.StartIndex.Value);
  97. }
  98. if (query.Limit.HasValue)
  99. {
  100. notifications = notifications.Take(query.Limit.Value);
  101. }
  102. result.Notifications = notifications.ToArray();
  103. }
  104. }
  105. return result;
  106. }
  107. }
  108. public NotificationsSummary GetNotificationsSummary(Guid userId)
  109. {
  110. var result = new NotificationsSummary();
  111. using (var cmd = _connection.CreateCommand())
  112. {
  113. cmd.CommandText = "select Level from Notifications where UserId=@UserId and IsRead=@IsRead";
  114. cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = userId;
  115. cmd.Parameters.Add(cmd, "@IsRead", DbType.Boolean).Value = false;
  116. using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess))
  117. {
  118. var levels = new List<NotificationLevel>();
  119. while (reader.Read())
  120. {
  121. levels.Add(GetLevel(reader, 0));
  122. }
  123. result.UnreadCount = levels.Count;
  124. if (levels.Count > 0)
  125. {
  126. result.MaxUnreadNotificationLevel = levels.Max();
  127. }
  128. }
  129. return result;
  130. }
  131. }
  132. /// <summary>
  133. /// Gets the notifications.
  134. /// </summary>
  135. /// <param name="reader">The reader.</param>
  136. /// <returns>IEnumerable{Notification}.</returns>
  137. private IEnumerable<Notification> GetNotifications(IDataReader reader)
  138. {
  139. while (reader.Read())
  140. {
  141. yield return GetNotification(reader);
  142. }
  143. }
  144. private Notification GetNotification(IDataReader reader)
  145. {
  146. var notification = new Notification
  147. {
  148. Id = reader.GetGuid(0),
  149. UserId = reader.GetGuid(1),
  150. Date = reader.GetDateTime(2).ToUniversalTime(),
  151. Name = reader.GetString(3)
  152. };
  153. if (!reader.IsDBNull(4))
  154. {
  155. notification.Description = reader.GetString(4);
  156. }
  157. if (!reader.IsDBNull(5))
  158. {
  159. notification.Url = reader.GetString(5);
  160. }
  161. notification.Level = GetLevel(reader, 6);
  162. notification.IsRead = reader.GetBoolean(7);
  163. notification.Category = reader.GetString(8);
  164. if (!reader.IsDBNull(9))
  165. {
  166. notification.RelatedId = reader.GetString(9);
  167. }
  168. return notification;
  169. }
  170. /// <summary>
  171. /// Gets the notification.
  172. /// </summary>
  173. /// <param name="id">The id.</param>
  174. /// <param name="userId">The user id.</param>
  175. /// <returns>Notification.</returns>
  176. /// <exception cref="System.ArgumentNullException">
  177. /// id
  178. /// or
  179. /// userId
  180. /// </exception>
  181. public Notification GetNotification(Guid id, Guid userId)
  182. {
  183. if (id == Guid.Empty)
  184. {
  185. throw new ArgumentNullException("id");
  186. }
  187. if (userId == Guid.Empty)
  188. {
  189. throw new ArgumentNullException("userId");
  190. }
  191. using (var cmd = _connection.CreateCommand())
  192. {
  193. cmd.CommandText = "select Id,UserId,Date,Name,Description,Url,Level,IsRead,Category,RelatedId where Id=@Id And UserId = @UserId";
  194. cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = id;
  195. cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = userId;
  196. using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow))
  197. {
  198. if (reader.Read())
  199. {
  200. return GetNotification(reader);
  201. }
  202. }
  203. return null;
  204. }
  205. }
  206. /// <summary>
  207. /// Gets the level.
  208. /// </summary>
  209. /// <param name="reader">The reader.</param>
  210. /// <param name="index">The index.</param>
  211. /// <returns>NotificationLevel.</returns>
  212. private NotificationLevel GetLevel(IDataReader reader, int index)
  213. {
  214. NotificationLevel level;
  215. var val = reader.GetString(index);
  216. Enum.TryParse(val, true, out level);
  217. return level;
  218. }
  219. /// <summary>
  220. /// Adds the notification.
  221. /// </summary>
  222. /// <param name="notification">The notification.</param>
  223. /// <param name="cancellationToken">The cancellation token.</param>
  224. /// <returns>Task.</returns>
  225. public async Task AddNotification(Notification notification, CancellationToken cancellationToken)
  226. {
  227. await ReplaceNotification(notification, cancellationToken).ConfigureAwait(false);
  228. if (NotificationAdded != null)
  229. {
  230. try
  231. {
  232. NotificationAdded(this, new NotificationUpdateEventArgs
  233. {
  234. Notification = notification
  235. });
  236. }
  237. catch (Exception ex)
  238. {
  239. _logger.ErrorException("Error in NotificationAdded event handler", ex);
  240. }
  241. }
  242. }
  243. /// <summary>
  244. /// Updates the notification.
  245. /// </summary>
  246. /// <param name="notification">The notification.</param>
  247. /// <param name="cancellationToken">The cancellation token.</param>
  248. /// <returns>Task.</returns>
  249. public async Task UpdateNotification(Notification notification, CancellationToken cancellationToken)
  250. {
  251. await ReplaceNotification(notification, cancellationToken).ConfigureAwait(false);
  252. if (NotificationUpdated != null)
  253. {
  254. try
  255. {
  256. NotificationUpdated(this, new NotificationUpdateEventArgs
  257. {
  258. Notification = notification
  259. });
  260. }
  261. catch (Exception ex)
  262. {
  263. _logger.ErrorException("Error in NotificationUpdated event handler", ex);
  264. }
  265. }
  266. }
  267. /// <summary>
  268. /// Replaces the notification.
  269. /// </summary>
  270. /// <param name="notification">The notification.</param>
  271. /// <param name="cancellationToken">The cancellation token.</param>
  272. /// <returns>Task.</returns>
  273. private async Task ReplaceNotification(Notification notification, CancellationToken cancellationToken)
  274. {
  275. if (notification.Id == Guid.Empty)
  276. {
  277. throw new ArgumentException("The notification must have an id");
  278. }
  279. if (notification.UserId == Guid.Empty)
  280. {
  281. throw new ArgumentException("The notification must have a user id");
  282. }
  283. cancellationToken.ThrowIfCancellationRequested();
  284. await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
  285. IDbTransaction transaction = null;
  286. try
  287. {
  288. transaction = _connection.BeginTransaction();
  289. _replaceNotificationCommand.GetParameter(0).Value = notification.Id;
  290. _replaceNotificationCommand.GetParameter(1).Value = notification.UserId;
  291. _replaceNotificationCommand.GetParameter(2).Value = notification.Date.ToUniversalTime();
  292. _replaceNotificationCommand.GetParameter(3).Value = notification.Name;
  293. _replaceNotificationCommand.GetParameter(4).Value = notification.Description;
  294. _replaceNotificationCommand.GetParameter(5).Value = notification.Url;
  295. _replaceNotificationCommand.GetParameter(6).Value = notification.Level.ToString();
  296. _replaceNotificationCommand.GetParameter(7).Value = notification.IsRead;
  297. _replaceNotificationCommand.GetParameter(8).Value = notification.Category;
  298. _replaceNotificationCommand.GetParameter(9).Value = notification.RelatedId;
  299. _replaceNotificationCommand.Transaction = transaction;
  300. _replaceNotificationCommand.ExecuteNonQuery();
  301. transaction.Commit();
  302. }
  303. catch (OperationCanceledException)
  304. {
  305. if (transaction != null)
  306. {
  307. transaction.Rollback();
  308. }
  309. throw;
  310. }
  311. catch (Exception e)
  312. {
  313. _logger.ErrorException("Failed to save notification:", e);
  314. if (transaction != null)
  315. {
  316. transaction.Rollback();
  317. }
  318. throw;
  319. }
  320. finally
  321. {
  322. if (transaction != null)
  323. {
  324. transaction.Dispose();
  325. }
  326. _writeLock.Release();
  327. }
  328. }
  329. /// <summary>
  330. /// Marks the read.
  331. /// </summary>
  332. /// <param name="notificationIdList">The notification id list.</param>
  333. /// <param name="userId">The user id.</param>
  334. /// <param name="isRead">if set to <c>true</c> [is read].</param>
  335. /// <param name="cancellationToken">The cancellation token.</param>
  336. /// <returns>Task.</returns>
  337. public async Task MarkRead(IEnumerable<Guid> notificationIdList, Guid userId, bool isRead, CancellationToken cancellationToken)
  338. {
  339. var idArray = notificationIdList.ToArray();
  340. await MarkReadInternal(idArray, userId, isRead, cancellationToken).ConfigureAwait(false);
  341. if (NotificationsMarkedRead != null)
  342. {
  343. try
  344. {
  345. NotificationsMarkedRead(this, new NotificationReadEventArgs
  346. {
  347. IdList = idArray.ToArray(),
  348. IsRead = isRead,
  349. UserId = userId
  350. });
  351. }
  352. catch (Exception ex)
  353. {
  354. _logger.ErrorException("Error in NotificationsMarkedRead event handler", ex);
  355. }
  356. }
  357. }
  358. private async Task MarkReadInternal(IEnumerable<Guid> notificationIdList, Guid userId, bool isRead, CancellationToken cancellationToken)
  359. {
  360. cancellationToken.ThrowIfCancellationRequested();
  361. await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
  362. IDbTransaction transaction = null;
  363. try
  364. {
  365. cancellationToken.ThrowIfCancellationRequested();
  366. transaction = _connection.BeginTransaction();
  367. _markReadCommand.GetParameter(0).Value = userId;
  368. _markReadCommand.GetParameter(1).Value = isRead;
  369. foreach (var id in notificationIdList)
  370. {
  371. _markReadCommand.GetParameter(2).Value = id;
  372. _markReadCommand.Transaction = transaction;
  373. _markReadCommand.ExecuteNonQuery();
  374. }
  375. transaction.Commit();
  376. }
  377. catch (OperationCanceledException)
  378. {
  379. if (transaction != null)
  380. {
  381. transaction.Rollback();
  382. }
  383. throw;
  384. }
  385. catch (Exception e)
  386. {
  387. _logger.ErrorException("Failed to save notification:", e);
  388. if (transaction != null)
  389. {
  390. transaction.Rollback();
  391. }
  392. throw;
  393. }
  394. finally
  395. {
  396. if (transaction != null)
  397. {
  398. transaction.Dispose();
  399. }
  400. _writeLock.Release();
  401. }
  402. }
  403. }
  404. }