< Summary - Jellyfin

Information
Class: Jellyfin.Server.Implementations.Activity.ActivityManager
Assembly: Jellyfin.Server.Implementations
File(s): /srv/git/jellyfin/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
Line coverage
37%
Covered lines: 13
Uncovered lines: 22
Coverable lines: 35
Total lines: 213
Line coverage: 37.1%
Branch coverage
14%
Covered branches: 3
Total branches: 21
Branch coverage: 14.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 9/14/2025 - 12:09:49 AM Line coverage: 100% (11/11) Total lines: 10312/10/2025 - 12:13:43 AM Line coverage: 41.9% (13/31) Branch coverage: 14.2% (3/21) Total lines: 19812/21/2025 - 12:12:15 AM Line coverage: 37.1% (13/35) Branch coverage: 14.2% (3/21) Total lines: 213 12/10/2025 - 12:13:43 AM Line coverage: 41.9% (13/31) Branch coverage: 14.2% (3/21) Total lines: 19812/21/2025 - 12:12:15 AM Line coverage: 37.1% (13/35) Branch coverage: 14.2% (3/21) Total lines: 213

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ConvertToOldModel(...)100%11100%
ApplyOrdering(...)25%991215.38%
MapOrderBy(...)0%9090%

File(s)

/srv/git/jellyfin/Jellyfin.Server.Implementations/Activity/ActivityManager.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4using System.Linq.Expressions;
 5using System.Threading.Tasks;
 6using Jellyfin.Data.Enums;
 7using Jellyfin.Data.Events;
 8using Jellyfin.Data.Queries;
 9using Jellyfin.Database.Implementations;
 10using Jellyfin.Database.Implementations.Entities;
 11using Jellyfin.Database.Implementations.Enums;
 12using Jellyfin.Extensions;
 13using MediaBrowser.Model.Activity;
 14using MediaBrowser.Model.Querying;
 15using Microsoft.EntityFrameworkCore;
 16
 17namespace Jellyfin.Server.Implementations.Activity;
 18
 19/// <summary>
 20/// Manages the storage and retrieval of <see cref="ActivityLog"/> instances.
 21/// </summary>
 22public 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    {
 2132        _provider = provider;
 2133    }
 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    {
 34151        return new ActivityLogEntry(entry.Name, entry.Type, entry.UserId)
 34152        {
 34153            Id = entry.Id,
 34154            Overview = entry.Overview,
 34155            ShortOverview = entry.ShortOverview,
 34156            ItemId = entry.ItemId,
 34157            Date = entry.DateCreated,
 34158            Severity = entry.LogSeverity
 34159        };
 160    }
 161
 162    private IOrderedQueryable<ExpandedActivityLog> ApplyOrdering(IQueryable<ExpandedActivityLog> query, IReadOnlyCollect
 163    {
 1164        if (sorting is null || sorting.Count == 0)
 165        {
 1166            return query.OrderByDescending(e => e.ActivityLog.DateCreated);
 167        }
 168
 0169        IOrderedQueryable<ExpandedActivityLog> ordered = null!;
 170
 0171        foreach (var (sortBy, sortOrder) in sorting)
 172        {
 0173            var orderBy = MapOrderBy(sortBy);
 174
 0175            if (ordered == null)
 176            {
 0177                ordered = sortOrder == SortOrder.Ascending
 0178                    ? query.OrderBy(orderBy)
 0179                    : query.OrderByDescending(orderBy);
 180            }
 181            else
 182            {
 0183                ordered = sortOrder == SortOrder.Ascending
 0184                    ? ordered.ThenBy(orderBy)
 0185                    : ordered.ThenByDescending(orderBy);
 186            }
 187        }
 188
 0189        return ordered;
 190    }
 191
 192    private Expression<Func<ExpandedActivityLog, object?>> MapOrderBy(ActivityLogSortBy sortBy)
 193    {
 0194        return sortBy switch
 0195        {
 0196            ActivityLogSortBy.Name => e => e.ActivityLog.Name,
 0197            ActivityLogSortBy.Overiew => e => e.ActivityLog.Overview,
 0198            ActivityLogSortBy.ShortOverview => e => e.ActivityLog.ShortOverview,
 0199            ActivityLogSortBy.Type => e => e.ActivityLog.Type,
 0200            ActivityLogSortBy.DateCreated => e => e.ActivityLog.DateCreated,
 0201            ActivityLogSortBy.Username => e => e.Username,
 0202            ActivityLogSortBy.LogSeverity => e => e.ActivityLog.LogSeverity,
 0203            _ => throw new ArgumentOutOfRangeException(nameof(sortBy), sortBy, "Unhandled ActivityLogSortBy")
 0204        };
 205    }
 206
 207    private class ExpandedActivityLog
 208    {
 209        public ActivityLog ActivityLog { get; set; } = null!;
 210
 211        public string? Username { get; set; }
 212    }
 213}