ActivityManager.cs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Linq.Expressions;
  5. using System.Threading.Tasks;
  6. using Jellyfin.Data.Enums;
  7. using Jellyfin.Data.Events;
  8. using Jellyfin.Data.Queries;
  9. using Jellyfin.Database.Implementations;
  10. using Jellyfin.Database.Implementations.Entities;
  11. using Jellyfin.Database.Implementations.Enums;
  12. using Jellyfin.Extensions;
  13. using MediaBrowser.Model.Activity;
  14. using MediaBrowser.Model.Querying;
  15. using Microsoft.EntityFrameworkCore;
  16. namespace Jellyfin.Server.Implementations.Activity;
  17. /// <summary>
  18. /// Manages the storage and retrieval of <see cref="ActivityLog"/> instances.
  19. /// </summary>
  20. public class ActivityManager : IActivityManager
  21. {
  22. private readonly IDbContextFactory<JellyfinDbContext> _provider;
  23. /// <summary>
  24. /// Initializes a new instance of the <see cref="ActivityManager"/> class.
  25. /// </summary>
  26. /// <param name="provider">The Jellyfin database provider.</param>
  27. public ActivityManager(IDbContextFactory<JellyfinDbContext> provider)
  28. {
  29. _provider = provider;
  30. }
  31. /// <inheritdoc/>
  32. public event EventHandler<GenericEventArgs<ActivityLogEntry>>? EntryCreated;
  33. /// <inheritdoc/>
  34. public async Task CreateAsync(ActivityLog entry)
  35. {
  36. var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
  37. await using (dbContext.ConfigureAwait(false))
  38. {
  39. dbContext.ActivityLogs.Add(entry);
  40. await dbContext.SaveChangesAsync().ConfigureAwait(false);
  41. }
  42. EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
  43. }
  44. /// <inheritdoc/>
  45. public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query)
  46. {
  47. // TODO allow sorting and filtering by item id. Currently not possible because ActivityLog stores the item id as a string.
  48. var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
  49. await using (dbContext.ConfigureAwait(false))
  50. {
  51. // TODO switch to LeftJoin in .NET 10.
  52. var entries = from a in dbContext.ActivityLogs
  53. join u in dbContext.Users on a.UserId equals u.Id into ugj
  54. from u in ugj.DefaultIfEmpty()
  55. select new ExpandedActivityLog { ActivityLog = a, Username = u.Username };
  56. if (query.HasUserId is not null)
  57. {
  58. entries = entries.Where(e => e.ActivityLog.UserId.Equals(default) != query.HasUserId.Value);
  59. }
  60. if (query.MinDate is not null)
  61. {
  62. entries = entries.Where(e => e.ActivityLog.DateCreated >= query.MinDate.Value);
  63. }
  64. if (query.MaxDate is not null)
  65. {
  66. entries = entries.Where(e => e.ActivityLog.DateCreated <= query.MaxDate.Value);
  67. }
  68. if (!string.IsNullOrEmpty(query.Name))
  69. {
  70. entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Name, $"%{query.Name}%"));
  71. }
  72. if (!string.IsNullOrEmpty(query.Overview))
  73. {
  74. entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Overview, $"%{query.Overview}%"));
  75. }
  76. if (!string.IsNullOrEmpty(query.ShortOverview))
  77. {
  78. entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.ShortOverview, $"%{query.ShortOverview}%"));
  79. }
  80. if (!string.IsNullOrEmpty(query.Type))
  81. {
  82. entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Type, $"%{query.Type}%"));
  83. }
  84. if (!query.ItemId.IsNullOrEmpty())
  85. {
  86. var itemId = query.ItemId.Value.ToString("N");
  87. entries = entries.Where(e => e.ActivityLog.ItemId == itemId);
  88. }
  89. if (!string.IsNullOrEmpty(query.Username))
  90. {
  91. entries = entries.Where(e => EF.Functions.Like(e.Username, $"%{query.Username}%"));
  92. }
  93. if (query.Severity is not null)
  94. {
  95. entries = entries.Where(e => e.ActivityLog.LogSeverity == query.Severity);
  96. }
  97. return new QueryResult<ActivityLogEntry>(
  98. query.Skip,
  99. await entries.CountAsync().ConfigureAwait(false),
  100. await ApplyOrdering(entries, query.OrderBy)
  101. .Skip(query.Skip ?? 0)
  102. .Take(query.Limit ?? 100)
  103. .Select(entity => new ActivityLogEntry(entity.ActivityLog.Name, entity.ActivityLog.Type, entity.ActivityLog.UserId)
  104. {
  105. Id = entity.ActivityLog.Id,
  106. Overview = entity.ActivityLog.Overview,
  107. ShortOverview = entity.ActivityLog.ShortOverview,
  108. ItemId = entity.ActivityLog.ItemId,
  109. Date = entity.ActivityLog.DateCreated,
  110. Severity = entity.ActivityLog.LogSeverity
  111. })
  112. .ToListAsync()
  113. .ConfigureAwait(false));
  114. }
  115. }
  116. /// <inheritdoc />
  117. public async Task CleanAsync(DateTime startDate)
  118. {
  119. var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
  120. await using (dbContext.ConfigureAwait(false))
  121. {
  122. await dbContext.ActivityLogs
  123. .Where(entry => entry.DateCreated <= startDate)
  124. .ExecuteDeleteAsync()
  125. .ConfigureAwait(false);
  126. }
  127. }
  128. private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
  129. {
  130. return new ActivityLogEntry(entry.Name, entry.Type, entry.UserId)
  131. {
  132. Id = entry.Id,
  133. Overview = entry.Overview,
  134. ShortOverview = entry.ShortOverview,
  135. ItemId = entry.ItemId,
  136. Date = entry.DateCreated,
  137. Severity = entry.LogSeverity
  138. };
  139. }
  140. private IOrderedQueryable<ExpandedActivityLog> ApplyOrdering(IQueryable<ExpandedActivityLog> query, IReadOnlyCollection<(ActivityLogSortBy, SortOrder)>? sorting)
  141. {
  142. if (sorting is null || sorting.Count == 0)
  143. {
  144. return query.OrderByDescending(e => e.ActivityLog.DateCreated);
  145. }
  146. IOrderedQueryable<ExpandedActivityLog> ordered = null!;
  147. foreach (var (sortBy, sortOrder) in sorting)
  148. {
  149. var orderBy = MapOrderBy(sortBy);
  150. if (ordered == null)
  151. {
  152. ordered = sortOrder == SortOrder.Ascending
  153. ? query.OrderBy(orderBy)
  154. : query.OrderByDescending(orderBy);
  155. }
  156. else
  157. {
  158. ordered = sortOrder == SortOrder.Ascending
  159. ? ordered.ThenBy(orderBy)
  160. : ordered.ThenByDescending(orderBy);
  161. }
  162. }
  163. return ordered;
  164. }
  165. private Expression<Func<ExpandedActivityLog, object?>> MapOrderBy(ActivityLogSortBy sortBy)
  166. {
  167. return sortBy switch
  168. {
  169. ActivityLogSortBy.Name => e => e.ActivityLog.Name,
  170. ActivityLogSortBy.Overiew => e => e.ActivityLog.Overview,
  171. ActivityLogSortBy.ShortOverview => e => e.ActivityLog.ShortOverview,
  172. ActivityLogSortBy.Type => e => e.ActivityLog.Type,
  173. ActivityLogSortBy.DateCreated => e => e.ActivityLog.DateCreated,
  174. ActivityLogSortBy.Username => e => e.Username,
  175. ActivityLogSortBy.LogSeverity => e => e.ActivityLog.LogSeverity,
  176. _ => throw new ArgumentOutOfRangeException(nameof(sortBy), sortBy, "Unhandled ActivityLogSortBy")
  177. };
  178. }
  179. private class ExpandedActivityLog
  180. {
  181. public ActivityLog ActivityLog { get; set; } = null!;
  182. public string? Username { get; set; }
  183. }
  184. }