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