Merged develop in

This commit is contained in:
Joseph Milazzo 2025-04-15 13:28:53 -05:00
commit 7c692a1b46
580 changed files with 21233 additions and 9031 deletions

View file

@ -16,6 +16,7 @@ using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface IAppUserProgressRepository
{
void Update(AppUserProgress userProgress);
@ -41,7 +42,7 @@ public interface IAppUserProgressRepository
Task UpdateAllProgressThatAreMoreThanChapterPages();
Task<IList<FullProgressDto>> GetUserProgressForChapter(int chapterId, int userId = 0);
}
#nullable disable
public class AppUserProgressRepository : IAppUserProgressRepository
{
private readonly DataContext _context;
@ -192,6 +193,7 @@ public class AppUserProgressRepository : IAppUserProgressRepository
.Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber)
.Select(p => p.chapter.Volume.MaxNumber)
.ToListAsync();
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max();
}

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using API.DTOs.CoverDb;
using API.Entities;
using API.Entities.Person;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

View file

@ -12,6 +12,7 @@ using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface IGenreRepository
{

View file

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

View file

@ -1,11 +1,9 @@
using System.Collections;
using System;
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;
@ -14,6 +12,7 @@ using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface IPersonRepository
{
@ -29,15 +28,13 @@ public interface IPersonRepository
Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role);
Task RemoveAllPeopleNoLongerAssociated();
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(int userId, List<int>? libraryIds = null);
Task<int> GetCountAsync();
Task<string> GetCoverImageAsync(int personId);
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<Person> GetPersonByName(string name);
Task<bool> IsNameUnique(string name);
Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId);
@ -124,12 +121,8 @@ public class PersonRepository : IPersonRepository
.ToListAsync();
}
public async Task<int> GetCountAsync()
{
return await _context.Person.CountAsync();
}
public async Task<string> GetCoverImageAsync(int personId)
public async Task<string?> GetCoverImageAsync(int personId)
{
return await _context.Person
.Where(c => c.Id == personId)
@ -137,7 +130,7 @@ public class PersonRepository : IPersonRepository
.SingleOrDefaultAsync();
}
public async Task<string> GetCoverImageByNameAsync(string name)
public async Task<string?> GetCoverImageByNameAsync(string name)
{
var normalized = name.ToNormalized();
return await _context.Person
@ -206,7 +199,7 @@ public class PersonRepository : IPersonRepository
.FirstOrDefaultAsync();
}
public async Task<PersonDto> GetPersonDtoByName(string name, int userId)
public async Task<PersonDto?> GetPersonDtoByName(string name, int userId)
{
var normalized = name.ToNormalized();
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
@ -218,11 +211,6 @@ public class PersonRepository : IPersonRepository
.FirstOrDefaultAsync();
}
public async Task<Person> GetPersonByName(string name)
{
return await _context.Person.FirstOrDefaultAsync(p => p.NormalizedName == name.ToNormalized());
}
public async Task<bool> IsNameUnique(string name)
{
return !(await _context.Person.AnyAsync(p => p.Name == name));

View file

@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
[Flags]
public enum ReadingListIncludes
@ -48,11 +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
@ -105,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)
@ -119,12 +125,19 @@ 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)
.Where(p => p.Role == role)
.OrderBy(p => p.Person.NormalizedName)
.Select(p => p.Person)
.Distinct()
@ -132,6 +145,77 @@ public class ReadingListRepository : IReadingListRepository
.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();
@ -170,7 +254,41 @@ public class ReadingListRepository : IReadingListRepository
.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)
@ -343,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();
}

View file

@ -12,6 +12,7 @@ using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface IScrobbleRepository
{
@ -166,11 +167,11 @@ 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);

View file

@ -39,6 +39,7 @@ using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
[Flags]
public enum SeriesIncludes

View file

@ -7,11 +7,13 @@ 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
{

View file

@ -8,6 +8,7 @@ using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface ISiteThemeRepository
{

View file

@ -11,6 +11,7 @@ using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface ITagRepository
{

View file

@ -23,6 +23,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
[Flags]
public enum AppUserIncludes
@ -56,7 +57,9 @@ 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);
@ -94,6 +97,7 @@ 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);
@ -166,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>
@ -395,6 +409,7 @@ public class UserRepository : IUserRepository
.FirstOrDefaultAsync(d => d.Id == streamId);
}
public async Task<IList<AppUserDashboardStream>> GetDashboardStreamWithFilter(int filterId)
{
return await _context.AppUserDashboardStream
@ -431,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))
{
@ -458,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
@ -546,7 +569,16 @@ public class UserRepository : IUserRepository
public async Task<IList<string>> GetRoles(int userId)
{
var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId);
if (user == null || _userManager == null) return ArraySegment<string>.Empty; // userManager is null on Unit Tests only
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);
}

View file

@ -15,6 +15,7 @@ using Kavita.Common;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
[Flags]
public enum VolumeIncludes