UTC Dates + CDisplayEx API Enhancements (#1781)

* Introduced a new claim on the Token to get UserId as well as Username, thus allowing for many places of reduced DB calls. All users will need to reauthenticate.

Introduced UTC Dates throughout the application, they are not exposed in all DTOs, that will come later when we fully switch over. For now, Utc dates will be updated along side timezone specific dates.

Refactored get-progress/progress api to be 50% faster by reducing how much data is loaded from the query.

* Speed up the following apis:
collection/search, download/bookmarks, reader/bookmark-info, recommended/quick-reads, recommended/quick-catchup-reads, recommended/highly-rated, recommended/more-in, recommended/rediscover, want-to-read/

* Added a migration to sync all dates with their new UTC counterpart.

* Added LastReadingProgressUtc onto ChapterDto for some browsing apis, but not all.

Added LastReadingProgressUtc to reading list items.

Refactored the migration to run raw SQL which is much faster.

* Added LastReadingProgressUtc onto ChapterDto for some browsing apis, but not all.

Added LastReadingProgressUtc to reading list items.

Refactored the migration to run raw SQL which is much faster.

* Fixed the unit tests

* Fixed an issue with auto mapper which was causing progress page number to not get sent to UI

* series/volume has chapter last reading progress

* Added filesize and library name on reading list item dto for CDisplayEx.

* Some minor code cleanup

* Forgot to fill a field
This commit is contained in:
Joe Milazzo 2023-02-11 04:01:24 -08:00 committed by GitHub
parent 36b48404c1
commit 7616eb5b0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 3003 additions and 131 deletions

View file

@ -112,18 +112,19 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
private static void OnEntityTracked(object sender, EntityTrackedEventArgs e)
{
if (!e.FromQuery && e.Entry.State == EntityState.Added && e.Entry.Entity is IEntityDate entity)
{
entity.Created = DateTime.Now;
entity.LastModified = DateTime.Now;
}
if (e.FromQuery || e.Entry.State != EntityState.Added || e.Entry.Entity is not IEntityDate entity) return;
entity.Created = DateTime.Now;
entity.LastModified = DateTime.Now;
entity.CreatedUtc = DateTime.UtcNow;
entity.LastModifiedUtc = DateTime.UtcNow;
}
private static void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
{
if (e.NewState == EntityState.Modified && e.Entry.Entity is IEntityDate entity)
entity.LastModified = DateTime.Now;
if (e.NewState != EntityState.Modified || e.Entry.Entity is not IEntityDate entity) return;
entity.LastModified = DateTime.Now;
entity.LastModifiedUtc = DateTime.UtcNow;
}
private void OnSaveChanges()

View file

@ -156,7 +156,8 @@ public static class DbFactory
FilePath = filePath,
Format = format,
Pages = pages,
LastModified = File.GetLastWriteTime(filePath)
LastModified = File.GetLastWriteTime(filePath),
LastModifiedUtc = File.GetLastWriteTimeUtc(filePath),
};
}

View file

@ -0,0 +1,153 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data;
/// <summary>
/// Introduced in v0.6.1.38 or v0.7.0,
/// </summary>
public static class MigrateToUtcDates
{
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
{
// if current version is > 0.6.1.38, then we can exit and not perform
var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync();
if (Version.Parse(settings.InstallVersion) > new Version(0, 6, 1, 38))
{
return;
}
logger.LogCritical("Running MigrateToUtcDates migration. Please be patient, this may take some time depending on the size of your library. Do not abort, this can break your Database");
#region Series
logger.LogInformation("Updating Dates on Series...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE Series SET
[LastModifiedUtc] = datetime([LastModified], 'utc'),
[CreatedUtc] = datetime([Created], 'utc'),
[LastChapterAddedUtc] = datetime([LastChapterAdded], 'utc'),
[LastFolderScannedUtc] = datetime([LastFolderScanned], 'utc')
;
");
logger.LogInformation("Updating Dates on Series...Done");
#endregion
#region Library
logger.LogInformation("Updating Dates on Libraries...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE Library SET
[LastModifiedUtc] = datetime([LastModified], 'utc'),
[CreatedUtc] = datetime([Created], 'utc')
;
");
logger.LogInformation("Updating Dates on Libraries...Done");
#endregion
#region Volume
try
{
logger.LogInformation("Updating Dates on Volumes...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE Volume SET
[LastModifiedUtc] = datetime([LastModified], 'utc'),
[CreatedUtc] = datetime([Created], 'utc');
");
logger.LogInformation("Updating Dates on Volumes...Done");
}
catch (Exception ex)
{
logger.LogCritical(ex, "Updating Dates on Volumes...Failed");
}
#endregion
#region Chapter
try
{
logger.LogInformation("Updating Dates on Chapters...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE Chapter SET
[LastModifiedUtc] = datetime([LastModified], 'utc'),
[CreatedUtc] = datetime([Created], 'utc')
;
");
logger.LogInformation("Updating Dates on Chapters...Done");
}
catch (Exception ex)
{
logger.LogCritical(ex, "Updating Dates on Chapters...Failed");
}
#endregion
#region AppUserBookmark
logger.LogInformation("Updating Dates on Bookmarks...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE AppUserBookmark SET
[LastModifiedUtc] = datetime([LastModified], 'utc'),
[CreatedUtc] = datetime([Created], 'utc')
;
");
logger.LogInformation("Updating Dates on Bookmarks...Done");
#endregion
#region AppUserProgress
logger.LogInformation("Updating Dates on Progress...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE AppUserProgresses SET
[LastModifiedUtc] = datetime([LastModified], 'utc'),
[CreatedUtc] = datetime([Created], 'utc')
;
");
logger.LogInformation("Updating Dates on Progress...Done");
#endregion
#region Device
logger.LogInformation("Updating Dates on Device...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE Device SET
[LastModifiedUtc] = datetime([LastModified], 'utc'),
[CreatedUtc] = datetime([Created], 'utc'),
[LastUsedUtc] = datetime([LastUsed], 'utc')
;
");
logger.LogInformation("Updating Dates on Device...Done");
#endregion
#region MangaFile
logger.LogInformation("Updating Dates on MangaFile...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE MangaFile SET
[LastModifiedUtc] = datetime([LastModified], 'utc'),
[CreatedUtc] = datetime([Created], 'utc'),
[LastFileAnalysisUtc] = datetime([LastFileAnalysis], 'utc')
;
");
logger.LogInformation("Updating Dates on MangaFile...Done");
#endregion
#region ReadingList
logger.LogInformation("Updating Dates on ReadingList...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE ReadingList SET
[LastModifiedUtc] = datetime([LastModified], 'utc'),
[CreatedUtc] = datetime([Created], 'utc')
;
");
logger.LogInformation("Updating Dates on ReadingList...Done");
#endregion
#region SiteTheme
logger.LogInformation("Updating Dates on SiteTheme...");
await dataContext.Database.ExecuteSqlRawAsync(@"
UPDATE SiteTheme SET
[LastModifiedUtc] = datetime([LastModified], 'utc'),
[CreatedUtc] = datetime([Created], 'utc')
;
");
logger.LogInformation("Updating Dates on SiteTheme...Done");
#endregion
logger.LogInformation("MigrateToUtcDates migration finished");
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,323 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
public partial class UtcTimes : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "CreatedUtc",
table: "Volume",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "LastModifiedUtc",
table: "Volume",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "CreatedUtc",
table: "SiteTheme",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "LastModifiedUtc",
table: "SiteTheme",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "CreatedUtc",
table: "Series",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "LastChapterAddedUtc",
table: "Series",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "LastFolderScannedUtc",
table: "Series",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "LastModifiedUtc",
table: "Series",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "CreatedUtc",
table: "ReadingList",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "LastModifiedUtc",
table: "ReadingList",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "CreatedUtc",
table: "MangaFile",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "LastFileAnalysisUtc",
table: "MangaFile",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "LastModifiedUtc",
table: "MangaFile",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "CreatedUtc",
table: "Library",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "LastModifiedUtc",
table: "Library",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "CreatedUtc",
table: "Device",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "LastModifiedUtc",
table: "Device",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "LastUsedUtc",
table: "Device",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "CreatedUtc",
table: "Chapter",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "LastModifiedUtc",
table: "Chapter",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "CreatedUtc",
table: "AspNetUsers",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "LastActiveUtc",
table: "AspNetUsers",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "CreatedUtc",
table: "AppUserProgresses",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "LastModifiedUtc",
table: "AppUserProgresses",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "CreatedUtc",
table: "AppUserBookmark",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<DateTime>(
name: "LastModifiedUtc",
table: "AppUserBookmark",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.CreateIndex(
name: "IX_AppUserProgresses_ChapterId",
table: "AppUserProgresses",
column: "ChapterId");
migrationBuilder.AddForeignKey(
name: "FK_AppUserProgresses_Chapter_ChapterId",
table: "AppUserProgresses",
column: "ChapterId",
principalTable: "Chapter",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_AppUserProgresses_Chapter_ChapterId",
table: "AppUserProgresses");
migrationBuilder.DropIndex(
name: "IX_AppUserProgresses_ChapterId",
table: "AppUserProgresses");
migrationBuilder.DropColumn(
name: "CreatedUtc",
table: "Volume");
migrationBuilder.DropColumn(
name: "LastModifiedUtc",
table: "Volume");
migrationBuilder.DropColumn(
name: "CreatedUtc",
table: "SiteTheme");
migrationBuilder.DropColumn(
name: "LastModifiedUtc",
table: "SiteTheme");
migrationBuilder.DropColumn(
name: "CreatedUtc",
table: "Series");
migrationBuilder.DropColumn(
name: "LastChapterAddedUtc",
table: "Series");
migrationBuilder.DropColumn(
name: "LastFolderScannedUtc",
table: "Series");
migrationBuilder.DropColumn(
name: "LastModifiedUtc",
table: "Series");
migrationBuilder.DropColumn(
name: "CreatedUtc",
table: "ReadingList");
migrationBuilder.DropColumn(
name: "LastModifiedUtc",
table: "ReadingList");
migrationBuilder.DropColumn(
name: "CreatedUtc",
table: "MangaFile");
migrationBuilder.DropColumn(
name: "LastFileAnalysisUtc",
table: "MangaFile");
migrationBuilder.DropColumn(
name: "LastModifiedUtc",
table: "MangaFile");
migrationBuilder.DropColumn(
name: "CreatedUtc",
table: "Library");
migrationBuilder.DropColumn(
name: "LastModifiedUtc",
table: "Library");
migrationBuilder.DropColumn(
name: "CreatedUtc",
table: "Device");
migrationBuilder.DropColumn(
name: "LastModifiedUtc",
table: "Device");
migrationBuilder.DropColumn(
name: "LastUsedUtc",
table: "Device");
migrationBuilder.DropColumn(
name: "CreatedUtc",
table: "Chapter");
migrationBuilder.DropColumn(
name: "LastModifiedUtc",
table: "Chapter");
migrationBuilder.DropColumn(
name: "CreatedUtc",
table: "AspNetUsers");
migrationBuilder.DropColumn(
name: "LastActiveUtc",
table: "AspNetUsers");
migrationBuilder.DropColumn(
name: "CreatedUtc",
table: "AppUserProgresses");
migrationBuilder.DropColumn(
name: "LastModifiedUtc",
table: "AppUserProgresses");
migrationBuilder.DropColumn(
name: "CreatedUtc",
table: "AppUserBookmark");
migrationBuilder.DropColumn(
name: "LastModifiedUtc",
table: "AppUserBookmark");
}
}
}

View file

@ -72,6 +72,9 @@ namespace API.Data.Migrations
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
@ -82,6 +85,9 @@ namespace API.Data.Migrations
b.Property<DateTime>("LastActive")
.HasColumnType("TEXT");
b.Property<DateTime>("LastActiveUtc")
.HasColumnType("TEXT");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
@ -146,12 +152,18 @@ namespace API.Data.Migrations
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");
b.Property<string>("FileName")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<int>("Page")
.HasColumnType("INTEGER");
@ -283,9 +295,15 @@ namespace API.Data.Migrations
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<int>("LibraryId")
.HasColumnType("INTEGER");
@ -302,6 +320,8 @@ namespace API.Data.Migrations
b.HasIndex("AppUserId");
b.HasIndex("ChapterId");
b.HasIndex("SeriesId");
b.ToTable("AppUserProgresses");
@ -373,6 +393,9 @@ namespace API.Data.Migrations
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");
b.Property<bool>("IsSpecial")
.HasColumnType("INTEGER");
@ -382,6 +405,9 @@ namespace API.Data.Migrations
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<int>("MaxHoursToRead")
.HasColumnType("INTEGER");
@ -475,6 +501,9 @@ namespace API.Data.Migrations
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");
b.Property<string>("EmailAddress")
.HasColumnType("TEXT");
@ -484,9 +513,15 @@ namespace API.Data.Migrations
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<DateTime>("LastUsed")
.HasColumnType("TEXT");
b.Property<DateTime>("LastUsedUtc")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
@ -554,6 +589,9 @@ namespace API.Data.Migrations
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");
b.Property<bool>("FolderWatching")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
@ -577,6 +615,9 @@ namespace API.Data.Migrations
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<DateTime>("LastScanned")
.HasColumnType("TEXT");
@ -611,6 +652,9 @@ namespace API.Data.Migrations
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");
b.Property<string>("Extension")
.HasColumnType("TEXT");
@ -623,9 +667,15 @@ namespace API.Data.Migrations
b.Property<DateTime>("LastFileAnalysis")
.HasColumnType("TEXT");
b.Property<DateTime>("LastFileAnalysisUtc")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<int>("Pages")
.HasColumnType("INTEGER");
@ -797,9 +847,15 @@ namespace API.Data.Migrations
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<string>("NormalizedTitle")
.HasColumnType("TEXT");
@ -874,6 +930,9 @@ namespace API.Data.Migrations
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");
b.Property<string>("FolderPath")
.HasColumnType("TEXT");
@ -883,12 +942,21 @@ namespace API.Data.Migrations
b.Property<DateTime>("LastChapterAdded")
.HasColumnType("TEXT");
b.Property<DateTime>("LastChapterAddedUtc")
.HasColumnType("TEXT");
b.Property<DateTime>("LastFolderScanned")
.HasColumnType("TEXT");
b.Property<DateTime>("LastFolderScannedUtc")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<int>("LibraryId")
.HasColumnType("INTEGER");
@ -1004,6 +1072,9 @@ namespace API.Data.Migrations
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");
b.Property<string>("FileName")
.HasColumnType("TEXT");
@ -1013,6 +1084,9 @@ namespace API.Data.Migrations
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
@ -1062,9 +1136,15 @@ namespace API.Data.Migrations
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<int>("MaxHoursToRead")
.HasColumnType("INTEGER");
@ -1333,6 +1413,12 @@ namespace API.Data.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.Chapter", null)
.WithMany("UserProgress")
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.Series", null)
.WithMany("Progress")
.HasForeignKey("SeriesId")
@ -1707,6 +1793,8 @@ namespace API.Data.Migrations
modelBuilder.Entity("API.Entities.Chapter", b =>
{
b.Navigation("Files");
b.Navigation("UserProgress");
});
modelBuilder.Entity("API.Entities.Library", b =>

View file

@ -1,8 +1,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
using API.Entities.Enums;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
@ -21,15 +24,18 @@ public interface IAppUserProgressRepository
Task<AppUserProgress> GetAnyProgress();
Task<IEnumerable<AppUserProgress>> GetUserProgressForSeriesAsync(int seriesId, int userId);
Task<IEnumerable<AppUserProgress>> GetAllProgress();
Task<ProgressDto> GetUserProgressDtoAsync(int chapterId, int userId);
}
public class AppUserProgressRepository : IAppUserProgressRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public AppUserProgressRepository(DataContext context)
public AppUserProgressRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public void Update(AppUserProgress userProgress)
@ -114,6 +120,14 @@ public class AppUserProgressRepository : IAppUserProgressRepository
return await _context.AppUserProgresses.ToListAsync();
}
public async Task<ProgressDto> GetUserProgressDtoAsync(int chapterId, int userId)
{
return await _context.AppUserProgresses
.Where(p => p.AppUserId == userId && p.ChapterId == chapterId)
.ProjectTo<ProgressDto>(_mapper.ConfigurationProvider)
.FirstOrDefaultAsync();
}
public async Task<AppUserProgress> GetUserProgressAsync(int chapterId, int userId)
{
return await _context.AppUserProgresses

View file

@ -18,7 +18,7 @@ public enum ChapterIncludes
{
None = 1,
Volumes = 2,
Files = 4
Files = 4,
}
public interface IChapterRepository
@ -28,8 +28,8 @@ 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);
Task<ChapterMetadataDto> GetChapterMetadataDtoAsync(int chapterId);
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<MangaFile>> GetFilesForChaptersAsync(IReadOnlyList<int> chapterIds);
@ -37,6 +37,7 @@ public interface IChapterRepository
Task<IList<string>> GetAllCoverImagesAsync();
Task<IList<Chapter>> GetAllChaptersWithNonWebPCovers();
Task<IEnumerable<string>> GetCoverImagesForLockedChaptersAsync();
Task<ChapterDto> AddChapterModifiers(int userId, ChapterDto chapter);
}
public class ChapterRepository : IChapterRepository
{
@ -121,24 +122,24 @@ public class ChapterRepository : IChapterRepository
return _context.Chapter
.Where(c => c.Id == chapterId)
.Select(c => c.Pages)
.SingleOrDefaultAsync();
.FirstOrDefaultAsync();
}
public async Task<ChapterDto> GetChapterDtoAsync(int chapterId)
public async Task<ChapterDto> GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files)
{
var chapter = await _context.Chapter
.Include(c => c.Files)
.Includes(includes)
.ProjectTo<ChapterDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.AsSplitQuery()
.SingleOrDefaultAsync(c => c.Id == chapterId);
.FirstOrDefaultAsync(c => c.Id == chapterId);
return chapter;
}
public async Task<ChapterMetadataDto> GetChapterMetadataDtoAsync(int chapterId)
public async Task<ChapterMetadataDto> GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files)
{
var chapter = await _context.Chapter
.Include(c => c.Files)
.Includes(includes)
.ProjectTo<ChapterMetadataDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.AsSplitQuery()
@ -168,14 +169,9 @@ public class ChapterRepository : IChapterRepository
/// <returns></returns>
public async Task<Chapter> GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files)
{
var query = _context.Chapter
.AsSplitQuery();
if (includes.HasFlag(ChapterIncludes.Files)) query = query.Include(c => c.Files);
if (includes.HasFlag(ChapterIncludes.Volumes)) query = query.Include(c => c.Volume);
return await query
.SingleOrDefaultAsync(c => c.Id == chapterId);
return await _context.Chapter
.Includes(includes)
.FirstOrDefaultAsync(c => c.Id == chapterId);
}
/// <summary>
@ -247,4 +243,24 @@ public class ChapterRepository : IChapterRepository
.AsNoTracking()
.ToListAsync();
}
public async Task<ChapterDto> AddChapterModifiers(int userId, ChapterDto chapter)
{
var progress = await _context.AppUserProgresses.Where(x =>
x.AppUserId == userId && x.ChapterId == chapter.Id)
.AsNoTracking()
.FirstOrDefaultAsync();
if (progress != null)
{
chapter.PagesRead = progress.PagesRead ;
chapter.LastReadingProgressUtc = progress.LastModifiedUtc;
}
else
{
chapter.PagesRead = 0;
chapter.LastReadingProgressUtc = DateTime.MinValue;
}
return chapter;
}
}

View file

@ -149,6 +149,7 @@ public class ReadingListRepository : IReadingListRepository
chapter.ReleaseDate,
ReadingListItem = data,
ChapterTitleName = chapter.TitleName,
FileSize = chapter.Files.Sum(f => f.Bytes)
})
.Join(_context.Volume, s => s.ReadingListItem.VolumeId, volume => volume.Id, (data, volume) => new
@ -158,6 +159,7 @@ public class ReadingListRepository : IReadingListRepository
data.ChapterNumber,
data.ReleaseDate,
data.ChapterTitleName,
data.FileSize,
VolumeId = volume.Id,
VolumeNumber = volume.Name,
})
@ -174,6 +176,8 @@ public class ReadingListRepository : IReadingListRepository
data.VolumeId,
data.ReleaseDate,
data.ChapterTitleName,
data.FileSize,
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()
})
.Select(data => new ReadingListItemDto()
@ -192,7 +196,9 @@ public class ReadingListRepository : IReadingListRepository
ReadingListId = data.ReadingListItem.ReadingListId,
ReleaseDate = data.ReleaseDate,
LibraryType = data.LibraryType,
ChapterTitleName = data.ChapterTitleName
ChapterTitleName = data.ChapterTitleName,
LibraryName = data.LibraryName,
FileSize = data.FileSize
})
.Where(o => userLibraries.Contains(o.LibraryId))
.OrderBy(rli => rli.Order)
@ -218,6 +224,7 @@ public class ReadingListRepository : IReadingListRepository
if (progressItem == null) continue;
progressItem.PagesRead = progress.PagesRead;
progressItem.LastReadingProgressUtc = progress.LastModifiedUtc;
}
return items;
@ -233,7 +240,7 @@ public class ReadingListRepository : IReadingListRepository
public async Task<IEnumerable<ReadingListItemDto>> AddReadingProgressModifiers(int userId, IList<ReadingListItemDto> items)
{
var chapterIds = items.Select(i => i.ChapterId).Distinct().ToList();
var chapterIds = items.Select(i => i.ChapterId).Distinct();
var userProgress = await _context.AppUserProgresses
.Where(p => p.AppUserId == userId && chapterIds.Contains(p.ChapterId))
.AsNoTracking()
@ -241,8 +248,10 @@ public class ReadingListRepository : IReadingListRepository
foreach (var item in items)
{
var progress = userProgress.Where(p => p.ChapterId == item.ChapterId);
item.PagesRead = progress.Sum(p => p.PagesRead);
var progress = userProgress.Where(p => p.ChapterId == item.ChapterId).ToList();
if (progress.Count == 0) continue;
item.PagesRead = progress.Sum(p => p.PagesRead);
item.LastReadingProgressUtc = progress.Max(p => p.LastModifiedUtc);
}
return items;

View file

@ -218,7 +218,10 @@ public class VolumeRepository : IVolumeRepository
{
foreach (var c in v.Chapters)
{
c.PagesRead = userProgress.Where(p => p.ChapterId == c.Id).Sum(p => p.PagesRead);
var progresses = userProgress.Where(p => p.ChapterId == c.Id).ToList();
if (progresses.Count == 0) continue;
c.PagesRead = progresses.Sum(p => p.PagesRead);
c.LastReadingProgressUtc = progresses.Max(p => p.LastModifiedUtc);
}
v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead);

View file

@ -51,7 +51,7 @@ public class UnitOfWork : IUnitOfWork
public ISettingsRepository SettingsRepository => new SettingsRepository(_context, _mapper);
public IAppUserProgressRepository AppUserProgressRepository => new AppUserProgressRepository(_context);
public IAppUserProgressRepository AppUserProgressRepository => new AppUserProgressRepository(_context, _mapper);
public ICollectionTagRepository CollectionTagRepository => new CollectionTagRepository(_context, _mapper);
public IChapterRepository ChapterRepository => new ChapterRepository(_context, _mapper);
public IReadingListRepository ReadingListRepository => new ReadingListRepository(_context, _mapper);