SQLiteUserDataRepository.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Controller.Entities;
  3. using MediaBrowser.Controller.Persistence;
  4. using MediaBrowser.Model.Logging;
  5. using MediaBrowser.Model.Serialization;
  6. using System;
  7. using System.Collections.Concurrent;
  8. using System.Data;
  9. using System.IO;
  10. using System.Threading;
  11. using System.Threading.Tasks;
  12. namespace MediaBrowser.Server.Implementations.Sqlite
  13. {
  14. /// <summary>
  15. /// Class SQLiteUserDataRepository
  16. /// </summary>
  17. public class SQLiteUserDataRepository : SqliteRepository, IUserDataRepository
  18. {
  19. private readonly ConcurrentDictionary<string, Task<UserItemData>> _userData = new ConcurrentDictionary<string, Task<UserItemData>>();
  20. /// <summary>
  21. /// The repository name
  22. /// </summary>
  23. public const string RepositoryName = "SQLite";
  24. /// <summary>
  25. /// Gets the name of the repository
  26. /// </summary>
  27. /// <value>The name.</value>
  28. public string Name
  29. {
  30. get
  31. {
  32. return RepositoryName;
  33. }
  34. }
  35. /// <summary>
  36. /// Gets a value indicating whether [enable delayed commands].
  37. /// </summary>
  38. /// <value><c>true</c> if [enable delayed commands]; otherwise, <c>false</c>.</value>
  39. protected override bool EnableDelayedCommands
  40. {
  41. get
  42. {
  43. return false;
  44. }
  45. }
  46. private readonly IJsonSerializer _jsonSerializer;
  47. /// <summary>
  48. /// The _app paths
  49. /// </summary>
  50. private readonly IApplicationPaths _appPaths;
  51. /// <summary>
  52. /// Initializes a new instance of the <see cref="SQLiteUserDataRepository" /> class.
  53. /// </summary>
  54. /// <param name="appPaths">The app paths.</param>
  55. /// <param name="jsonSerializer">The json serializer.</param>
  56. /// <param name="logManager">The log manager.</param>
  57. /// <exception cref="System.ArgumentNullException">
  58. /// jsonSerializer
  59. /// or
  60. /// appPaths
  61. /// </exception>
  62. public SQLiteUserDataRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager)
  63. : base(logManager)
  64. {
  65. if (jsonSerializer == null)
  66. {
  67. throw new ArgumentNullException("jsonSerializer");
  68. }
  69. if (appPaths == null)
  70. {
  71. throw new ArgumentNullException("appPaths");
  72. }
  73. _jsonSerializer = jsonSerializer;
  74. _appPaths = appPaths;
  75. }
  76. /// <summary>
  77. /// Opens the connection to the database
  78. /// </summary>
  79. /// <returns>Task.</returns>
  80. public async Task Initialize()
  81. {
  82. var dbFile = Path.Combine(_appPaths.DataPath, "userdata.db");
  83. await ConnectToDb(dbFile).ConfigureAwait(false);
  84. string[] queries = {
  85. "create table if not exists useritemdata (key nvarchar, userId GUID, Rating float null, PlaybackPositionTicks bigint, PlayCount int, IsFavorite bit, Played bit, LastPlayedDate bigint null)",
  86. "create unique index if not exists useritemdataindex on useritemdata (key, userId)",
  87. "create table if not exists schema_version (table_name primary key, version)",
  88. //pragmas
  89. "pragma temp_store = memory"
  90. };
  91. RunQueries(queries);
  92. }
  93. /// <summary>
  94. /// Saves the user data.
  95. /// </summary>
  96. /// <param name="userId">The user id.</param>
  97. /// <param name="key">The key.</param>
  98. /// <param name="userData">The user data.</param>
  99. /// <param name="cancellationToken">The cancellation token.</param>
  100. /// <returns>Task.</returns>
  101. /// <exception cref="System.ArgumentNullException">userData
  102. /// or
  103. /// cancellationToken
  104. /// or
  105. /// userId
  106. /// or
  107. /// userDataId</exception>
  108. public async Task SaveUserData(Guid userId, string key, UserItemData userData, CancellationToken cancellationToken)
  109. {
  110. if (userData == null)
  111. {
  112. throw new ArgumentNullException("userData");
  113. }
  114. if (cancellationToken == null)
  115. {
  116. throw new ArgumentNullException("cancellationToken");
  117. }
  118. if (userId == Guid.Empty)
  119. {
  120. throw new ArgumentNullException("userId");
  121. }
  122. if (string.IsNullOrEmpty(key))
  123. {
  124. throw new ArgumentNullException("key");
  125. }
  126. cancellationToken.ThrowIfCancellationRequested();
  127. try
  128. {
  129. await PersistUserData(userId, key, userData, cancellationToken).ConfigureAwait(false);
  130. var newValue = Task.FromResult(userData);
  131. // Once it succeeds, put it into the dictionary to make it available to everyone else
  132. _userData.AddOrUpdate(GetInternalKey(userId, key), newValue, delegate { return newValue; });
  133. }
  134. catch (Exception ex)
  135. {
  136. Logger.ErrorException("Error saving user data", ex);
  137. throw;
  138. }
  139. }
  140. /// <summary>
  141. /// Gets the internal key.
  142. /// </summary>
  143. /// <param name="userId">The user id.</param>
  144. /// <param name="key">The key.</param>
  145. /// <returns>System.String.</returns>
  146. private string GetInternalKey(Guid userId, string key)
  147. {
  148. return userId + key;
  149. }
  150. /// <summary>
  151. /// Persists the user data.
  152. /// </summary>
  153. /// <param name="userId">The user id.</param>
  154. /// <param name="key">The key.</param>
  155. /// <param name="userData">The user data.</param>
  156. /// <param name="cancellationToken">The cancellation token.</param>
  157. /// <returns>Task.</returns>
  158. public async Task PersistUserData(Guid userId, string key, UserItemData userData, CancellationToken cancellationToken)
  159. {
  160. cancellationToken.ThrowIfCancellationRequested();
  161. using (var cmd = connection.CreateCommand())
  162. {
  163. cmd.CommandText = "replace into useritemdata (key, userId, Rating,PlaybackPositionTicks,PlayCount,IsFavorite,Played,LastPlayedDate) values (@1, @2, @3, @4, @5, @6, @7, @8)";
  164. cmd.AddParam("@1", key);
  165. cmd.AddParam("@2", userId);
  166. cmd.AddParam("@3", userData.Rating);
  167. cmd.AddParam("@4", userData.PlaybackPositionTicks);
  168. cmd.AddParam("@5", userData.PlayCount);
  169. cmd.AddParam("@6", userData.IsFavorite);
  170. cmd.AddParam("@7", userData.Played);
  171. if (userData.LastPlayedDate.HasValue)
  172. {
  173. cmd.AddParam("@8", userData.LastPlayedDate.Value.Ticks);
  174. }
  175. else
  176. {
  177. cmd.AddParam("@8", null);
  178. }
  179. using (var tran = connection.BeginTransaction())
  180. {
  181. try
  182. {
  183. cmd.Transaction = tran;
  184. await cmd.ExecuteNonQueryAsync(cancellationToken);
  185. tran.Commit();
  186. }
  187. catch (OperationCanceledException)
  188. {
  189. tran.Rollback();
  190. }
  191. catch (Exception e)
  192. {
  193. Logger.ErrorException("Failed to commit transaction.", e);
  194. tran.Rollback();
  195. }
  196. }
  197. }
  198. }
  199. /// <summary>
  200. /// Gets the user data.
  201. /// </summary>
  202. /// <param name="userId">The user id.</param>
  203. /// <param name="key">The key.</param>
  204. /// <returns>Task{UserItemData}.</returns>
  205. /// <exception cref="System.ArgumentNullException">
  206. /// userId
  207. /// or
  208. /// key
  209. /// </exception>
  210. public Task<UserItemData> GetUserData(Guid userId, string key)
  211. {
  212. if (userId == Guid.Empty)
  213. {
  214. throw new ArgumentNullException("userId");
  215. }
  216. if (string.IsNullOrEmpty(key))
  217. {
  218. throw new ArgumentNullException("key");
  219. }
  220. return _userData.GetOrAdd(GetInternalKey(userId, key), keyName => RetrieveUserData(userId, key));
  221. }
  222. /// <summary>
  223. /// Retrieves the user data.
  224. /// </summary>
  225. /// <param name="userId">The user id.</param>
  226. /// <param name="key">The key.</param>
  227. /// <returns>Task{UserItemData}.</returns>
  228. private async Task<UserItemData> RetrieveUserData(Guid userId, string key)
  229. {
  230. using (var cmd = connection.CreateCommand())
  231. {
  232. cmd.CommandText = "select Rating,PlaybackPositionTicks,PlayCount,IsFavorite,Played,LastPlayedDate from useritemdata where key = @key and userId=@userId";
  233. var idParam = cmd.Parameters.Add("@key", DbType.String);
  234. idParam.Value = key;
  235. var userIdParam = cmd.Parameters.Add("@userId", DbType.Guid);
  236. userIdParam.Value = userId;
  237. var userdata = new UserItemData();
  238. using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow).ConfigureAwait(false))
  239. {
  240. if (reader.Read())
  241. {
  242. if (!reader.IsDBNull(0))
  243. {
  244. userdata.Rating = reader.GetDouble(0);
  245. }
  246. userdata.PlaybackPositionTicks = reader.GetInt64(1);
  247. userdata.PlayCount = reader.GetInt32(2);
  248. userdata.IsFavorite = reader.GetBoolean(3);
  249. userdata.Played = reader.GetBoolean(4);
  250. if (!reader.IsDBNull(5))
  251. {
  252. userdata.LastPlayedDate = new DateTime(reader.GetInt64(5));
  253. }
  254. }
  255. }
  256. return userdata;
  257. }
  258. }
  259. }
  260. }