| | 1 | | #nullable disable |
| | 2 | |
|
| | 3 | | #pragma warning disable CS1591 |
| | 4 | |
|
| | 5 | | using System; |
| | 6 | | using System.Collections.Generic; |
| | 7 | | using System.IO; |
| | 8 | | using System.Threading; |
| | 9 | | using Jellyfin.Data.Entities; |
| | 10 | | using MediaBrowser.Controller.Configuration; |
| | 11 | | using MediaBrowser.Controller.Entities; |
| | 12 | | using MediaBrowser.Controller.Library; |
| | 13 | | using MediaBrowser.Controller.Persistence; |
| | 14 | | using Microsoft.Data.Sqlite; |
| | 15 | | using Microsoft.Extensions.Logging; |
| | 16 | |
|
| | 17 | | namespace Emby.Server.Implementations.Data |
| | 18 | | { |
| | 19 | | public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository |
| | 20 | | { |
| | 21 | | private readonly IUserManager _userManager; |
| | 22 | |
|
| | 23 | | public SqliteUserDataRepository( |
| | 24 | | ILogger<SqliteUserDataRepository> logger, |
| | 25 | | IServerConfigurationManager config, |
| | 26 | | IUserManager userManager) |
| 22 | 27 | | : base(logger) |
| | 28 | | { |
| 22 | 29 | | _userManager = userManager; |
| | 30 | |
|
| 22 | 31 | | DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db"); |
| 22 | 32 | | } |
| | 33 | |
|
| | 34 | | /// <summary> |
| | 35 | | /// Opens the connection to the database. |
| | 36 | | /// </summary> |
| | 37 | | public override void Initialize() |
| | 38 | | { |
| 22 | 39 | | base.Initialize(); |
| | 40 | |
|
| 22 | 41 | | using (var connection = GetConnection()) |
| | 42 | | { |
| 22 | 43 | | var userDatasTableExists = TableExists(connection, "UserDatas"); |
| 22 | 44 | | var userDataTableExists = TableExists(connection, "userdata"); |
| | 45 | |
|
| 22 | 46 | | var users = userDatasTableExists ? null : _userManager.Users; |
| 22 | 47 | | using var transaction = connection.BeginTransaction(); |
| 22 | 48 | | connection.Execute(string.Join( |
| 22 | 49 | | ';', |
| 22 | 50 | | "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null |
| 22 | 51 | | "drop index if exists idx_userdata", |
| 22 | 52 | | "drop index if exists idx_userdata1", |
| 22 | 53 | | "drop index if exists idx_userdata2", |
| 22 | 54 | | "drop index if exists userdataindex1", |
| 22 | 55 | | "drop index if exists userdataindex", |
| 22 | 56 | | "drop index if exists userdataindex3", |
| 22 | 57 | | "drop index if exists userdataindex4", |
| 22 | 58 | | "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)", |
| 22 | 59 | | "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)", |
| 22 | 60 | | "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)", |
| 22 | 61 | | "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)", |
| 22 | 62 | | "create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)")); |
| | 63 | |
|
| 22 | 64 | | if (!userDataTableExists) |
| | 65 | | { |
| 22 | 66 | | transaction.Commit(); |
| 22 | 67 | | return; |
| | 68 | | } |
| | 69 | |
|
| 0 | 70 | | var existingColumnNames = GetColumnNames(connection, "userdata"); |
| | 71 | |
|
| 0 | 72 | | AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames); |
| 0 | 73 | | AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames); |
| 0 | 74 | | AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames); |
| | 75 | |
|
| 0 | 76 | | if (userDatasTableExists) |
| | 77 | | { |
| 0 | 78 | | return; |
| | 79 | | } |
| | 80 | |
|
| 0 | 81 | | ImportUserIds(connection, users); |
| | 82 | |
|
| 0 | 83 | | connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackP |
| | 84 | |
|
| 0 | 85 | | transaction.Commit(); |
| | 86 | | } |
| 22 | 87 | | } |
| | 88 | |
|
| | 89 | | private void ImportUserIds(ManagedConnection db, IEnumerable<User> users) |
| | 90 | | { |
| 0 | 91 | | var userIdsWithUserData = GetAllUserIdsWithUserData(db); |
| | 92 | |
|
| 0 | 93 | | using (var statement = db.PrepareStatement("update userdata set InternalUserId=@InternalUserId where UserId= |
| | 94 | | { |
| 0 | 95 | | foreach (var user in users) |
| | 96 | | { |
| 0 | 97 | | if (!userIdsWithUserData.Contains(user.Id)) |
| | 98 | | { |
| | 99 | | continue; |
| | 100 | | } |
| | 101 | |
|
| 0 | 102 | | statement.TryBind("@UserId", user.Id); |
| 0 | 103 | | statement.TryBind("@InternalUserId", user.InternalId); |
| | 104 | |
|
| 0 | 105 | | statement.ExecuteNonQuery(); |
| | 106 | | } |
| | 107 | | } |
| 0 | 108 | | } |
| | 109 | |
|
| | 110 | | private List<Guid> GetAllUserIdsWithUserData(ManagedConnection db) |
| | 111 | | { |
| 0 | 112 | | var list = new List<Guid>(); |
| | 113 | |
|
| 0 | 114 | | using (var statement = PrepareStatement(db, "select DISTINCT UserId from UserData where UserId not null")) |
| | 115 | | { |
| 0 | 116 | | foreach (var row in statement.ExecuteQuery()) |
| | 117 | | { |
| | 118 | | try |
| | 119 | | { |
| 0 | 120 | | list.Add(row.GetGuid(0)); |
| 0 | 121 | | } |
| 0 | 122 | | catch (Exception ex) |
| | 123 | | { |
| 0 | 124 | | Logger.LogError(ex, "Error while getting user"); |
| 0 | 125 | | } |
| | 126 | | } |
| | 127 | | } |
| | 128 | |
|
| 0 | 129 | | return list; |
| | 130 | | } |
| | 131 | |
|
| | 132 | | /// <inheritdoc /> |
| | 133 | | public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken) |
| | 134 | | { |
| 0 | 135 | | ArgumentNullException.ThrowIfNull(userData); |
| | 136 | |
|
| 0 | 137 | | if (userId <= 0) |
| | 138 | | { |
| 0 | 139 | | throw new ArgumentNullException(nameof(userId)); |
| | 140 | | } |
| | 141 | |
|
| 0 | 142 | | ArgumentException.ThrowIfNullOrEmpty(key); |
| | 143 | |
|
| 0 | 144 | | PersistUserData(userId, key, userData, cancellationToken); |
| 0 | 145 | | } |
| | 146 | |
|
| | 147 | | /// <inheritdoc /> |
| | 148 | | public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken) |
| | 149 | | { |
| 0 | 150 | | ArgumentNullException.ThrowIfNull(userData); |
| | 151 | |
|
| 0 | 152 | | if (userId <= 0) |
| | 153 | | { |
| 0 | 154 | | throw new ArgumentNullException(nameof(userId)); |
| | 155 | | } |
| | 156 | |
|
| 0 | 157 | | PersistAllUserData(userId, userData, cancellationToken); |
| 0 | 158 | | } |
| | 159 | |
|
| | 160 | | /// <summary> |
| | 161 | | /// Persists the user data. |
| | 162 | | /// </summary> |
| | 163 | | /// <param name="internalUserId">The user id.</param> |
| | 164 | | /// <param name="key">The key.</param> |
| | 165 | | /// <param name="userData">The user data.</param> |
| | 166 | | /// <param name="cancellationToken">The cancellation token.</param> |
| | 167 | | public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellati |
| | 168 | | { |
| 0 | 169 | | cancellationToken.ThrowIfCancellationRequested(); |
| | 170 | |
|
| 0 | 171 | | using (var connection = GetConnection()) |
| 0 | 172 | | using (var transaction = connection.BeginTransaction()) |
| | 173 | | { |
| 0 | 174 | | SaveUserData(connection, internalUserId, key, userData); |
| 0 | 175 | | transaction.Commit(); |
| 0 | 176 | | } |
| 0 | 177 | | } |
| | 178 | |
|
| | 179 | | private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData) |
| | 180 | | { |
| 0 | 181 | | using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isF |
| | 182 | | { |
| 0 | 183 | | statement.TryBind("@userId", internalUserId); |
| 0 | 184 | | statement.TryBind("@key", key); |
| | 185 | |
|
| 0 | 186 | | if (userData.Rating.HasValue) |
| | 187 | | { |
| 0 | 188 | | statement.TryBind("@rating", userData.Rating.Value); |
| | 189 | | } |
| | 190 | | else |
| | 191 | | { |
| 0 | 192 | | statement.TryBindNull("@rating"); |
| | 193 | | } |
| | 194 | |
|
| 0 | 195 | | statement.TryBind("@played", userData.Played); |
| 0 | 196 | | statement.TryBind("@playCount", userData.PlayCount); |
| 0 | 197 | | statement.TryBind("@isFavorite", userData.IsFavorite); |
| 0 | 198 | | statement.TryBind("@playbackPositionTicks", userData.PlaybackPositionTicks); |
| | 199 | |
|
| 0 | 200 | | if (userData.LastPlayedDate.HasValue) |
| | 201 | | { |
| 0 | 202 | | statement.TryBind("@lastPlayedDate", userData.LastPlayedDate.Value.ToDateTimeParamValue()); |
| | 203 | | } |
| | 204 | | else |
| | 205 | | { |
| 0 | 206 | | statement.TryBindNull("@lastPlayedDate"); |
| | 207 | | } |
| | 208 | |
|
| 0 | 209 | | if (userData.AudioStreamIndex.HasValue) |
| | 210 | | { |
| 0 | 211 | | statement.TryBind("@AudioStreamIndex", userData.AudioStreamIndex.Value); |
| | 212 | | } |
| | 213 | | else |
| | 214 | | { |
| 0 | 215 | | statement.TryBindNull("@AudioStreamIndex"); |
| | 216 | | } |
| | 217 | |
|
| 0 | 218 | | if (userData.SubtitleStreamIndex.HasValue) |
| | 219 | | { |
| 0 | 220 | | statement.TryBind("@SubtitleStreamIndex", userData.SubtitleStreamIndex.Value); |
| | 221 | | } |
| | 222 | | else |
| | 223 | | { |
| 0 | 224 | | statement.TryBindNull("@SubtitleStreamIndex"); |
| | 225 | | } |
| | 226 | |
|
| 0 | 227 | | statement.ExecuteNonQuery(); |
| 0 | 228 | | } |
| 0 | 229 | | } |
| | 230 | |
|
| | 231 | | /// <summary> |
| | 232 | | /// Persist all user data for the specified user. |
| | 233 | | /// </summary> |
| | 234 | | private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellation |
| | 235 | | { |
| 0 | 236 | | cancellationToken.ThrowIfCancellationRequested(); |
| | 237 | |
|
| 0 | 238 | | using (var connection = GetConnection()) |
| 0 | 239 | | using (var transaction = connection.BeginTransaction()) |
| | 240 | | { |
| 0 | 241 | | foreach (var userItemData in userDataList) |
| | 242 | | { |
| 0 | 243 | | SaveUserData(connection, internalUserId, userItemData.Key, userItemData); |
| | 244 | | } |
| | 245 | |
|
| 0 | 246 | | transaction.Commit(); |
| 0 | 247 | | } |
| 0 | 248 | | } |
| | 249 | |
|
| | 250 | | /// <summary> |
| | 251 | | /// Gets the user data. |
| | 252 | | /// </summary> |
| | 253 | | /// <param name="userId">The user id.</param> |
| | 254 | | /// <param name="key">The key.</param> |
| | 255 | | /// <returns>Task{UserItemData}.</returns> |
| | 256 | | /// <exception cref="ArgumentNullException"> |
| | 257 | | /// userId |
| | 258 | | /// or |
| | 259 | | /// key. |
| | 260 | | /// </exception> |
| | 261 | | public UserItemData GetUserData(long userId, string key) |
| | 262 | | { |
| 2 | 263 | | if (userId <= 0) |
| | 264 | | { |
| 0 | 265 | | throw new ArgumentNullException(nameof(userId)); |
| | 266 | | } |
| | 267 | |
|
| 2 | 268 | | ArgumentException.ThrowIfNullOrEmpty(key); |
| | 269 | |
|
| 2 | 270 | | using (var connection = GetConnection(true)) |
| | 271 | | { |
| 2 | 272 | | using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite |
| | 273 | | { |
| 2 | 274 | | statement.TryBind("@UserId", userId); |
| 2 | 275 | | statement.TryBind("@Key", key); |
| | 276 | |
|
| 4 | 277 | | foreach (var row in statement.ExecuteQuery()) |
| | 278 | | { |
| 0 | 279 | | return ReadRow(row); |
| | 280 | | } |
| | 281 | | } |
| | 282 | |
|
| 2 | 283 | | return null; |
| | 284 | | } |
| 2 | 285 | | } |
| | 286 | |
|
| | 287 | | public UserItemData GetUserData(long userId, List<string> keys) |
| | 288 | | { |
| 2 | 289 | | ArgumentNullException.ThrowIfNull(keys); |
| | 290 | |
|
| 2 | 291 | | if (keys.Count == 0) |
| | 292 | | { |
| 0 | 293 | | return null; |
| | 294 | | } |
| | 295 | |
|
| 2 | 296 | | return GetUserData(userId, keys[0]); |
| | 297 | | } |
| | 298 | |
|
| | 299 | | /// <summary> |
| | 300 | | /// Return all user-data associated with the given user. |
| | 301 | | /// </summary> |
| | 302 | | /// <param name="userId">The internal user id.</param> |
| | 303 | | /// <returns>The list of user item data.</returns> |
| | 304 | | public List<UserItemData> GetAllUserData(long userId) |
| | 305 | | { |
| 0 | 306 | | if (userId <= 0) |
| | 307 | | { |
| 0 | 308 | | throw new ArgumentNullException(nameof(userId)); |
| | 309 | | } |
| | 310 | |
|
| 0 | 311 | | var list = new List<UserItemData>(); |
| | 312 | |
|
| 0 | 313 | | using (var connection = GetConnection()) |
| | 314 | | { |
| 0 | 315 | | using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite |
| | 316 | | { |
| 0 | 317 | | statement.TryBind("@UserId", userId); |
| | 318 | |
|
| 0 | 319 | | foreach (var row in statement.ExecuteQuery()) |
| | 320 | | { |
| 0 | 321 | | list.Add(ReadRow(row)); |
| | 322 | | } |
| | 323 | | } |
| | 324 | | } |
| | 325 | |
|
| 0 | 326 | | return list; |
| | 327 | | } |
| | 328 | |
|
| | 329 | | /// <summary> |
| | 330 | | /// Read a row from the specified reader into the provided userData object. |
| | 331 | | /// </summary> |
| | 332 | | /// <param name="reader">The list of result set values.</param> |
| | 333 | | /// <returns>The user item data.</returns> |
| | 334 | | private UserItemData ReadRow(SqliteDataReader reader) |
| | 335 | | { |
| 0 | 336 | | var userData = new UserItemData |
| 0 | 337 | | { |
| 0 | 338 | | Key = reader.GetString(0) |
| 0 | 339 | | }; |
| | 340 | |
|
| 0 | 341 | | if (reader.TryGetDouble(2, out var rating)) |
| | 342 | | { |
| 0 | 343 | | userData.Rating = rating; |
| | 344 | | } |
| | 345 | |
|
| 0 | 346 | | userData.Played = reader.GetBoolean(3); |
| 0 | 347 | | userData.PlayCount = reader.GetInt32(4); |
| 0 | 348 | | userData.IsFavorite = reader.GetBoolean(5); |
| 0 | 349 | | userData.PlaybackPositionTicks = reader.GetInt64(6); |
| | 350 | |
|
| 0 | 351 | | if (reader.TryReadDateTime(7, out var lastPlayedDate)) |
| | 352 | | { |
| 0 | 353 | | userData.LastPlayedDate = lastPlayedDate; |
| | 354 | | } |
| | 355 | |
|
| 0 | 356 | | if (reader.TryGetInt32(8, out var audioStreamIndex)) |
| | 357 | | { |
| 0 | 358 | | userData.AudioStreamIndex = audioStreamIndex; |
| | 359 | | } |
| | 360 | |
|
| 0 | 361 | | if (reader.TryGetInt32(9, out var subtitleStreamIndex)) |
| | 362 | | { |
| 0 | 363 | | userData.SubtitleStreamIndex = subtitleStreamIndex; |
| | 364 | | } |
| | 365 | |
|
| 0 | 366 | | return userData; |
| | 367 | | } |
| | 368 | | } |
| | 369 | | } |