Merge branch 'develop' into feature/oidc

This commit is contained in:
Amelia 2025-06-29 18:20:13 +02:00
commit 465723fedf
No known key found for this signature in database
GPG key ID: D6D0ECE365407EAA
358 changed files with 34968 additions and 5203 deletions

View file

@ -0,0 +1,112 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
using API.Entities.Enums;
using API.Extensions;
using API.Extensions.QueryExtensions;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
public interface IAppUserReadingProfileRepository
{
/// <summary>
/// Return the given profile if it belongs the user
/// </summary>
/// <param name="userId"></param>
/// <param name="profileId"></param>
/// <returns></returns>
Task<AppUserReadingProfile?> GetUserProfile(int userId, int profileId);
/// <summary>
/// Returns all reading profiles for the user
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
Task<IList<AppUserReadingProfile>> GetProfilesForUser(int userId, bool skipImplicit = false);
/// <summary>
/// Returns all reading profiles for the user
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
Task<IList<UserReadingProfileDto>> GetProfilesDtoForUser(int userId, bool skipImplicit = false);
/// <summary>
/// Is there a user reading profile with this name (normalized)
/// </summary>
/// <param name="userId"></param>
/// <param name="name"></param>
/// <returns></returns>
Task<bool> IsProfileNameInUse(int userId, string name);
void Add(AppUserReadingProfile readingProfile);
void Update(AppUserReadingProfile readingProfile);
void Remove(AppUserReadingProfile readingProfile);
void RemoveRange(IEnumerable<AppUserReadingProfile> readingProfiles);
}
public class AppUserReadingProfileRepository(DataContext context, IMapper mapper): IAppUserReadingProfileRepository
{
public async Task<AppUserReadingProfile?> GetUserProfile(int userId, int profileId)
{
return await context.AppUserReadingProfiles
.Where(rp => rp.AppUserId == userId && rp.Id == profileId)
.FirstOrDefaultAsync();
}
public async Task<IList<AppUserReadingProfile>> GetProfilesForUser(int userId, bool skipImplicit = false)
{
return await context.AppUserReadingProfiles
.Where(rp => rp.AppUserId == userId)
.WhereIf(skipImplicit, rp => rp.Kind != ReadingProfileKind.Implicit)
.ToListAsync();
}
/// <summary>
/// Returns all Reading Profiles for the User
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<IList<UserReadingProfileDto>> GetProfilesDtoForUser(int userId, bool skipImplicit = false)
{
return await context.AppUserReadingProfiles
.Where(rp => rp.AppUserId == userId)
.WhereIf(skipImplicit, rp => rp.Kind != ReadingProfileKind.Implicit)
.ProjectTo<UserReadingProfileDto>(mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<bool> IsProfileNameInUse(int userId, string name)
{
var normalizedName = name.ToNormalized();
return await context.AppUserReadingProfiles
.Where(rp => rp.NormalizedName == normalizedName && rp.AppUserId == userId)
.AnyAsync();
}
public void Add(AppUserReadingProfile readingProfile)
{
context.AppUserReadingProfiles.Add(readingProfile);
}
public void Update(AppUserReadingProfile readingProfile)
{
context.AppUserReadingProfiles.Update(readingProfile).State = EntityState.Modified;
}
public void Remove(AppUserReadingProfile readingProfile)
{
context.AppUserReadingProfiles.Remove(readingProfile);
}
public void RemoveRange(IEnumerable<AppUserReadingProfile> readingProfiles)
{
context.AppUserReadingProfiles.RemoveRange(readingProfiles);
}
}

View file

@ -108,14 +108,17 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
public async Task<bool> NeedsDataRefresh(int seriesId)
{
// TODO: Add unit test
var row = await _context.ExternalSeriesMetadata
.Where(s => s.SeriesId == seriesId)
.FirstOrDefaultAsync();
return row == null || row.ValidUntilUtc <= DateTime.UtcNow;
}
public async Task<SeriesDetailPlusDto?> GetSeriesDetailPlusDto(int seriesId)
{
// TODO: Add unit test
var seriesDetailDto = await _context.ExternalSeriesMetadata
.Where(m => m.SeriesId == seriesId)
.Include(m => m.ExternalRatings)
@ -144,7 +147,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
.ToListAsync();
IEnumerable<UserReviewDto> reviews = new List<UserReviewDto>();
IEnumerable<UserReviewDto> reviews = [];
if (seriesDetailDto.ExternalReviews != null && seriesDetailDto.ExternalReviews.Any())
{
reviews = seriesDetailDto.ExternalReviews
@ -231,6 +234,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
.Include(s => s.ExternalSeriesMetadata)
.Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
.Where(s => s.Library.AllowMetadataMatching)
.WhereIf(filter.LibraryType >= 0, s => s.Library.Type == (LibraryType) filter.LibraryType)
.FilterMatchState(filter.MatchStateOption)
.OrderBy(s => s.NormalizedName)
.ProjectTo<ManageMatchSeriesDto>(_mapper.ConfigurationProvider)

View file

@ -3,9 +3,11 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.DTOs.Metadata;
using API.DTOs.Metadata.Browse;
using API.Entities;
using API.Extensions;
using API.Extensions.QueryExtensions;
using API.Helpers;
using API.Services.Tasks.Scanner.Parser;
using AutoMapper;
using AutoMapper.QueryableExtensions;
@ -27,6 +29,7 @@ public interface IGenreRepository
Task<GenreTagDto> GetRandomGenre();
Task<GenreTagDto> GetGenreById(int id);
Task<List<string>> GetAllGenresNotInListAsync(ICollection<string> genreNames);
Task<PagedList<BrowseGenreDto>> GetBrowseableGenre(int userId, UserParams userParams);
}
public class GenreRepository : IGenreRepository
@ -111,7 +114,7 @@ public class GenreRepository : IGenreRepository
/// <summary>
/// Returns a set of Genre tags for a set of library Ids.
/// UserId will restrict returned Genres based on user's age restriction and library access.
/// AppUserId will restrict returned Genres based on user's age restriction and library access.
/// </summary>
/// <param name="userId"></param>
/// <param name="libraryIds"></param>
@ -165,4 +168,38 @@ public class GenreRepository : IGenreRepository
// Return the original non-normalized genres for the missing ones
return missingGenres.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList();
}
public async Task<PagedList<BrowseGenreDto>> GetBrowseableGenre(int userId, UserParams userParams)
{
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
var allLibrariesCount = await _context.Library.CountAsync();
var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync();
var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync();
var query = _context.Genre
.RestrictAgainstAgeRestriction(ageRating)
.WhereIf(allLibrariesCount != userLibs.Count,
genre => genre.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) ||
genre.SeriesMetadatas.Any(sm => seriesIds.Contains(sm.SeriesId)))
.Select(g => new BrowseGenreDto
{
Id = g.Id,
Title = g.Title,
SeriesCount = g.SeriesMetadatas
.Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId))
.RestrictAgainstAgeRestriction(ageRating)
.Distinct()
.Count(),
ChapterCount = g.Chapters
.Where(cp => allLibrariesCount == userLibs.Count || seriesIds.Contains(cp.Volume.SeriesId))
.RestrictAgainstAgeRestriction(ageRating)
.Distinct()
.Count(),
})
.OrderBy(g => g.Title);
return await PagedList<BrowseGenreDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
}
}

View file

@ -5,11 +5,13 @@ using API.Entities;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface IMangaFileRepository
{
void Update(MangaFile file);
Task<IList<MangaFile>> GetAllWithMissingExtension();
Task<MangaFile?> GetByKoreaderHash(string hash);
}
public class MangaFileRepository : IMangaFileRepository
@ -32,4 +34,13 @@ public class MangaFileRepository : IMangaFileRepository
.Where(f => string.IsNullOrEmpty(f.Extension))
.ToListAsync();
}
public async Task<MangaFile?> GetByKoreaderHash(string hash)
{
if (string.IsNullOrEmpty(hash)) return null;
return await _context.MangaFile
.FirstOrDefaultAsync(f => f.KoreaderHash != null &&
f.KoreaderHash.Equals(hash.ToUpper()));
}
}

View file

@ -2,13 +2,19 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Data.Misc;
using API.DTOs;
using API.DTOs.Filtering.v2;
using API.DTOs.Metadata.Browse;
using API.DTOs.Metadata.Browse.Requests;
using API.DTOs.Person;
using API.Entities.Enums;
using API.Entities.Person;
using API.Extensions;
using API.Extensions.QueryExtensions;
using API.Extensions.QueryExtensions.Filtering;
using API.Helpers;
using API.Helpers.Converters;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
@ -45,7 +51,7 @@ public interface IPersonRepository
Task<string?> GetCoverImageAsync(int personId);
Task<string?> GetCoverImageByNameAsync(string name);
Task<IEnumerable<PersonRole>> GetRolesForPersonByName(int personId, int userId);
Task<PagedList<BrowsePersonDto>> GetAllWritersAndSeriesCount(int userId, UserParams userParams);
Task<PagedList<BrowsePersonDto>> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, UserParams userParams);
Task<Person?> GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None);
Task<PersonDto?> GetPersonDtoByName(string name, int userId, PersonIncludes includes = PersonIncludes.Aliases);
/// <summary>
@ -57,7 +63,7 @@ public interface IPersonRepository
Task<Person?> GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases);
Task<bool> IsNameUnique(string name);
Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId);
Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId, int userId);
Task<IEnumerable<StandaloneChapterDto>> GetChaptersForPersonByRole(int personId, int userId, PersonRole role);
/// <summary>
/// Returns all people with a matching name, or alias
@ -173,20 +179,25 @@ public class PersonRepository : IPersonRepository
public async Task<IEnumerable<PersonRole>> GetRolesForPersonByName(int personId, int userId)
{
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
var userLibs = _context.Library.GetUserLibraries(userId);
// Query roles from ChapterPeople
var chapterRoles = await _context.Person
.Where(p => p.Id == personId)
.SelectMany(p => p.ChapterPeople)
.RestrictAgainstAgeRestriction(ageRating)
.SelectMany(p => p.ChapterPeople.Select(cp => cp.Role))
.RestrictByLibrary(userLibs)
.Select(cp => cp.Role)
.Distinct()
.ToListAsync();
// Query roles from SeriesMetadataPeople
var seriesRoles = await _context.Person
.Where(p => p.Id == personId)
.SelectMany(p => p.SeriesMetadataPeople)
.RestrictAgainstAgeRestriction(ageRating)
.SelectMany(p => p.SeriesMetadataPeople.Select(smp => smp.Role))
.RestrictByLibrary(userLibs)
.Select(smp => smp.Role)
.Distinct()
.ToListAsync();
@ -194,36 +205,91 @@ public class PersonRepository : IPersonRepository
return chapterRoles.Union(seriesRoles).Distinct();
}
public async Task<PagedList<BrowsePersonDto>> GetAllWritersAndSeriesCount(int userId, UserParams userParams)
public async Task<PagedList<BrowsePersonDto>> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, UserParams userParams)
{
List<PersonRole> roles = [PersonRole.Writer, PersonRole.CoverArtist];
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
var query = _context.Person
.Where(p => p.SeriesMetadataPeople.Any(smp => roles.Contains(smp.Role)) || p.ChapterPeople.Any(cmp => roles.Contains(cmp.Role)))
.RestrictAgainstAgeRestriction(ageRating)
.Select(p => new BrowsePersonDto
{
Id = p.Id,
Name = p.Name,
Description = p.Description,
CoverImage = p.CoverImage,
SeriesCount = p.SeriesMetadataPeople
.Where(smp => roles.Contains(smp.Role))
.Select(smp => smp.SeriesMetadata.SeriesId)
.Distinct()
.Count(),
IssueCount = p.ChapterPeople
.Where(cp => roles.Contains(cp.Role))
.Select(cp => cp.Chapter.Id)
.Distinct()
.Count()
})
.OrderBy(p => p.Name);
var query = await CreateFilteredPersonQueryable(userId, filter, ageRating);
return await PagedList<BrowsePersonDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
}
private async Task<IQueryable<BrowsePersonDto>> CreateFilteredPersonQueryable(int userId, BrowsePersonFilterDto filter, AgeRestriction ageRating)
{
var allLibrariesCount = await _context.Library.CountAsync();
var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync();
var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync();
var query = _context.Person.AsNoTracking();
// Apply filtering based on statements
query = BuildPersonFilterQuery(userId, filter, query);
// Apply restrictions
query = query.RestrictAgainstAgeRestriction(ageRating)
.WhereIf(allLibrariesCount != userLibs.Count,
person => person.ChapterPeople.Any(cp => seriesIds.Contains(cp.Chapter.Volume.SeriesId)) ||
person.SeriesMetadataPeople.Any(smp => seriesIds.Contains(smp.SeriesMetadata.SeriesId)));
// Apply sorting and limiting
var sortedQuery = query.SortBy(filter.SortOptions);
var limitedQuery = ApplyPersonLimit(sortedQuery, filter.LimitTo);
return limitedQuery.Select(p => new BrowsePersonDto
{
Id = p.Id,
Name = p.Name,
Description = p.Description,
CoverImage = p.CoverImage,
SeriesCount = p.SeriesMetadataPeople
.Select(smp => smp.SeriesMetadata)
.Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId))
.RestrictAgainstAgeRestriction(ageRating)
.Distinct()
.Count(),
ChapterCount = p.ChapterPeople
.Select(chp => chp.Chapter)
.Where(ch => allLibrariesCount == userLibs.Count || seriesIds.Contains(ch.Volume.SeriesId))
.RestrictAgainstAgeRestriction(ageRating)
.Distinct()
.Count(),
});
}
private static IQueryable<Person> BuildPersonFilterQuery(int userId, BrowsePersonFilterDto filterDto, IQueryable<Person> query)
{
if (filterDto.Statements == null || filterDto.Statements.Count == 0) return query;
var queries = filterDto.Statements
.Select(statement => BuildPersonFilterGroup(userId, statement, query))
.ToList();
return filterDto.Combination == FilterCombination.And
? queries.Aggregate((q1, q2) => q1.Intersect(q2))
: queries.Aggregate((q1, q2) => q1.Union(q2));
}
private static IQueryable<Person> BuildPersonFilterGroup(int userId, PersonFilterStatementDto statement, IQueryable<Person> query)
{
var value = PersonFilterFieldValueConverter.ConvertValue(statement.Field, statement.Value);
return statement.Field switch
{
PersonFilterField.Name => query.HasPersonName(true, statement.Comparison, (string)value),
PersonFilterField.Role => query.HasPersonRole(true, statement.Comparison, (IList<PersonRole>)value),
PersonFilterField.SeriesCount => query.HasPersonSeriesCount(true, statement.Comparison, (int)value),
PersonFilterField.ChapterCount => query.HasPersonChapterCount(true, statement.Comparison, (int)value),
_ => throw new ArgumentOutOfRangeException(nameof(statement.Field), $"Unexpected value for field: {statement.Field}")
};
}
private static IQueryable<Person> ApplyPersonLimit(IQueryable<Person> query, int limit)
{
return limit <= 0 ? query : query.Take(limit);
}
public async Task<Person?> GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None)
{
return await _context.Person.Where(p => p.Id == personId)
@ -235,11 +301,13 @@ public class PersonRepository : IPersonRepository
{
var normalized = name.ToNormalized();
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
var userLibs = _context.Library.GetUserLibraries(userId);
return await _context.Person
.Where(p => p.NormalizedName == normalized)
.Includes(includes)
.RestrictAgainstAgeRestriction(ageRating)
.RestrictByLibrary(userLibs)
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
.FirstOrDefaultAsync();
}
@ -261,14 +329,18 @@ public class PersonRepository : IPersonRepository
.AnyAsync(p => p.Name == name || p.Aliases.Any(pa => pa.Alias == name)));
}
public async Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId)
public async Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId, int userId)
{
List<PersonRole> notValidRoles = [PersonRole.Location, PersonRole.Team, PersonRole.Other, PersonRole.Publisher, PersonRole.Translator];
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync();
return await _context.Person
.Where(p => p.Id == personId)
.SelectMany(p => p.SeriesMetadataPeople.Where(smp => !notValidRoles.Contains(smp.Role)))
.SelectMany(p => p.SeriesMetadataPeople)
.Select(smp => smp.SeriesMetadata)
.Select(sm => sm.Series)
.RestrictAgainstAgeRestriction(ageRating)
.Where(s => userLibs.Contains(s.LibraryId))
.Distinct()
.OrderByDescending(s => s.ExternalSeriesMetadata.AverageExternalRating)
.Take(20)
@ -279,11 +351,13 @@ public class PersonRepository : IPersonRepository
public async Task<IEnumerable<StandaloneChapterDto>> GetChaptersForPersonByRole(int personId, int userId, PersonRole role)
{
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
var userLibs = _context.Library.GetUserLibraries(userId);
return await _context.ChapterPeople
.Where(cp => cp.PersonId == personId && cp.Role == role)
.Select(cp => cp.Chapter)
.RestrictAgainstAgeRestriction(ageRating)
.RestrictByLibrary(userLibs)
.OrderBy(ch => ch.SortOrder)
.Take(20)
.ProjectTo<StandaloneChapterDto>(_mapper.ConfigurationProvider)
@ -313,8 +387,8 @@ public class PersonRepository : IPersonRepository
return await _context.Person
.Includes(includes)
.Where(p => EF.Functions.Like(p.Name, $"%{searchQuery}%")
|| p.Aliases.Any(pa => EF.Functions.Like(pa.Alias, $"%{searchQuery}%")))
.Where(p => EF.Functions.Like(p.NormalizedName, $"%{searchQuery}%")
|| p.Aliases.Any(pa => EF.Functions.Like(pa.NormalizedAlias, $"%{searchQuery}%")))
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
@ -334,27 +408,31 @@ public class PersonRepository : IPersonRepository
.ToListAsync();
}
public async Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.Aliases)
public async Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None)
{
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
var userLibs = _context.Library.GetUserLibraries(userId);
return await _context.Person
.Includes(includes)
.OrderBy(p => p.Name)
.RestrictAgainstAgeRestriction(ageRating)
.RestrictByLibrary(userLibs)
.OrderBy(p => p.Name)
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.Aliases)
public async Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.None)
{
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
var userLibs = _context.Library.GetUserLibraries(userId);
return await _context.Person
.Where(p => p.SeriesMetadataPeople.Any(smp => smp.Role == role) || p.ChapterPeople.Any(cp => cp.Role == role)) // Filter by role in both series and chapters
.Includes(includes)
.OrderBy(p => p.Name)
.RestrictAgainstAgeRestriction(ageRating)
.RestrictByLibrary(userLibs)
.OrderBy(p => p.Name)
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}

View file

@ -29,8 +29,23 @@ public interface IScrobbleRepository
Task<IList<ScrobbleError>> GetAllScrobbleErrorsForSeries(int seriesId);
Task ClearScrobbleErrors();
Task<bool> HasErrorForSeries(int seriesId);
Task<ScrobbleEvent?> GetEvent(int userId, int seriesId, ScrobbleEventType eventType);
/// <summary>
/// Get all events for a specific user and type
/// </summary>
/// <param name="userId"></param>
/// <param name="seriesId"></param>
/// <param name="eventType"></param>
/// <param name="isNotProcessed">If true, only returned not processed events</param>
/// <returns></returns>
Task<ScrobbleEvent?> GetEvent(int userId, int seriesId, ScrobbleEventType eventType, bool isNotProcessed = false);
Task<IEnumerable<ScrobbleEvent>> GetUserEventsForSeries(int userId, int seriesId);
/// <summary>
/// Return the events with given ids, when belonging to the passed user
/// </summary>
/// <param name="userId"></param>
/// <param name="scrobbleEventIds"></param>
/// <returns></returns>
Task<IList<ScrobbleEvent>> GetUserEvents(int userId, IList<long> scrobbleEventIds);
Task<PagedList<ScrobbleEventDto>> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination);
Task<IList<ScrobbleEvent>> GetAllEventsForSeries(int seriesId);
Task<IList<ScrobbleEvent>> GetAllEventsWithSeriesIds(IEnumerable<int> seriesIds);
@ -146,22 +161,32 @@ public class ScrobbleRepository : IScrobbleRepository
return await _context.ScrobbleError.AnyAsync(n => n.SeriesId == seriesId);
}
public async Task<ScrobbleEvent?> GetEvent(int userId, int seriesId, ScrobbleEventType eventType)
public async Task<ScrobbleEvent?> GetEvent(int userId, int seriesId, ScrobbleEventType eventType, bool isNotProcessed = false)
{
return await _context.ScrobbleEvent.FirstOrDefaultAsync(e =>
e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType);
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();
}
public async Task<IEnumerable<ScrobbleEvent>> GetUserEventsForSeries(int userId, int seriesId)
{
return await _context.ScrobbleEvent
.Where(e => e.AppUserId == userId && !e.IsProcessed)
.Where(e => e.AppUserId == userId && !e.IsProcessed && e.SeriesId == seriesId)
.Include(e => e.Series)
.OrderBy(e => e.LastModifiedUtc)
.AsSplitQuery()
.ToListAsync();
}
public async Task<IList<ScrobbleEvent>> GetUserEvents(int userId, IList<long> scrobbleEventIds)
{
return await _context.ScrobbleEvent
.Where(e => e.AppUserId == userId && scrobbleEventIds.Contains(e.Id))
.ToListAsync();
}
public async Task<PagedList<ScrobbleEventDto>> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination)
{
var query = _context.ScrobbleEvent

View file

@ -82,6 +82,7 @@ public interface ISeriesRepository
void Attach(Series series);
void Attach(SeriesRelation relation);
void Update(Series series);
void Update(SeriesMetadata seriesMetadata);
void Remove(Series series);
void Remove(IEnumerable<Series> series);
void Detach(Series series);
@ -219,6 +220,11 @@ public class SeriesRepository : ISeriesRepository
_context.Entry(series).State = EntityState.Modified;
}
public void Update(SeriesMetadata seriesMetadata)
{
_context.Entry(seriesMetadata).State = EntityState.Modified;
}
public void Remove(Series series)
{
_context.Series.Remove(series);
@ -735,6 +741,7 @@ public class SeriesRepository : ISeriesRepository
{
return await _context.Series
.Where(s => s.Id == seriesId)
.Include(s => s.ExternalSeriesMetadata)
.Select(series => new PlusSeriesRequestDto()
{
MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format),
@ -744,6 +751,7 @@ public class SeriesRepository : ISeriesRepository
ScrobblingService.AniListWeblinkWebsite),
MalId = ScrobblingService.ExtractId<long?>(series.Metadata.WebLinks,
ScrobblingService.MalWeblinkWebsite),
CbrId = series.ExternalSeriesMetadata.CbrId,
GoogleBooksId = ScrobblingService.ExtractId<string?>(series.Metadata.WebLinks,
ScrobblingService.GoogleBooksWeblinkWebsite),
MangaDexId = ScrobblingService.ExtractId<string?>(series.Metadata.WebLinks,
@ -1088,8 +1096,6 @@ public class SeriesRepository : ISeriesRepository
return query.Where(s => false);
}
// First setup any FilterField.Libraries in the statements, as these don't have any traditional query statements applied here
query = ApplyLibraryFilter(filter, query);
@ -1290,7 +1296,7 @@ public class SeriesRepository : ISeriesRepository
FilterField.ReadingDate => query.HasReadingDate(true, statement.Comparison, (DateTime) value, userId),
FilterField.ReadLast => query.HasReadLast(true, statement.Comparison, (int) value, userId),
FilterField.AverageRating => query.HasAverageRating(true, statement.Comparison, (float) value),
_ => throw new ArgumentOutOfRangeException()
_ => throw new ArgumentOutOfRangeException(nameof(statement.Field), $"Unexpected value for field: {statement.Field}")
};
}

View file

@ -2,9 +2,11 @@
using System.Linq;
using System.Threading.Tasks;
using API.DTOs.Metadata;
using API.DTOs.Metadata.Browse;
using API.Entities;
using API.Extensions;
using API.Extensions.QueryExtensions;
using API.Helpers;
using API.Services.Tasks.Scanner.Parser;
using AutoMapper;
using AutoMapper.QueryableExtensions;
@ -23,6 +25,7 @@ public interface ITagRepository
Task RemoveAllTagNoLongerAssociated();
Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(int userId, IList<int>? libraryIds = null);
Task<List<string>> GetAllTagsNotInListAsync(ICollection<string> tags);
Task<PagedList<BrowseTagDto>> GetBrowseableTag(int userId, UserParams userParams);
}
public class TagRepository : ITagRepository
@ -104,6 +107,40 @@ public class TagRepository : ITagRepository
return missingTags.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList();
}
public async Task<PagedList<BrowseTagDto>> GetBrowseableTag(int userId, UserParams userParams)
{
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
var allLibrariesCount = await _context.Library.CountAsync();
var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync();
var seriesIds = _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id);
var query = _context.Tag
.RestrictAgainstAgeRestriction(ageRating)
.WhereIf(userLibs.Count != allLibrariesCount,
tag => tag.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) ||
tag.SeriesMetadatas.Any(sm => seriesIds.Contains(sm.SeriesId)))
.Select(g => new BrowseTagDto
{
Id = g.Id,
Title = g.Title,
SeriesCount = g.SeriesMetadatas
.Where(sm => allLibrariesCount == userLibs.Count || seriesIds.Contains(sm.SeriesId))
.RestrictAgainstAgeRestriction(ageRating)
.Distinct()
.Count(),
ChapterCount = g.Chapters
.Where(ch => allLibrariesCount == userLibs.Count || seriesIds.Contains(ch.Volume.SeriesId))
.RestrictAgainstAgeRestriction(ageRating)
.Distinct()
.Count()
})
.OrderBy(g => g.Title);
return await PagedList<BrowseTagDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
}
public async Task<IList<Tag>> GetAllTagsAsync()
{
return await _context.Tag.ToListAsync();

View file

@ -768,7 +768,7 @@ public class UserRepository : IUserRepository
/// <summary>
/// Fetches the UserId by API Key. This does not include any extra information
/// Fetches the AppUserId by API Key. This does not include any extra information
/// </summary>
/// <param name="apiKey"></param>
/// <returns></returns>