Merged develop in
This commit is contained in:
commit
d12a79892f
1443 changed files with 215765 additions and 44113 deletions
|
|
@ -5,8 +5,10 @@ using System.Text;
|
|||
using System.Threading.Tasks;
|
||||
using API.Data.ManualMigrations;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Progress;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
|
|
@ -14,9 +16,11 @@ using Microsoft.EntityFrameworkCore;
|
|||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
public interface IAppUserProgressRepository
|
||||
{
|
||||
void Update(AppUserProgress userProgress);
|
||||
void Remove(AppUserProgress userProgress);
|
||||
Task<int> CleanupAbandonedChapters();
|
||||
Task<bool> UserHasProgress(LibraryType libraryType, int userId);
|
||||
Task<AppUserProgress?> GetUserProgressAsync(int chapterId, int userId);
|
||||
|
|
@ -36,8 +40,9 @@ public interface IAppUserProgressRepository
|
|||
Task<DateTime?> GetLatestProgressForSeries(int seriesId, int userId);
|
||||
Task<DateTime?> GetFirstProgressForSeries(int seriesId, int userId);
|
||||
Task UpdateAllProgressThatAreMoreThanChapterPages();
|
||||
Task<IList<FullProgressDto>> GetUserProgressForChapter(int chapterId, int userId = 0);
|
||||
}
|
||||
#nullable disable
|
||||
|
||||
public class AppUserProgressRepository : IAppUserProgressRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
|
|
@ -54,6 +59,11 @@ public class AppUserProgressRepository : IAppUserProgressRepository
|
|||
_context.Entry(userProgress).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public void Remove(AppUserProgress userProgress)
|
||||
{
|
||||
_context.Remove(userProgress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will remove any entries that have chapterIds that no longer exists. This will execute the save as well.
|
||||
/// </summary>
|
||||
|
|
@ -167,9 +177,10 @@ public class AppUserProgressRepository : IAppUserProgressRepository
|
|||
(appUserProgresses, chapter) => new {appUserProgresses, chapter})
|
||||
.Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId &&
|
||||
p.appUserProgresses.PagesRead >= p.chapter.Pages)
|
||||
.Select(p => p.chapter.Range)
|
||||
.Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber)
|
||||
.Select(p => p.chapter.MaxNumber)
|
||||
.ToListAsync();
|
||||
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(Parser.MaxNumberFromRange(d)));
|
||||
return list.Count == 0 ? 0 : (int) list.DefaultIfEmpty().Max(d => d);
|
||||
}
|
||||
|
||||
public async Task<float> GetHighestFullyReadVolumeForSeries(int seriesId, int userId)
|
||||
|
|
@ -179,8 +190,10 @@ public class AppUserProgressRepository : IAppUserProgressRepository
|
|||
(appUserProgresses, chapter) => new {appUserProgresses, chapter})
|
||||
.Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId &&
|
||||
p.appUserProgresses.PagesRead >= p.chapter.Pages)
|
||||
.Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber)
|
||||
.Select(p => p.chapter.Volume.MaxNumber)
|
||||
.ToListAsync();
|
||||
|
||||
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max();
|
||||
}
|
||||
|
||||
|
|
@ -231,6 +244,33 @@ public class AppUserProgressRepository : IAppUserProgressRepository
|
|||
await _context.Database.ExecuteSqlRawAsync(batchSql);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="userId">If 0, will pull all records</param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<FullProgressDto>> GetUserProgressForChapter(int chapterId, int userId = 0)
|
||||
{
|
||||
return await _context.AppUserProgresses
|
||||
.WhereIf(userId > 0, p => p.AppUserId == userId)
|
||||
.Where(p => p.ChapterId == chapterId)
|
||||
.Include(p => p.AppUser)
|
||||
.Select(p => new FullProgressDto()
|
||||
{
|
||||
AppUserId = p.AppUserId,
|
||||
ChapterId = p.ChapterId,
|
||||
PagesRead = p.PagesRead,
|
||||
Id = p.Id,
|
||||
Created = p.Created,
|
||||
CreatedUtc = p.CreatedUtc,
|
||||
LastModified = p.LastModified,
|
||||
LastModifiedUtc = p.LastModifiedUtc,
|
||||
UserName = p.AppUser.UserName
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
public async Task<AppUserProgress?> GetUserProgressAsync(int chapterId, int userId)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -22,11 +22,16 @@ public enum ChapterIncludes
|
|||
None = 1,
|
||||
Volumes = 2,
|
||||
Files = 4,
|
||||
People = 8,
|
||||
Genres = 16,
|
||||
Tags = 32
|
||||
}
|
||||
|
||||
public interface IChapterRepository
|
||||
{
|
||||
void Update(Chapter chapter);
|
||||
void Remove(Chapter chapter);
|
||||
void Remove(IList<Chapter> chapters);
|
||||
Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds, ChapterIncludes includes = ChapterIncludes.None);
|
||||
Task<IChapterInfoDto?> GetChapterInfoDtoAsync(int chapterId);
|
||||
Task<int> GetChapterTotalPagesAsync(int chapterId);
|
||||
|
|
@ -34,7 +39,7 @@ public interface IChapterRepository
|
|||
Task<ChapterDto?> GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files);
|
||||
Task<ChapterMetadataDto?> GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files);
|
||||
Task<IList<MangaFile>> GetFilesForChapterAsync(int chapterId);
|
||||
Task<IList<Chapter>> GetChaptersAsync(int volumeId);
|
||||
Task<IList<Chapter>> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None);
|
||||
Task<IList<MangaFile>> GetFilesForChaptersAsync(IReadOnlyList<int> chapterIds);
|
||||
Task<string?> GetChapterCoverImageAsync(int chapterId);
|
||||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
|
|
@ -42,6 +47,7 @@ public interface IChapterRepository
|
|||
Task<IEnumerable<string>> GetCoverImagesForLockedChaptersAsync();
|
||||
Task<ChapterDto> AddChapterModifiers(int userId, ChapterDto chapter);
|
||||
IEnumerable<Chapter> GetChaptersForSeries(int seriesId);
|
||||
Task<IList<Chapter>> GetAllChaptersForSeries(int seriesId);
|
||||
}
|
||||
public class ChapterRepository : IChapterRepository
|
||||
{
|
||||
|
|
@ -59,6 +65,16 @@ public class ChapterRepository : IChapterRepository
|
|||
_context.Entry(chapter).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public void Remove(Chapter chapter)
|
||||
{
|
||||
_context.Chapter.Remove(chapter);
|
||||
}
|
||||
|
||||
public void Remove(IList<Chapter> chapters)
|
||||
{
|
||||
_context.Chapter.RemoveRange(chapters);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds, ChapterIncludes includes = ChapterIncludes.None)
|
||||
{
|
||||
return await _context.Chapter
|
||||
|
|
@ -78,7 +94,7 @@ public class ChapterRepository : IChapterRepository
|
|||
.Where(c => c.Id == chapterId)
|
||||
.Join(_context.Volume, c => c.VolumeId, v => v.Id, (chapter, volume) => new
|
||||
{
|
||||
ChapterNumber = chapter.Range,
|
||||
ChapterNumber = chapter.MinNumber,
|
||||
VolumeNumber = volume.Name,
|
||||
VolumeId = volume.Id,
|
||||
chapter.IsSpecial,
|
||||
|
|
@ -102,8 +118,8 @@ public class ChapterRepository : IChapterRepository
|
|||
})
|
||||
.Select(data => new ChapterInfoDto()
|
||||
{
|
||||
ChapterNumber = data.ChapterNumber,
|
||||
VolumeNumber = data.VolumeNumber + string.Empty,
|
||||
ChapterNumber = data.ChapterNumber + string.Empty, // TODO: Fix this
|
||||
VolumeNumber = data.VolumeNumber + string.Empty, // TODO: Fix this
|
||||
VolumeId = data.VolumeId,
|
||||
IsSpecial = data.IsSpecial,
|
||||
SeriesId = data.SeriesId,
|
||||
|
|
@ -175,6 +191,7 @@ public class ChapterRepository : IChapterRepository
|
|||
{
|
||||
return await _context.Chapter
|
||||
.Includes(includes)
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.FirstOrDefaultAsync(c => c.Id == chapterId);
|
||||
}
|
||||
|
||||
|
|
@ -183,10 +200,12 @@ public class ChapterRepository : IChapterRepository
|
|||
/// </summary>
|
||||
/// <param name="volumeId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<Chapter>> GetChaptersAsync(int volumeId)
|
||||
public async Task<IList<Chapter>> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => c.VolumeId == volumeId)
|
||||
.Includes(includes)
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
|
@ -267,11 +286,28 @@ public class ChapterRepository : IChapterRepository
|
|||
return chapter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Includes Volumes
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<Chapter> GetChaptersForSeries(int seriesId)
|
||||
{
|
||||
return _context.Chapter
|
||||
.Where(c => c.Volume.SeriesId == seriesId)
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.Include(c => c.Volume)
|
||||
.AsEnumerable();
|
||||
}
|
||||
|
||||
public async Task<IList<Chapter>> GetAllChaptersForSeries(int seriesId)
|
||||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => c.Volume.SeriesId == seriesId)
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.Include(c => c.Volume)
|
||||
.Include(c => c.People)
|
||||
.ThenInclude(cp => cp.Person)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,44 +3,64 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.Misc;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.DTOs.Collection;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Extensions.QueryExtensions.Filtering;
|
||||
using API.Services.Plus;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
|
||||
#nullable enable
|
||||
|
||||
[Flags]
|
||||
public enum CollectionTagIncludes
|
||||
{
|
||||
None = 1,
|
||||
SeriesMetadata = 2,
|
||||
SeriesMetadataWithSeries = 4
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum CollectionIncludes
|
||||
{
|
||||
None = 1,
|
||||
Series = 2,
|
||||
}
|
||||
|
||||
public interface ICollectionTagRepository
|
||||
{
|
||||
void Add(CollectionTag tag);
|
||||
void Remove(CollectionTag tag);
|
||||
Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync();
|
||||
Task<IEnumerable<CollectionTagDto>> SearchTagDtosAsync(string searchQuery, int userId);
|
||||
void Remove(AppUserCollection tag);
|
||||
Task<string?> GetCoverImageAsync(int collectionTagId);
|
||||
Task<IEnumerable<CollectionTagDto>> GetAllPromotedTagDtosAsync(int userId);
|
||||
Task<CollectionTag?> GetTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.None);
|
||||
void Update(CollectionTag tag);
|
||||
Task<int> RemoveTagsWithoutSeries();
|
||||
Task<IEnumerable<CollectionTag>> GetAllTagsAsync(CollectionTagIncludes includes = CollectionTagIncludes.None);
|
||||
Task<AppUserCollection?> GetCollectionAsync(int tagId, CollectionIncludes includes = CollectionIncludes.None);
|
||||
void Update(AppUserCollection tag);
|
||||
Task<int> RemoveCollectionsWithoutSeries();
|
||||
|
||||
Task<IEnumerable<AppUserCollection>> GetAllCollectionsAsync(CollectionIncludes includes = CollectionIncludes.None);
|
||||
/// <summary>
|
||||
/// Returns all of the user's collections with the option of other user's promoted
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="includePromoted"></param>
|
||||
/// <returns></returns>
|
||||
Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosAsync(int userId, bool includePromoted = false);
|
||||
Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false);
|
||||
|
||||
Task<IEnumerable<CollectionTag>> GetAllTagsByNamesAsync(IEnumerable<string> normalizedTitles,
|
||||
CollectionTagIncludes includes = CollectionTagIncludes.None);
|
||||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
Task<bool> TagExists(string title);
|
||||
Task<IList<CollectionTag>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||
Task<bool> CollectionExists(string title, int userId);
|
||||
Task<IList<AppUserCollection>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||
Task<IList<string>> GetRandomCoverImagesAsync(int collectionId);
|
||||
Task<IList<AppUserCollection>> GetCollectionsForUserAsync(int userId, CollectionIncludes includes = CollectionIncludes.None);
|
||||
Task UpdateCollectionAgeRating(AppUserCollection tag);
|
||||
Task<IEnumerable<AppUserCollection>> GetCollectionsByIds(IEnumerable<int> tags, CollectionIncludes includes = CollectionIncludes.None);
|
||||
Task<IList<AppUserCollection>> GetAllCollectionsForSyncing(DateTime expirationTime);
|
||||
}
|
||||
|
||||
public class CollectionTagRepository : ICollectionTagRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
|
|
@ -52,17 +72,12 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public void Add(CollectionTag tag)
|
||||
public void Remove(AppUserCollection tag)
|
||||
{
|
||||
_context.CollectionTag.Add(tag);
|
||||
_context.AppUserCollection.Remove(tag);
|
||||
}
|
||||
|
||||
public void Remove(CollectionTag tag)
|
||||
{
|
||||
_context.CollectionTag.Remove(tag);
|
||||
}
|
||||
|
||||
public void Update(CollectionTag tag)
|
||||
public void Update(AppUserCollection tag)
|
||||
{
|
||||
_context.Entry(tag).State = EntityState.Modified;
|
||||
}
|
||||
|
|
@ -70,38 +85,53 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
/// <summary>
|
||||
/// Removes any collection tags without any series
|
||||
/// </summary>
|
||||
public async Task<int> RemoveTagsWithoutSeries()
|
||||
public async Task<int> RemoveCollectionsWithoutSeries()
|
||||
{
|
||||
var tagsToDelete = await _context.CollectionTag
|
||||
.Include(c => c.SeriesMetadatas)
|
||||
.Where(c => c.SeriesMetadatas.Count == 0)
|
||||
var tagsToDelete = await _context.AppUserCollection
|
||||
.Include(c => c.Items)
|
||||
.Where(c => c.Items.Count == 0)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
_context.RemoveRange(tagsToDelete);
|
||||
|
||||
return await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CollectionTag>> GetAllTagsAsync(CollectionTagIncludes includes = CollectionTagIncludes.None)
|
||||
public async Task<IEnumerable<AppUserCollection>> GetAllCollectionsAsync(CollectionIncludes includes = CollectionIncludes.None)
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
return await _context.AppUserCollection
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.Includes(includes)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CollectionTag>> GetAllTagsByNamesAsync(IEnumerable<string> normalizedTitles, CollectionTagIncludes includes = CollectionTagIncludes.None)
|
||||
public async Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosAsync(int userId, bool includePromoted = false)
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
.Where(c => normalizedTitles.Contains(c.NormalizedTitle))
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.Includes(includes)
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
return await _context.AppUserCollection
|
||||
.Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted))
|
||||
.WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating)
|
||||
.OrderBy(uc => uc.Title)
|
||||
.ProjectTo<AppUserCollectionDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
return await _context.AppUserCollection
|
||||
.Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted))
|
||||
.Where(uc => uc.Items.Any(s => s.Id == seriesId))
|
||||
.WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating)
|
||||
.OrderBy(uc => uc.Title)
|
||||
.ProjectTo<AppUserCollectionDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<string?> GetCoverImageAsync(int collectionTagId)
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
return await _context.AppUserCollection
|
||||
.Where(c => c.Id == collectionTagId)
|
||||
.Select(c => c.CoverImage)
|
||||
.SingleOrDefaultAsync();
|
||||
|
|
@ -109,23 +139,30 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
|
||||
public async Task<IList<string>> GetAllCoverImagesAsync()
|
||||
{
|
||||
return (await _context.CollectionTag
|
||||
return await _context.AppUserCollection
|
||||
.Select(t => t.CoverImage)
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.ToListAsync())!;
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> TagExists(string title)
|
||||
/// <summary>
|
||||
/// If any tag exists for that given user's collections
|
||||
/// </summary>
|
||||
/// <param name="title"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> CollectionExists(string title, int userId)
|
||||
{
|
||||
var normalized = title.ToNormalized();
|
||||
return await _context.CollectionTag
|
||||
return await _context.AppUserCollection
|
||||
.Where(uc => uc.AppUserId == userId)
|
||||
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized));
|
||||
}
|
||||
|
||||
public async Task<IList<CollectionTag>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
|
||||
public async Task<IList<AppUserCollection>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
|
||||
{
|
||||
var extension = encodeFormat.GetExtension();
|
||||
return await _context.CollectionTag
|
||||
return await _context.AppUserCollection
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
|
@ -133,44 +170,61 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
public async Task<IList<string>> GetRandomCoverImagesAsync(int collectionId)
|
||||
{
|
||||
var random = new Random();
|
||||
var data = await _context.CollectionTag
|
||||
var data = await _context.AppUserCollection
|
||||
.Where(t => t.Id == collectionId)
|
||||
.SelectMany(t => t.SeriesMetadatas)
|
||||
.Select(sm => sm.Series.CoverImage)
|
||||
.SelectMany(uc => uc.Items.Select(series => series.CoverImage))
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.ToListAsync();
|
||||
|
||||
return data
|
||||
.OrderBy(_ => random.Next())
|
||||
.Take(4)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync()
|
||||
public async Task<IList<AppUserCollection>> GetCollectionsForUserAsync(int userId, CollectionIncludes includes = CollectionIncludes.None)
|
||||
{
|
||||
|
||||
return await _context.CollectionTag
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||
return await _context.AppUserCollection
|
||||
.Where(c => c.AppUserId == userId)
|
||||
.Includes(includes)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CollectionTagDto>> GetAllPromotedTagDtosAsync(int userId)
|
||||
public async Task UpdateCollectionAgeRating(AppUserCollection tag)
|
||||
{
|
||||
var userRating = await GetUserAgeRestriction(userId);
|
||||
return await _context.CollectionTag
|
||||
.Where(c => c.Promoted)
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||
var maxAgeRating = await _context.AppUserCollection
|
||||
.Where(t => t.Id == tag.Id)
|
||||
.SelectMany(uc => uc.Items.Select(s => s.Metadata))
|
||||
.Select(sm => sm.AgeRating)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
tag.AgeRating = maxAgeRating.Count != 0 ? maxAgeRating.Max() : AgeRating.Unknown;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AppUserCollection>> GetCollectionsByIds(IEnumerable<int> tags, CollectionIncludes includes = CollectionIncludes.None)
|
||||
{
|
||||
return await _context.AppUserCollection
|
||||
.Where(c => tags.Contains(c.Id))
|
||||
.Includes(includes)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<CollectionTag?> GetTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.None)
|
||||
public async Task<IList<AppUserCollection>> GetAllCollectionsForSyncing(DateTime expirationTime)
|
||||
{
|
||||
return await _context.CollectionTag
|
||||
return await _context.AppUserCollection
|
||||
.Where(c => c.Source == ScrobbleProvider.Mal)
|
||||
.Where(c => c.LastSyncUtc <= expirationTime)
|
||||
.Include(c => c.Items)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<AppUserCollection?> GetCollectionAsync(int tagId, CollectionIncludes includes = CollectionIncludes.None)
|
||||
{
|
||||
return await _context.AppUserCollection
|
||||
.Where(c => c.Id == tagId)
|
||||
.Includes(includes)
|
||||
.AsSplitQuery()
|
||||
|
|
@ -190,16 +244,12 @@ public class CollectionTagRepository : ICollectionTagRepository
|
|||
.SingleAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CollectionTagDto>> SearchTagDtosAsync(string searchQuery, int userId)
|
||||
public async Task<IEnumerable<AppUserCollectionDto>> SearchTagDtosAsync(string searchQuery, int userId)
|
||||
{
|
||||
var userRating = await GetUserAgeRestriction(userId);
|
||||
return await _context.CollectionTag
|
||||
.Where(s => EF.Functions.Like(s.Title!, $"%{searchQuery}%")
|
||||
|| EF.Functions.Like(s.NormalizedTitle!, $"%{searchQuery}%"))
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.OrderBy(s => s.NormalizedTitle)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
|
||||
return await _context.AppUserCollection
|
||||
.Search(searchQuery, userId, userRating)
|
||||
.ProjectTo<AppUserCollectionDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
86
API/Data/Repositories/CoverDbRepository.cs
Normal file
86
API/Data/Repositories/CoverDbRepository.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using API.DTOs.CoverDb;
|
||||
using API.Entities;
|
||||
using API.Entities.Person;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// This is a manual repository, not a DB repo
|
||||
/// </summary>
|
||||
public class CoverDbRepository
|
||||
{
|
||||
private readonly List<CoverDbAuthor> _authors;
|
||||
|
||||
public CoverDbRepository(string filePath)
|
||||
{
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||
.Build();
|
||||
|
||||
// Read and deserialize YAML file
|
||||
var yamlContent = File.ReadAllText(filePath);
|
||||
var peopleData = deserializer.Deserialize<CoverDbPeople>(yamlContent);
|
||||
_authors = peopleData.People;
|
||||
}
|
||||
|
||||
public CoverDbAuthor? FindAuthorByNameOrAlias(string name)
|
||||
{
|
||||
return _authors.Find(author =>
|
||||
author.Name.Equals(name, StringComparison.OrdinalIgnoreCase) ||
|
||||
author.Aliases.Contains(name, StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public CoverDbAuthor? FindBestAuthorMatch(Person person)
|
||||
{
|
||||
var aniListId = person.AniListId > 0 ? $"{person.AniListId}" : string.Empty;
|
||||
var highestScore = 0;
|
||||
CoverDbAuthor? bestMatch = null;
|
||||
|
||||
foreach (var author in _authors)
|
||||
{
|
||||
var score = 0;
|
||||
|
||||
// Check metadata IDs and add points if they match
|
||||
if (!string.IsNullOrEmpty(author.Ids.AmazonId) && author.Ids.AmazonId == person.Asin)
|
||||
{
|
||||
score += 10;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(author.Ids.AnilistId) && author.Ids.AnilistId == aniListId)
|
||||
{
|
||||
score += 10;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(author.Ids.HardcoverId) && author.Ids.HardcoverId == person.HardcoverId)
|
||||
{
|
||||
score += 10;
|
||||
}
|
||||
|
||||
// Check for exact name match
|
||||
if (author.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
score += 7;
|
||||
}
|
||||
|
||||
// Check for alias match
|
||||
if (author.Aliases.Contains(person.Name, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
score += 5;
|
||||
}
|
||||
|
||||
// Update the best match if current score is higher
|
||||
if (score <= highestScore) continue;
|
||||
|
||||
highestScore = score;
|
||||
bestMatch = author;
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
}
|
||||
37
API/Data/Repositories/EmailHistoryRepository.cs
Normal file
37
API/Data/Repositories/EmailHistoryRepository.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.Email;
|
||||
using API.Entities;
|
||||
using API.Helpers;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
|
||||
public interface IEmailHistoryRepository
|
||||
{
|
||||
Task<IList<EmailHistoryDto>> GetEmailDtos(UserParams userParams);
|
||||
}
|
||||
|
||||
public class EmailHistoryRepository : IEmailHistoryRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public EmailHistoryRepository(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
|
||||
public async Task<IList<EmailHistoryDto>> GetEmailDtos(UserParams userParams)
|
||||
{
|
||||
return await _context.EmailHistory
|
||||
.OrderByDescending(h => h.SendDate)
|
||||
.ProjectTo<EmailHistoryDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.DTOs;
|
||||
using API.DTOs.KavitaPlus.Manage;
|
||||
using API.DTOs.Recommendation;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.DTOs.SeriesDetail;
|
||||
|
|
@ -31,13 +32,12 @@ public interface IExternalSeriesMetadataRepository
|
|||
void Remove(IEnumerable<ExternalRecommendation>? recommendations);
|
||||
void Remove(ExternalSeriesMetadata metadata);
|
||||
Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId);
|
||||
Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId);
|
||||
Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId);
|
||||
Task<bool> NeedsDataRefresh(int seriesId);
|
||||
Task<SeriesDetailPlusDto?> GetSeriesDetailPlusDto(int seriesId);
|
||||
Task LinkRecommendationsToSeries(Series series);
|
||||
Task<bool> IsBlacklistedSeries(int seriesId);
|
||||
Task CreateBlacklistedSeries(int seriesId, bool saveChanges = true);
|
||||
Task RemoveFromBlacklist(int seriesId);
|
||||
Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit);
|
||||
Task<IList<int>> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false);
|
||||
Task<IList<ManageMatchSeriesDto>> GetAllSeries(ManageMatchFilterDto filter);
|
||||
}
|
||||
|
||||
public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepository
|
||||
|
|
@ -106,7 +106,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId)
|
||||
public async Task<bool> NeedsDataRefresh(int seriesId)
|
||||
{
|
||||
var row = await _context.ExternalSeriesMetadata
|
||||
.Where(s => s.SeriesId == seriesId)
|
||||
|
|
@ -114,7 +114,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||
return row == null || row.ValidUntilUtc <= DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public async Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId)
|
||||
public async Task<SeriesDetailPlusDto?> GetSeriesDetailPlusDto(int seriesId)
|
||||
{
|
||||
var seriesDetailDto = await _context.ExternalSeriesMetadata
|
||||
.Where(m => m.SeriesId == seriesId)
|
||||
|
|
@ -157,8 +157,8 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||
.OrderByDescending(r => r.Score);
|
||||
}
|
||||
|
||||
IEnumerable<RatingDto> ratings = new List<RatingDto>();
|
||||
if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Any())
|
||||
IEnumerable<RatingDto> ratings = [];
|
||||
if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Count != 0)
|
||||
{
|
||||
ratings = seriesDetailDto.ExternalRatings
|
||||
.Select(r => _mapper.Map<RatingDto>(r));
|
||||
|
|
@ -191,6 +191,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||
.Where(r => EF.Functions.Like(r.Name, series.Name) ||
|
||||
EF.Functions.Like(r.Name, series.LocalizedName))
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var rec in recMatches)
|
||||
{
|
||||
rec.SeriesId = series.Id;
|
||||
|
|
@ -201,55 +202,38 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
|
|||
|
||||
public Task<bool> IsBlacklistedSeries(int seriesId)
|
||||
{
|
||||
return _context.SeriesBlacklist.AnyAsync(s => s.SeriesId == seriesId);
|
||||
return _context.Series
|
||||
.Where(s => s.Id == seriesId)
|
||||
.Select(s => s.IsBlacklisted)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance against SeriesId and Saves to the DB
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="saveChanges"></param>
|
||||
public async Task CreateBlacklistedSeries(int seriesId, bool saveChanges = true)
|
||||
{
|
||||
if (seriesId <= 0 || await _context.SeriesBlacklist.AnyAsync(s => s.SeriesId == seriesId)) return;
|
||||
|
||||
await _context.SeriesBlacklist.AddAsync(new SeriesBlacklist()
|
||||
{
|
||||
SeriesId = seriesId
|
||||
});
|
||||
if (saveChanges)
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the Series from Blacklist and Saves to the DB
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
public async Task RemoveFromBlacklist(int seriesId)
|
||||
{
|
||||
var seriesBlacklist = await _context.SeriesBlacklist.FirstOrDefaultAsync(sb => sb.SeriesId == seriesId);
|
||||
|
||||
if (seriesBlacklist != null)
|
||||
{
|
||||
// Remove the SeriesBlacklist entity from the context
|
||||
_context.SeriesBlacklist.Remove(seriesBlacklist);
|
||||
|
||||
// Save the changes to the database
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit)
|
||||
public async Task<IList<int>> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false)
|
||||
{
|
||||
return await _context.Series
|
||||
.Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
|
||||
.Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow)
|
||||
.Where(s => s.Library.AllowMetadataMatching)
|
||||
.WhereIf(includeStaleData, s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow)
|
||||
.Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.AniListId == 0)
|
||||
.Where(s => !s.IsBlacklisted && !s.DontMatch)
|
||||
.OrderByDescending(s => s.Library.Type)
|
||||
.ThenBy(s => s.NormalizedName)
|
||||
.Select(s => s.Id)
|
||||
.Take(limit)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<ManageMatchSeriesDto>> GetAllSeries(ManageMatchFilterDto filter)
|
||||
{
|
||||
return await _context.Series
|
||||
.Include(s => s.Library)
|
||||
.Include(s => s.ExternalSeriesMetadata)
|
||||
.Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
|
||||
.Where(s => s.Library.AllowMetadataMatching)
|
||||
.FilterMatchState(filter.MatchStateOption)
|
||||
.OrderBy(s => s.NormalizedName)
|
||||
.ProjectTo<ManageMatchSeriesDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ using API.DTOs.Metadata;
|
|||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
public interface IGenreRepository
|
||||
{
|
||||
|
|
@ -19,12 +21,12 @@ public interface IGenreRepository
|
|||
Task<Genre?> FindByNameAsync(string genreName);
|
||||
Task<IList<Genre>> GetAllGenresAsync();
|
||||
Task<IList<Genre>> GetAllGenresByNamesAsync(IEnumerable<string> normalizedNames);
|
||||
Task<IList<GenreTagDto>> GetAllGenreDtosAsync(int userId);
|
||||
Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false);
|
||||
Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds, int userId);
|
||||
Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(int userId, IList<int>? libraryIds = null, QueryContext context = QueryContext.None);
|
||||
Task<int> GetCountAsync();
|
||||
Task<GenreTagDto> GetRandomGenre();
|
||||
Task<GenreTagDto> GetGenreById(int id);
|
||||
Task<List<string>> GetAllGenresNotInListAsync(ICollection<string> genreNames);
|
||||
}
|
||||
|
||||
public class GenreRepository : IGenreRepository
|
||||
|
|
@ -69,27 +71,6 @@ public class GenreRepository : IGenreRepository
|
|||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a set of Genre tags for a set of library Ids. UserId will restrict returned Genres based on user's age restriction.
|
||||
/// </summary>
|
||||
/// <param name="libraryIds"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(IList<int> libraryIds, int userId)
|
||||
{
|
||||
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
return await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.SelectMany(s => s.Metadata.Genres)
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.OrderBy(p => p.NormalizedTitle)
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<int> GetCountAsync()
|
||||
{
|
||||
return await _context.Genre.CountAsync();
|
||||
|
|
@ -128,14 +109,60 @@ public class GenreRepository : IGenreRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<GenreTagDto>> GetAllGenreDtosAsync(int userId)
|
||||
/// <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.
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="libraryIds"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<GenreTagDto>> GetAllGenreDtosForLibrariesAsync(int userId, IList<int>? libraryIds = null, QueryContext context = QueryContext.None)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
return await _context.Genre
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.OrderBy(g => g.NormalizedTitle)
|
||||
.AsNoTracking()
|
||||
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
var userLibs = await _context.Library.GetUserLibraries(userId, context).ToListAsync();
|
||||
|
||||
if (libraryIds is {Count: > 0})
|
||||
{
|
||||
userLibs = userLibs.Where(libraryIds.Contains).ToList();
|
||||
}
|
||||
|
||||
return await _context.Series
|
||||
.Where(s => userLibs.Contains(s.LibraryId))
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.SelectMany(s => s.Metadata.Genres)
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.OrderBy(p => p.NormalizedTitle)
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all genres that are not already present in the system.
|
||||
/// Normalizes genres for lookup, but returns non-normalized names for creation.
|
||||
/// </summary>
|
||||
/// <param name="genreNames">The list of genre names (non-normalized).</param>
|
||||
/// <returns>A list of genre names that do not exist in the system.</returns>
|
||||
public async Task<List<string>> GetAllGenresNotInListAsync(ICollection<string> genreNames)
|
||||
{
|
||||
// Group the genres by their normalized names, keeping track of the original names
|
||||
var normalizedToOriginalMap = genreNames
|
||||
.Distinct()
|
||||
.GroupBy(Parser.Normalize)
|
||||
.ToDictionary(group => group.Key, group => group.First()); // Take the first original name for each normalized name
|
||||
|
||||
var normalizedGenreNames = normalizedToOriginalMap.Keys.ToList();
|
||||
|
||||
// Query the database for existing genres using the normalized names
|
||||
var existingGenres = await _context.Genre
|
||||
.Where(g => normalizedGenreNames.Contains(g.NormalizedTitle)) // Assuming you have a normalized field
|
||||
.Select(g => g.NormalizedTitle)
|
||||
.ToListAsync();
|
||||
|
||||
// Find the normalized genres that do not exist in the database
|
||||
var missingGenres = normalizedGenreNames.Except(existingGenres).ToList();
|
||||
|
||||
// Return the original non-normalized genres for the missing ones
|
||||
return missingGenres.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ using Kavita.Common.Extensions;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
[Flags]
|
||||
public enum LibraryIncludes
|
||||
|
|
@ -39,7 +40,7 @@ public interface ILibraryRepository
|
|||
Task<bool> LibraryExists(string libraryName);
|
||||
Task<Library?> GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None);
|
||||
IEnumerable<LibraryDto> GetLibraryDtosForUsernameAsync(string userName);
|
||||
Task<IEnumerable<Library>> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None);
|
||||
Task<IEnumerable<Library>> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None, bool track = true);
|
||||
Task<IEnumerable<Library>> GetLibrariesForUserIdAsync(int userId);
|
||||
IEnumerable<int> GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None);
|
||||
Task<LibraryType> GetLibraryTypeAsync(int libraryId);
|
||||
|
|
@ -105,13 +106,16 @@ public class LibraryRepository : ILibraryRepository
|
|||
/// </summary>
|
||||
/// <param name="includes"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<Library>> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None)
|
||||
public async Task<IEnumerable<Library>> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None, bool track = true)
|
||||
{
|
||||
return await _context.Library
|
||||
var query = _context.Library
|
||||
.Include(l => l.AppUsers)
|
||||
.Includes(includes)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
.AsSplitQuery();
|
||||
|
||||
if (track) return await query.ToListAsync();
|
||||
|
||||
return await query.AsNoTracking().ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -258,7 +262,7 @@ public class LibraryRepository : ILibraryRepository
|
|||
public async Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int>? libraryIds)
|
||||
{
|
||||
var ret = await _context.Series
|
||||
.WhereIf(libraryIds is {Count: > 0} , s => libraryIds.Contains(s.LibraryId))
|
||||
.WhereIf(libraryIds is {Count: > 0} , s => libraryIds!.Contains(s.LibraryId))
|
||||
.Select(s => s.Metadata.Language)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
|
|
@ -319,7 +323,7 @@ public class LibraryRepository : ILibraryRepository
|
|||
/// <returns></returns>
|
||||
public async Task<bool> DoAnySeriesFoldersMatch(IEnumerable<string> folders)
|
||||
{
|
||||
var normalized = folders.Select(Services.Tasks.Scanner.Parser.Parser.NormalizePath);
|
||||
var normalized = folders.Select(Parser.NormalizePath);
|
||||
return await _context.Series.AnyAsync(s => normalized.Contains(s.FolderPath));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,15 +9,18 @@ using AutoMapper.QueryableExtensions;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
public interface IMediaErrorRepository
|
||||
{
|
||||
void Attach(MediaError error);
|
||||
void Remove(MediaError error);
|
||||
void Remove(IList<MediaError> errors);
|
||||
Task<MediaError> Find(string filename);
|
||||
IEnumerable<MediaErrorDto> GetAllErrorDtosAsync();
|
||||
Task<bool> ExistsAsync(MediaError error);
|
||||
Task DeleteAll();
|
||||
Task<List<MediaError>> GetAllErrorsAsync(IList<string> comments);
|
||||
}
|
||||
|
||||
public class MediaErrorRepository : IMediaErrorRepository
|
||||
|
|
@ -43,6 +46,11 @@ public class MediaErrorRepository : IMediaErrorRepository
|
|||
_context.MediaError.Remove(error);
|
||||
}
|
||||
|
||||
public void Remove(IList<MediaError> errors)
|
||||
{
|
||||
_context.MediaError.RemoveRange(errors);
|
||||
}
|
||||
|
||||
public Task<MediaError?> Find(string filename)
|
||||
{
|
||||
return _context.MediaError.Where(e => e.FilePath == filename).SingleOrDefaultAsync();
|
||||
|
|
@ -70,4 +78,11 @@ public class MediaErrorRepository : IMediaErrorRepository
|
|||
_context.MediaError.RemoveRange(await _context.MediaError.ToListAsync());
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public Task<List<MediaError>> GetAllErrorsAsync(IList<string> comments)
|
||||
{
|
||||
return _context.MediaError
|
||||
.Where(m => comments.Contains(m.Comment))
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,46 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Person;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Helpers;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
public interface IPersonRepository
|
||||
{
|
||||
void Attach(Person person);
|
||||
void Attach(IEnumerable<Person> person);
|
||||
void Remove(Person person);
|
||||
void Remove(ChapterPeople person);
|
||||
void Remove(SeriesMetadataPeople person);
|
||||
void Update(Person person);
|
||||
|
||||
Task<IList<Person>> GetAllPeople();
|
||||
Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId);
|
||||
Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role);
|
||||
Task RemoveAllPeopleNoLongerAssociated();
|
||||
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds, int userId);
|
||||
Task<int> GetCountAsync();
|
||||
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(int userId, List<int>? libraryIds = null);
|
||||
|
||||
Task<IList<Person>> GetAllPeopleByRoleAndNames(PersonRole role, IEnumerable<string> normalizeNames);
|
||||
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<Person?> GetPersonById(int personId);
|
||||
Task<PersonDto?> GetPersonDtoByName(string name, int userId);
|
||||
Task<bool> IsNameUnique(string name);
|
||||
|
||||
Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId);
|
||||
Task<IEnumerable<StandaloneChapterDto>> GetChaptersForPersonByRole(int personId, int userId, PersonRole role);
|
||||
Task<IList<Person>> GetPeopleByNames(List<string> normalizedNames);
|
||||
Task<Person?> GetPersonByAniListId(int aniListId);
|
||||
}
|
||||
|
||||
public class PersonRepository : IPersonRepository
|
||||
|
|
@ -42,17 +59,37 @@ public class PersonRepository : IPersonRepository
|
|||
_context.Person.Attach(person);
|
||||
}
|
||||
|
||||
public void Attach(IEnumerable<Person> person)
|
||||
{
|
||||
_context.Person.AttachRange(person);
|
||||
}
|
||||
|
||||
public void Remove(Person person)
|
||||
{
|
||||
_context.Person.Remove(person);
|
||||
}
|
||||
|
||||
public void Remove(ChapterPeople person)
|
||||
{
|
||||
_context.ChapterPeople.Remove(person);
|
||||
}
|
||||
|
||||
public void Remove(SeriesMetadataPeople person)
|
||||
{
|
||||
_context.SeriesMetadataPeople.Remove(person);
|
||||
}
|
||||
|
||||
public void Update(Person person)
|
||||
{
|
||||
_context.Person.Update(person);
|
||||
}
|
||||
|
||||
public async Task RemoveAllPeopleNoLongerAssociated()
|
||||
{
|
||||
var peopleWithNoConnections = await _context.Person
|
||||
.Include(p => p.SeriesMetadatas)
|
||||
.Include(p => p.ChapterMetadatas)
|
||||
.Where(p => p.SeriesMetadatas.Count == 0 && p.ChapterMetadatas.Count == 0)
|
||||
.Include(p => p.SeriesMetadataPeople)
|
||||
.Include(p => p.ChapterPeople)
|
||||
.Where(p => p.SeriesMetadataPeople.Count == 0 && p.ChapterPeople.Count == 0)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
|
|
@ -61,13 +98,21 @@ public class PersonRepository : IPersonRepository
|
|||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(List<int> libraryIds, int userId)
|
||||
|
||||
public async Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(int userId, List<int>? libraryIds = null)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
||||
|
||||
if (libraryIds is {Count: > 0})
|
||||
{
|
||||
userLibs = userLibs.Where(libraryIds.Contains).ToList();
|
||||
}
|
||||
|
||||
return await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Where(s => userLibs.Contains(s.LibraryId))
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.SelectMany(s => s.Metadata.People)
|
||||
.SelectMany(s => s.Metadata.People.Select(p => p.Person))
|
||||
.Distinct()
|
||||
.OrderBy(p => p.Name)
|
||||
.AsNoTracking()
|
||||
|
|
@ -76,18 +121,144 @@ public class PersonRepository : IPersonRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<int> GetCountAsync()
|
||||
{
|
||||
return await _context.Person.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<Person>> GetAllPeopleByRoleAndNames(PersonRole role, IEnumerable<string> normalizeNames)
|
||||
public async Task<string?> GetCoverImageAsync(int personId)
|
||||
{
|
||||
return await _context.Person
|
||||
.Where(p => p.Role == role && normalizeNames.Contains(p.NormalizedName))
|
||||
.Where(c => c.Id == personId)
|
||||
.Select(c => c.CoverImage)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<string?> GetCoverImageByNameAsync(string name)
|
||||
{
|
||||
var normalized = name.ToNormalized();
|
||||
return await _context.Person
|
||||
.Where(c => c.NormalizedName == normalized)
|
||||
.Select(c => c.CoverImage)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PersonRole>> GetRolesForPersonByName(int personId, int userId)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
// Query roles from ChapterPeople
|
||||
var chapterRoles = await _context.Person
|
||||
.Where(p => p.Id == personId)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.SelectMany(p => p.ChapterPeople.Select(cp => cp.Role))
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
// Query roles from SeriesMetadataPeople
|
||||
var seriesRoles = await _context.Person
|
||||
.Where(p => p.Id == personId)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.SelectMany(p => p.SeriesMetadataPeople.Select(smp => smp.Role))
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
// Combine and return distinct roles
|
||||
return chapterRoles.Union(seriesRoles).Distinct();
|
||||
}
|
||||
|
||||
public async Task<PagedList<BrowsePersonDto>> GetAllWritersAndSeriesCount(int userId, 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);
|
||||
|
||||
return await PagedList<BrowsePersonDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
public async Task<Person?> GetPersonById(int personId)
|
||||
{
|
||||
return await _context.Person.Where(p => p.Id == personId)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<PersonDto?> GetPersonDtoByName(string name, int userId)
|
||||
{
|
||||
var normalized = name.ToNormalized();
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
return await _context.Person
|
||||
.Where(p => p.NormalizedName == normalized)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> IsNameUnique(string name)
|
||||
{
|
||||
return !(await _context.Person.AnyAsync(p => p.Name == name));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId)
|
||||
{
|
||||
List<PersonRole> notValidRoles = [PersonRole.Location, PersonRole.Team, PersonRole.Other, PersonRole.Publisher, PersonRole.Translator];
|
||||
return await _context.Person
|
||||
.Where(p => p.Id == personId)
|
||||
.SelectMany(p => p.SeriesMetadataPeople.Where(smp => !notValidRoles.Contains(smp.Role)))
|
||||
.Select(smp => smp.SeriesMetadata)
|
||||
.Select(sm => sm.Series)
|
||||
.Distinct()
|
||||
.OrderByDescending(s => s.ExternalSeriesMetadata.AverageExternalRating)
|
||||
.Take(20)
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<StandaloneChapterDto>> GetChaptersForPersonByRole(int personId, int userId, PersonRole role)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
return await _context.ChapterPeople
|
||||
.Where(cp => cp.PersonId == personId && cp.Role == role)
|
||||
.Select(cp => cp.Chapter)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.OrderBy(ch => ch.SortOrder)
|
||||
.Take(20)
|
||||
.ProjectTo<StandaloneChapterDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<Person>> GetPeopleByNames(List<string> normalizedNames)
|
||||
{
|
||||
return await _context.Person
|
||||
.Where(p => normalizedNames.Contains(p.NormalizedName))
|
||||
.OrderBy(p => p.Name)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Person?> GetPersonByAniListId(int aniListId)
|
||||
{
|
||||
return await _context.Person
|
||||
.Where(p => p.AniListId == aniListId)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<Person>> GetAllPeople()
|
||||
{
|
||||
|
|
@ -99,6 +270,7 @@ public class PersonRepository : IPersonRepository
|
|||
public async Task<IList<PersonDto>> GetAllPersonDtosAsync(int userId)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
return await _context.Person
|
||||
.OrderBy(p => p.Name)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
|
|
@ -109,8 +281,9 @@ public class PersonRepository : IPersonRepository
|
|||
public async Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role)
|
||||
{
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
||||
return await _context.Person
|
||||
.Where(p => p.Role == role)
|
||||
.Where(p => p.SeriesMetadataPeople.Any(smp => smp.Role == role) || p.ChapterPeople.Any(cp => cp.Role == role)) // Filter by role in both series and chapters
|
||||
.OrderBy(p => p.Name)
|
||||
.RestrictAgainstAgeRestriction(ageRating)
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.Misc;
|
||||
using API.DTOs;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.Entities;
|
||||
|
|
@ -16,6 +17,7 @@ using Microsoft.AspNetCore.Identity;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
[Flags]
|
||||
public enum ReadingListIncludes
|
||||
|
|
@ -36,6 +38,8 @@ public interface IReadingListRepository
|
|||
Task<IEnumerable<ReadingListItem>> GetReadingListItemsByIdAsync(int readingListId);
|
||||
Task<IEnumerable<ReadingListDto>> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId,
|
||||
bool includePromoted);
|
||||
Task<IEnumerable<ReadingListDto>> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId,
|
||||
bool includePromoted);
|
||||
void Remove(ReadingListItem item);
|
||||
void Add(ReadingList list);
|
||||
void BulkRemove(IEnumerable<ReadingListItem> items);
|
||||
|
|
@ -45,10 +49,15 @@ public interface IReadingListRepository
|
|||
Task<IList<string>> GetRandomCoverImagesAsync(int readingListId);
|
||||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
Task<bool> ReadingListExists(string name);
|
||||
IEnumerable<PersonDto> GetReadingListCharactersAsync(int readingListId);
|
||||
Task<bool> ReadingListExistsForUser(string name, int userId);
|
||||
IEnumerable<PersonDto> GetReadingListPeopleAsync(int readingListId, PersonRole role);
|
||||
Task<ReadingListCast> GetReadingListAllPeopleAsync(int readingListId);
|
||||
Task<IList<ReadingList>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||
Task<int> RemoveReadingListsWithoutSeries();
|
||||
Task<ReadingList?> GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items);
|
||||
Task<IEnumerable<ReadingList>> GetReadingListsByIds(IList<int> ids, ReadingListIncludes includes = ReadingListIncludes.Items);
|
||||
Task<IEnumerable<ReadingList>> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items);
|
||||
Task<ReadingListInfoDto?> GetReadingListInfoAsync(int readingListId);
|
||||
}
|
||||
|
||||
public class ReadingListRepository : IReadingListRepository
|
||||
|
|
@ -82,7 +91,7 @@ public class ReadingListRepository : IReadingListRepository
|
|||
return await _context.ReadingList
|
||||
.Where(c => c.Id == readingListId)
|
||||
.Select(c => c.CoverImage)
|
||||
.SingleOrDefaultAsync();
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<string>> GetAllCoverImagesAsync()
|
||||
|
|
@ -101,6 +110,7 @@ public class ReadingListRepository : IReadingListRepository
|
|||
.SelectMany(r => r.Items.Select(ri => ri.Chapter.CoverImage))
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.ToListAsync();
|
||||
|
||||
return data
|
||||
.OrderBy(_ => random.Next())
|
||||
.Take(4)
|
||||
|
|
@ -115,17 +125,97 @@ public class ReadingListRepository : IReadingListRepository
|
|||
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized));
|
||||
}
|
||||
|
||||
public IEnumerable<PersonDto> GetReadingListCharactersAsync(int readingListId)
|
||||
public async Task<bool> ReadingListExistsForUser(string name, int userId)
|
||||
{
|
||||
var normalized = name.ToNormalized();
|
||||
return await _context.ReadingList
|
||||
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId);
|
||||
}
|
||||
|
||||
public IEnumerable<PersonDto> GetReadingListPeopleAsync(int readingListId, PersonRole role)
|
||||
{
|
||||
return _context.ReadingListItem
|
||||
.Where(item => item.ReadingListId == readingListId)
|
||||
.SelectMany(item => item.Chapter.People.Where(p => p.Role == PersonRole.Character))
|
||||
.OrderBy(p => p.NormalizedName)
|
||||
.SelectMany(item => item.Chapter.People)
|
||||
.Where(p => p.Role == role)
|
||||
.OrderBy(p => p.Person.NormalizedName)
|
||||
.Select(p => p.Person)
|
||||
.Distinct()
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.AsEnumerable();
|
||||
}
|
||||
|
||||
public async Task<ReadingListCast> GetReadingListAllPeopleAsync(int readingListId)
|
||||
{
|
||||
var allPeople = await _context.ReadingListItem
|
||||
.Where(item => item.ReadingListId == readingListId)
|
||||
.SelectMany(item => item.Chapter.People)
|
||||
.OrderBy(p => p.Person.NormalizedName)
|
||||
.Select(p => new
|
||||
{
|
||||
Role = p.Role,
|
||||
Person = _mapper.Map<PersonDto>(p.Person)
|
||||
})
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
// Create the ReadingListCast object
|
||||
var cast = new ReadingListCast();
|
||||
|
||||
// Group people by role and populate the appropriate collections
|
||||
foreach (var personGroup in allPeople.GroupBy(p => p.Role))
|
||||
{
|
||||
var people = personGroup.Select(pg => pg.Person).ToList();
|
||||
|
||||
switch (personGroup.Key)
|
||||
{
|
||||
case PersonRole.Writer:
|
||||
cast.Writers = people;
|
||||
break;
|
||||
case PersonRole.CoverArtist:
|
||||
cast.CoverArtists = people;
|
||||
break;
|
||||
case PersonRole.Publisher:
|
||||
cast.Publishers = people;
|
||||
break;
|
||||
case PersonRole.Character:
|
||||
cast.Characters = people;
|
||||
break;
|
||||
case PersonRole.Penciller:
|
||||
cast.Pencillers = people;
|
||||
break;
|
||||
case PersonRole.Inker:
|
||||
cast.Inkers = people;
|
||||
break;
|
||||
case PersonRole.Imprint:
|
||||
cast.Imprints = people;
|
||||
break;
|
||||
case PersonRole.Colorist:
|
||||
cast.Colorists = people;
|
||||
break;
|
||||
case PersonRole.Letterer:
|
||||
cast.Letterers = people;
|
||||
break;
|
||||
case PersonRole.Editor:
|
||||
cast.Editors = people;
|
||||
break;
|
||||
case PersonRole.Translator:
|
||||
cast.Translators = people;
|
||||
break;
|
||||
case PersonRole.Team:
|
||||
cast.Teams = people;
|
||||
break;
|
||||
case PersonRole.Location:
|
||||
cast.Locations = people;
|
||||
break;
|
||||
case PersonRole.Other:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return cast;
|
||||
}
|
||||
|
||||
public async Task<IList<ReadingList>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
|
||||
{
|
||||
var extension = encodeFormat.GetExtension();
|
||||
|
|
@ -156,6 +246,51 @@ public class ReadingListRepository : IReadingListRepository
|
|||
.FirstOrDefaultAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ReadingList>> GetReadingListsByIds(IList<int> ids, ReadingListIncludes includes = ReadingListIncludes.Items)
|
||||
{
|
||||
return await _context.ReadingList
|
||||
.Where(c => ids.Contains(c.Id))
|
||||
.Includes(includes)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
public async Task<IEnumerable<ReadingList>> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items)
|
||||
{
|
||||
return await _context.ReadingList
|
||||
.Where(rl => rl.Items.Any(rli => rli.SeriesId == seriesId))
|
||||
.Includes(includes)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a Partial ReadingListInfoDto. The HourEstimate needs to be calculated outside the repo
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<ReadingListInfoDto?> GetReadingListInfoAsync(int readingListId)
|
||||
{
|
||||
// Get sum of these across all ReadingListItems: long wordCount, int pageCount, bool isEpub (assume false if any ReadingListeItem.Series.Format is non-epub)
|
||||
var readingList = await _context.ReadingList
|
||||
.Where(rl => rl.Id == readingListId)
|
||||
.Include(rl => rl.Items)
|
||||
.ThenInclude(item => item.Series)
|
||||
.Include(rl => rl.Items)
|
||||
.ThenInclude(item => item.Volume)
|
||||
.Include(rl => rl.Items)
|
||||
.ThenInclude(item => item.Chapter)
|
||||
.Select(rl => new ReadingListInfoDto()
|
||||
{
|
||||
WordCount = rl.Items.Sum(item => item.Chapter.WordCount),
|
||||
Pages = rl.Items.Sum(item => item.Chapter.Pages),
|
||||
IsAllEpub = rl.Items.All(item => item.Series.Format == MangaFormat.Epub),
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return readingList;
|
||||
}
|
||||
|
||||
|
||||
public void Remove(ReadingListItem item)
|
||||
{
|
||||
_context.ReadingListItem.Remove(item);
|
||||
|
|
@ -169,10 +304,11 @@ public class ReadingListRepository : IReadingListRepository
|
|||
|
||||
public async Task<PagedList<ReadingListDto>> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams, bool sortByLastModified = true)
|
||||
{
|
||||
var userAgeRating = (await _context.AppUser.SingleAsync(u => u.Id == userId)).AgeRestriction;
|
||||
var user = await _context.AppUser.FirstAsync(u => u.Id == userId);
|
||||
var query = _context.ReadingList
|
||||
.Where(l => l.AppUserId == userId || (includePromoted && l.Promoted ))
|
||||
.Where(l => l.AgeRating >= userAgeRating);
|
||||
.RestrictAgainstAgeRestriction(user.GetAgeRestriction());
|
||||
|
||||
query = sortByLastModified ? query.OrderByDescending(l => l.LastModified) : query.OrderBy(l => l.NormalizedTitle);
|
||||
|
||||
var finalQuery = query.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
|
||||
|
|
@ -183,8 +319,10 @@ public class ReadingListRepository : IReadingListRepository
|
|||
|
||||
public async Task<IEnumerable<ReadingListDto>> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, bool includePromoted)
|
||||
{
|
||||
var user = await _context.AppUser.FirstAsync(u => u.Id == userId);
|
||||
var query = _context.ReadingList
|
||||
.Where(l => l.AppUserId == userId || (includePromoted && l.Promoted ))
|
||||
.RestrictAgainstAgeRestriction(user.GetAgeRestriction())
|
||||
.Where(l => l.Items.Any(i => i.SeriesId == seriesId))
|
||||
.AsSplitQuery()
|
||||
.OrderBy(l => l.Title)
|
||||
|
|
@ -194,6 +332,21 @@ public class ReadingListRepository : IReadingListRepository
|
|||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ReadingListDto>> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId, bool includePromoted)
|
||||
{
|
||||
var user = await _context.AppUser.FirstAsync(u => u.Id == userId);
|
||||
var query = _context.ReadingList
|
||||
.Where(l => l.AppUserId == userId || (includePromoted && l.Promoted ))
|
||||
.RestrictAgainstAgeRestriction(user.GetAgeRestriction())
|
||||
.Where(l => l.Items.Any(i => i.ChapterId == chapterId))
|
||||
.AsSplitQuery()
|
||||
.OrderBy(l => l.Title)
|
||||
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking();
|
||||
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<ReadingList?> GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None)
|
||||
{
|
||||
return await _context.ReadingList
|
||||
|
|
@ -206,13 +359,7 @@ public class ReadingListRepository : IReadingListRepository
|
|||
|
||||
public async Task<IEnumerable<ReadingListItemDto>> GetReadingListItemDtosByIdAsync(int readingListId, int userId)
|
||||
{
|
||||
var userLibraries = _context.Library
|
||||
.Include(l => l.AppUsers)
|
||||
.Where(library => library.AppUsers.Any(user => user.Id == userId))
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.Select(library => library.Id)
|
||||
.ToList();
|
||||
var userLibraries = _context.Library.GetUserLibraries(userId);
|
||||
|
||||
var items = await _context.ReadingListItem
|
||||
.Where(s => s.ReadingListId == readingListId)
|
||||
|
|
@ -223,7 +370,9 @@ public class ReadingListRepository : IReadingListRepository
|
|||
chapter.ReleaseDate,
|
||||
ReadingListItem = data,
|
||||
ChapterTitleName = chapter.TitleName,
|
||||
FileSize = chapter.Files.Sum(f => f.Bytes)
|
||||
FileSize = chapter.Files.Sum(f => f.Bytes),
|
||||
chapter.Summary,
|
||||
chapter.IsSpecial
|
||||
|
||||
})
|
||||
.Join(_context.Volume, s => s.ReadingListItem.VolumeId, volume => volume.Id, (data, volume) => new
|
||||
|
|
@ -234,6 +383,8 @@ public class ReadingListRepository : IReadingListRepository
|
|||
data.ReleaseDate,
|
||||
data.ChapterTitleName,
|
||||
data.FileSize,
|
||||
data.Summary,
|
||||
data.IsSpecial,
|
||||
VolumeId = volume.Id,
|
||||
VolumeNumber = volume.Name,
|
||||
})
|
||||
|
|
@ -251,6 +402,8 @@ public class ReadingListRepository : IReadingListRepository
|
|||
data.ReleaseDate,
|
||||
data.ChapterTitleName,
|
||||
data.FileSize,
|
||||
data.Summary,
|
||||
data.IsSpecial,
|
||||
LibraryName = _context.Library.Where(l => l.Id == s.LibraryId).Select(l => l.Name).Single(),
|
||||
LibraryType = _context.Library.Where(l => l.Id == s.LibraryId).Select(l => l.Type).Single()
|
||||
})
|
||||
|
|
@ -272,7 +425,9 @@ public class ReadingListRepository : IReadingListRepository
|
|||
LibraryType = data.LibraryType,
|
||||
ChapterTitleName = data.ChapterTitleName,
|
||||
LibraryName = data.LibraryName,
|
||||
FileSize = data.FileSize
|
||||
FileSize = data.FileSize,
|
||||
Summary = data.Summary,
|
||||
IsSpecial = data.IsSpecial
|
||||
})
|
||||
.Where(o => userLibraries.Contains(o.LibraryId))
|
||||
.OrderBy(rli => rli.Order)
|
||||
|
|
@ -306,8 +461,10 @@ public class ReadingListRepository : IReadingListRepository
|
|||
|
||||
public async Task<ReadingListDto?> GetReadingListDtoByIdAsync(int readingListId, int userId)
|
||||
{
|
||||
var user = await _context.AppUser.FirstAsync(u => u.Id == userId);
|
||||
return await _context.ReadingList
|
||||
.Where(r => r.Id == readingListId && (r.AppUserId == userId || r.Promoted))
|
||||
.RestrictAgainstAgeRestriction(user.GetAgeRestriction())
|
||||
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using AutoMapper.QueryableExtensions;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
public interface IScrobbleRepository
|
||||
{
|
||||
|
|
@ -19,16 +20,21 @@ public interface IScrobbleRepository
|
|||
void Attach(ScrobbleError error);
|
||||
void Remove(ScrobbleEvent evt);
|
||||
void Remove(IEnumerable<ScrobbleEvent> events);
|
||||
void Remove(IEnumerable<ScrobbleError> errors);
|
||||
void Update(ScrobbleEvent evt);
|
||||
Task<IList<ScrobbleEvent>> GetByEvent(ScrobbleEventType type, bool isProcessed = false);
|
||||
Task<IList<ScrobbleEvent>> GetProcessedEvents(int daysAgo);
|
||||
Task<bool> Exists(int userId, int seriesId, ScrobbleEventType eventType);
|
||||
Task<IEnumerable<ScrobbleErrorDto>> GetScrobbleErrors();
|
||||
Task<IList<ScrobbleError>> GetAllScrobbleErrorsForSeries(int seriesId);
|
||||
Task ClearScrobbleErrors();
|
||||
Task<bool> HasErrorForSeries(int seriesId);
|
||||
Task<ScrobbleEvent?> GetEvent(int userId, int seriesId, ScrobbleEventType eventType);
|
||||
Task<IEnumerable<ScrobbleEvent>> GetUserEventsForSeries(int userId, int seriesId);
|
||||
Task<PagedList<ScrobbleEventDto>> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination);
|
||||
Task<IList<ScrobbleEvent>> GetAllEventsForSeries(int seriesId);
|
||||
Task<IList<ScrobbleEvent>> GetAllEventsWithSeriesIds(IEnumerable<int> seriesIds);
|
||||
Task<IList<ScrobbleEvent>> GetEvents();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -65,6 +71,11 @@ public class ScrobbleRepository : IScrobbleRepository
|
|||
_context.ScrobbleEvent.RemoveRange(events);
|
||||
}
|
||||
|
||||
public void Remove(IEnumerable<ScrobbleError> errors)
|
||||
{
|
||||
_context.ScrobbleError.RemoveRange(errors);
|
||||
}
|
||||
|
||||
public void Update(ScrobbleEvent evt)
|
||||
{
|
||||
_context.Entry(evt).State = EntityState.Modified;
|
||||
|
|
@ -78,6 +89,7 @@ public class ScrobbleRepository : IScrobbleRepository
|
|||
.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()
|
||||
|
|
@ -88,6 +100,11 @@ public class ScrobbleRepository : IScrobbleRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all processed events that were processed 7 or more days ago
|
||||
/// </summary>
|
||||
/// <param name="daysAgo"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<ScrobbleEvent>> GetProcessedEvents(int daysAgo)
|
||||
{
|
||||
var date = DateTime.UtcNow.Subtract(TimeSpan.FromDays(daysAgo));
|
||||
|
|
@ -111,6 +128,13 @@ public class ScrobbleRepository : IScrobbleRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<ScrobbleError>> GetAllScrobbleErrorsForSeries(int seriesId)
|
||||
{
|
||||
return await _context.ScrobbleError
|
||||
.Where(e => e.SeriesId == seriesId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task ClearScrobbleErrors()
|
||||
{
|
||||
_context.ScrobbleError.RemoveRange(_context.ScrobbleError);
|
||||
|
|
@ -143,14 +167,35 @@ public class ScrobbleRepository : IScrobbleRepository
|
|||
var query = _context.ScrobbleEvent
|
||||
.Where(e => e.AppUserId == userId)
|
||||
.Include(e => e.Series)
|
||||
.SortBy(filter.Field, filter.IsDescending)
|
||||
.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<ScrobbleEventDto>(_mapper.ConfigurationProvider);
|
||||
|
||||
return await PagedList<ScrobbleEventDto>.CreateAsync(query, pagination.PageNumber, pagination.PageSize);
|
||||
}
|
||||
|
||||
public async Task<IList<ScrobbleEvent>> GetAllEventsForSeries(int seriesId)
|
||||
{
|
||||
return await _context.ScrobbleEvent
|
||||
.Where(e => e.SeriesId == seriesId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<ScrobbleEvent>> GetAllEventsWithSeriesIds(IEnumerable<int> seriesIds)
|
||||
{
|
||||
return await _context.ScrobbleEvent
|
||||
.Where(e => seriesIds.Contains(e.SeriesId))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<ScrobbleEvent>> GetEvents()
|
||||
{
|
||||
return await _context.ScrobbleEvent
|
||||
.Include(e => e.AppUser)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,24 +1,32 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.KavitaPlus.Metadata;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.DTOs.Settings;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Entities.MetadataMatching;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
public interface ISettingsRepository
|
||||
{
|
||||
void Update(ServerSetting settings);
|
||||
void Update(MetadataSettings settings);
|
||||
void RemoveRange(List<MetadataFieldMapping> fieldMappings);
|
||||
Task<ServerSettingDto> GetSettingsDtoAsync();
|
||||
Task<ServerSetting> GetSettingAsync(ServerSettingKey key);
|
||||
Task<IEnumerable<ServerSetting>> GetSettingsAsync();
|
||||
void Remove(ServerSetting setting);
|
||||
Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId);
|
||||
Task<MetadataSettings> GetMetadataSettings();
|
||||
Task<MetadataSettingsDto> GetMetadataSettingDto();
|
||||
}
|
||||
public class SettingsRepository : ISettingsRepository
|
||||
{
|
||||
|
|
@ -36,6 +44,16 @@ public class SettingsRepository : ISettingsRepository
|
|||
_context.Entry(settings).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public void Update(MetadataSettings settings)
|
||||
{
|
||||
_context.Entry(settings).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public void RemoveRange(List<MetadataFieldMapping> fieldMappings)
|
||||
{
|
||||
_context.MetadataFieldMapping.RemoveRange(fieldMappings);
|
||||
}
|
||||
|
||||
public void Remove(ServerSetting setting)
|
||||
{
|
||||
_context.Remove(setting);
|
||||
|
|
@ -48,6 +66,21 @@ public class SettingsRepository : ISettingsRepository
|
|||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<MetadataSettings> GetMetadataSettings()
|
||||
{
|
||||
return await _context.MetadataSettings
|
||||
.Include(m => m.FieldMappings)
|
||||
.FirstAsync();
|
||||
}
|
||||
|
||||
public async Task<MetadataSettingsDto> GetMetadataSettingDto()
|
||||
{
|
||||
return await _context.MetadataSettings
|
||||
.Include(m => m.FieldMappings)
|
||||
.ProjectTo<MetadataSettingsDto>(_mapper.ConfigurationProvider)
|
||||
.FirstAsync();
|
||||
}
|
||||
|
||||
public async Task<ServerSettingDto> GetSettingsDtoAsync()
|
||||
{
|
||||
var settings = await _context.ServerSetting
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using AutoMapper.QueryableExtensions;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
public interface ISiteThemeRepository
|
||||
{
|
||||
|
|
@ -19,6 +20,8 @@ public interface ISiteThemeRepository
|
|||
Task<SiteThemeDto?> GetThemeDtoByName(string themeName);
|
||||
Task<SiteTheme> GetDefaultTheme();
|
||||
Task<IEnumerable<SiteTheme>> GetThemes();
|
||||
Task<SiteTheme?> GetTheme(int themeId);
|
||||
Task<bool> IsThemeInUse(int themeId);
|
||||
}
|
||||
|
||||
public class SiteThemeRepository : ISiteThemeRepository
|
||||
|
|
@ -88,6 +91,19 @@ public class SiteThemeRepository : ISiteThemeRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<SiteTheme> GetTheme(int themeId)
|
||||
{
|
||||
return await _context.SiteTheme
|
||||
.Where(t => t.Id == themeId)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> IsThemeInUse(int themeId)
|
||||
{
|
||||
return await _context.AppUserPreferences
|
||||
.AnyAsync(p => p.Theme.Id == themeId);
|
||||
}
|
||||
|
||||
public async Task<SiteThemeDto?> GetThemeDto(int themeId)
|
||||
{
|
||||
return await _context.SiteTheme
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ using API.DTOs.Metadata;
|
|||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
public interface ITagRepository
|
||||
{
|
||||
|
|
@ -19,7 +21,8 @@ public interface ITagRepository
|
|||
Task<IList<Tag>> GetAllTagsByNameAsync(IEnumerable<string> normalizedNames);
|
||||
Task<IList<TagDto>> GetAllTagDtosAsync(int userId);
|
||||
Task RemoveAllTagNoLongerAssociated();
|
||||
Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(IList<int> libraryIds, int userId);
|
||||
Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(int userId, IList<int>? libraryIds = null);
|
||||
Task<List<string>> GetAllTagsNotInListAsync(ICollection<string> tags);
|
||||
}
|
||||
|
||||
public class TagRepository : ITagRepository
|
||||
|
|
@ -57,11 +60,18 @@ public class TagRepository : ITagRepository
|
|||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(IList<int> libraryIds, int userId)
|
||||
public async Task<IList<TagDto>> GetAllTagDtosForLibrariesAsync(int userId, IList<int>? libraryIds = null)
|
||||
{
|
||||
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
||||
|
||||
if (libraryIds is {Count: > 0})
|
||||
{
|
||||
userLibs = userLibs.Where(libraryIds.Contains).ToList();
|
||||
}
|
||||
|
||||
return await _context.Series
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Where(s => userLibs.Contains(s.LibraryId))
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.SelectMany(s => s.Metadata.Tags)
|
||||
.AsSplitQuery()
|
||||
|
|
@ -72,6 +82,28 @@ public class TagRepository : ITagRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetAllTagsNotInListAsync(ICollection<string> tags)
|
||||
{
|
||||
// Create a dictionary mapping normalized names to non-normalized names
|
||||
var normalizedToOriginalMap = tags.Distinct()
|
||||
.GroupBy(Parser.Normalize)
|
||||
.ToDictionary(group => group.Key, group => group.First());
|
||||
|
||||
var normalizedTagNames = normalizedToOriginalMap.Keys.ToList();
|
||||
|
||||
// Query the database for existing genres using the normalized names
|
||||
var existingTags = await _context.Tag
|
||||
.Where(g => normalizedTagNames.Contains(g.NormalizedTitle)) // Assuming you have a normalized field
|
||||
.Select(g => g.NormalizedTitle)
|
||||
.ToListAsync();
|
||||
|
||||
// Find the normalized genres that do not exist in the database
|
||||
var missingTags = normalizedTagNames.Except(existingTags).ToList();
|
||||
|
||||
// Return the original non-normalized genres for the missing ones
|
||||
return missingTags.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList();
|
||||
}
|
||||
|
||||
public async Task<IList<Tag>> GetAllTagsAsync()
|
||||
{
|
||||
return await _context.Tag.ToListAsync();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using API.DTOs;
|
|||
using API.DTOs.Account;
|
||||
using API.DTOs.Dashboard;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.KavitaPlus.Account;
|
||||
using API.DTOs.Reader;
|
||||
using API.DTOs.Scrobbling;
|
||||
using API.DTOs.SeriesDetail;
|
||||
|
|
@ -15,12 +16,14 @@ using API.Entities;
|
|||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Extensions.QueryExtensions.Filtering;
|
||||
using API.Helpers;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
[Flags]
|
||||
public enum AppUserIncludes
|
||||
|
|
@ -38,7 +41,8 @@ public enum AppUserIncludes
|
|||
SmartFilters = 1024,
|
||||
DashboardStreams = 2048,
|
||||
SideNavStreams = 4096,
|
||||
ExternalSources = 8192 // 2^13
|
||||
ExternalSources = 8192,
|
||||
Collections = 16384 // 2^14
|
||||
}
|
||||
|
||||
public interface IUserRepository
|
||||
|
|
@ -53,10 +57,13 @@ public interface IUserRepository
|
|||
void Delete(AppUser? user);
|
||||
void Delete(AppUserBookmark bookmark);
|
||||
void Delete(IEnumerable<AppUserDashboardStream> streams);
|
||||
void Delete(AppUserDashboardStream stream);
|
||||
void Delete(IEnumerable<AppUserSideNavStream> streams);
|
||||
void Delete(AppUserSideNavStream stream);
|
||||
Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true);
|
||||
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
|
||||
Task<bool> IsUserAdminAsync(AppUser? user);
|
||||
Task<IList<string>> GetRoles(int userId);
|
||||
Task<AppUserRating?> GetUserRatingAsync(int seriesId, int userId);
|
||||
Task<IList<UserReviewDto>> GetUserRatingDtosForSeriesAsync(int seriesId, int userId);
|
||||
Task<AppUserPreferences?> GetPreferencesAsync(string username);
|
||||
|
|
@ -76,9 +83,9 @@ public interface IUserRepository
|
|||
Task<IEnumerable<AppUserPreferences>> GetAllPreferencesByThemeAsync(int themeId);
|
||||
Task<bool> HasAccessToLibrary(int libraryId, int userId);
|
||||
Task<bool> HasAccessToSeries(int userId, int seriesId);
|
||||
Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None);
|
||||
Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true);
|
||||
Task<AppUser?> GetUserByConfirmationToken(string token);
|
||||
Task<AppUser> GetDefaultAdminUser();
|
||||
Task<AppUser> GetDefaultAdminUser(AppUserIncludes includes = AppUserIncludes.None);
|
||||
Task<IEnumerable<AppUserRating>> GetSeriesWithRatings(int userId);
|
||||
Task<IEnumerable<AppUserRating>> GetSeriesWithReviews(int userId);
|
||||
Task<bool> HasHoldOnSeries(int userId, int seriesId);
|
||||
|
|
@ -90,10 +97,13 @@ public interface IUserRepository
|
|||
Task<IList<AppUserDashboardStream>> GetDashboardStreamWithFilter(int filterId);
|
||||
Task<IList<SideNavStreamDto>> GetSideNavStreams(int userId, bool visibleOnly = false);
|
||||
Task<AppUserSideNavStream?> GetSideNavStream(int streamId);
|
||||
Task<AppUserSideNavStream?> GetSideNavStreamWithUser(int streamId);
|
||||
Task<IList<AppUserSideNavStream>> GetSideNavStreamWithFilter(int filterId);
|
||||
Task<IList<AppUserSideNavStream>> GetSideNavStreamsByLibraryId(int libraryId);
|
||||
Task<IList<AppUserSideNavStream>> GetSideNavStreamWithExternalSource(int externalSourceId);
|
||||
Task<IList<AppUserSideNavStream>> GetDashboardStreamsByIds(IList<int> streamIds);
|
||||
Task<IEnumerable<UserTokenInfo>> GetUserTokenInfo();
|
||||
Task<AppUser?> GetUserByDeviceEmail(string deviceEmail);
|
||||
}
|
||||
|
||||
public class UserRepository : IUserRepository
|
||||
|
|
@ -160,11 +170,21 @@ public class UserRepository : IUserRepository
|
|||
_context.AppUserDashboardStream.RemoveRange(streams);
|
||||
}
|
||||
|
||||
public void Delete(AppUserDashboardStream stream)
|
||||
{
|
||||
_context.AppUserDashboardStream.Remove(stream);
|
||||
}
|
||||
|
||||
public void Delete(IEnumerable<AppUserSideNavStream> streams)
|
||||
{
|
||||
_context.AppUserSideNavStream.RemoveRange(streams);
|
||||
}
|
||||
|
||||
public void Delete(AppUserSideNavStream stream)
|
||||
{
|
||||
_context.AppUserSideNavStream.Remove(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags.
|
||||
/// </summary>
|
||||
|
|
@ -281,10 +301,17 @@ public class UserRepository : IUserRepository
|
|||
.AnyAsync(s => s.Id == seriesId);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None)
|
||||
public async Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true)
|
||||
{
|
||||
return await _context.AppUser
|
||||
.Includes(includeFlags)
|
||||
var query = _context.AppUser
|
||||
.Includes(includeFlags);
|
||||
if (track)
|
||||
{
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
return await query
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
|
@ -298,11 +325,13 @@ public class UserRepository : IUserRepository
|
|||
/// Returns the first admin account created
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<AppUser> GetDefaultAdminUser()
|
||||
public async Task<AppUser> GetDefaultAdminUser(AppUserIncludes includes = AppUserIncludes.None)
|
||||
{
|
||||
return (await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole))
|
||||
return await _context.AppUser
|
||||
.Includes(includes)
|
||||
.Where(u => u.UserRoles.Any(r => r.Role.Name == PolicyConstants.AdminRole))
|
||||
.OrderBy(u => u.Created)
|
||||
.First();
|
||||
.FirstAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AppUserRating>> GetSeriesWithRatings(int userId)
|
||||
|
|
@ -380,6 +409,7 @@ public class UserRepository : IUserRepository
|
|||
.FirstOrDefaultAsync(d => d.Id == streamId);
|
||||
}
|
||||
|
||||
|
||||
public async Task<IList<AppUserDashboardStream>> GetDashboardStreamWithFilter(int filterId)
|
||||
{
|
||||
return await _context.AppUserDashboardStream
|
||||
|
|
@ -416,10 +446,10 @@ public class UserRepository : IUserRepository
|
|||
.Select(d => d.LibraryId)
|
||||
.ToList();
|
||||
|
||||
var libraryDtos = _context.Library
|
||||
var libraryDtos = await _context.Library
|
||||
.Where(l => libraryIds.Contains(l.Id))
|
||||
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
|
||||
.ToList();
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var dto in sideNavStreams.Where(dto => dto.StreamType == SideNavStreamType.Library))
|
||||
{
|
||||
|
|
@ -443,13 +473,21 @@ public class UserRepository : IUserRepository
|
|||
return sideNavStreams;
|
||||
}
|
||||
|
||||
public async Task<AppUserSideNavStream> GetSideNavStream(int streamId)
|
||||
public async Task<AppUserSideNavStream?> GetSideNavStream(int streamId)
|
||||
{
|
||||
return await _context.AppUserSideNavStream
|
||||
.Include(d => d.SmartFilter)
|
||||
.FirstOrDefaultAsync(d => d.Id == streamId);
|
||||
}
|
||||
|
||||
public async Task<AppUserSideNavStream?> GetSideNavStreamWithUser(int streamId)
|
||||
{
|
||||
return await _context.AppUserSideNavStream
|
||||
.Include(d => d.SmartFilter)
|
||||
.Include(d => d.AppUser)
|
||||
.FirstOrDefaultAsync(d => d.Id == streamId);
|
||||
}
|
||||
|
||||
public async Task<IList<AppUserSideNavStream>> GetSideNavStreamWithFilter(int filterId)
|
||||
{
|
||||
return await _context.AppUserSideNavStream
|
||||
|
|
@ -479,10 +517,47 @@ public class UserRepository : IUserRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<UserTokenInfo>> GetUserTokenInfo()
|
||||
{
|
||||
var users = await _context.AppUser
|
||||
.Select(u => new
|
||||
{
|
||||
u.Id,
|
||||
u.UserName,
|
||||
u.AniListAccessToken, // JWT Token
|
||||
u.MalAccessToken // JWT Token
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var userTokenInfos = users.Select(user => new UserTokenInfo
|
||||
{
|
||||
UserId = user.Id,
|
||||
Username = user.UserName,
|
||||
IsAniListTokenSet = !string.IsNullOrEmpty(user.AniListAccessToken),
|
||||
AniListValidUntilUtc = JwtHelper.GetTokenExpiry(user.AniListAccessToken),
|
||||
IsAniListTokenValid = JwtHelper.IsTokenValid(user.AniListAccessToken),
|
||||
IsMalTokenSet = !string.IsNullOrEmpty(user.MalAccessToken),
|
||||
});
|
||||
|
||||
return userTokenInfos;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first user with a device email matching
|
||||
/// </summary>
|
||||
/// <param name="deviceEmail"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<AppUser> GetUserByDeviceEmail(string deviceEmail)
|
||||
{
|
||||
return await _context.AppUser
|
||||
.Where(u => u.Devices.Any(d => d.EmailAddress == deviceEmail))
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
||||
{
|
||||
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
||||
return (await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole)).OrderBy(u => u.CreatedUtc);
|
||||
}
|
||||
|
||||
public async Task<bool> IsUserAdminAsync(AppUser? user)
|
||||
|
|
@ -491,6 +566,23 @@ public class UserRepository : IUserRepository
|
|||
return await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
|
||||
}
|
||||
|
||||
public async Task<IList<string>> GetRoles(int userId)
|
||||
{
|
||||
var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId);
|
||||
if (user == null) return ArraySegment<string>.Empty;
|
||||
|
||||
if (_userManager == null)
|
||||
{
|
||||
// userManager is null on Unit Tests only
|
||||
return await _context.UserRoles
|
||||
.Where(ur => ur.UserId == userId)
|
||||
.Select(ur => ur.Role.Name)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
return await _userManager.GetRolesAsync(user);
|
||||
}
|
||||
|
||||
public async Task<AppUserRating?> GetUserRatingAsync(int seriesId, int userId)
|
||||
{
|
||||
return await _context.AppUserRating
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -6,6 +7,7 @@ using API.DTOs;
|
|||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Services;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
|
|
@ -13,22 +15,39 @@ using Kavita.Common;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
[Flags]
|
||||
public enum VolumeIncludes
|
||||
{
|
||||
None = 1,
|
||||
Chapters = 2,
|
||||
People = 4,
|
||||
Tags = 8,
|
||||
/// <summary>
|
||||
/// This will include Chapters by default
|
||||
/// </summary>
|
||||
Files = 16
|
||||
}
|
||||
|
||||
public interface IVolumeRepository
|
||||
{
|
||||
void Add(Volume volume);
|
||||
void Update(Volume volume);
|
||||
void Remove(Volume volume);
|
||||
void Remove(IList<Volume> volumes);
|
||||
Task<IList<MangaFile>> GetFilesForVolume(int volumeId);
|
||||
Task<string?> GetVolumeCoverImageAsync(int volumeId);
|
||||
Task<IList<int>> GetChapterIdsByVolumeIds(IReadOnlyList<int> volumeIds);
|
||||
Task<IEnumerable<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId);
|
||||
Task<Volume?> GetVolumeAsync(int volumeId);
|
||||
Task<IList<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId, VolumeIncludes includes = VolumeIncludes.Chapters);
|
||||
Task<Volume?> GetVolumeAsync(int volumeId, VolumeIncludes includes = VolumeIncludes.Files);
|
||||
Task<VolumeDto?> GetVolumeDtoAsync(int volumeId, int userId);
|
||||
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false);
|
||||
Task<IEnumerable<Volume>> GetVolumes(int seriesId);
|
||||
Task<IList<Volume>> GetVolumesById(IList<int> volumeIds, VolumeIncludes includes = VolumeIncludes.None);
|
||||
Task<Volume?> GetVolumeByIdAsync(int volumeId);
|
||||
Task<IList<Volume>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||
Task<IEnumerable<string>> GetCoverImagesForLockedVolumesAsync();
|
||||
}
|
||||
public class VolumeRepository : IVolumeRepository
|
||||
{
|
||||
|
|
@ -55,6 +74,10 @@ public class VolumeRepository : IVolumeRepository
|
|||
{
|
||||
_context.Volume.Remove(volume);
|
||||
}
|
||||
public void Remove(IList<Volume> volumes)
|
||||
{
|
||||
_context.Volume.RemoveRange(volumes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of non-tracked files for a given volume.
|
||||
|
|
@ -111,9 +134,18 @@ public class VolumeRepository : IVolumeRepository
|
|||
|
||||
if (includeChapters)
|
||||
{
|
||||
query = query.Include(v => v.Chapters).AsSplitQuery();
|
||||
query = query
|
||||
.Includes(VolumeIncludes.Chapters)
|
||||
.AsSplitQuery();
|
||||
}
|
||||
return await query.ToListAsync();
|
||||
var volumes = await query.ToListAsync();
|
||||
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
volume.Chapters = volume.Chapters.OrderBy(c => c.SortOrder).ToList();
|
||||
}
|
||||
|
||||
return volumes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -126,11 +158,11 @@ public class VolumeRepository : IVolumeRepository
|
|||
{
|
||||
var volume = await _context.Volume
|
||||
.Where(vol => vol.Id == volumeId)
|
||||
.Include(vol => vol.Chapters)
|
||||
.ThenInclude(c => c.Files)
|
||||
.Includes(VolumeIncludes.Chapters | VolumeIncludes.Files)
|
||||
.AsSplitQuery()
|
||||
.OrderBy(v => v.MinNumber)
|
||||
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
||||
.SingleOrDefaultAsync(vol => vol.Id == volumeId);
|
||||
.FirstOrDefaultAsync(vol => vol.Id == volumeId);
|
||||
|
||||
if (volume == null) return null;
|
||||
|
||||
|
|
@ -149,8 +181,16 @@ public class VolumeRepository : IVolumeRepository
|
|||
{
|
||||
return await _context.Volume
|
||||
.Where(vol => vol.SeriesId == seriesId)
|
||||
.Include(vol => vol.Chapters)
|
||||
.ThenInclude(c => c.Files)
|
||||
.Includes(VolumeIncludes.Chapters | VolumeIncludes.Files)
|
||||
.AsSplitQuery()
|
||||
.OrderBy(vol => vol.MinNumber)
|
||||
.ToListAsync();
|
||||
}
|
||||
public async Task<IList<Volume>> GetVolumesById(IList<int> volumeIds, VolumeIncludes includes = VolumeIncludes.None)
|
||||
{
|
||||
return await _context.Volume
|
||||
.Where(vol => volumeIds.Contains(vol.Id))
|
||||
.Includes(includes)
|
||||
.AsSplitQuery()
|
||||
.OrderBy(vol => vol.MinNumber)
|
||||
.ToListAsync();
|
||||
|
|
@ -161,11 +201,10 @@ public class VolumeRepository : IVolumeRepository
|
|||
/// </summary>
|
||||
/// <param name="volumeId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<Volume?> GetVolumeAsync(int volumeId)
|
||||
public async Task<Volume?> GetVolumeAsync(int volumeId, VolumeIncludes includes = VolumeIncludes.Files)
|
||||
{
|
||||
return await _context.Volume
|
||||
.Include(vol => vol.Chapters)
|
||||
.ThenInclude(c => c.Files)
|
||||
.Includes(includes)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync(vol => vol.Id == volumeId);
|
||||
}
|
||||
|
|
@ -177,51 +216,37 @@ public class VolumeRepository : IVolumeRepository
|
|||
/// <param name="seriesId"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId)
|
||||
public async Task<IList<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId, VolumeIncludes includes = VolumeIncludes.Chapters)
|
||||
{
|
||||
var volumes = await _context.Volume
|
||||
.Where(vol => vol.SeriesId == seriesId)
|
||||
.Include(vol => vol.Chapters)
|
||||
.ThenInclude(c => c.People)
|
||||
.Include(vol => vol.Chapters)
|
||||
.ThenInclude(c => c.Tags)
|
||||
.Includes(includes)
|
||||
.OrderBy(volume => volume.MinNumber)
|
||||
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
await AddVolumeModifiers(userId, volumes);
|
||||
SortSpecialChapters(volumes);
|
||||
|
||||
return volumes;
|
||||
}
|
||||
|
||||
public async Task<Volume?> GetVolumeByIdAsync(int volumeId)
|
||||
{
|
||||
return await _context.Volume.SingleOrDefaultAsync(x => x.Id == volumeId);
|
||||
return await _context.Volume.FirstOrDefaultAsync(x => x.Id == volumeId);
|
||||
}
|
||||
|
||||
public async Task<IList<Volume>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
|
||||
{
|
||||
var extension = encodeFormat.GetExtension();
|
||||
return await _context.Volume
|
||||
.Include(v => v.Chapters)
|
||||
.Includes(VolumeIncludes.Chapters)
|
||||
.Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension))
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
private static void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
|
||||
{
|
||||
foreach (var v in volumes.Where(vDto => vDto.MinNumber == 0))
|
||||
{
|
||||
v.Chapters = v.Chapters.OrderByNatural(x => x.Range).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task AddVolumeModifiers(int userId, IReadOnlyCollection<VolumeDto> volumes)
|
||||
{
|
||||
var volIds = volumes.Select(s => s.Id);
|
||||
|
|
@ -246,4 +271,17 @@ public class VolumeRepository : IVolumeRepository
|
|||
.Sum(p => p.PagesRead);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns cover images for locked chapters
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<string>> GetCoverImagesForLockedVolumesAsync()
|
||||
{
|
||||
return (await _context.Volume
|
||||
.Where(c => c.CoverImageLocked)
|
||||
.Select(c => c.CoverImage)
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.ToListAsync())!;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue