Kavita+ Overhaul & New Changelog (#3507)

This commit is contained in:
Joe Milazzo 2025-01-20 08:14:57 -06:00 committed by GitHub
parent d880c1690c
commit a5707617f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
249 changed files with 14775 additions and 2300 deletions

View file

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Enums.UserPreferences;
using API.Entities.History;
using API.Entities.Interfaces;
using API.Entities.Metadata;
using API.Entities.Scrobble;
@ -68,6 +69,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
public DbSet<AppUserCollection> AppUserCollection { get; set; } = null!;
public DbSet<ChapterPeople> ChapterPeople { get; set; } = null!;
public DbSet<SeriesMetadataPeople> SeriesMetadataPeople { get; set; } = null!;
public DbSet<EmailHistory> EmailHistory { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder builder)

View file

@ -0,0 +1,56 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Entities.Metadata;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.5 - Migrating Kavita+ BlacklistedSeries table to Series entity to streamline implementation and generate a "Needs Manual Match" entry for the Series
/// </summary>
public static class ManualMigrateBlacklistTableToSeries
{
public static async Task Migrate(DataContext context, ILogger<Program> logger)
{
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateBlacklistTableToSeries"))
{
return;
}
logger.LogCritical("Running ManualMigrateBlacklistTableToSeries migration - Please be patient, this may take some time. This is not an error");
// Get all series in the Blacklist table and set their IsBlacklist = true
var blacklistedSeries = await context.SeriesBlacklist
.Include(s => s.Series.ExternalSeriesMetadata)
.Select(s => s.Series)
.ToListAsync();
foreach (var series in blacklistedSeries)
{
series.IsBlacklisted = true;
series.ExternalSeriesMetadata ??= new ExternalSeriesMetadata() { SeriesId = series.Id };
context.Series.Entry(series).State = EntityState.Modified;
}
// Remove everything in SeriesBlacklist (it will be removed in another migration)
context.SeriesBlacklist.RemoveRange(context.SeriesBlacklist);
if (context.ChangeTracker.HasChanges())
{
await context.SaveChangesAsync();
}
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
{
Name = "ManualMigrateBlacklistTableToSeries",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await context.SaveChangesAsync();
logger.LogCritical("Running ManualMigrateBlacklistTableToSeries migration - Completed. This is not an error");
}
}

View file

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.Enums;
using API.Entities.History;
using Flurl.Util;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;

View file

@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Extensions;
using API.Helpers.Builders;
using API.Services;

View file

@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Extensions;
using API.Helpers.Builders;
using API.Services;

View file

@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

View file

@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

View file

@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

View file

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.Enums;
using API.Entities.History;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

View file

@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;

View file

@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;

View file

@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Extensions;
using API.Helpers.Builders;
using API.Services.Tasks.Scanner.Parser;

View file

@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

View file

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using API.Data.Repositories;
using API.Entities;
using API.Entities.Enums;
using API.Entities.History;
using API.Extensions.QueryExtensions;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;

View file

@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;

View file

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.Enums;
using API.Entities.History;
using API.Services;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;

View file

@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;

View file

@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;

View file

@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;

View file

@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services;
using CsvHelper;
using CsvHelper.Configuration.Attributes;

View file

@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using API.Services;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;

View file

@ -4,6 +4,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using API.DTOs.Filtering.v2;
using API.Entities;
using API.Entities.History;
using API.Helpers;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;

View file

@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Entities.History;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class SeriesDontMatchAndBlacklist : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "DontMatch",
table: "Series",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "IsBlacklisted",
table: "Series",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DontMatch",
table: "Series");
migrationBuilder.DropColumn(
name: "IsBlacklisted",
table: "Series");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class EmailHistory : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "EmailHistory",
columns: table => new
{
Id = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Sent = table.Column<bool>(type: "INTEGER", nullable: false),
SendDate = table.Column<DateTime>(type: "TEXT", nullable: false),
EmailTemplate = table.Column<string>(type: "TEXT", nullable: true),
Subject = table.Column<string>(type: "TEXT", nullable: true),
Body = table.Column<string>(type: "TEXT", nullable: true),
DeliveryStatus = table.Column<string>(type: "TEXT", nullable: true),
ErrorMessage = table.Column<string>(type: "TEXT", nullable: true),
AppUserId = table.Column<int>(type: "INTEGER", nullable: false),
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
CreatedUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
LastModified = table.Column<DateTime>(type: "TEXT", nullable: false),
LastModifiedUtc = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EmailHistory", x => x.Id);
table.ForeignKey(
name: "FK_EmailHistory_AspNetUsers_AppUserId",
column: x => x.AppUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_EmailHistory_AppUserId",
table: "EmailHistory",
column: "AppUserId");
migrationBuilder.CreateIndex(
name: "IX_EmailHistory_Sent_AppUserId_EmailTemplate_SendDate",
table: "EmailHistory",
columns: new[] { "Sent", "AppUserId", "EmailTemplate", "SendDate" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EmailHistory");
}
}
}

View file

@ -15,7 +15,7 @@ namespace API.Data.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
modelBuilder.Entity("API.Entities.AppRole", b =>
{
@ -1000,6 +1000,57 @@ namespace API.Data.Migrations
b.ToTable("Device");
});
modelBuilder.Entity("API.Entities.EmailHistory", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AppUserId")
.HasColumnType("INTEGER");
b.Property<string>("Body")
.HasColumnType("TEXT");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");
b.Property<string>("DeliveryStatus")
.HasColumnType("TEXT");
b.Property<string>("EmailTemplate")
.HasColumnType("TEXT");
b.Property<string>("ErrorMessage")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<DateTime>("SendDate")
.HasColumnType("TEXT");
b.Property<bool>("Sent")
.HasColumnType("INTEGER");
b.Property<string>("Subject")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AppUserId");
b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate");
b.ToTable("EmailHistory");
});
modelBuilder.Entity("API.Entities.FolderPath", b =>
{
b.Property<int>("Id")
@ -1866,12 +1917,18 @@ namespace API.Data.Migrations
b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT");
b.Property<bool>("DontMatch")
.HasColumnType("INTEGER");
b.Property<string>("FolderPath")
.HasColumnType("TEXT");
b.Property<int>("Format")
.HasColumnType("INTEGER");
b.Property<bool>("IsBlacklisted")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastChapterAdded")
.HasColumnType("TEXT");
@ -2660,6 +2717,17 @@ namespace API.Data.Migrations
b.Navigation("AppUser");
});
modelBuilder.Entity("API.Entities.EmailHistory", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
.WithMany()
.HasForeignKey("AppUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AppUser");
});
modelBuilder.Entity("API.Entities.FolderPath", b =>
{
b.HasOne("API.Entities.Library", "Library")

View file

@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.DTOs.Email;
using API.Entities;
using API.Helpers;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
public interface IEmailHistoryRepository
{
Task<IList<EmailHistoryDto>> GetEmailDtos(UserParams userParams);
}
public class EmailHistoryRepository : IEmailHistoryRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public EmailHistoryRepository(DataContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<IList<EmailHistoryDto>> GetEmailDtos(UserParams userParams)
{
return await _context.EmailHistory
.OrderByDescending(h => h.SendDate)
.ProjectTo<EmailHistoryDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
}

View file

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using API.Constants;
using API.DTOs;
using API.DTOs.KavitaPlus.Manage;
using API.DTOs.Recommendation;
using API.DTOs.Scrobbling;
using API.DTOs.SeriesDetail;
@ -31,14 +32,12 @@ public interface IExternalSeriesMetadataRepository
void Remove(IEnumerable<ExternalRecommendation>? recommendations);
void Remove(ExternalSeriesMetadata metadata);
Task<ExternalSeriesMetadata?> GetExternalSeriesMetadata(int seriesId);
Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId);
Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId);
Task<bool> NeedsDataRefresh(int seriesId);
Task<SeriesDetailPlusDto?> GetSeriesDetailPlusDto(int seriesId);
Task LinkRecommendationsToSeries(Series series);
Task LinkRecommendationsToSeries(int seriesId);
Task<bool> IsBlacklistedSeries(int seriesId);
Task CreateBlacklistedSeries(int seriesId, bool saveChanges = true);
Task RemoveFromBlacklist(int seriesId);
Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit);
Task<IList<ManageMatchSeriesDto>> GetAllSeries(ManageMatchFilterDto filter);
}
public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepository
@ -107,7 +106,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
.FirstOrDefaultAsync();
}
public async Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId)
public async Task<bool> NeedsDataRefresh(int seriesId)
{
var row = await _context.ExternalSeriesMetadata
.Where(s => s.SeriesId == seriesId)
@ -115,7 +114,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
return row == null || row.ValidUntilUtc <= DateTime.UtcNow;
}
public async Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId)
public async Task<SeriesDetailPlusDto?> GetSeriesDetailPlusDto(int seriesId)
{
var seriesDetailDto = await _context.ExternalSeriesMetadata
.Where(m => m.SeriesId == seriesId)
@ -180,13 +179,6 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
return seriesDetailPlusDto;
}
public async Task LinkRecommendationsToSeries(int seriesId)
{
var series = await _context.Series.Where(s => s.Id == seriesId).AsNoTracking().SingleOrDefaultAsync();
if (series == null) return;
await LinkRecommendationsToSeries(series);
}
/// <summary>
/// Searches Recommendations without a SeriesId on record and attempts to link based on Series Name/Localized Name
/// </summary>
@ -210,45 +202,12 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
public Task<bool> IsBlacklistedSeries(int seriesId)
{
return _context.SeriesBlacklist.AnyAsync(s => s.SeriesId == seriesId);
return _context.Series
.Where(s => s.Id == seriesId)
.Select(s => s.IsBlacklisted)
.FirstOrDefaultAsync();
}
/// <summary>
/// Creates a new instance against SeriesId and Saves to the DB
/// </summary>
/// <param name="seriesId"></param>
/// <param name="saveChanges"></param>
public async Task CreateBlacklistedSeries(int seriesId, bool saveChanges = true)
{
if (seriesId <= 0 || await _context.SeriesBlacklist.AnyAsync(s => s.SeriesId == seriesId)) return;
await _context.SeriesBlacklist.AddAsync(new SeriesBlacklist()
{
SeriesId = seriesId
});
if (saveChanges)
{
await _context.SaveChangesAsync();
}
}
/// <summary>
/// Removes the Series from Blacklist and Saves to the DB
/// </summary>
/// <param name="seriesId"></param>
public async Task RemoveFromBlacklist(int seriesId)
{
var seriesBlacklist = await _context.SeriesBlacklist.FirstOrDefaultAsync(sb => sb.SeriesId == seriesId);
if (seriesBlacklist != null)
{
// Remove the SeriesBlacklist entity from the context
_context.SeriesBlacklist.Remove(seriesBlacklist);
// Save the changes to the database
await _context.SaveChangesAsync();
}
}
public async Task<IList<int>> GetAllSeriesIdsWithoutMetadata(int limit)
{
@ -261,4 +220,14 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
.Take(limit)
.ToListAsync();
}
public async Task<IList<ManageMatchSeriesDto>> GetAllSeries(ManageMatchFilterDto filter)
{
return await _context.Series
.Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type))
.FilterMatchState(filter.MatchStateOption)
.OrderBy(s => s.NormalizedName)
.ProjectTo<ManageMatchSeriesDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
}

View file

@ -29,6 +29,7 @@ public interface IScrobbleRepository
Task<ScrobbleEvent?> GetEvent(int userId, int seriesId, ScrobbleEventType eventType);
Task<IEnumerable<ScrobbleEvent>> GetUserEventsForSeries(int userId, int seriesId);
Task<PagedList<ScrobbleEventDto>> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination);
Task<IList<ScrobbleEvent>> GetAllEventsForSeries(int seriesId);
}
/// <summary>
@ -153,4 +154,10 @@ public class ScrobbleRepository : IScrobbleRepository
return await PagedList<ScrobbleEventDto>.CreateAsync(query, pagination.PageNumber, pagination.PageSize);
}
public async Task<IList<ScrobbleEvent>> GetAllEventsForSeries(int seriesId)
{
return await _context.ScrobbleEvent.Where(e => e.SeriesId == seriesId)
.ToListAsync();
}
}

View file

@ -15,6 +15,7 @@ using API.DTOs.Filtering;
using API.DTOs.Filtering.v2;
using API.DTOs.Metadata;
using API.DTOs.ReadingLists;
using API.DTOs.Recommendation;
using API.DTOs.Scrobbling;
using API.DTOs.Search;
using API.DTOs.SeriesDetail;
@ -165,6 +166,7 @@ public interface ISeriesRepository
Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None);
Task<PlusSeriesDto?> GetPlusSeriesDto(int seriesId);
Task<int> GetCountAsync();
Task<Series?> MatchSeries(ExternalSeriesDetailDto externalSeries);
}
public class SeriesRepository : ISeriesRepository
@ -709,7 +711,7 @@ public class SeriesRepository : ISeriesRepository
.Where(s => s.Id == seriesId)
.Select(series => new PlusSeriesDto()
{
MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type),
MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format),
SeriesName = series.Name,
AltSeriesName = series.LocalizedName,
AniListId = ScrobblingService.ExtractId<int?>(series.Metadata.WebLinks,
@ -2037,9 +2039,6 @@ public class SeriesRepository : ISeriesRepository
/// Uses multiple names to find a match against a series. If not, returns null.
/// </summary>
/// <remarks>This does not restrict to the user at all. That is handled at the API level.</remarks>
/// <param name="userId"></param>
/// <param name="names"></param>
/// <returns></returns>
public async Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIds(IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl)
{
var libraryIds = await _context.Library
@ -2073,6 +2072,47 @@ public class SeriesRepository : ISeriesRepository
.FirstOrDefaultAsync(); // Some users may have improperly configured libraries
}
public async Task<Series?> MatchSeries(ExternalSeriesDetailDto externalSeries)
{
var libraryIds = await _context.Library
.Where(lib => externalSeries.PlusMediaFormat.ConvertToLibraryTypes().Contains(lib.Type))
.Select(l => l.Id)
.ToListAsync();
var normalizedNames = (externalSeries.Synonyms ?? Enumerable.Empty<string>())
.Prepend(externalSeries.Name)
.Select(n => n.ToNormalized())
.ToList();
var aniListWebLink =
ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, externalSeries.AniListId);
var malWebLink =
ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, externalSeries.MALId);
Series? result = null;
if (!string.IsNullOrEmpty(aniListWebLink) || !string.IsNullOrEmpty(malWebLink))
{
result = await _context.Series
.Where(s => !string.IsNullOrEmpty(s.Metadata.WebLinks))
.Where(s => libraryIds.Contains(s.Library.Id))
.WhereIf(!string.IsNullOrEmpty(aniListWebLink), s => s.Metadata.WebLinks.Contains(aniListWebLink))
.WhereIf(!string.IsNullOrEmpty(malWebLink), s => s.Metadata.WebLinks.Contains(malWebLink))
.Include(s => s.Metadata)
.AsSplitQuery()
.FirstOrDefaultAsync();
}
if (result != null) return result;
return await _context.Series
.Where(s => normalizedNames.Contains(s.NormalizedName) ||
normalizedNames.Contains(s.NormalizedLocalizedName))
.Where(s => libraryIds.Contains(s.Library.Id))
.AsSplitQuery()
.Include(s => s.Metadata)
.FirstOrDefaultAsync(); // Some users may have improperly configured libraries
}
/// <summary>
/// Returns the Average rating for all users within Kavita instance
/// </summary>

View file

@ -7,6 +7,7 @@ using API.DTOs;
using API.DTOs.Account;
using API.DTOs.Dashboard;
using API.DTOs.Filtering.v2;
using API.DTOs.KavitaPlus.Account;
using API.DTOs.Reader;
using API.DTOs.Scrobbling;
using API.DTOs.SeriesDetail;
@ -15,6 +16,7 @@ using API.Entities;
using API.Extensions;
using API.Extensions.QueryExtensions;
using API.Extensions.QueryExtensions.Filtering;
using API.Helpers;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Identity;
@ -96,6 +98,8 @@ public interface IUserRepository
Task<IList<AppUserSideNavStream>> GetSideNavStreamsByLibraryId(int libraryId);
Task<IList<AppUserSideNavStream>> GetSideNavStreamWithExternalSource(int externalSourceId);
Task<IList<AppUserSideNavStream>> GetDashboardStreamsByIds(IList<int> streamIds);
Task<IEnumerable<UserTokenInfo>> GetUserTokenInfo();
Task<AppUser?> GetUserByDeviceEmail(string deviceEmail);
}
public class UserRepository : IUserRepository
@ -490,6 +494,43 @@ public class UserRepository : IUserRepository
.ToListAsync();
}
public async Task<IEnumerable<UserTokenInfo>> GetUserTokenInfo()
{
var users = await _context.AppUser
.Select(u => new
{
u.Id,
u.UserName,
u.AniListAccessToken, // JWT Token
u.MalAccessToken // JWT Token
})
.ToListAsync();
var userTokenInfos = users.Select(user => new UserTokenInfo
{
UserId = user.Id,
Username = user.UserName,
IsAniListTokenSet = !string.IsNullOrEmpty(user.AniListAccessToken),
AniListValidUntilUtc = JwtHelper.GetTokenExpiry(user.AniListAccessToken),
IsAniListTokenValid = JwtHelper.IsTokenValid(user.AniListAccessToken),
IsMalTokenSet = !string.IsNullOrEmpty(user.MalAccessToken),
});
return userTokenInfos;
}
/// <summary>
/// Returns the first user with a device email matching
/// </summary>
/// <param name="deviceEmail"></param>
/// <returns></returns>
public async Task<AppUser> GetUserByDeviceEmail(string deviceEmail)
{
return await _context.AppUser
.Where(u => u.Devices.Any(d => d.EmailAddress == deviceEmail))
.FirstOrDefaultAsync();
}
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
{

View file

@ -32,6 +32,7 @@ public interface IUnitOfWork
IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; }
IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; }
IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; }
IEmailHistoryRepository EmailHistoryRepository { get; }
bool Commit();
Task<bool> CommitAsync();
bool HasChanges();
@ -72,6 +73,7 @@ public class UnitOfWork : IUnitOfWork
AppUserSmartFilterRepository = new AppUserSmartFilterRepository(_context, _mapper);
AppUserExternalSourceRepository = new AppUserExternalSourceRepository(_context, _mapper);
ExternalSeriesMetadataRepository = new ExternalSeriesMetadataRepository(_context, _mapper);
EmailHistoryRepository = new EmailHistoryRepository(_context, _mapper);
}
/// <summary>
@ -100,6 +102,7 @@ public class UnitOfWork : IUnitOfWork
public IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; }
public IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; }
public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; }
public IEmailHistoryRepository EmailHistoryRepository { get; }
/// <summary>
/// Commits changes to the DB. Completes the open transaction.