Sort by Average Rating and Big Want to Read fix (#2672)

This commit is contained in:
Joe Milazzo 2024-02-01 06:23:45 -06:00 committed by GitHub
parent 03e7d38482
commit 1fd72ada36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 3552 additions and 105 deletions

View file

@ -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>

View file

@ -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;

View file

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

View file

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

View file

@ -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
}

View file

@ -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)

View 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");
}
}

View 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");
}
}

View 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");
}
}

File diff suppressed because it is too large Load diff

View 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");
}
}
}

View file

@ -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")

View file

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

View file

@ -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>

View 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; }
}

View 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; }
}

View file

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

View file

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

View file

@ -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;

View file

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

View file

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

View file

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