using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Kavita.API.Repositories;
using Kavita.Common.Helpers;
using Kavita.Database.Extensions;
using Kavita.Models.DTOs.Scrobbling;
using Kavita.Models.Entities.Scrobble;
using Microsoft.EntityFrameworkCore;
namespace Kavita.Database.Repositories;
///
/// This handles everything around Scrobbling
///
public class ScrobbleRepository(DataContext context, IMapper mapper) : IScrobbleRepository
{
public void Attach(ScrobbleEvent evt)
{
context.ScrobbleEvent.Attach(evt);
}
public void Attach(ScrobbleError error)
{
context.ScrobbleError.Attach(error);
}
public void Remove(ScrobbleEvent evt)
{
context.ScrobbleEvent.Remove(evt);
}
public void Remove(IEnumerable events)
{
context.ScrobbleEvent.RemoveRange(events);
}
public void Remove(IEnumerable errors)
{
context.ScrobbleError.RemoveRange(errors);
}
public void Update(ScrobbleEvent evt)
{
context.Entry(evt).State = EntityState.Modified;
}
public async Task> GetByEvent(ScrobbleEventType type, bool isProcessed = false,
CancellationToken ct = default)
{
return await context.ScrobbleEvent
.Include(s => s.Series)
.ThenInclude(s => s.Library)
.Include(s => s.Series)
.ThenInclude(s => s.Metadata)
.Include(s => s.AppUser)
.ThenInclude(u => u.UserPreferences)
.Where(s => s.ScrobbleEventType == type)
.Where(s => s.IsProcessed == isProcessed)
.AsSplitQuery()
.GroupBy(s => s.SeriesId)
.Select(g => g.OrderByDescending(e => e.ChapterNumber)
.ThenByDescending(e => e.VolumeNumber)
.First())
.ToListAsync(ct);
}
///
/// Returns all processed events processed 7 or more days ago
///
///
///
///
public async Task> GetProcessedEvents(int daysAgo, CancellationToken ct = default)
{
var date = DateTime.UtcNow.Subtract(TimeSpan.FromDays(daysAgo));
return await context.ScrobbleEvent
.Where(s => s.IsProcessed)
.Where(s => s.ProcessDateUtc != null && s.ProcessDateUtc < date)
.ToListAsync(ct);
}
public async Task Exists(int userId, int seriesId, ScrobbleEventType eventType, CancellationToken ct = default)
{
return await context.ScrobbleEvent.AnyAsync(e =>
e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType, ct);
}
public async Task> GetScrobbleErrors(CancellationToken ct = default)
{
return await context.ScrobbleError
.OrderBy(e => e.LastModifiedUtc)
.ProjectTo(mapper.ConfigurationProvider)
.ToListAsync(ct);
}
public async Task> GetAllScrobbleErrorsForSeries(int seriesId, CancellationToken ct = default)
{
return await context.ScrobbleError
.Where(e => e.SeriesId == seriesId)
.ToListAsync(ct);
}
public async Task ClearScrobbleErrors(CancellationToken ct = default)
{
context.ScrobbleError.RemoveRange(context.ScrobbleError);
await context.SaveChangesAsync(ct);
}
public async Task HasErrorForSeries(int seriesId, CancellationToken ct = default)
{
return await context.ScrobbleError.AnyAsync(n => n.SeriesId == seriesId, ct);
}
public async Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType,
bool isNotProcessed = false, CancellationToken ct = default)
{
return await context.ScrobbleEvent
.Where(e => e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType)
.WhereIf(isNotProcessed, e => !e.IsProcessed)
.OrderBy(e => e.LastModifiedUtc)
.FirstOrDefaultAsync(ct);
}
public async Task> GetUserEventsForSeries(int userId, int seriesId,
CancellationToken ct = default)
{
return await context.ScrobbleEvent
.Where(e => e.AppUserId == userId && !e.IsProcessed && e.SeriesId == seriesId)
.Include(e => e.Series)
.OrderBy(e => e.LastModifiedUtc)
.AsSplitQuery()
.ToListAsync(ct);
}
public async Task> GetUserEvents(int userId, IList scrobbleEventIds,
CancellationToken ct = default)
{
return await context.ScrobbleEvent
.Where(e => e.AppUserId == userId && scrobbleEventIds.Contains(e.Id))
.ToListAsync(ct);
}
public async Task> GetUserEvents(int userId, ScrobbleEventFilter filter,
UserParams pagination, CancellationToken ct = default)
{
var query = context.ScrobbleEvent
.Where(e => e.AppUserId == userId)
.Include(e => e.Series)
.WhereIf(!string.IsNullOrEmpty(filter.Query), s =>
EF.Functions.Like(s.Series.Name, $"%{filter.Query}%")
)
.WhereIf(!filter.IncludeReviews, e => e.ScrobbleEventType != ScrobbleEventType.Review)
.SortBy(filter.Field, filter.IsDescending)
.AsSplitQuery()
.ProjectTo(mapper.ConfigurationProvider);
return await PagedList.CreateAsync(query, pagination.PageNumber, pagination.PageSize, ct);
}
public async Task> GetAllEventsForSeries(int seriesId, CancellationToken ct = default)
{
return await context.ScrobbleEvent
.Where(e => e.SeriesId == seriesId)
.ToListAsync(ct);
}
public async Task> GetAllEventsWithSeriesIds(IEnumerable seriesIds,
CancellationToken ct = default)
{
return await context.ScrobbleEvent
.Where(e => seriesIds.Contains(e.SeriesId))
.ToListAsync(ct);
}
public async Task> GetEvents(CancellationToken ct = default)
{
return await context.ScrobbleEvent
.Include(e => e.AppUser)
.ToListAsync(ct);
}
}