Remove From On Deck (#2131)

* Allow admins to customize the amount of progress time or last item added time for on deck calculation

* Implemented the ability to remove series from on deck. They will be removed until the user reads a new chapter.

Quite a few db lookup reduction calls for reading based stuff, like continue point, bookmarks, etc.
This commit is contained in:
Joe Milazzo 2023-07-15 09:27:21 -05:00 committed by GitHub
parent 90a6c89486
commit 348bc062ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 2597 additions and 87 deletions

View file

@ -52,6 +52,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
public DbSet<ScrobbleEvent> ScrobbleEvent { get; set; } = null!;
public DbSet<ScrobbleError> ScrobbleError { get; set; } = null!;
public DbSet<ScrobbleHold> ScrobbleHold { get; set; } = null!;
public DbSet<AppUserOnDeckRemoval> AppUserOnDeckRemoval { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder builder)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,93 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class OnDeckRemoval : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Title",
table: "ReadingList",
type: "TEXT",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "NormalizedTitle",
table: "ReadingList",
type: "TEXT",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "TEXT",
oldNullable: true);
migrationBuilder.CreateTable(
name: "AppUserOnDeckRemoval",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
SeriesId = table.Column<int>(type: "INTEGER", nullable: false),
AppUserId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AppUserOnDeckRemoval", x => x.Id);
table.ForeignKey(
name: "FK_AppUserOnDeckRemoval_AspNetUsers_AppUserId",
column: x => x.AppUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AppUserOnDeckRemoval_Series_SeriesId",
column: x => x.SeriesId,
principalTable: "Series",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AppUserOnDeckRemoval_AppUserId",
table: "AppUserOnDeckRemoval",
column: "AppUserId");
migrationBuilder.CreateIndex(
name: "IX_AppUserOnDeckRemoval_SeriesId",
table: "AppUserOnDeckRemoval",
column: "SeriesId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AppUserOnDeckRemoval");
migrationBuilder.AlterColumn<string>(
name: "Title",
table: "ReadingList",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
migrationBuilder.AlterColumn<string>(
name: "NormalizedTitle",
table: "ReadingList",
type: "TEXT",
nullable: true,
oldClrType: typeof(string),
oldType: "TEXT");
}
}
}

View file

@ -15,7 +15,7 @@ namespace API.Data.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
modelBuilder.HasAnnotation("ProductVersion", "7.0.8");
modelBuilder.Entity("API.Entities.AppRole", b =>
{
@ -183,6 +183,27 @@ namespace API.Data.Migrations
b.ToTable("AppUserBookmark");
});
modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.HasIndex("SeriesId");
b.ToTable("AppUserOnDeckRemoval");
});
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
{
b.Property<int>("Id")
@ -943,6 +964,7 @@ namespace API.Data.Migrations
.HasColumnType("TEXT");
b.Property<string>("NormalizedTitle")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("Promoted")
@ -958,6 +980,7 @@ namespace API.Data.Migrations
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
@ -1082,7 +1105,7 @@ namespace API.Data.Migrations
b.Property<int>("LibraryId")
.HasColumnType("INTEGER");
b.Property<int?>("MalId")
b.Property<long?>("MalId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("ProcessDateUtc")
@ -1626,6 +1649,25 @@ namespace API.Data.Migrations
b.Navigation("AppUser");
});
modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
.WithMany()
.HasForeignKey("AppUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.Series", "Series")
.WithMany()
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AppUser");
b.Navigation("Series");
});
modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")

View file

@ -14,6 +14,7 @@ using API.DTOs.Metadata;
using API.DTOs.ReadingLists;
using API.DTOs.Search;
using API.DTOs.SeriesDetail;
using API.DTOs.Settings;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Metadata;
@ -137,6 +138,8 @@ public interface ISeriesRepository
Task<IList<Series>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true);
Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl);
Task<int> GetAverageUserRating(int seriesId);
Task RemoveFromOnDeck(int seriesId, int userId);
Task ClearOnDeckRemoval(int seriesId, int userId);
}
public class SeriesRepository : ISeriesRepository
@ -757,16 +760,33 @@ public class SeriesRepository : ISeriesRepository
/// <returns></returns>
public async Task<PagedList<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter)
{
var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30);
var cutoffLastAddedPoint = DateTime.Now - TimeSpan.FromDays(7);
var settings = await _context.ServerSetting
.Select(x => x)
.AsNoTracking()
.ToListAsync();
var serverSettings = _mapper.Map<ServerSettingDto>(settings);
var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(serverSettings.OnDeckProgressDays);
var cutoffLastAddedPoint = DateTime.Now - TimeSpan.FromDays(serverSettings.OnDeckUpdateDays);
var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard)
.Where(id => libraryId == 0 || id == libraryId);
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
// Don't allow any series the user has explicitly removed
var onDeckRemovals = _context.AppUserOnDeckRemoval
.Where(d => d.AppUserId == userId)
.Select(d => d.SeriesId)
.AsEnumerable();
// var onDeckRemovals = _context.AppUser.Where(u => u.Id == userId)
// .SelectMany(u => u.OnDeckRemovals.Select(d => d.Id))
// .AsEnumerable();
var query = _context.Series
.Where(s => usersSeriesIds.Contains(s.Id))
.Where(s => !onDeckRemovals.Contains(s.Id))
.Select(s => new
{
Series = s,
@ -1670,6 +1690,30 @@ public class SeriesRepository : ISeriesRepository
return avg.HasValue ? (int) avg.Value : 0;
}
public async Task RemoveFromOnDeck(int seriesId, int userId)
{
var existingEntry = await _context.AppUserOnDeckRemoval
.Where(u => u.Id == userId && u.SeriesId == seriesId)
.AnyAsync();
if (existingEntry) return;
_context.AppUserOnDeckRemoval.Add(new AppUserOnDeckRemoval()
{
SeriesId = seriesId,
AppUserId = userId
});
await _context.SaveChangesAsync();
}
public async Task ClearOnDeckRemoval(int seriesId, int userId)
{
var existingEntry = await _context.AppUserOnDeckRemoval
.Where(u => u.Id == userId && u.SeriesId == seriesId)
.FirstOrDefaultAsync();
if (existingEntry == null) return;
_context.AppUserOnDeckRemoval.Remove(existingEntry);
await _context.SaveChangesAsync();
}
public async Task<bool> IsSeriesInWantToRead(int userId, int seriesId)
{
var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync();

View file

@ -108,6 +108,8 @@ public static class Seed
new() {Key = ServerSettingKey.HostName, Value = string.Empty},
new() {Key = ServerSettingKey.EncodeMediaAs, Value = EncodeFormat.PNG.ToString()},
new() {Key = ServerSettingKey.LicenseKey, Value = string.Empty},
new() {Key = ServerSettingKey.OnDeckProgressDays, Value = $"{30}"},
new() {Key = ServerSettingKey.OnDeckUpdateDays, Value = $"{7}"},
new() {
Key = ServerSettingKey.CacheSize, Value = Configuration.DefaultCacheMemory + string.Empty
}, // Not used from DB, but DB is sync with appSettings.json