Sort by Average Rating and Big Want to Read fix (#2672)
This commit is contained in:
parent
03e7d38482
commit
1fd72ada36
42 changed files with 3552 additions and 105 deletions
|
|
@ -53,6 +53,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="30.1.0" />
|
||||
<PackageReference Include="MailKit" Version="4.3.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
|
|
|||
|
|
@ -1250,7 +1250,7 @@ public class OpdsController : BaseApiController
|
|||
if (progress != null)
|
||||
{
|
||||
link.LastRead = progress.PageNum;
|
||||
link.LastReadDate = progress.LastModifiedUtc.ToString("o"); // Adhere to ISO 8601
|
||||
link.LastReadDate = progress.LastModifiedUtc.ToString("s"); // Adhere to ISO 8601
|
||||
}
|
||||
link.IsPageStream = true;
|
||||
return link;
|
||||
|
|
|
|||
|
|
@ -38,18 +38,16 @@ public class ServerController : BaseApiController
|
|||
private readonly IStatsService _statsService;
|
||||
private readonly ICleanupService _cleanupService;
|
||||
private readonly IScannerService _scannerService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEasyCachingProviderFactory _cachingProviderFactory;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IEmailService _emailService;
|
||||
|
||||
public ServerController(ILogger<ServerController> logger,
|
||||
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
|
||||
ICleanupService cleanupService, IScannerService scannerService, IAccountService accountService,
|
||||
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService,
|
||||
IStatsService statsService, ICleanupService cleanupService, IScannerService scannerService,
|
||||
ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IEasyCachingProviderFactory cachingProviderFactory,
|
||||
ILocalizationService localizationService, IEmailService emailService)
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_logger = logger;
|
||||
_backupService = backupService;
|
||||
|
|
@ -58,12 +56,10 @@ public class ServerController : BaseApiController
|
|||
_statsService = statsService;
|
||||
_cleanupService = cleanupService;
|
||||
_scannerService = scannerService;
|
||||
_accountService = accountService;
|
||||
_taskScheduler = taskScheduler;
|
||||
_unitOfWork = unitOfWork;
|
||||
_cachingProviderFactory = cachingProviderFactory;
|
||||
_localizationService = localizationService;
|
||||
_emailService = emailService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -180,11 +176,22 @@ public class ServerController : BaseApiController
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks for updates and pushes an event to the UI
|
||||
/// </summary>
|
||||
/// <remarks>Some users have websocket issues so this is not always reliable to alert the user</remarks>
|
||||
[HttpGet("check-for-updates")]
|
||||
public async Task<ActionResult> CheckForAnnouncements()
|
||||
{
|
||||
await _taskScheduler.CheckForUpdate();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks for updates, if no updates that are > current version installed, returns null
|
||||
/// </summary>
|
||||
[HttpGet("check-update")]
|
||||
public async Task<ActionResult<UpdateNotificationDto>> CheckForUpdates()
|
||||
public async Task<ActionResult<UpdateNotificationDto?>> CheckForUpdates()
|
||||
{
|
||||
return Ok(await _versionUpdaterService.CheckForUpdate());
|
||||
}
|
||||
|
|
@ -268,14 +275,4 @@ public class ServerController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks for updates and pushes an event to the UI
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("check-for-updates")]
|
||||
public async Task<ActionResult> CheckForAnnouncements()
|
||||
{
|
||||
await _taskScheduler.CheckForUpdate();
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using API.DTOs;
|
|||
using API.DTOs.Filtering;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.DTOs.WantToRead;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
|
|
@ -91,15 +92,15 @@ public class WantToReadController : BaseApiController
|
|||
AppUserIncludes.WantToRead);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var existingIds = user.WantToRead.Select(s => s.Id).ToList();
|
||||
existingIds.AddRange(dto.SeriesIds);
|
||||
var existingIds = user.WantToRead.Select(s => s.SeriesId).ToList();
|
||||
var idsToAdd = dto.SeriesIds.Except(existingIds);
|
||||
|
||||
var idsToAdd = existingIds.Distinct().ToList();
|
||||
|
||||
var seriesToAdd = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(idsToAdd);
|
||||
foreach (var series in seriesToAdd)
|
||||
foreach (var id in idsToAdd)
|
||||
{
|
||||
user.WantToRead.Add(series);
|
||||
user.WantToRead.Add(new AppUserWantToRead()
|
||||
{
|
||||
SeriesId = id
|
||||
});
|
||||
}
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok();
|
||||
|
|
@ -127,7 +128,9 @@ public class WantToReadController : BaseApiController
|
|||
AppUserIncludes.WantToRead);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
user.WantToRead = user.WantToRead.Where(s => !dto.SeriesIds.Contains(s.Id)).ToList();
|
||||
user.WantToRead = user.WantToRead
|
||||
.Where(s => !dto.SeriesIds.Contains(s.SeriesId))
|
||||
.ToList();
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok();
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
|
|
|
|||
|
|
@ -30,4 +30,8 @@ public enum SortField
|
|||
/// Last time the user had any reading progress
|
||||
/// </summary>
|
||||
ReadProgress = 7,
|
||||
/// <summary>
|
||||
/// Kavita+ Only - External Average Rating
|
||||
/// </summary>
|
||||
AverageRating = 8
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
public DbSet<ExternalRating> ExternalRating { get; set; } = null!;
|
||||
public DbSet<ExternalSeriesMetadata> ExternalSeriesMetadata { get; set; } = null!;
|
||||
public DbSet<ExternalRecommendation> ExternalRecommendation { get; set; } = null!;
|
||||
public DbSet<ManualMigrationHistory> ManualMigrationHistory { get; set; } = null!;
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
|
|
|
|||
86
API/Data/ManualMigrations/MigrateManualHistory.cs
Normal file
86
API/Data/ManualMigrations/MigrateManualHistory.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// Introduced in v0.7.14, will store history so that going forward, migrations can just check against the history
|
||||
/// and I don't need to remove old migrations
|
||||
/// </summary>
|
||||
public static class MigrateManualHistory
|
||||
{
|
||||
public static async Task Migrate(DataContext dataContext, ILogger<Program> logger)
|
||||
{
|
||||
logger.LogCritical(
|
||||
"Running MigrateManualHistory migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
if (await dataContext.ManualMigrationHistory.AnyAsync())
|
||||
{
|
||||
logger.LogCritical(
|
||||
"Running MigrateManualHistory migration - Completed. This is not an error");
|
||||
return;
|
||||
}
|
||||
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateUserLibrarySideNavStream",
|
||||
ProductVersion = "0.7.9.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateSmartFilterEncoding",
|
||||
ProductVersion = "0.7.11.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateLibrariesToHaveAllFileTypes",
|
||||
ProductVersion = "0.7.11.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateEmailTemplates",
|
||||
ProductVersion = "0.7.14.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateVolumeNumber",
|
||||
ProductVersion = "0.7.14.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateWantToReadExport",
|
||||
ProductVersion = "0.7.14.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateWantToReadImport",
|
||||
ProductVersion = "0.7.14.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "MigrateManualHistory",
|
||||
ProductVersion = "0.7.14.0",
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await dataContext.SaveChangesAsync();
|
||||
|
||||
logger.LogCritical(
|
||||
"Running MigrateManualHistory migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
||||
81
API/Data/ManualMigrations/MigrateWantToReadExport.cs
Normal file
81
API/Data/ManualMigrations/MigrateWantToReadExport.cs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using API.Services;
|
||||
using CsvHelper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// v0.7.13.12/v0.7.14 - Want to read is extracted and saved in a csv
|
||||
/// </summary>
|
||||
/// <remarks>This must run BEFORE any DB migrations</remarks>
|
||||
public static class MigrateWantToReadExport
|
||||
{
|
||||
public static async Task Migrate(DataContext dataContext, IDirectoryService directoryService, ILogger<Program> logger)
|
||||
{
|
||||
logger.LogCritical(
|
||||
"Running MigrateWantToReadExport migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
var columnExists = false;
|
||||
await using var command = dataContext.Database.GetDbConnection().CreateCommand();
|
||||
command.CommandText = "PRAGMA table_info('Series')";
|
||||
|
||||
await dataContext.Database.OpenConnectionAsync();
|
||||
await using var result = await command.ExecuteReaderAsync();
|
||||
while (await result.ReadAsync())
|
||||
{
|
||||
var columnName = result["name"].ToString();
|
||||
if (columnName != "AppUserId") continue;
|
||||
|
||||
logger.LogInformation("Column 'AppUserId' exists in the 'Series' table. Running migration...");
|
||||
// Your migration logic here
|
||||
columnExists = true;
|
||||
break;
|
||||
}
|
||||
|
||||
await result.CloseAsync();
|
||||
|
||||
if (!columnExists)
|
||||
{
|
||||
logger.LogCritical(
|
||||
"Running MigrateWantToReadExport migration - Completed. This is not an error");
|
||||
return;
|
||||
}
|
||||
|
||||
await using var command2 = dataContext.Database.GetDbConnection().CreateCommand();
|
||||
command.CommandText = "Select AppUserId, Id from Series WHERE AppUserId IS NOT NULL ORDER BY AppUserId;";
|
||||
|
||||
await dataContext.Database.OpenConnectionAsync();
|
||||
await using var result2 = await command.ExecuteReaderAsync();
|
||||
|
||||
await using var writer = new StreamWriter(Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv"));
|
||||
await using var csvWriter = new CsvWriter(writer, CultureInfo.InvariantCulture);
|
||||
|
||||
// Write header
|
||||
csvWriter.WriteField("AppUserId");
|
||||
csvWriter.WriteField("Id");
|
||||
await csvWriter.NextRecordAsync();
|
||||
|
||||
// Write data
|
||||
while (await result2.ReadAsync())
|
||||
{
|
||||
var appUserId = result2["AppUserId"].ToString();
|
||||
var id = result2["Id"].ToString();
|
||||
|
||||
csvWriter.WriteField(appUserId);
|
||||
csvWriter.WriteField(id);
|
||||
await csvWriter.NextRecordAsync();
|
||||
}
|
||||
|
||||
|
||||
await result2.CloseAsync();
|
||||
writer.Close();
|
||||
|
||||
logger.LogCritical(
|
||||
"Running MigrateWantToReadExport migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
||||
60
API/Data/ManualMigrations/MigrateWantToReadImport.cs
Normal file
60
API/Data/ManualMigrations/MigrateWantToReadImport.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data.Repositories;
|
||||
using API.Entities;
|
||||
using API.Services;
|
||||
using CsvHelper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// v0.7.13.12/v0.7.14 - Want to read is imported from a csv
|
||||
/// </summary>
|
||||
public static class MigrateWantToReadImport
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, IDirectoryService directoryService, ILogger<Program> logger)
|
||||
{
|
||||
var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv");
|
||||
var outputFile = Path.Join(directoryService.ConfigDirectory, "imported-want-to-read-migration.csv");
|
||||
|
||||
logger.LogCritical(
|
||||
"Running MigrateWantToReadImport migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
if (!File.Exists(importFile) || File.Exists(outputFile))
|
||||
{
|
||||
logger.LogCritical(
|
||||
"Running MigrateWantToReadImport migration - Completed. This is not an error");
|
||||
return;
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(importFile);
|
||||
using var csvReader = new CsvReader(reader, CultureInfo.InvariantCulture);
|
||||
// Read the records from the CSV file
|
||||
await csvReader.ReadAsync();
|
||||
csvReader.ReadHeader(); // Skip the header row
|
||||
|
||||
while (await csvReader.ReadAsync())
|
||||
{
|
||||
// Read the values of AppUserId and Id columns
|
||||
var appUserId = csvReader.GetField<int>("AppUserId");
|
||||
var seriesId = csvReader.GetField<int>("Id");
|
||||
var user = await unitOfWork.UserRepository.GetUserByIdAsync(appUserId, AppUserIncludes.WantToRead);
|
||||
if (user == null || user.WantToRead.Any(w => w.SeriesId == seriesId)) continue;
|
||||
|
||||
user.WantToRead.Add(new AppUserWantToRead()
|
||||
{
|
||||
SeriesId = seriesId
|
||||
});
|
||||
}
|
||||
|
||||
await unitOfWork.CommitAsync();
|
||||
reader.Close();
|
||||
|
||||
File.WriteAllLines(outputFile, await File.ReadAllLinesAsync(importFile));
|
||||
logger.LogCritical(
|
||||
"Running MigrateWantToReadImport migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
||||
2844
API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs
generated
Normal file
2844
API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
106
API/Data/Migrations/20240130190617_WantToReadFix.cs
Normal file
106
API/Data/Migrations/20240130190617_WantToReadFix.cs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class WantToReadFix : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Series_AspNetUsers_AppUserId",
|
||||
table: "Series");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Series_AppUserId",
|
||||
table: "Series");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AppUserId",
|
||||
table: "Series");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AppUserWantToRead",
|
||||
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_AppUserWantToRead", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserWantToRead_AspNetUsers_AppUserId",
|
||||
column: x => x.AppUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserWantToRead_Series_SeriesId",
|
||||
column: x => x.SeriesId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ManualMigrationHistory",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ProductVersion = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
RanAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ManualMigrationHistory", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserWantToRead_AppUserId",
|
||||
table: "AppUserWantToRead",
|
||||
column: "AppUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserWantToRead_SeriesId",
|
||||
table: "AppUserWantToRead",
|
||||
column: "SeriesId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AppUserWantToRead");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ManualMigrationHistory");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AppUserId",
|
||||
table: "Series",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Series_AppUserId",
|
||||
table: "Series",
|
||||
column: "AppUserId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Series_AspNetUsers_AppUserId",
|
||||
table: "Series",
|
||||
column: "AppUserId",
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -602,6 +602,27 @@ namespace API.Data.Migrations
|
|||
b.ToTable("AppUserTableOfContent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserWantToRead", 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("AppUserWantToRead");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -980,6 +1001,26 @@ namespace API.Data.Migrations
|
|||
b.ToTable("MangaFile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ManualMigrationHistory", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProductVersion")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("RanAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ManualMigrationHistory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.MediaError", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -1551,9 +1592,6 @@ namespace API.Data.Migrations
|
|||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AvgHoursToRead")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
|
@ -1634,8 +1672,6 @@ namespace API.Data.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.ToTable("Series");
|
||||
|
|
@ -2252,6 +2288,25 @@ namespace API.Data.Migrations
|
|||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserWantToRead", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
.WithMany("WantToRead")
|
||||
.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.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Volume", "Volume")
|
||||
|
|
@ -2479,10 +2534,6 @@ namespace API.Data.Migrations
|
|||
|
||||
modelBuilder.Entity("API.Entities.Series", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", null)
|
||||
.WithMany("WantToRead")
|
||||
.HasForeignKey("AppUserId");
|
||||
|
||||
b.HasOne("API.Entities.Library", "Library")
|
||||
.WithMany("Series")
|
||||
.HasForeignKey("LibraryId")
|
||||
|
|
|
|||
|
|
@ -363,8 +363,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Where(l => EF.Functions.Like(l.Name, $"%{searchQuery}%"))
|
||||
.IsRestricted(QueryContext.Search)
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(l => l.Name.ToLower())
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
|
|
@ -383,8 +383,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Include(s => s.Library)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(s => s.SortName!.ToLower())
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<SearchResultDto>(_mapper.ConfigurationProvider)
|
||||
.AsEnumerable();
|
||||
|
||||
|
|
@ -420,8 +420,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%"))
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(r => r.NormalizedTitle)
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
|
|
@ -431,7 +431,6 @@ public class SeriesRepository : ISeriesRepository
|
|||
.Where(c => c.Promoted || isAdmin)
|
||||
.RestrictAgainstAgeRestriction(userRating)
|
||||
.OrderBy(s => s.NormalizedTitle)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(c => c.NormalizedTitle)
|
||||
|
|
@ -443,8 +442,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
.SelectMany(sm => sm.People.Where(t => t.Name != null && EF.Functions.Like(t.Name, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(p => p.NormalizedName)
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
|
|
@ -453,8 +452,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
.SelectMany(sm => sm.Genres.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(t => t.NormalizedTitle)
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
|
|
@ -463,8 +462,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
.SelectMany(sm => sm.Tags.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
|
||||
.AsSplitQuery()
|
||||
.Distinct()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(t => t.NormalizedTitle)
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
|
|
@ -482,8 +481,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
result.Files = await _context.MangaFile
|
||||
.Where(m => EF.Functions.Like(m.FilePath, $"%{searchQuery}%") && fileIds.Contains(m.Id))
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(f => f.FilePath)
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<MangaFileDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
|
@ -499,8 +498,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
)
|
||||
.Where(c => c.Files.All(f => fileIds.Contains(f.Id)))
|
||||
.AsSplitQuery()
|
||||
.Take(maxRecords)
|
||||
.OrderBy(c => c.TitleName)
|
||||
.Take(maxRecords)
|
||||
.ProjectTo<ChapterDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
|
|
@ -991,6 +990,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
SortField.TimeToRead => query.DoOrderBy(s => s.AvgHoursToRead, filter.SortOptions),
|
||||
SortField.ReleaseYear => query.DoOrderBy(s => s.Metadata.ReleaseYear, filter.SortOptions),
|
||||
SortField.ReadProgress => query.DoOrderBy(s => s.Progress.Where(p => p.SeriesId == s.Id).Select(p => p.LastModified).Max(), filter.SortOptions),
|
||||
SortField.AverageRating => query.DoOrderBy(s => s.ExternalSeriesMetadata.ExternalRatings
|
||||
.Where(p => p.SeriesId == s.Id).Average(p => p.AverageScore), filter.SortOptions),
|
||||
_ => query
|
||||
};
|
||||
|
||||
|
|
@ -1043,7 +1044,9 @@ public class SeriesRepository : ISeriesRepository
|
|||
var wantToReadStmt = filter.Statements.FirstOrDefault(stmt => stmt.Field == FilterField.WantToRead);
|
||||
if (wantToReadStmt == null) return query;
|
||||
|
||||
var seriesIds = _context.AppUser.Where(u => u.Id == userId).SelectMany(u => u.WantToRead).Select(s => s.Id);
|
||||
var seriesIds = _context.AppUser.Where(u => u.Id == userId)
|
||||
.SelectMany(u => u.WantToRead)
|
||||
.Select(s => s.SeriesId);
|
||||
if (bool.Parse(wantToReadStmt.Value))
|
||||
{
|
||||
query = query.Where(s => seriesIds.Contains(s.Id));
|
||||
|
|
@ -1869,7 +1872,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
var query = _context.AppUser
|
||||
.Where(user => user.Id == userId)
|
||||
.SelectMany(u => u.WantToRead)
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Where(s => libraryIds.Contains(s.Series.LibraryId))
|
||||
.Select(w => w.Series)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking();
|
||||
|
||||
|
|
@ -1884,7 +1888,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
var query = _context.AppUser
|
||||
.Where(user => user.Id == userId)
|
||||
.SelectMany(u => u.WantToRead)
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Where(s => libraryIds.Contains(s.Series.LibraryId))
|
||||
.Select(w => w.Series)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking();
|
||||
|
||||
|
|
@ -1899,7 +1904,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
return await _context.AppUser
|
||||
.Where(user => user.Id == userId)
|
||||
.SelectMany(u => u.WantToRead)
|
||||
.Where(s => libraryIds.Contains(s.LibraryId))
|
||||
.Where(s => libraryIds.Contains(s.Series.LibraryId))
|
||||
.Select(w => w.Series)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
|
@ -1994,7 +2000,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync();
|
||||
return await _context.AppUser
|
||||
.Where(user => user.Id == userId)
|
||||
.SelectMany(u => u.WantToRead.Where(s => s.Id == seriesId && libraryIds.Contains(s.LibraryId)))
|
||||
.SelectMany(u => u.WantToRead.Where(s => s.SeriesId == seriesId && libraryIds.Contains(s.Series.LibraryId)))
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.AnyAsync();
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public class AppUser : IdentityUser<int>, IHasConcurrencyToken
|
|||
/// <summary>
|
||||
/// A list of Series the user want's to read
|
||||
/// </summary>
|
||||
public ICollection<Series> WantToRead { get; set; } = null!;
|
||||
public ICollection<AppUserWantToRead> WantToRead { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// A list of Devices which allows the user to send files to
|
||||
/// </summary>
|
||||
|
|
|
|||
20
API/Entities/AppUserWantToRead.cs
Normal file
20
API/Entities/AppUserWantToRead.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
namespace API.Entities;
|
||||
|
||||
public class AppUserWantToRead
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public required int SeriesId { get; set; }
|
||||
public virtual Series Series { get; set; }
|
||||
|
||||
|
||||
// Relationships
|
||||
/// <summary>
|
||||
/// Navigational Property for EF. Links to a unique AppUser
|
||||
/// </summary>
|
||||
public AppUser AppUser { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// User this table of content belongs to
|
||||
/// </summary>
|
||||
public int AppUserId { get; set; }
|
||||
}
|
||||
14
API/Entities/ManualMigrationHistory.cs
Normal file
14
API/Entities/ManualMigrationHistory.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using System;
|
||||
|
||||
namespace API.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// This will track manual migrations so that I can use simple selects to check if a Manual Migration is needed
|
||||
/// </summary>
|
||||
public class ManualMigrationHistory
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string ProductVersion { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public DateTime RanAt { get; set; }
|
||||
}
|
||||
|
|
@ -37,6 +37,8 @@ public static class BookmarkSort
|
|||
SortField.TimeToRead => query.DoOrderBy(s => s.Series.AvgHoursToRead, sortOptions),
|
||||
SortField.ReleaseYear => query.DoOrderBy(s => s.Series.Metadata.ReleaseYear, sortOptions),
|
||||
SortField.ReadProgress => query.DoOrderBy(s => s.Series.Progress.Where(p => p.SeriesId == s.Series.Id).Select(p => p.LastModified).Max(), sortOptions),
|
||||
SortField.AverageRating => query.DoOrderBy(s => s.Series.ExternalSeriesMetadata.ExternalRatings
|
||||
.Where(p => p.SeriesId == s.Series.Id).Average(p => p.AverageScore), sortOptions),
|
||||
_ => query
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ public static class SeriesSort
|
|||
SortField.ReadProgress => query.DoOrderBy(s => s.Progress.Where(p => p.SeriesId == s.Id && p.AppUserId == userId)
|
||||
.Select(p => p.LastModified)
|
||||
.Max(), sortOptions),
|
||||
SortField.AverageRating => query.DoOrderBy(s => s.ExternalSeriesMetadata.ExternalRatings
|
||||
.Where(p => p.SeriesId == s.Id).Average(p => p.AverageScore), sortOptions),
|
||||
_ => query
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -73,8 +73,10 @@ public static class LogLevelOptions
|
|||
|
||||
if (isRequestLoggingMiddleware)
|
||||
{
|
||||
if (e.Properties.ContainsKey("Path") && e.Properties["Path"].ToString().Replace("\"", string.Empty) == "/api/health") return false;
|
||||
if (e.Properties.ContainsKey("Path") && e.Properties["Path"].ToString().Replace("\"", string.Empty) == "/hubs/messages") return false;
|
||||
var path = e.Properties["Path"].ToString().Replace("\"", string.Empty);
|
||||
if (e.Properties.ContainsKey("Path") && path == "/api/health") return false;
|
||||
if (e.Properties.ContainsKey("Path") && path == "/hubs/messages") return false;
|
||||
if (e.Properties.ContainsKey("Path") && path.StartsWith("/api/image")) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -87,6 +87,30 @@ public class Program
|
|||
}
|
||||
}
|
||||
|
||||
// Apply Before manual migrations that need to run before actual migrations
|
||||
try
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// Apply all migrations on startup
|
||||
var dataContext = services.GetRequiredService<DataContext>();
|
||||
var directoryService = services.GetRequiredService<IDirectoryService>();
|
||||
|
||||
logger.LogInformation("Running Migrations");
|
||||
|
||||
// v0.7.14
|
||||
await MigrateWantToReadExport.Migrate(dataContext, directoryService, logger);
|
||||
|
||||
await unitOfWork.CommitAsync();
|
||||
logger.LogInformation("Running Migrations - complete");
|
||||
}).GetAwaiter()
|
||||
.GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogCritical(ex, "An error occurred during migration");
|
||||
}
|
||||
|
||||
await context.Database.MigrateAsync();
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -288,8 +288,8 @@ public class CleanupService : ICleanupService
|
|||
var seriesIds = series.Select(s => s.Id).ToList();
|
||||
if (seriesIds.Count == 0) continue;
|
||||
|
||||
user.WantToRead ??= new List<Series>();
|
||||
user.WantToRead = user.WantToRead.Where(s => !seriesIds.Contains(s.Id)).ToList();
|
||||
user.WantToRead ??= new List<AppUserWantToRead>();
|
||||
user.WantToRead = user.WantToRead.Where(s => !seriesIds.Contains(s.SeriesId)).ToList();
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -248,6 +248,8 @@ public class Startup
|
|||
// v0.7.14
|
||||
await MigrateEmailTemplates.Migrate(directoryService, logger);
|
||||
await MigrateVolumeNumber.Migrate(unitOfWork, dataContext, logger);
|
||||
await MigrateWantToReadImport.Migrate(unitOfWork, directoryService, logger);
|
||||
await MigrateManualHistory.Migrate(dataContext, logger);
|
||||
|
||||
// Update the version in the DB after all migrations are run
|
||||
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue