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); } }