OPDS Enhancements, Epub fixes, and a lot more (#4035)

Co-authored-by: Amelia <77553571+Fesaa@users.noreply.github.com>
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
Co-authored-by: Fabian Pammer <fpammer@mantro.net>
Co-authored-by: Vinícius Licz <vinilicz@gmail.com>
This commit is contained in:
Joe Milazzo
2025-09-20 15:16:21 -05:00
committed by GitHub
parent 9891df898f
commit 26ff71f42b
339 changed files with 6923 additions and 1971 deletions
+3
View File
@@ -139,6 +139,9 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
builder.Entity<AppUserPreferences>()
.Property(b => b.AllowAutomaticWebtoonReaderDetection)
.HasDefaultValue(true);
builder.Entity<AppUserPreferences>()
.Property(b => b.ColorScapeEnabled)
.HasDefaultValue(true);
builder.Entity<Library>()
.Property(b => b.AllowScrobbling)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class ColorScapeSetting : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "ColorScapeEnabled",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ColorScapeEnabled",
table: "AppUserPreferences");
}
}
}
@@ -551,6 +551,11 @@ namespace API.Data.Migrations
b.Property<bool>("CollapseSeriesRelationships")
.HasColumnType("INTEGER");
b.Property<bool>("ColorScapeEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<bool>("EmulateBook")
.HasColumnType("INTEGER");
@@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using API.DTOs.Dashboard;
using API.Entities;
using API.Helpers;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
@@ -16,6 +17,7 @@ public interface IAppUserSmartFilterRepository
void Attach(AppUserSmartFilter filter);
void Delete(AppUserSmartFilter filter);
IEnumerable<SmartFilterDto> GetAllDtosByUserId(int userId);
Task<PagedList<SmartFilterDto>> GetPagedDtosByUserIdAsync(int userId, UserParams userParams);
Task<AppUserSmartFilter?> GetById(int smartFilterId);
}
@@ -54,6 +56,15 @@ public class AppUserSmartFilterRepository : IAppUserSmartFilterRepository
.AsEnumerable();
}
public Task<PagedList<SmartFilterDto>> GetPagedDtosByUserIdAsync(int userId, UserParams userParams)
{
var filters = _context.AppUserSmartFilter
.Where(f => f.AppUserId == userId)
.ProjectTo<SmartFilterDto>(_mapper.ConfigurationProvider);
return PagedList<SmartFilterDto>.CreateAsync(filters, userParams);
}
public async Task<AppUserSmartFilter?> GetById(int smartFilterId)
{
return await _context.AppUserSmartFilter
+49 -4
View File
@@ -40,10 +40,12 @@ public interface IChapterRepository
Task<IChapterInfoDto?> GetChapterInfoDtoAsync(int chapterId);
Task<int> GetChapterTotalPagesAsync(int chapterId);
Task<Chapter?> GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files);
Task<ChapterDto?> GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files);
Task<ChapterDto?> GetChapterDtoAsync(int chapterId, int userId);
Task<IList<ChapterDto>> GetChapterDtoByIdsAsync(IEnumerable<int> chapterIds, int userId);
Task<ChapterMetadataDto?> GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files);
Task<IList<MangaFile>> GetFilesForChapterAsync(int chapterId);
Task<IList<Chapter>> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None);
Task<IList<ChapterDto>> GetChapterDtosAsync(int volumeId, int userId);
Task<IList<MangaFile>> GetFilesForChaptersAsync(IReadOnlyList<int> chapterIds);
Task<string?> GetChapterCoverImageAsync(int chapterId);
Task<IList<string>> GetAllCoverImagesAsync();
@@ -153,18 +155,39 @@ public class ChapterRepository : IChapterRepository
.Select(c => c.Pages)
.FirstOrDefaultAsync();
}
public async Task<ChapterDto?> GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files)
public async Task<ChapterDto?> GetChapterDtoAsync(int chapterId, int userId)
{
var chapter = await _context.Chapter
.Includes(includes)
.Includes(ChapterIncludes.Files | ChapterIncludes.People)
.ProjectTo<ChapterDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.AsSplitQuery()
.FirstOrDefaultAsync(c => c.Id == chapterId);
if (userId > 0 && chapter != null)
{
await AddChapterModifiers(userId, chapter);
}
return chapter;
}
public async Task<IList<ChapterDto>> GetChapterDtoByIdsAsync(IEnumerable<int> chapterIds, int userId)
{
var chapters = await _context.Chapter
.Where(c => chapterIds.Contains(c.Id))
.Includes(ChapterIncludes.Files | ChapterIncludes.People)
.ProjectTo<ChapterDto>(_mapper.ConfigurationProvider)
.AsSplitQuery()
.ToListAsync() ;
foreach (var chapter in chapters)
{
await AddChapterModifiers(userId, chapter);
}
return chapters;
}
public async Task<ChapterMetadataDto?> GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files)
{
var chapter = await _context.Chapter
@@ -218,6 +241,28 @@ public class ChapterRepository : IChapterRepository
.ToListAsync();
}
/// <summary>
/// Returns Chapters for a volume id with Progress
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
public async Task<IList<ChapterDto>> GetChapterDtosAsync(int volumeId, int userId)
{
var chapts = await _context.Chapter
.Where(c => c.VolumeId == volumeId)
.Includes(ChapterIncludes.Files | ChapterIncludes.People)
.OrderBy(c => c.SortOrder)
.ProjectTo<ChapterDto>(_mapper.ConfigurationProvider)
.ToListAsync();
foreach (var chapter in chapts)
{
await AddChapterModifiers(userId, chapter);
}
return chapts;
}
/// <summary>
/// Returns the cover image for a chapter id.
/// </summary>
@@ -9,6 +9,7 @@ using API.Entities.Enums;
using API.Extensions;
using API.Extensions.QueryExtensions;
using API.Extensions.QueryExtensions.Filtering;
using API.Helpers;
using API.Services.Plus;
using AutoMapper;
using AutoMapper.QueryableExtensions;
@@ -49,6 +50,7 @@ public interface ICollectionTagRepository
/// <param name="includePromoted"></param>
/// <returns></returns>
Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosAsync(int userId, bool includePromoted = false);
Task<PagedList<AppUserCollectionDto>> GetCollectionDtosPagedAsync(int userId, UserParams userParams, bool includePromoted = false);
Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false);
Task<IList<string>> GetAllCoverImagesAsync();
@@ -117,6 +119,18 @@ public class CollectionTagRepository : ICollectionTagRepository
.ToListAsync();
}
public async Task<PagedList<AppUserCollectionDto>> GetCollectionDtosPagedAsync(int userId, UserParams userParams, bool includePromoted = false)
{
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
var collections = _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);
return await PagedList<AppUserCollectionDto>.CreateAsync(collections, userParams);
}
public async Task<IEnumerable<AppUserCollectionDto>> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false)
{
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
+4 -4
View File
@@ -26,8 +26,8 @@ public interface IGenreRepository
Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false);
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<GenreTagDto?> GetRandomGenre();
Task<GenreTagDto?> GetGenreById(int id);
Task<List<string>> GetAllGenresNotInListAsync(ICollection<string> genreNames);
Task<PagedList<BrowseGenreDto>> GetBrowseableGenre(int userId, UserParams userParams);
}
@@ -79,7 +79,7 @@ public class GenreRepository : IGenreRepository
return await _context.Genre.CountAsync();
}
public async Task<GenreTagDto> GetRandomGenre()
public async Task<GenreTagDto?> GetRandomGenre()
{
var genreCount = await GetCountAsync();
if (genreCount == 0) return null;
@@ -92,7 +92,7 @@ public class GenreRepository : IGenreRepository
.FirstOrDefaultAsync();
}
public async Task<GenreTagDto> GetGenreById(int id)
public async Task<GenreTagDto?> GetGenreById(int id)
{
return await _context.Genre
.Where(g => g.Id == id)
+14 -6
View File
@@ -31,7 +31,7 @@ public interface IReadingListRepository
{
Task<PagedList<ReadingListDto>> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams, bool sortByLastModified = true);
Task<ReadingList?> GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None);
Task<IEnumerable<ReadingListItemDto>> GetReadingListItemDtosByIdAsync(int readingListId, int userId);
Task<IEnumerable<ReadingListItemDto>> GetReadingListItemDtosByIdAsync(int readingListId, int userId, UserParams? userParams = null);
Task<ReadingListDto?> GetReadingListDtoByIdAsync(int readingListId, int userId);
Task<IEnumerable<ReadingListItemDto>> AddReadingProgressModifiers(int userId, IList<ReadingListItemDto> items);
Task<ReadingListDto?> GetReadingListDtoByTitleAsync(int userId, string title);
@@ -357,11 +357,11 @@ public class ReadingListRepository : IReadingListRepository
.SingleOrDefaultAsync();
}
public async Task<IEnumerable<ReadingListItemDto>> GetReadingListItemDtosByIdAsync(int readingListId, int userId)
public async Task<IEnumerable<ReadingListItemDto>> GetReadingListItemDtosByIdAsync(int readingListId, int userId, UserParams? userParams = null)
{
var userLibraries = _context.Library.GetUserLibraries(userId);
var items = await _context.ReadingListItem
var query = _context.ReadingListItem
.Where(s => s.ReadingListId == readingListId)
.Join(_context.Chapter, s => s.ChapterId, chapter => chapter.Id, (data, chapter) => new
{
@@ -431,9 +431,17 @@ public class ReadingListRepository : IReadingListRepository
})
.Where(o => userLibraries.Contains(o.LibraryId))
.OrderBy(rli => rli.Order)
.AsSplitQuery()
.AsNoTracking()
.ToListAsync();
.AsSplitQuery();
if (userParams != null)
{
query = query
.Skip(userParams.PageNumber * userParams.PageSize)
.Take(userParams.PageSize);
}
var items = await query.ToListAsync();
foreach (var item in items)
{
+24 -14
View File
@@ -29,20 +29,20 @@ namespace API.Data.Repositories;
public enum AppUserIncludes
{
None = 1,
Progress = 2,
Bookmarks = 4,
ReadingLists = 8,
Ratings = 16,
UserPreferences = 32,
WantToRead = 64,
ReadingListsWithItems = 128,
Devices = 256,
ScrobbleHolds = 512,
SmartFilters = 1024,
DashboardStreams = 2048,
SideNavStreams = 4096,
ExternalSources = 8192,
Collections = 16384, // 2^14
Progress = 1 << 1,
Bookmarks = 1 << 2,
ReadingLists = 1 << 3,
Ratings = 1 << 4,
UserPreferences = 1 << 5,
WantToRead = 1 << 6,
ReadingListsWithItems = 1 << 7,
Devices = 1 << 8,
ScrobbleHolds = 1 << 9,
SmartFilters = 1 << 10,
DashboardStreams = 1 << 11,
SideNavStreams = 1 << 12,
ExternalSources = 1 << 13,
Collections = 1 << 14,
ChapterRatings = 1 << 15,
}
@@ -118,6 +118,7 @@ public interface IUserRepository
Task<AppUser?> GetByOidcId(string? oidcId, AppUserIncludes includes = AppUserIncludes.None);
Task<AnnotationDto?> GetAnnotationDtoById(int userId, int annotationId);
Task<List<AnnotationDto>> GetAnnotationDtosBySeries(int userId, int seriesId);
}
public class UserRepository : IUserRepository
@@ -612,6 +613,14 @@ public class UserRepository : IUserRepository
.FirstOrDefaultAsync();
}
public async Task<List<AnnotationDto>> GetAnnotationDtosBySeries(int userId, int seriesId)
{
return await _context.AppUserAnnotation
.Where(a => a.AppUserId == userId && a.SeriesId == seriesId)
.ProjectTo<AnnotationDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
{
@@ -629,6 +638,7 @@ public class UserRepository : IUserRepository
var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId);
if (user == null) return ArraySegment<string>.Empty;
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (_userManager == null)
{
// userManager is null on Unit Tests only