aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server.Implementations')
-rw-r--r--Jellyfin.Server.Implementations/Activity/ActivityManager.cs229
-rw-r--r--Jellyfin.Server.Implementations/Devices/DeviceManager.cs2
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs88
3 files changed, 222 insertions, 97 deletions
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
index 8d492f7cd..7ee573f53 100644
--- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -1,103 +1,198 @@
using System;
+using System.Collections.Generic;
using System.Linq;
+using System.Linq.Expressions;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Data.Queries;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Querying;
using Microsoft.EntityFrameworkCore;
-namespace Jellyfin.Server.Implementations.Activity
+namespace Jellyfin.Server.Implementations.Activity;
+
+/// <summary>
+/// Manages the storage and retrieval of <see cref="ActivityLog"/> instances.
+/// </summary>
+public class ActivityManager : IActivityManager
{
+ private readonly IDbContextFactory<JellyfinDbContext> _provider;
+
/// <summary>
- /// Manages the storage and retrieval of <see cref="ActivityLog"/> instances.
+ /// Initializes a new instance of the <see cref="ActivityManager"/> class.
/// </summary>
- public class ActivityManager : IActivityManager
+ /// <param name="provider">The Jellyfin database provider.</param>
+ public ActivityManager(IDbContextFactory<JellyfinDbContext> provider)
{
- private readonly IDbContextFactory<JellyfinDbContext> _provider;
+ _provider = provider;
+ }
+
+ /// <inheritdoc/>
+ public event EventHandler<GenericEventArgs<ActivityLogEntry>>? EntryCreated;
- /// <summary>
- /// Initializes a new instance of the <see cref="ActivityManager"/> class.
- /// </summary>
- /// <param name="provider">The Jellyfin database provider.</param>
- public ActivityManager(IDbContextFactory<JellyfinDbContext> provider)
+ /// <inheritdoc/>
+ public async Task CreateAsync(ActivityLog entry)
+ {
+ var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- _provider = provider;
+ dbContext.ActivityLogs.Add(entry);
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
- /// <inheritdoc/>
- public event EventHandler<GenericEventArgs<ActivityLogEntry>>? EntryCreated;
+ EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
+ }
+
+ /// <inheritdoc/>
+ public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query)
+ {
+ // TODO allow sorting and filtering by item id. Currently not possible because ActivityLog stores the item id as a string.
- /// <inheritdoc/>
- public async Task CreateAsync(ActivityLog entry)
+ var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
+ // TODO switch to LeftJoin in .NET 10.
+ var entries = from a in dbContext.ActivityLogs
+ join u in dbContext.Users on a.UserId equals u.Id into ugj
+ from u in ugj.DefaultIfEmpty()
+ select new ExpandedActivityLog { ActivityLog = a, Username = u.Username };
+
+ if (query.HasUserId is not null)
{
- dbContext.ActivityLogs.Add(entry);
- await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ entries = entries.Where(e => e.ActivityLog.UserId.Equals(default) != query.HasUserId.Value);
}
- EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
- }
+ if (query.MinDate is not null)
+ {
+ entries = entries.Where(e => e.ActivityLog.DateCreated >= query.MinDate.Value);
+ }
- /// <inheritdoc/>
- public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query)
- {
- var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
+ if (!string.IsNullOrEmpty(query.Name))
{
- var entries = dbContext.ActivityLogs
- .OrderByDescending(entry => entry.DateCreated)
- .Where(entry => query.MinDate == null || entry.DateCreated >= query.MinDate)
- .Where(entry => !query.HasUserId.HasValue || entry.UserId.Equals(default) != query.HasUserId.Value);
-
- return new QueryResult<ActivityLogEntry>(
- query.Skip,
- await entries.CountAsync().ConfigureAwait(false),
- await entries
- .Skip(query.Skip ?? 0)
- .Take(query.Limit ?? 100)
- .Select(entity => new ActivityLogEntry(entity.Name, entity.Type, entity.UserId)
- {
- Id = entity.Id,
- Overview = entity.Overview,
- ShortOverview = entity.ShortOverview,
- ItemId = entity.ItemId,
- Date = entity.DateCreated,
- Severity = entity.LogSeverity
- })
- .ToListAsync()
- .ConfigureAwait(false));
+ entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Name, $"%{query.Name}%"));
}
- }
- /// <inheritdoc />
- public async Task CleanAsync(DateTime startDate)
- {
- var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
+ if (!string.IsNullOrEmpty(query.Overview))
+ {
+ entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Overview, $"%{query.Overview}%"));
+ }
+
+ if (!string.IsNullOrEmpty(query.ShortOverview))
+ {
+ entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.ShortOverview, $"%{query.ShortOverview}%"));
+ }
+
+ if (!string.IsNullOrEmpty(query.Type))
+ {
+ entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Type, $"%{query.Type}%"));
+ }
+
+ if (!query.ItemId.IsNullOrEmpty())
+ {
+ var itemId = query.ItemId.Value.ToString("N");
+ entries = entries.Where(e => e.ActivityLog.ItemId == itemId);
+ }
+
+ if (!string.IsNullOrEmpty(query.Username))
+ {
+ entries = entries.Where(e => EF.Functions.Like(e.Username, $"%{query.Username}%"));
+ }
+
+ if (query.Severity is not null)
{
- await dbContext.ActivityLogs
- .Where(entry => entry.DateCreated <= startDate)
- .ExecuteDeleteAsync()
- .ConfigureAwait(false);
+ entries = entries.Where(e => e.ActivityLog.LogSeverity == query.Severity);
}
+
+ return new QueryResult<ActivityLogEntry>(
+ query.Skip,
+ await entries.CountAsync().ConfigureAwait(false),
+ await ApplyOrdering(entries, query.OrderBy)
+ .Skip(query.Skip ?? 0)
+ .Take(query.Limit ?? 100)
+ .Select(entity => new ActivityLogEntry(entity.ActivityLog.Name, entity.ActivityLog.Type, entity.ActivityLog.UserId)
+ {
+ Id = entity.ActivityLog.Id,
+ Overview = entity.ActivityLog.Overview,
+ ShortOverview = entity.ActivityLog.ShortOverview,
+ ItemId = entity.ActivityLog.ItemId,
+ Date = entity.ActivityLog.DateCreated,
+ Severity = entity.ActivityLog.LogSeverity
+ })
+ .ToListAsync()
+ .ConfigureAwait(false));
}
+ }
- private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
+ /// <inheritdoc />
+ public async Task CleanAsync(DateTime startDate)
+ {
+ var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- return new ActivityLogEntry(entry.Name, entry.Type, entry.UserId)
- {
- Id = entry.Id,
- Overview = entry.Overview,
- ShortOverview = entry.ShortOverview,
- ItemId = entry.ItemId,
- Date = entry.DateCreated,
- Severity = entry.LogSeverity
- };
+ await dbContext.ActivityLogs
+ .Where(entry => entry.DateCreated <= startDate)
+ .ExecuteDeleteAsync()
+ .ConfigureAwait(false);
+ }
+ }
+
+ private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
+ {
+ return new ActivityLogEntry(entry.Name, entry.Type, entry.UserId)
+ {
+ Id = entry.Id,
+ Overview = entry.Overview,
+ ShortOverview = entry.ShortOverview,
+ ItemId = entry.ItemId,
+ Date = entry.DateCreated,
+ Severity = entry.LogSeverity
+ };
+ }
+
+ private IOrderedQueryable<ExpandedActivityLog> ApplyOrdering(IQueryable<ExpandedActivityLog> query, IReadOnlyCollection<(ActivityLogSortBy, SortOrder)>? sorting)
+ {
+ if (sorting is null || sorting.Count == 0)
+ {
+ return query.OrderByDescending(e => e.ActivityLog.DateCreated);
}
+
+ IOrderedQueryable<ExpandedActivityLog> ordered = null!;
+
+ foreach (var (sortBy, sortOrder) in sorting)
+ {
+ var orderBy = MapOrderBy(sortBy);
+ ordered = sortOrder == SortOrder.Ascending
+ ? (ordered ?? query).OrderBy(orderBy)
+ : (ordered ?? query).OrderByDescending(orderBy);
+ }
+
+ return ordered;
+ }
+
+ private Expression<Func<ExpandedActivityLog, object?>> MapOrderBy(ActivityLogSortBy sortBy)
+ {
+ return sortBy switch
+ {
+ ActivityLogSortBy.Name => e => e.ActivityLog.Name,
+ ActivityLogSortBy.Overiew => e => e.ActivityLog.Overview,
+ ActivityLogSortBy.ShortOverview => e => e.ActivityLog.ShortOverview,
+ ActivityLogSortBy.Type => e => e.ActivityLog.Type,
+ ActivityLogSortBy.DateCreated => e => e.ActivityLog.DateCreated,
+ ActivityLogSortBy.Username => e => e.Username,
+ ActivityLogSortBy.LogSeverity => e => e.ActivityLog.LogSeverity,
+ _ => throw new ArgumentOutOfRangeException(nameof(sortBy), sortBy, "Unhandled ActivityLogSortBy")
+ };
+ }
+
+ private class ExpandedActivityLog
+ {
+ public ActivityLog ActivityLog { get; set; } = null!;
+
+ public string? Username { get; set; }
}
}
diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
index 51a118645..bcf348f8c 100644
--- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
+++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
@@ -158,7 +158,7 @@ namespace Jellyfin.Server.Implementations.Devices
devices = devices.Skip(query.Skip.Value);
}
- if (query.Limit.HasValue)
+ if (query.Limit.HasValue && query.Limit.Value > 0)
{
devices = devices.Take(query.Limit.Value);
}
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 84168291a..dfe46ef8f 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -250,7 +250,7 @@ public sealed class BaseItemRepository
public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
{
ArgumentNullException.ThrowIfNull(filter);
- if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0))
+ if (!filter.EnableTotalRecordCount || ((filter.Limit ?? 0) == 0 && (filter.StartIndex ?? 0) == 0))
{
var returnList = GetItemList(filter);
return new QueryResult<BaseItemDto>(
@@ -326,7 +326,7 @@ public sealed class BaseItemRepository
.OrderByDescending(g => g.MaxDateCreated)
.Select(g => g);
- if (filter.Limit.HasValue)
+ if (filter.Limit.HasValue && filter.Limit.Value > 0)
{
subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value);
}
@@ -367,7 +367,7 @@ public sealed class BaseItemRepository
.OrderByDescending(g => g.LastPlayedDate)
.Select(g => g.Key!);
- if (filter.Limit.HasValue)
+ if (filter.Limit.HasValue && filter.Limit.Value > 0)
{
query = query.Take(filter.Limit.Value);
}
@@ -425,19 +425,14 @@ public sealed class BaseItemRepository
private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{
- if (filter.Limit.HasValue || filter.StartIndex.HasValue)
+ if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
{
- var offset = filter.StartIndex ?? 0;
-
- if (offset > 0)
- {
- dbQuery = dbQuery.Skip(offset);
- }
+ dbQuery = dbQuery.Skip(filter.StartIndex.Value);
+ }
- if (filter.Limit.HasValue)
- {
- dbQuery = dbQuery.Take(filter.Limit.Value);
- }
+ if (filter.Limit.HasValue && filter.Limit.Value > 0)
+ {
+ dbQuery = dbQuery.Take(filter.Limit.Value);
}
return dbQuery;
@@ -1190,7 +1185,7 @@ public sealed class BaseItemRepository
{
ArgumentNullException.ThrowIfNull(filter);
- if (!filter.Limit.HasValue)
+ if (!(filter.Limit.HasValue && filter.Limit.Value > 0))
{
filter.EnableTotalRecordCount = false;
}
@@ -1269,19 +1264,14 @@ public sealed class BaseItemRepository
result.TotalRecordCount = query.Count();
}
- if (filter.Limit.HasValue || filter.StartIndex.HasValue)
+ if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
{
- var offset = filter.StartIndex ?? 0;
-
- if (offset > 0)
- {
- query = query.Skip(offset);
- }
+ query = query.Skip(filter.StartIndex.Value);
+ }
- if (filter.Limit.HasValue)
- {
- query = query.Take(filter.Limit.Value);
- }
+ if (filter.Limit.HasValue && filter.Limit.Value > 0)
+ {
+ query = query.Take(filter.Limit.Value);
}
IQueryable<BaseItemEntity>? itemCountQuery = null;
@@ -1362,7 +1352,7 @@ public sealed class BaseItemRepository
private static void PrepareFilterQuery(InternalItemsQuery query)
{
- if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
+ if (query.Limit.HasValue && query.Limit.Value > 0 && query.EnableGroupByMetadataKey)
{
query.Limit = query.Limit.Value + 4;
}
@@ -1373,14 +1363,54 @@ public sealed class BaseItemRepository
}
}
- private string GetCleanValue(string value)
+ /// <summary>
+ /// Gets the clean value for search and sorting purposes.
+ /// </summary>
+ /// <param name="value">The value to clean.</param>
+ /// <returns>The cleaned value.</returns>
+ public static string GetCleanValue(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return value;
}
- return value.RemoveDiacritics().ToLowerInvariant();
+ var noDiacritics = value.RemoveDiacritics();
+
+ // Build a string where any punctuation or symbol is treated as a separator (space).
+ var sb = new StringBuilder(noDiacritics.Length);
+ var previousWasSpace = false;
+ foreach (var ch in noDiacritics)
+ {
+ char outCh;
+ if (char.IsLetterOrDigit(ch) || char.IsWhiteSpace(ch))
+ {
+ outCh = ch;
+ }
+ else
+ {
+ outCh = ' ';
+ }
+
+ // normalize any whitespace character to a single ASCII space.
+ if (char.IsWhiteSpace(outCh))
+ {
+ if (!previousWasSpace)
+ {
+ sb.Append(' ');
+ previousWasSpace = true;
+ }
+ }
+ else
+ {
+ sb.Append(outCh);
+ previousWasSpace = false;
+ }
+ }
+
+ // trim leading/trailing spaces that may have been added.
+ var collapsed = sb.ToString().Trim();
+ return collapsed.ToLowerInvariant();
}
private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inheritedTags)