.NET 7 + Spring Cleaning (#1677)

* Updated to net7.0

* Updated GA to .net 7

* Updated System.IO.Abstractions to use New factory.

* Converted Regex into SourceGenerator in Parser.

* Updated more regex to source generators.

* Enabled Nullability and more regex changes throughout codebase.

* Parser is 100% GeneratedRegexified

* Lots of nullability code

* Enabled nullability for all repositories.

* Fixed another unit test

* Refactored some code around and took care of some todos.

* Updating code for nullability and cleaning up methods that aren't used anymore. Refctored all uses of Parser.Normalize() to use new extension

* More nullability exercises. 500 warnings to go.

* Fixed a bug where custom file uploads for entities wouldn't save in webP.

* Nullability is done for all DTOs

* Fixed all unit tests and nullability for the project. Only OPDS is left which will be done with an upcoming OPDS enhancement.

* Use localization in book service after validating

* Code smells

* Switched to preview build of swashbuckle for .net7 support

* Fixed up merge issues

* Disable emulate comic book when on single page reader

* Fixed a regression where double page renderer wouldn't layout the images correctly

* Updated to swashbuckle which support .net 7

* Fixed a bad GA action

* Some code cleanup

* More code smells

* Took care of most of nullable issues

* Fixed a broken test due to having more than one test run in parallel

* I'm really not sure why the unit tests are failing or are so extremely slow on .net 7

* Updated all dependencies

* Fixed up build and removed hardcoded framework from build scripts. (this merge removes Regex Source generators). Unit tests are completely busted.

* Unit tests and code cleanup. Needs shakeout now.

* Adjusted Series model since a few fields are not-nullable. Removed dead imports on the project.

* Refactored to use Builder pattern for all unit tests.

* Switched nullability down to warnings. It wasn't possible to switch due to constraint issues in DB Migration.
This commit is contained in:
Joe Milazzo 2023-03-05 14:55:13 -06:00 committed by GitHub
parent 76fe3fd64a
commit 5d1dd7b3f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
283 changed files with 4221 additions and 4593 deletions

View file

@ -23,29 +23,29 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
ChangeTracker.StateChanged += OnEntityStateChanged;
}
public DbSet<Library> Library { get; set; }
public DbSet<Series> Series { get; set; }
public DbSet<Chapter> Chapter { get; set; }
public DbSet<Volume> Volume { get; set; }
public DbSet<AppUser> AppUser { get; set; }
public DbSet<MangaFile> MangaFile { get; set; }
public DbSet<AppUserProgress> AppUserProgresses { get; set; }
public DbSet<AppUserRating> AppUserRating { get; set; }
public DbSet<ServerSetting> ServerSetting { get; set; }
public DbSet<AppUserPreferences> AppUserPreferences { get; set; }
public DbSet<SeriesMetadata> SeriesMetadata { get; set; }
public DbSet<CollectionTag> CollectionTag { get; set; }
public DbSet<AppUserBookmark> AppUserBookmark { get; set; }
public DbSet<ReadingList> ReadingList { get; set; }
public DbSet<ReadingListItem> ReadingListItem { get; set; }
public DbSet<Person> Person { get; set; }
public DbSet<Genre> Genre { get; set; }
public DbSet<Tag> Tag { get; set; }
public DbSet<SiteTheme> SiteTheme { get; set; }
public DbSet<SeriesRelation> SeriesRelation { get; set; }
public DbSet<FolderPath> FolderPath { get; set; }
public DbSet<Device> Device { get; set; }
public DbSet<ServerStatistics> ServerStatistics { get; set; }
public DbSet<Library> Library { get; set; } = null!;
public DbSet<Series> Series { get; set; } = null!;
public DbSet<Chapter> Chapter { get; set; } = null!;
public DbSet<Volume> Volume { get; set; } = null!;
public DbSet<AppUser> AppUser { get; set; } = null!;
public DbSet<MangaFile> MangaFile { get; set; } = null!;
public DbSet<AppUserProgress> AppUserProgresses { get; set; } = null!;
public DbSet<AppUserRating> AppUserRating { get; set; } = null!;
public DbSet<ServerSetting> ServerSetting { get; set; } = null!;
public DbSet<AppUserPreferences> AppUserPreferences { get; set; } = null!;
public DbSet<SeriesMetadata> SeriesMetadata { get; set; } = null!;
public DbSet<CollectionTag> CollectionTag { get; set; } = null!;
public DbSet<AppUserBookmark> AppUserBookmark { get; set; } = null!;
public DbSet<ReadingList> ReadingList { get; set; } = null!;
public DbSet<ReadingListItem> ReadingListItem { get; set; } = null!;
public DbSet<Person> Person { get; set; } = null!;
public DbSet<Genre> Genre { get; set; } = null!;
public DbSet<Tag> Tag { get; set; } = null!;
public DbSet<SiteTheme> SiteTheme { get; set; } = null!;
public DbSet<SeriesRelation> SeriesRelation { get; set; } = null!;
public DbSet<FolderPath> FolderPath { get; set; } = null!;
public DbSet<Device> Device { get; set; } = null!;
public DbSet<ServerStatistics> ServerStatistics { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder builder)
@ -110,7 +110,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
}
private static void OnEntityTracked(object sender, EntityTrackedEventArgs e)
private static void OnEntityTracked(object? sender, EntityTrackedEventArgs e)
{
if (e.FromQuery || e.Entry.State != EntityState.Added || e.Entry.Entity is not IEntityDate entity) return;
@ -120,7 +120,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
entity.LastModifiedUtc = DateTime.UtcNow;
}
private static void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
private static void OnEntityStateChanged(object? sender, EntityStateChangedEventArgs e)
{
if (e.NewState != EntityState.Modified || e.Entry.Entity is not IEntityDate entity) return;
entity.LastModified = DateTime.Now;
@ -142,28 +142,28 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
public override int SaveChanges()
{
this.OnSaveChanges();
OnSaveChanges();
return base.SaveChanges();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
this.OnSaveChanges();
OnSaveChanges();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
this.OnSaveChanges();
OnSaveChanges();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
this.OnSaveChanges();
OnSaveChanges();
return base.SaveChangesAsync(cancellationToken);
}

View file

@ -1,13 +1,12 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using API.Data.Metadata;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Metadata;
using API.Extensions;
using API.Parser;
using API.Services.Tasks;
using Kavita.Common;
namespace API.Data;
@ -16,6 +15,17 @@ namespace API.Data;
/// </summary>
public static class DbFactory
{
public static Library Library(string name, LibraryType type)
{
return new Library()
{
Name = name,
Type = type,
Series = new List<Series>(),
Folders = new List<FolderPath>(),
AppUsers = new List<AppUser>()
};
}
public static Series Series(string name)
{
return new Series
@ -23,8 +33,8 @@ public static class DbFactory
Name = name,
OriginalName = name,
LocalizedName = name,
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
NormalizedName = name.ToNormalized(),
NormalizedLocalizedName = name.ToNormalized(),
SortName = name,
Volumes = new List<Volume>(),
Metadata = SeriesMetadata(new List<CollectionTag>())
@ -42,8 +52,8 @@ public static class DbFactory
Name = name,
OriginalName = name,
LocalizedName = localizedName,
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(localizedName),
NormalizedName = name.ToNormalized(),
NormalizedLocalizedName = localizedName.ToNormalized(),
SortName = name,
Volumes = new List<Volume>(),
Metadata = SeriesMetadata(new List<CollectionTag>())
@ -85,28 +95,31 @@ public static class DbFactory
};
}
public static CollectionTag CollectionTag(int id, string title, string summary, bool promoted)
public static CollectionTag CollectionTag(int id, string title, string? summary = null, bool promoted = false)
{
title = title.Trim();
return new CollectionTag()
{
Id = id,
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(title?.Trim()),
Title = title?.Trim(),
NormalizedTitle = title.ToNormalized(),
Title = title,
Summary = summary?.Trim(),
Promoted = promoted,
SeriesMetadatas = new List<SeriesMetadata>()
};
}
public static ReadingList ReadingList(string title, string summary, bool promoted)
public static ReadingList ReadingList(string title, string? summary = null, bool promoted = false, AgeRating rating = AgeRating.Unknown)
{
title = title.Trim();
return new ReadingList()
{
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(title?.Trim()),
Title = title?.Trim(),
NormalizedTitle = title.ToNormalized(),
Title = title,
Summary = summary?.Trim(),
Promoted = promoted,
Items = new List<ReadingListItem>()
Items = new List<ReadingListItem>(),
AgeRating = rating
};
}
@ -126,7 +139,7 @@ public static class DbFactory
return new Genre()
{
Title = name.Trim().SentenceCase(),
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
NormalizedTitle = name.ToNormalized()
};
}
@ -135,7 +148,7 @@ public static class DbFactory
return new Tag()
{
Title = name.Trim().SentenceCase(),
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
NormalizedTitle = name.ToNormalized()
};
}
@ -144,7 +157,7 @@ public static class DbFactory
return new Person()
{
Name = name.Trim(),
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
NormalizedName = name.ToNormalized(),
Role = role
};
}
@ -169,4 +182,17 @@ public static class DbFactory
};
}
public static AppUser AppUser(string username, string email, SiteTheme defaultTheme)
{
return new AppUser()
{
UserName = username,
Email = email,
ApiKey = HashUtil.ApiKey(),
UserPreferences = new AppUserPreferences
{
Theme = defaultTheme
}
};
}
}

View file

@ -119,7 +119,7 @@ public class ComicInfo
.SingleOrDefault(t => t.ToDescription().ToUpperInvariant().Equals(value.ToUpperInvariant()), Entities.Enums.AgeRating.Unknown);
}
public static void CleanComicInfo(ComicInfo info)
public static void CleanComicInfo(ComicInfo? info)
{
if (info == null) return;

View file

@ -1,7 +1,5 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

View file

@ -1,10 +1,8 @@
using System;
using System.Threading.Tasks;
using API.Constants;
using API.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SQLitePCL;
namespace API.Data;

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using API.Entities.Enums;
using CsvHelper;
@ -15,9 +14,9 @@ namespace API.Data;
internal sealed class SeriesRelationMigrationOutput
{
public string SeriesName { get; set; }
public required string SeriesName { get; set; }
public int SeriesId { get; set; }
public string TargetSeriesName { get; set; }
public required string TargetSeriesName { get; set; }
public int TargetId { get; set; }
public RelationKind Relationship { get; set; }
}

View file

@ -2,9 +2,7 @@
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using API.Entities.Enums;
using API.Entities.Metadata;
using CsvHelper;
using Microsoft.EntityFrameworkCore;

View file

@ -1,13 +1,4 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using API.Entities.Enums;
using API.Entities.Metadata;
using CsvHelper;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace API.Data;

View file

@ -9,13 +9,13 @@ public class RecentlyAddedSeries
public LibraryType LibraryType { get; init; }
public DateTime Created { get; init; }
public int SeriesId { get; init; }
public string SeriesName { get; init; }
public string? SeriesName { get; init; }
public MangaFormat Format { get; init; }
public int ChapterId { get; init; }
public int VolumeId { get; init; }
public string ChapterNumber { get; init; }
public string ChapterRange { get; init; }
public string ChapterTitle { get; init; }
public string? ChapterNumber { get; init; }
public string? ChapterRange { get; init; }
public string? ChapterTitle { get; init; }
public bool IsSpecial { get; init; }
public int VolumeNumber { get; init; }
public AgeRating AgeRating { get; init; }

View file

@ -15,13 +15,13 @@ public interface IAppUserProgressRepository
void Update(AppUserProgress userProgress);
Task<int> CleanupAbandonedChapters();
Task<bool> UserHasProgress(LibraryType libraryType, int userId);
Task<AppUserProgress> GetUserProgressAsync(int chapterId, int userId);
Task<AppUserProgress?> GetUserProgressAsync(int chapterId, int userId);
Task<bool> HasAnyProgressOnSeriesAsync(int seriesId, int userId);
/// <summary>
/// This is built exclusively for <see cref="MigrateUserProgressLibraryId"/>
/// </summary>
/// <returns></returns>
Task<AppUserProgress> GetAnyProgress();
Task<AppUserProgress?> GetAnyProgress();
Task<IEnumerable<AppUserProgress>> GetUserProgressForSeriesAsync(int seriesId, int userId);
Task<IEnumerable<AppUserProgress>> GetAllProgress();
Task<ProgressDto> GetUserProgressDtoAsync(int chapterId, int userId);
@ -97,7 +97,7 @@ public class AppUserProgressRepository : IAppUserProgressRepository
.AnyAsync(aup => aup.PagesRead > 0 && aup.AppUserId == userId && aup.SeriesId == seriesId);
}
public async Task<AppUserProgress> GetAnyProgress()
public async Task<AppUserProgress?> GetAnyProgress()
{
return await _context.AppUserProgresses.FirstOrDefaultAsync();
}
@ -119,7 +119,7 @@ public class AppUserProgressRepository : IAppUserProgressRepository
{
return await _context.AppUserProgresses.ToListAsync();
}
public async Task<ProgressDto> GetUserProgressDtoAsync(int chapterId, int userId)
{
return await _context.AppUserProgresses
@ -128,7 +128,7 @@ public class AppUserProgressRepository : IAppUserProgressRepository
.FirstOrDefaultAsync();
}
public async Task<AppUserProgress> GetUserProgressAsync(int chapterId, int userId)
public async Task<AppUserProgress?> GetUserProgressAsync(int chapterId, int userId)
{
return await _context.AppUserProgresses
.Where(p => p.ChapterId == chapterId && p.AppUserId == userId)

View file

@ -25,15 +25,15 @@ public interface IChapterRepository
{
void Update(Chapter chapter);
Task<IEnumerable<Chapter>> GetChaptersByIdsAsync(IList<int> chapterIds, ChapterIncludes includes = ChapterIncludes.None);
Task<IChapterInfoDto> GetChapterInfoDtoAsync(int chapterId);
Task<IChapterInfoDto?> GetChapterInfoDtoAsync(int chapterId);
Task<int> GetChapterTotalPagesAsync(int chapterId);
Task<Chapter> GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files);
Task<ChapterDto> GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files);
Task<ChapterMetadataDto> GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files);
Task<Chapter?> GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files);
Task<ChapterDto?> GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files);
Task<ChapterMetadataDto?> GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files);
Task<IList<MangaFile>> GetFilesForChapterAsync(int chapterId);
Task<IList<Chapter>> GetChaptersAsync(int volumeId);
Task<IList<MangaFile>> GetFilesForChaptersAsync(IReadOnlyList<int> chapterIds);
Task<string> GetChapterCoverImageAsync(int chapterId);
Task<string?> GetChapterCoverImageAsync(int chapterId);
Task<IList<string>> GetAllCoverImagesAsync();
Task<IList<Chapter>> GetAllChaptersWithNonWebPCovers();
Task<IEnumerable<string>> GetCoverImagesForLockedChaptersAsync();
@ -68,7 +68,7 @@ public class ChapterRepository : IChapterRepository
/// Populates a partial IChapterInfoDto
/// </summary>
/// <returns></returns>
public async Task<IChapterInfoDto> GetChapterInfoDtoAsync(int chapterId)
public async Task<IChapterInfoDto?> GetChapterInfoDtoAsync(int chapterId)
{
var chapterInfo = await _context.Chapter
.Where(c => c.Id == chapterId)
@ -124,7 +124,7 @@ public class ChapterRepository : IChapterRepository
.Select(c => c.Pages)
.FirstOrDefaultAsync();
}
public async Task<ChapterDto> GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files)
public async Task<ChapterDto?> GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files)
{
var chapter = await _context.Chapter
.Includes(includes)
@ -136,7 +136,7 @@ public class ChapterRepository : IChapterRepository
return chapter;
}
public async Task<ChapterMetadataDto> GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files)
public async Task<ChapterMetadataDto?> GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files)
{
var chapter = await _context.Chapter
.Includes(includes)
@ -167,7 +167,7 @@ public class ChapterRepository : IChapterRepository
/// <param name="chapterId"></param>
/// <param name="includes"></param>
/// <returns></returns>
public async Task<Chapter> GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files)
public async Task<Chapter?> GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files)
{
return await _context.Chapter
.Includes(includes)
@ -191,23 +191,20 @@ public class ChapterRepository : IChapterRepository
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
public async Task<string> GetChapterCoverImageAsync(int chapterId)
public async Task<string?> GetChapterCoverImageAsync(int chapterId)
{
return await _context.Chapter
.Where(c => c.Id == chapterId)
.Select(c => c.CoverImage)
.AsNoTracking()
.SingleOrDefaultAsync();
}
public async Task<IList<string>> GetAllCoverImagesAsync()
{
return await _context.Chapter
return (await _context.Chapter
.Select(c => c.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.AsNoTracking()
.ToListAsync();
.ToListAsync())!;
}
public async Task<IList<Chapter>> GetAllChaptersWithNonWebPCovers()
@ -223,12 +220,11 @@ public class ChapterRepository : IChapterRepository
/// <returns></returns>
public async Task<IEnumerable<string>> GetCoverImagesForLockedChaptersAsync()
{
return await _context.Chapter
return (await _context.Chapter
.Where(c => c.CoverImageLocked)
.Select(c => c.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.AsNoTracking()
.ToListAsync();
.ToListAsync())!;
}
/// <summary>

View file

@ -25,10 +25,9 @@ public interface ICollectionTagRepository
void Remove(CollectionTag tag);
Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync();
Task<IEnumerable<CollectionTagDto>> SearchTagDtosAsync(string searchQuery, int userId);
Task<string> GetCoverImageAsync(int collectionTagId);
Task<string?> GetCoverImageAsync(int collectionTagId);
Task<IEnumerable<CollectionTagDto>> GetAllPromotedTagDtosAsync(int userId);
Task<CollectionTag> GetTagAsync(int tagId);
Task<CollectionTag> GetFullTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.SeriesMetadata);
Task<CollectionTag?> GetTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.None);
void Update(CollectionTag tag);
Task<int> RemoveTagsWithoutSeries();
Task<IEnumerable<CollectionTag>> GetAllTagsAsync(CollectionTagIncludes includes = CollectionTagIncludes.None);
@ -84,29 +83,27 @@ public class CollectionTagRepository : ICollectionTagRepository
.ToListAsync();
}
public async Task<string> GetCoverImageAsync(int collectionTagId)
public async Task<string?> GetCoverImageAsync(int collectionTagId)
{
return await _context.CollectionTag
.Where(c => c.Id == collectionTagId)
.Select(c => c.CoverImage)
.AsNoTracking()
.SingleOrDefaultAsync();
}
public async Task<IList<string>> GetAllCoverImagesAsync()
{
return await _context.CollectionTag
return (await _context.CollectionTag
.Select(t => t.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.AsNoTracking()
.ToListAsync();
.ToListAsync())!;
}
public async Task<bool> TagExists(string title)
{
var normalized = Services.Tasks.Scanner.Parser.Parser.Normalize(title);
var normalized = title.ToNormalized();
return await _context.CollectionTag
.AnyAsync(x => x.NormalizedTitle.Equals(normalized));
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized));
}
public async Task<IEnumerable<CollectionTagDto>> GetAllTagDtosAsync()
@ -131,14 +128,8 @@ public class CollectionTagRepository : ICollectionTagRepository
.ToListAsync();
}
public async Task<CollectionTag> GetTagAsync(int tagId)
{
return await _context.CollectionTag
.Where(c => c.Id == tagId)
.SingleOrDefaultAsync();
}
public async Task<CollectionTag> GetFullTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.SeriesMetadata)
public async Task<CollectionTag?> GetTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.None)
{
return await _context.CollectionTag
.Where(c => c.Id == tagId)
@ -164,8 +155,8 @@ public class CollectionTagRepository : ICollectionTagRepository
{
var userRating = await GetUserAgeRestriction(userId);
return await _context.CollectionTag
.Where(s => EF.Functions.Like(s.Title, $"%{searchQuery}%")
|| EF.Functions.Like(s.NormalizedTitle, $"%{searchQuery}%"))
.Where(s => EF.Functions.Like(s.Title!, $"%{searchQuery}%")
|| EF.Functions.Like(s.NormalizedTitle!, $"%{searchQuery}%"))
.RestrictAgainstAgeRestriction(userRating)
.OrderBy(s => s.NormalizedTitle)
.AsNoTracking()

View file

@ -13,7 +13,7 @@ public interface IDeviceRepository
{
void Update(Device device);
Task<IEnumerable<DeviceDto>> GetDevicesForUserAsync(int userId);
Task<Device> GetDeviceById(int deviceId);
Task<Device?> GetDeviceById(int deviceId);
}
public class DeviceRepository : IDeviceRepository
@ -41,7 +41,7 @@ public class DeviceRepository : IDeviceRepository
.ToListAsync();
}
public async Task<Device> GetDeviceById(int deviceId)
public async Task<Device?> GetDeviceById(int deviceId)
{
return await _context.Device
.Where(d => d.Id == deviceId)

View file

@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Data.Misc;
using API.DTOs.Metadata;
using API.Entities;
using API.Extensions;
@ -15,7 +14,7 @@ public interface IGenreRepository
{
void Attach(Genre genre);
void Remove(Genre genre);
Task<Genre> FindByNameAsync(string genreName);
Task<Genre?> FindByNameAsync(string genreName);
Task<IList<Genre>> GetAllGenresAsync();
Task<IList<GenreTagDto>> GetAllGenreDtosAsync(int userId);
Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false);
@ -44,11 +43,11 @@ public class GenreRepository : IGenreRepository
_context.Genre.Remove(genre);
}
public async Task<Genre> FindByNameAsync(string genreName)
public async Task<Genre?> FindByNameAsync(string genreName)
{
var normalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(genreName);
var normalizedName = genreName.ToNormalized();
return await _context.Genre
.FirstOrDefaultAsync(g => g.NormalizedTitle.Equals(normalizedName));
.FirstOrDefaultAsync(g => g.NormalizedTitle != null && g.NormalizedTitle.Equals(normalizedName));
}
public async Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false)

View file

@ -31,13 +31,12 @@ public interface ILibraryRepository
{
void Add(Library library);
void Update(Library library);
void Delete(Library library);
void Delete(Library? library);
Task<IEnumerable<LibraryDto>> GetLibraryDtosAsync();
Task<bool> LibraryExists(string libraryName);
Task<Library> GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None);
Task<Library?> GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None);
Task<IEnumerable<LibraryDto>> GetLibraryDtosForUsernameAsync(string userName);
Task<IEnumerable<Library>> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None);
Task<bool> DeleteLibrary(int libraryId);
Task<IEnumerable<Library>> GetLibrariesForUserIdAsync(int userId);
IEnumerable<int> GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None);
Task<LibraryType> GetLibraryTypeAsync(int libraryId);
@ -49,7 +48,7 @@ public interface ILibraryRepository
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync();
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
Task<bool> DoAnySeriesFoldersMatch(IEnumerable<string> folders);
Task<string> GetLibraryCoverImageAsync(int libraryId);
Task<string?> GetLibraryCoverImageAsync(int libraryId);
Task<IList<string>> GetAllCoverImagesAsync();
Task<IDictionary<int, LibraryType>> GetLibraryTypesForIdsAsync(IEnumerable<int> libraryIds);
}
@ -75,8 +74,9 @@ public class LibraryRepository : ILibraryRepository
_context.Entry(library).State = EntityState.Modified;
}
public void Delete(Library library)
public void Delete(Library? library)
{
if (library == null) return;
_context.Library.Remove(library);
}
@ -107,14 +107,6 @@ public class LibraryRepository : ILibraryRepository
return await query.ToListAsync();
}
public async Task<bool> DeleteLibrary(int libraryId)
{
var library = await GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders | LibraryIncludes.Series);
_context.Library.Remove(library);
return await _context.SaveChangesAsync() > 0;
}
/// <summary>
/// This does not track
/// </summary>
@ -164,7 +156,7 @@ public class LibraryRepository : ILibraryRepository
public IEnumerable<JumpKeyDto> GetJumpBarAsync(int libraryId)
{
var seriesSortCharacters = _context.Series.Where(s => s.LibraryId == libraryId)
.Select(s => s.SortName.ToUpper())
.Select(s => s.SortName!.ToUpper())
.OrderBy(s => s)
.AsEnumerable()
.Select(s => s[0]);
@ -207,7 +199,7 @@ public class LibraryRepository : ILibraryRepository
.ToListAsync();
}
public async Task<Library> GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None)
public async Task<Library?> GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None)
{
var query = _context.Library
@ -237,54 +229,11 @@ public class LibraryRepository : ILibraryRepository
return query.AsSplitQuery();
}
/// <summary>
/// This returns a Library with all it's Series -> Volumes -> Chapters. This is expensive. Should only be called when needed.
/// </summary>
/// <param name="libraryId"></param>
/// <returns></returns>
public async Task<Library> GetFullLibraryForIdAsync(int libraryId)
{
return await _context.Library
.Where(x => x.Id == libraryId)
.Include(f => f.Folders)
.Include(l => l.Series)
.ThenInclude(s => s.Metadata)
.Include(l => l.Series)
.ThenInclude(s => s.Volumes)
.ThenInclude(v => v.Chapters)
.ThenInclude(c => c.Files)
.AsSplitQuery()
.SingleAsync();
}
/// <summary>
/// This is a heavy call, pulls all entities for a Library, except this version only grabs for one series id
/// </summary>
/// <param name="libraryId"></param>
/// <param name="seriesId"></param>
/// <returns></returns>
public async Task<Library> GetFullLibraryForIdAsync(int libraryId, int seriesId)
{
return await _context.Library
.Where(x => x.Id == libraryId)
.Include(f => f.Folders)
.Include(l => l.Series.Where(s => s.Id == seriesId))
.ThenInclude(s => s.Metadata)
.Include(l => l.Series.Where(s => s.Id == seriesId))
.ThenInclude(s => s.Volumes)
.ThenInclude(v => v.Chapters)
.ThenInclude(c => c.Files)
.AsSplitQuery()
.SingleAsync();
}
public async Task<bool> LibraryExists(string libraryName)
{
return await _context.Library
.AsNoTracking()
.AnyAsync(x => x.Name.Equals(libraryName));
.AnyAsync(x => x.Name != null && x.Name.Equals(libraryName));
}
public async Task<IEnumerable<LibraryDto>> GetLibrariesForUserAsync(AppUser user)
@ -381,7 +330,7 @@ public class LibraryRepository : ILibraryRepository
return await _context.Series.AnyAsync(s => normalized.Contains(s.FolderPath));
}
public Task<string> GetLibraryCoverImageAsync(int libraryId)
public Task<string?> GetLibraryCoverImageAsync(int libraryId)
{
return _context.Library
.Where(l => l.Id == libraryId)
@ -392,11 +341,10 @@ public class LibraryRepository : ILibraryRepository
public async Task<IList<string>> GetAllCoverImagesAsync()
{
return await _context.ReadingList
return (await _context.ReadingList
.Select(t => t.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.AsNoTracking()
.ToListAsync();
.ToListAsync())!;
}
public async Task<IDictionary<int, LibraryType>> GetLibraryTypesForIdsAsync(IEnumerable<int> libraryIds)

View file

@ -2,7 +2,6 @@
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
@ -17,12 +16,10 @@ public interface IMangaFileRepository
public class MangaFileRepository : IMangaFileRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
public MangaFileRepository(DataContext context, IMapper mapper)
public MangaFileRepository(DataContext context)
{
_context = context;
_mapper = mapper;
}
public void Update(MangaFile file)

View file

@ -1,10 +1,9 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.DTOs.ReadingLists;
using API.Entities;
using API.Entities.Enums;
using API.Extensions;
using API.Helpers;
using API.Services;
using AutoMapper;
@ -16,11 +15,11 @@ namespace API.Data.Repositories;
public interface IReadingListRepository
{
Task<PagedList<ReadingListDto>> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams);
Task<ReadingList> GetReadingListByIdAsync(int readingListId);
Task<ReadingList?> GetReadingListByIdAsync(int readingListId);
Task<IEnumerable<ReadingListItemDto>> GetReadingListItemDtosByIdAsync(int readingListId, int userId);
Task<ReadingListDto> GetReadingListDtoByIdAsync(int readingListId, int userId);
Task<ReadingListDto?> GetReadingListDtoByIdAsync(int readingListId, int userId);
Task<IEnumerable<ReadingListItemDto>> AddReadingProgressModifiers(int userId, IList<ReadingListItemDto> items);
Task<ReadingListDto> GetReadingListDtoByTitleAsync(int userId, string title);
Task<ReadingListDto?> GetReadingListDtoByTitleAsync(int userId, string title);
Task<IEnumerable<ReadingListItem>> GetReadingListItemsByIdAsync(int readingListId);
Task<IEnumerable<ReadingListDto>> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId,
bool includePromoted);
@ -29,7 +28,7 @@ public interface IReadingListRepository
void BulkRemove(IEnumerable<ReadingListItem> items);
void Update(ReadingList list);
Task<int> Count();
Task<string> GetCoverImageAsync(int readingListId);
Task<string?> GetCoverImageAsync(int readingListId);
Task<IList<string>> GetAllCoverImagesAsync();
Task<bool> ReadingListExists(string name);
Task<List<ReadingList>> GetAllReadingListsAsync();
@ -61,29 +60,27 @@ public class ReadingListRepository : IReadingListRepository
return await _context.ReadingList.CountAsync();
}
public async Task<string> GetCoverImageAsync(int readingListId)
public async Task<string?> GetCoverImageAsync(int readingListId)
{
return await _context.ReadingList
.Where(c => c.Id == readingListId)
.Select(c => c.CoverImage)
.AsNoTracking()
.SingleOrDefaultAsync();
}
public async Task<IList<string>> GetAllCoverImagesAsync()
{
return await _context.ReadingList
return (await _context.ReadingList
.Select(t => t.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.AsNoTracking()
.ToListAsync();
.ToListAsync())!;
}
public async Task<bool> ReadingListExists(string name)
{
var normalized = Services.Tasks.Scanner.Parser.Parser.Normalize(name);
var normalized = name.ToNormalized();
return await _context.ReadingList
.AnyAsync(x => x.NormalizedTitle.Equals(normalized));
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized));
}
public async Task<List<ReadingList>> GetAllReadingListsAsync()
@ -132,7 +129,7 @@ public class ReadingListRepository : IReadingListRepository
return await query.ToListAsync();
}
public async Task<ReadingList> GetReadingListByIdAsync(int readingListId)
public async Task<ReadingList?> GetReadingListByIdAsync(int readingListId)
{
return await _context.ReadingList
.Where(r => r.Id == readingListId)
@ -241,7 +238,7 @@ public class ReadingListRepository : IReadingListRepository
return items;
}
public async Task<ReadingListDto> GetReadingListDtoByIdAsync(int readingListId, int userId)
public async Task<ReadingListDto?> GetReadingListDtoByIdAsync(int readingListId, int userId)
{
return await _context.ReadingList
.Where(r => r.Id == readingListId && (r.AppUserId == userId || r.Promoted))
@ -261,14 +258,14 @@ public class ReadingListRepository : IReadingListRepository
{
var progress = userProgress.Where(p => p.ChapterId == item.ChapterId).ToList();
if (progress.Count == 0) continue;
item.PagesRead = progress.Sum(p => p.PagesRead);
item.PagesRead = progress.Sum(p => p.PagesRead);
item.LastReadingProgressUtc = progress.Max(p => p.LastModifiedUtc);
}
return items;
}
public async Task<ReadingListDto> GetReadingListDtoByTitleAsync(int userId, string title)
public async Task<ReadingListDto?> GetReadingListDtoByTitleAsync(int userId, string title)
{
return await _context.ReadingList
.Where(r => r.Title.Equals(title) && r.AppUserId == userId)

View file

@ -78,7 +78,7 @@ public interface ISeriesRepository
Task<SearchResultGroupDto> SearchSeries(int userId, bool isAdmin, IList<int> libraryIds, string searchQuery);
Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId, SeriesIncludes includes = SeriesIncludes.None);
Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId, int userId);
Task<Series> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata);
Task<Series?> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata);
Task<IList<Series>> GetSeriesByIdsAsync(IList<int> seriesIds);
Task<int[]> GetChapterIdsForSeriesAsync(IList<int> seriesIds);
Task<IDictionary<int, IList<int>>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds);
@ -89,20 +89,19 @@ public interface ISeriesRepository
/// <param name="series"></param>
/// <returns></returns>
Task AddSeriesModifiers(int userId, List<SeriesDto> series);
Task<string> GetSeriesCoverImageAsync(int seriesId);
Task<string?> GetSeriesCoverImageAsync(int seriesId);
Task<PagedList<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter);
Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter);
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
Task<SeriesMetadataDto?> GetSeriesMetadata(int seriesId);
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
Task<IList<MangaFile>> GetFilesForSeries(int seriesId);
Task<IEnumerable<SeriesDto>> GetSeriesDtoForIdsAsync(IEnumerable<int> seriesIds, int userId);
Task<IList<string>> GetAllCoverImagesAsync();
Task<IEnumerable<string>> GetLockedCoverImagesAsync();
Task<PagedList<Series>> GetFullSeriesForLibraryIdAsync(int libraryId, UserParams userParams);
Task<Series> GetFullSeriesForSeriesIdAsync(int seriesId);
Task<Series?> GetFullSeriesForSeriesIdAsync(int seriesId);
Task<Chunk> GetChunkInfo(int libraryId = 0);
Task<IList<SeriesMetadata>> GetSeriesMetadataForIdsAsync(IEnumerable<int> seriesIds);
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30);
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind);
@ -111,20 +110,19 @@ public interface ISeriesRepository
Task<PagedList<SeriesDto>> GetHighlyRated(int userId, int libraryId, UserParams userParams);
Task<PagedList<SeriesDto>> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams);
Task<PagedList<SeriesDto>> GetRediscover(int userId, int libraryId, UserParams userParams);
Task<SeriesDto> GetSeriesForMangaFile(int mangaFileId, int userId);
Task<SeriesDto> GetSeriesForChapter(int chapterId, int userId);
Task<SeriesDto?> GetSeriesForMangaFile(int mangaFileId, int userId);
Task<SeriesDto?> GetSeriesForChapter(int chapterId, int userId);
Task<PagedList<SeriesDto>> GetWantToReadForUserAsync(int userId, UserParams userParams, FilterDto filter);
Task<bool> IsSeriesInWantToRead(int userId, int seriesId);
Task<Series> GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None);
Task<Series?> GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None);
Task<IEnumerable<Series>> GetAllSeriesByNameAsync(IList<string> normalizedNames,
int userId, SeriesIncludes includes = SeriesIncludes.None);
Task<IEnumerable<SeriesDto>> GetAllSeriesDtosByNameAsync(IEnumerable<string> normalizedNames,
int userId, SeriesIncludes includes = SeriesIncludes.None);
Task<Series> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true);
Task<Series?> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true);
Task<IList<Series>> RemoveSeriesNotInList(IList<ParsedSeries> seenSeries, int libraryId);
Task<IDictionary<string, IList<SeriesModified>>> GetFolderPathMap(int libraryId);
Task<AgeRating> GetMaxAgeRatingFromSeriesAsync(IEnumerable<int> seriesIds);
Task<AgeRating?> GetMaxAgeRatingFromSeriesAsync(IEnumerable<int> seriesIds);
/// <summary>
/// This is only used for <see cref="MigrateUserProgressLibraryId"/>
/// </summary>
@ -138,6 +136,12 @@ public class SeriesRepository : ISeriesRepository
{
private readonly DataContext _context;
private readonly IMapper _mapper;
// [GeneratedRegex(@"\d{4}", RegexOptions.Compiled, 50000)]
// private static partial Regex YearRegex();
private readonly Regex _yearRegex = new Regex(@"\d{4}", RegexOptions.Compiled, Services.Tasks.Scanner.Parser.Parser.RegexTimeout);
public SeriesRepository(DataContext context, IMapper mapper)
{
_context = context;
@ -202,6 +206,7 @@ public class SeriesRepository : ISeriesRepository
/// <returns></returns>
public async Task<PagedList<Series>> GetFullSeriesForLibraryIdAsync(int libraryId, UserParams userParams)
{
#nullable disable
var query = _context.Series
.Where(s => s.LibraryId == libraryId)
@ -229,11 +234,12 @@ public class SeriesRepository : ISeriesRepository
.ThenInclude(v => v.Chapters)
.ThenInclude(c => c.Tags)
.Include(s => s.Volumes)
.ThenInclude(v => v.Chapters)
.Include(s => s.Volumes)!
.ThenInclude(v => v.Chapters)!
.ThenInclude(c => c.Files)
.AsSplitQuery()
.OrderBy(s => s.SortName.ToLower());
#nullable enable
return await PagedList<Series>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
}
@ -243,8 +249,9 @@ public class SeriesRepository : ISeriesRepository
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
public async Task<Series> GetFullSeriesForSeriesIdAsync(int seriesId)
public async Task<Series?> GetFullSeriesForSeriesIdAsync(int seriesId)
{
#nullable disable
return await _context.Series
.Where(s => s.Id == seriesId)
.Include(s => s.Relations)
@ -274,6 +281,7 @@ public class SeriesRepository : ISeriesRepository
.ThenInclude(c => c.Files)
.AsSplitQuery()
.SingleOrDefaultAsync();
#nullable enable
}
/// <summary>
@ -313,7 +321,7 @@ public class SeriesRepository : ISeriesRepository
{
const int maxRecords = 15;
var result = new SearchResultGroupDto();
var searchQueryNormalized = Services.Tasks.Scanner.Parser.Parser.Normalize(searchQuery);
var searchQueryNormalized = searchQuery.ToNormalized();
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
var seriesIds = _context.Series
@ -332,20 +340,20 @@ public class SeriesRepository : ISeriesRepository
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
.ToListAsync();
var justYear = Regex.Match(searchQuery, @"\d{4}", RegexOptions.None, Services.Tasks.Scanner.Parser.Parser.RegexTimeout).Value;
var justYear = _yearRegex.Match(searchQuery).Value;
var hasYearInQuery = !string.IsNullOrEmpty(justYear);
var yearComparison = hasYearInQuery ? int.Parse(justYear) : 0;
result.Series = _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
.Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%")
|| EF.Functions.Like(s.OriginalName, $"%{searchQuery}%")
|| EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%")
|| EF.Functions.Like(s.NormalizedName, $"%{searchQueryNormalized}%")
|| (hasYearInQuery && s.Metadata.ReleaseYear == yearComparison))
.Where(s => (EF.Functions.Like(s.Name, $"%{searchQuery}%")
|| (s.OriginalName != null && EF.Functions.Like(s.OriginalName, $"%{searchQuery}%"))
|| (s.LocalizedName != null && EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%"))
|| (EF.Functions.Like(s.NormalizedName, $"%{searchQueryNormalized}%"))
|| (hasYearInQuery && s.Metadata.ReleaseYear == yearComparison)))
.RestrictAgainstAgeRestriction(userRating)
.Include(s => s.Library)
.OrderBy(s => s.SortName.ToLower())
.OrderBy(s => s.SortName!.ToLower())
.AsNoTracking()
.AsSplitQuery()
.Take(maxRecords)
@ -362,8 +370,8 @@ public class SeriesRepository : ISeriesRepository
.ToListAsync();
result.Collections = await _context.CollectionTag
.Where(c => EF.Functions.Like(c.Title, $"%{searchQuery}%")
|| EF.Functions.Like(c.NormalizedTitle, $"%{searchQueryNormalized}%"))
.Where(c => (EF.Functions.Like(c.Title, $"%{searchQuery}%"))
|| (EF.Functions.Like(c.NormalizedTitle, $"%{searchQueryNormalized}%")))
.Where(c => c.Promoted || isAdmin)
.RestrictAgainstAgeRestriction(userRating)
.OrderBy(s => s.NormalizedTitle)
@ -376,7 +384,7 @@ public class SeriesRepository : ISeriesRepository
result.Persons = await _context.SeriesMetadata
.Where(sm => seriesIds.Contains(sm.SeriesId))
.SelectMany(sm => sm.People.Where(t => EF.Functions.Like(t.Name, $"%{searchQuery}%")))
.SelectMany(sm => sm.People.Where(t => t.Name != null && EF.Functions.Like(t.Name, $"%{searchQuery}%")))
.AsSplitQuery()
.Take(maxRecords)
.Distinct()
@ -448,7 +456,7 @@ public class SeriesRepository : ISeriesRepository
/// <param name="seriesId"></param>
/// <param name="includes"></param>
/// <returns></returns>
public async Task<Series> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata)
public async Task<Series?> GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata)
{
return await _context.Series
.Where(s => s.Id == seriesId)
@ -581,12 +589,11 @@ public class SeriesRepository : ISeriesRepository
}
}
public async Task<string> GetSeriesCoverImageAsync(int seriesId)
public async Task<string?> GetSeriesCoverImageAsync(int seriesId)
{
return await _context.Series
.Where(s => s.Id == seriesId)
.Select(s => s.CoverImage)
.AsNoTracking()
.SingleOrDefaultAsync();
}
@ -711,7 +718,7 @@ public class SeriesRepository : ISeriesRepository
var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30);
var cutoffLastAddedPoint = DateTime.Now - TimeSpan.FromDays(7);
var libraryIds = _context.Library.GetUserLibraries(userId, QueryContext.Dashboard)
var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard)
.Where(id => libraryId == 0 || id == libraryId);
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
@ -767,12 +774,12 @@ public class SeriesRepository : ISeriesRepository
.WhereIf(hasAgeRating, s => filter.AgeRating.Contains(s.Metadata.AgeRating))
.WhereIf(hasTagsFilter, s => s.Metadata.Tags.Any(t => filter.Tags.Contains(t.Id)))
.WhereIf(hasLanguageFilter, s => filter.Languages.Contains(s.Metadata.Language))
.WhereIf(hasReleaseYearMinFilter, s => s.Metadata.ReleaseYear >= filter.ReleaseYearRange.Min)
.WhereIf(hasReleaseYearMaxFilter, s => s.Metadata.ReleaseYear <= filter.ReleaseYearRange.Max)
.WhereIf(hasReleaseYearMinFilter, s => s.Metadata.ReleaseYear >= filter.ReleaseYearRange!.Min)
.WhereIf(hasReleaseYearMaxFilter, s => s.Metadata.ReleaseYear <= filter.ReleaseYearRange!.Max)
.WhereIf(hasPublicationFilter, s => filter.PublicationStatus.Contains(s.Metadata.PublicationStatus))
.WhereIf(hasSeriesNameFilter, s => EF.Functions.Like(s.Name, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(s.OriginalName, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(s.LocalizedName, $"%{filter.SeriesNameQuery}%"))
|| EF.Functions.Like(s.OriginalName!, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(s.LocalizedName!, $"%{filter.SeriesNameQuery}%"))
.WhereIf(onlyParentSeries,
s => s.RelationOf.Count == 0 || s.RelationOf.All(p => p.RelationKind == RelationKind.Prequel))
@ -841,12 +848,12 @@ public class SeriesRepository : ISeriesRepository
.WhereIf(hasAgeRating, s => filter.AgeRating.Contains(s.Metadata.AgeRating))
.WhereIf(hasTagsFilter, s => s.Metadata.Tags.Any(t => filter.Tags.Contains(t.Id)))
.WhereIf(hasLanguageFilter, s => filter.Languages.Contains(s.Metadata.Language))
.WhereIf(hasReleaseYearMinFilter, s => s.Metadata.ReleaseYear >= filter.ReleaseYearRange.Min)
.WhereIf(hasReleaseYearMaxFilter, s => s.Metadata.ReleaseYear <= filter.ReleaseYearRange.Max)
.WhereIf(hasReleaseYearMinFilter, s => s.Metadata.ReleaseYear >= filter.ReleaseYearRange!.Min)
.WhereIf(hasReleaseYearMaxFilter, s => s.Metadata.ReleaseYear <= filter.ReleaseYearRange!.Max)
.WhereIf(hasPublicationFilter, s => filter.PublicationStatus.Contains(s.Metadata.PublicationStatus))
.WhereIf(hasSeriesNameFilter, s => EF.Functions.Like(s.Name, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(s.OriginalName, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(s.LocalizedName, $"%{filter.SeriesNameQuery}%"))
|| EF.Functions.Like(s.OriginalName!, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(s.LocalizedName!, $"%{filter.SeriesNameQuery}%"))
.Where(s => userLibraries.Contains(s.LibraryId)
&& formats.Contains(s.Format))
.AsNoTracking();
@ -862,7 +869,7 @@ public class SeriesRepository : ISeriesRepository
{
query = filter.SortOptions.SortField switch
{
SortField.SortName => query.OrderBy(s => s.SortName.ToLower()),
SortField.SortName => query.OrderBy(s => s.SortName!.ToLower()),
SortField.CreatedDate => query.OrderBy(s => s.Created),
SortField.LastModifiedDate => query.OrderBy(s => s.LastModified),
SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded),
@ -874,7 +881,7 @@ public class SeriesRepository : ISeriesRepository
{
query = filter.SortOptions.SortField switch
{
SortField.SortName => query.OrderByDescending(s => s.SortName.ToLower()),
SortField.SortName => query.OrderByDescending(s => s.SortName!.ToLower()),
SortField.CreatedDate => query.OrderByDescending(s => s.Created),
SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded),
@ -886,7 +893,7 @@ public class SeriesRepository : ISeriesRepository
return query;
}
public async Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId)
public async Task<SeriesMetadataDto?> GetSeriesMetadata(int seriesId)
{
var metadataDto = await _context.SeriesMetadata
.Where(metadata => metadata.SeriesId == seriesId)
@ -968,20 +975,18 @@ public class SeriesRepository : ISeriesRepository
public async Task<IList<string>> GetAllCoverImagesAsync()
{
return await _context.Series
return (await _context.Series
.Select(s => s.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.AsNoTracking()
.ToListAsync();
.ToListAsync())!;
}
public async Task<IEnumerable<string>> GetLockedCoverImagesAsync()
{
return await _context.Series
return (await _context.Series
.Where(s => s.CoverImageLocked && !string.IsNullOrEmpty(s.CoverImage))
.Select(s => s.CoverImage)
.AsNoTracking()
.ToListAsync();
.ToListAsync())!;
}
/// <summary>
@ -1061,31 +1066,35 @@ public class SeriesRepository : ISeriesRepository
var items = (await GetRecentlyAddedChaptersQuery(userId));
if (userRating.AgeRating != AgeRating.NotApplicable)
{
items = items.RestrictAgainstAgeRestriction(userRating);
items = items.RestrictAgainstAgeRestriction(userRating);
}
foreach (var item in items)
{
if (seriesMap.Keys.Count == pageSize) break;
if (seriesMap.Keys.Count == pageSize) break;
if (seriesMap.ContainsKey(item.SeriesName))
{
seriesMap[item.SeriesName].Count += 1;
}
else
{
seriesMap[item.SeriesName] = new GroupedSeriesDto()
{
LibraryId = item.LibraryId,
LibraryType = item.LibraryType,
SeriesId = item.SeriesId,
SeriesName = item.SeriesName,
Created = item.Created,
Id = index,
Format = item.Format,
Count = 1,
};
index += 1;
}
if (item.SeriesName == null) continue;
if (seriesMap.TryGetValue(item.SeriesName, out var value))
{
value.Count += 1;
}
else
{
seriesMap[item.SeriesName] = new GroupedSeriesDto()
{
LibraryId = item.LibraryId,
LibraryType = item.LibraryType,
SeriesId = item.SeriesId,
SeriesName = item.SeriesName,
Created = item.Created,
Id = index,
Format = item.Format,
Count = 1,
};
index += 1;
}
}
return seriesMap.Values.AsEnumerable();
@ -1093,7 +1102,7 @@ public class SeriesRepository : ISeriesRepository
public async Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind)
{
var libraryIds = _context.Library.GetUserLibraries(userId);
var libraryIds = GetLibraryIdsForUser(userId);
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
var usersSeriesIds = _context.Series
@ -1120,7 +1129,7 @@ public class SeriesRepository : ISeriesRepository
public async Task<PagedList<SeriesDto>> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams)
{
var libraryIds = _context.Library.GetUserLibraries(userId, QueryContext.Recommended)
var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended)
.Where(id => libraryId == 0 || id == libraryId);
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
@ -1148,7 +1157,7 @@ public class SeriesRepository : ISeriesRepository
/// <returns></returns>
public async Task<PagedList<SeriesDto>> GetRediscover(int userId, int libraryId, UserParams userParams)
{
var libraryIds = _context.Library.GetUserLibraries(userId, QueryContext.Recommended)
var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended)
.Where(id => libraryId == 0 || id == libraryId);
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
@ -1166,9 +1175,9 @@ public class SeriesRepository : ISeriesRepository
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
}
public async Task<SeriesDto> GetSeriesForMangaFile(int mangaFileId, int userId)
public async Task<SeriesDto?> GetSeriesForMangaFile(int mangaFileId, int userId)
{
var libraryIds = _context.Library.GetUserLibraries(userId, QueryContext.Search);
var libraryIds = GetLibraryIdsForUser(userId, 0, QueryContext.Search);
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
return await _context.MangaFile
@ -1183,9 +1192,9 @@ public class SeriesRepository : ISeriesRepository
.SingleOrDefaultAsync();
}
public async Task<SeriesDto> GetSeriesForChapter(int chapterId, int userId)
public async Task<SeriesDto?> GetSeriesForChapter(int chapterId, int userId)
{
var libraryIds = _context.Library.GetUserLibraries(userId);
var libraryIds = GetLibraryIdsForUser(userId);
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
return await _context.Chapter
.Where(m => m.Id == chapterId)
@ -1204,11 +1213,11 @@ public class SeriesRepository : ISeriesRepository
/// <param name="folder">This will be normalized in the query</param>
/// <param name="includes">Additional relationships to include with the base query</param>
/// <returns></returns>
public async Task<Series> GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None)
public async Task<Series?> GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None)
{
var normalized = Services.Tasks.Scanner.Parser.Parser.NormalizePath(folder);
return await _context.Series
.Where(s => s.FolderPath.Equals(normalized))
.Where(s => s.FolderPath != null && s.FolderPath.Equals(normalized))
.Includes(includes)
.SingleOrDefaultAsync();
}
@ -1220,7 +1229,8 @@ public class SeriesRepository : ISeriesRepository
var userRating = await _context.AppUser.GetUserAgeRestriction(userId);
return await _context.Series
.Where(s => normalizedNames.Contains(s.NormalizedName) || normalizedNames.Contains(s.NormalizedLocalizedName))
.Where(s => normalizedNames.Contains(s.NormalizedName) ||
normalizedNames.Contains(s.NormalizedLocalizedName))
.Where(s => libraryIds.Contains(s.LibraryId))
.RestrictAgainstAgeRestriction(userRating)
.Includes(includes)
@ -1252,21 +1262,23 @@ public class SeriesRepository : ISeriesRepository
/// <param name="format"></param>
/// <param name="withFullIncludes">Defaults to true. This will query against all foreign keys (deep). If false, just the series will come back</param>
/// <returns></returns>
public Task<Series> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true)
public Task<Series?> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true)
{
var normalizedSeries = Services.Tasks.Scanner.Parser.Parser.Normalize(seriesName);
var normalizedLocalized = Services.Tasks.Scanner.Parser.Parser.Normalize(localizedName);
var normalizedSeries = seriesName.ToNormalized();
var normalizedLocalized = localizedName.ToNormalized();
var query = _context.Series
.Where(s => s.LibraryId == libraryId)
.Where(s => s.Format == format && format != MangaFormat.Unknown)
.Where(s => s.NormalizedName.Equals(normalizedSeries)
|| (s.NormalizedLocalizedName.Equals(normalizedSeries) && s.NormalizedLocalizedName != string.Empty)
|| s.OriginalName.Equals(seriesName));
|| (s.NormalizedLocalizedName == normalizedSeries)
|| (s.OriginalName == seriesName));
if (!string.IsNullOrEmpty(normalizedLocalized))
{
// TODO: Apply WhereIf
query = query.Where(s =>
s.NormalizedName.Equals(normalizedLocalized) || s.NormalizedLocalizedName.Equals(normalizedLocalized));
s.NormalizedName.Equals(normalizedLocalized)
|| (s.NormalizedLocalizedName != null && s.NormalizedLocalizedName.Equals(normalizedLocalized)));
}
if (!withFullIncludes)
@ -1274,10 +1286,13 @@ public class SeriesRepository : ISeriesRepository
return query.SingleOrDefaultAsync();
}
#nullable disable
return query.Include(s => s.Metadata)
.ThenInclude(m => m.People)
.Include(s => s.Metadata)
.ThenInclude(m => m.Genres)
.Include(s => s.Library)
.Include(s => s.Volumes)
.ThenInclude(v => v.Chapters)
@ -1300,6 +1315,7 @@ public class SeriesRepository : ISeriesRepository
.ThenInclude(c => c.Files)
.AsSplitQuery()
.SingleOrDefaultAsync();
#nullable enable
}
@ -1356,7 +1372,7 @@ public class SeriesRepository : ISeriesRepository
public async Task<PagedList<SeriesDto>> GetHighlyRated(int userId, int libraryId, UserParams userParams)
{
var libraryIds = _context.Library.GetUserLibraries(userId, QueryContext.Recommended)
var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended)
.Where(id => libraryId == 0 || id == libraryId);
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
var distinctSeriesIdsWithHighRating = _context.AppUserRating
@ -1378,7 +1394,7 @@ public class SeriesRepository : ISeriesRepository
public async Task<PagedList<SeriesDto>> GetQuickReads(int userId, int libraryId, UserParams userParams)
{
var libraryIds = _context.Library.GetUserLibraries(userId, QueryContext.Recommended)
var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended)
.Where(id => libraryId == 0 || id == libraryId);
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
@ -1405,7 +1421,7 @@ public class SeriesRepository : ISeriesRepository
public async Task<PagedList<SeriesDto>> GetQuickCatchupReads(int userId, int libraryId, UserParams userParams)
{
var libraryIds = _context.Library.GetUserLibraries(userId, QueryContext.Recommended)
var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended)
.Where(id => libraryId == 0 || id == libraryId);
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
@ -1581,6 +1597,7 @@ public class SeriesRepository : ISeriesRepository
var map = new Dictionary<string, IList<SeriesModified>>();
foreach (var series in info)
{
if (series.FolderPath == null) continue;
if (!map.ContainsKey(series.FolderPath))
{
map.Add(series.FolderPath, new List<SeriesModified>()
@ -1603,7 +1620,7 @@ public class SeriesRepository : ISeriesRepository
/// </summary>
/// <param name="seriesIds"></param>
/// <returns></returns>
public async Task<AgeRating> GetMaxAgeRatingFromSeriesAsync(IEnumerable<int> seriesIds)
public async Task<AgeRating?> GetMaxAgeRatingFromSeriesAsync(IEnumerable<int> seriesIds)
{
return await _context.Series
.Where(s => seriesIds.Contains(s.Id))
@ -1612,4 +1629,32 @@ public class SeriesRepository : ISeriesRepository
.OrderBy(s => s)
.LastOrDefaultAsync();
}
/// <summary>
/// Returns all library ids for a user
/// </summary>
/// <param name="userId"></param>
/// <param name="libraryId">0 for no library filter</param>
/// <param name="queryContext">Defaults to None - The context behind this query, so appropriate restrictions can be placed</param>
/// <returns></returns>
private IQueryable<int> GetLibraryIdsForUser(int userId, int libraryId = 0, QueryContext queryContext = QueryContext.None)
{
var user = _context.AppUser
.AsSplitQuery()
.AsNoTracking()
.Where(u => u.Id == userId)
.AsSingleQuery();
if (libraryId == 0)
{
return user.SelectMany(l => l.Libraries)
.IsRestricted(queryContext)
.Select(lib => lib.Id);
}
return user.SelectMany(l => l.Libraries)
.Where(lib => lib.Id == libraryId)
.IsRestricted(queryContext)
.Select(lib => lib.Id);
}
}

View file

@ -5,7 +5,6 @@ using API.DTOs.Settings;
using API.Entities;
using API.Entities.Enums;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
@ -44,7 +43,7 @@ public class SettingsRepository : ISettingsRepository
public Task<ServerSetting> GetSettingAsync(ServerSettingKey key)
{
return _context.ServerSetting.SingleOrDefaultAsync(x => x.Key == key);
return _context.ServerSetting.SingleOrDefaultAsync(x => x.Key == key)!;
}
public async Task<IEnumerable<ServerSetting>> GetSettingsAsync()

View file

@ -15,11 +15,11 @@ public interface ISiteThemeRepository
void Remove(SiteTheme theme);
void Update(SiteTheme siteTheme);
Task<IEnumerable<SiteThemeDto>> GetThemeDtos();
Task<SiteThemeDto> GetThemeDto(int themeId);
Task<SiteThemeDto> GetThemeDtoByName(string themeName);
Task<SiteThemeDto?> GetThemeDto(int themeId);
Task<SiteThemeDto?> GetThemeDtoByName(string themeName);
Task<SiteTheme> GetDefaultTheme();
Task<IEnumerable<SiteTheme>> GetThemes();
Task<SiteTheme> GetThemeById(int themeId);
Task<SiteTheme?> GetThemeById(int themeId);
}
public class SiteThemeRepository : ISiteThemeRepository
@ -55,7 +55,7 @@ public class SiteThemeRepository : ISiteThemeRepository
.ToListAsync();
}
public async Task<SiteThemeDto> GetThemeDtoByName(string themeName)
public async Task<SiteThemeDto?> GetThemeDtoByName(string themeName)
{
return await _context.SiteTheme
.Where(t => t.Name.Equals(themeName))
@ -76,8 +76,8 @@ public class SiteThemeRepository : ISiteThemeRepository
if (result == null)
{
return await _context.SiteTheme
.Where(t => t.NormalizedName == "dark")
.SingleOrDefaultAsync();
.Where(t => t.NormalizedName == Seed.DefaultThemes[0].NormalizedName)
.SingleAsync();
}
return result;
@ -89,14 +89,14 @@ public class SiteThemeRepository : ISiteThemeRepository
.ToListAsync();
}
public async Task<SiteTheme> GetThemeById(int themeId)
public async Task<SiteTheme?> GetThemeById(int themeId)
{
return await _context.SiteTheme
.Where(t => t.Id == themeId)
.SingleOrDefaultAsync();
}
public async Task<SiteThemeDto> GetThemeDto(int themeId)
public async Task<SiteThemeDto?> GetThemeDto(int themeId)
{
return await _context.SiteTheme
.Where(t => t.Id == themeId)

View file

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using API.Constants;
@ -9,11 +8,11 @@ using API.DTOs.Account;
using API.DTOs.Filtering;
using API.DTOs.Reader;
using API.Entities;
using API.Extensions;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SixLabors.ImageSharp.PixelFormats;
namespace API.Data.Repositories;
@ -38,31 +37,31 @@ public interface IUserRepository
void Update(AppUserPreferences preferences);
void Update(AppUserBookmark bookmark);
void Add(AppUserBookmark bookmark);
public void Delete(AppUser user);
public void Delete(AppUser? user);
void Delete(AppUserBookmark bookmark);
Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync();
Task<IEnumerable<MemberDto>> GetPendingMemberDtosAsync();
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
Task<bool> IsUserAdminAsync(AppUser user);
Task<AppUserRating> GetUserRatingAsync(int seriesId, int userId);
Task<AppUserPreferences> GetPreferencesAsync(string username);
Task<bool> IsUserAdminAsync(AppUser? user);
Task<AppUserRating?> GetUserRatingAsync(int seriesId, int userId);
Task<AppUserPreferences?> GetPreferencesAsync(string username);
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForSeries(int userId, int seriesId);
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForVolume(int userId, int volumeId);
Task<IEnumerable<BookmarkDto>> GetBookmarkDtosForChapter(int userId, int chapterId);
Task<IEnumerable<BookmarkDto>> GetAllBookmarkDtos(int userId, FilterDto filter);
Task<IEnumerable<AppUserBookmark>> GetAllBookmarksAsync();
Task<AppUserBookmark> GetBookmarkForPage(int page, int chapterId, int userId);
Task<AppUserBookmark> GetBookmarkAsync(int bookmarkId);
Task<AppUserBookmark?> GetBookmarkForPage(int page, int chapterId, int userId);
Task<AppUserBookmark?> GetBookmarkAsync(int bookmarkId);
Task<int> GetUserIdByApiKeyAsync(string apiKey);
Task<AppUser> GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None);
Task<AppUser> GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None);
Task<AppUser?> GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None);
Task<AppUser?> GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None);
Task<int> GetUserIdByUsernameAsync(string username);
Task<IList<AppUserBookmark>> GetAllBookmarksByIds(IList<int> bookmarkIds);
Task<AppUser> GetUserByEmailAsync(string email);
Task<AppUser?> GetUserByEmailAsync(string email);
Task<IEnumerable<AppUserPreferences>> GetAllPreferencesByThemeAsync(int themeId);
Task<bool> HasAccessToLibrary(int libraryId, int userId);
Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None);
Task<AppUser> GetUserByConfirmationToken(string token);
Task<AppUser?> GetUserByConfirmationToken(string token);
}
public class UserRepository : IUserRepository
@ -98,8 +97,9 @@ public class UserRepository : IUserRepository
_context.AppUserBookmark.Add(bookmark);
}
public void Delete(AppUser user)
public void Delete(AppUser? user)
{
if (user == null) return;
_context.AppUser.Remove(user);
}
@ -114,15 +114,12 @@ public class UserRepository : IUserRepository
/// <param name="username"></param>
/// <param name="includeFlags">Includes() you want. Pass multiple with flag1 | flag2 </param>
/// <returns></returns>
public async Task<AppUser> GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None)
public async Task<AppUser?> GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None)
{
var query = _context.Users
.Where(x => x.UserName == username);
// TODO: Move to QueryExtensions
query = AddIncludesToQuery(query, includeFlags);
return await query.SingleOrDefaultAsync();
return await _context.Users
.Where(x => x.UserName == username)
.Includes(includeFlags)
.SingleOrDefaultAsync();
}
/// <summary>
@ -131,14 +128,12 @@ public class UserRepository : IUserRepository
/// <param name="userId"></param>
/// <param name="includeFlags">Includes() you want. Pass multiple with flag1 | flag2 </param>
/// <returns></returns>
public async Task<AppUser> GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None)
public async Task<AppUser?> GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None)
{
var query = _context.Users
.Where(x => x.Id == userId);
query = AddIncludesToQuery(query, includeFlags);
return await query.SingleOrDefaultAsync();
return await _context.Users
.Where(x => x.Id == userId)
.Includes(includeFlags)
.SingleOrDefaultAsync();
}
public async Task<IEnumerable<AppUserBookmark>> GetAllBookmarksAsync()
@ -146,65 +141,20 @@ public class UserRepository : IUserRepository
return await _context.AppUserBookmark.ToListAsync();
}
public async Task<AppUserBookmark> GetBookmarkForPage(int page, int chapterId, int userId)
public async Task<AppUserBookmark?> GetBookmarkForPage(int page, int chapterId, int userId)
{
return await _context.AppUserBookmark
.Where(b => b.Page == page && b.ChapterId == chapterId && b.AppUserId == userId)
.SingleOrDefaultAsync();
}
public async Task<AppUserBookmark> GetBookmarkAsync(int bookmarkId)
public async Task<AppUserBookmark?> GetBookmarkAsync(int bookmarkId)
{
return await _context.AppUserBookmark
.Where(b => b.Id == bookmarkId)
.SingleOrDefaultAsync();
}
private static IQueryable<AppUser> AddIncludesToQuery(IQueryable<AppUser> query, AppUserIncludes includeFlags)
{
if (includeFlags.HasFlag(AppUserIncludes.Bookmarks))
{
query = query.Include(u => u.Bookmarks);
}
if (includeFlags.HasFlag(AppUserIncludes.Progress))
{
query = query.Include(u => u.Progresses);
}
if (includeFlags.HasFlag(AppUserIncludes.ReadingLists))
{
query = query.Include(u => u.ReadingLists);
}
if (includeFlags.HasFlag(AppUserIncludes.ReadingListsWithItems))
{
query = query.Include(u => u.ReadingLists).ThenInclude(r => r.Items);
}
if (includeFlags.HasFlag(AppUserIncludes.Ratings))
{
query = query.Include(u => u.Ratings);
}
if (includeFlags.HasFlag(AppUserIncludes.UserPreferences))
{
query = query.Include(u => u.UserPreferences);
}
if (includeFlags.HasFlag(AppUserIncludes.WantToRead))
{
query = query.Include(u => u.WantToRead);
}
if (includeFlags.HasFlag(AppUserIncludes.Devices))
{
query = query.Include(u => u.Devices);
}
return query.AsSplitQuery();
}
/// <summary>
/// This fetches the Id for a user. Use whenever you just need an ID.
@ -233,10 +183,10 @@ public class UserRepository : IUserRepository
.ToListAsync();
}
public async Task<AppUser> GetUserByEmailAsync(string email)
public async Task<AppUser?> GetUserByEmailAsync(string email)
{
var lowerEmail = email.ToLower();
return await _context.AppUser.SingleOrDefaultAsync(u => u.Email.ToLower().Equals(lowerEmail));
return await _context.AppUser.SingleOrDefaultAsync(u => u.Email != null && u.Email.ToLower().Equals(lowerEmail));
}
@ -259,13 +209,15 @@ public class UserRepository : IUserRepository
public async Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None)
{
var query = AddIncludesToQuery(_context.Users.AsQueryable(), includeFlags);
return await query.ToListAsync();
return await _context.AppUser
.Includes(includeFlags)
.ToListAsync();
}
public async Task<AppUser> GetUserByConfirmationToken(string token)
public async Task<AppUser?> GetUserByConfirmationToken(string token)
{
return await _context.AppUser.SingleOrDefaultAsync(u => u.ConfirmationToken.Equals(token));
return await _context.AppUser
.SingleOrDefaultAsync(u => u.ConfirmationToken != null && u.ConfirmationToken.Equals(token));
}
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
@ -273,19 +225,20 @@ public class UserRepository : IUserRepository
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
}
public async Task<bool> IsUserAdminAsync(AppUser user)
public async Task<bool> IsUserAdminAsync(AppUser? user)
{
if (user == null) return false;
return await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
}
public async Task<AppUserRating> GetUserRatingAsync(int seriesId, int userId)
public async Task<AppUserRating?> GetUserRatingAsync(int seriesId, int userId)
{
return await _context.AppUserRating
.Where(r => r.SeriesId == seriesId && r.AppUserId == userId)
.SingleOrDefaultAsync();
}
public async Task<AppUserPreferences> GetPreferencesAsync(string username)
public async Task<AppUserPreferences?> GetPreferencesAsync(string username)
{
return await _context.AppUserPreferences
.Include(p => p.AppUser)
@ -342,16 +295,16 @@ public class UserRepository : IUserRepository
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync();
var seriesNameQueryNormalized = Services.Tasks.Scanner.Parser.Parser.Normalize(filter.SeriesNameQuery);
var seriesNameQueryNormalized = filter.SeriesNameQuery.ToNormalized();
var filterSeriesQuery = query.Join(_context.Series, b => b.SeriesId, s => s.Id, (bookmark, series) => new
{
bookmark,
series
})
.Where(o => EF.Functions.Like(o.series.Name, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(o.series.OriginalName, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(o.series.LocalizedName, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(o.series.NormalizedName, $"%{seriesNameQueryNormalized}%")
.Where(o => (EF.Functions.Like(o.series.Name, $"%{filter.SeriesNameQuery}%"))
|| (o.series.OriginalName != null && EF.Functions.Like(o.series.OriginalName, $"%{filter.SeriesNameQuery}%"))
|| (o.series.LocalizedName != null && EF.Functions.Like(o.series.LocalizedName, $"%{filter.SeriesNameQuery}%"))
|| (EF.Functions.Like(o.series.NormalizedName, $"%{seriesNameQueryNormalized}%"))
);
query = filterSeriesQuery.Select(o => o.bookmark);
@ -370,7 +323,7 @@ public class UserRepository : IUserRepository
public async Task<int> GetUserIdByApiKeyAsync(string apiKey)
{
return await _context.AppUser
.Where(u => u.ApiKey.Equals(apiKey))
.Where(u => u.ApiKey != null && u.ApiKey.Equals(apiKey))
.Select(u => u.Id)
.SingleOrDefaultAsync();
}
@ -391,7 +344,7 @@ public class UserRepository : IUserRepository
Email = u.Email,
Created = u.Created,
LastActive = u.LastActive,
Roles = u.UserRoles.Select(r => r.Role.Name).ToList(),
Roles = u.UserRoles.Select(r => r.Role.Name).ToList()!,
AgeRestriction = new AgeRestrictionDto()
{
AgeRating = u.AgeRestriction,
@ -429,7 +382,7 @@ public class UserRepository : IUserRepository
Email = u.Email,
Created = u.Created,
LastActive = u.LastActive,
Roles = u.UserRoles.Select(r => r.Role.Name).ToList(),
Roles = u.UserRoles.Select(r => r.Role.Name).ToList()!,
AgeRestriction = new AgeRestrictionDto()
{
AgeRating = u.AgeRestriction,

View file

@ -16,14 +16,14 @@ public interface IVolumeRepository
void Update(Volume volume);
void Remove(Volume volume);
Task<IList<MangaFile>> GetFilesForVolume(int volumeId);
Task<string> GetVolumeCoverImageAsync(int volumeId);
Task<string?> GetVolumeCoverImageAsync(int volumeId);
Task<IList<int>> GetChapterIdsByVolumeIds(IReadOnlyList<int> volumeIds);
Task<IEnumerable<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId);
Task<Volume> GetVolumeAsync(int volumeId);
Task<Volume?> GetVolumeAsync(int volumeId);
Task<VolumeDto> GetVolumeDtoAsync(int volumeId, int userId);
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false);
Task<IEnumerable<Volume>> GetVolumes(int seriesId);
Task<Volume> GetVolumeByIdAsync(int volumeId);
Task<Volume?> GetVolumeByIdAsync(int volumeId);
}
public class VolumeRepository : IVolumeRepository
{
@ -72,12 +72,11 @@ public class VolumeRepository : IVolumeRepository
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
public async Task<string> GetVolumeCoverImageAsync(int volumeId)
public async Task<string?> GetVolumeCoverImageAsync(int volumeId)
{
return await _context.Volume
.Where(v => v.Id == volumeId)
.Select(v => v.CoverImage)
.AsNoTracking()
.SingleOrDefaultAsync();
}
@ -155,7 +154,7 @@ public class VolumeRepository : IVolumeRepository
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
public async Task<Volume> GetVolumeAsync(int volumeId)
public async Task<Volume?> GetVolumeAsync(int volumeId)
{
return await _context.Volume
.Include(vol => vol.Chapters)
@ -191,7 +190,7 @@ public class VolumeRepository : IVolumeRepository
return volumes;
}
public async Task<Volume> GetVolumeByIdAsync(int volumeId)
public async Task<Volume?> GetVolumeByIdAsync(int volumeId)
{
return await _context.Volume.SingleOrDefaultAsync(x => x.Id == volumeId);
}

View file

@ -8,6 +8,7 @@ using API.Constants;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Enums.Theme;
using API.Extensions;
using API.Services;
using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
@ -29,7 +30,7 @@ public static class Seed
new()
{
Name = "Dark",
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize("Dark"),
NormalizedName = "Dark".ToNormalized(),
Provider = ThemeProvider.System,
FileName = "dark.scss",
IsDefault = true,
@ -42,13 +43,13 @@ public static class Seed
.GetFields(BindingFlags.Public | BindingFlags.Static)
.Where(f => f.FieldType == typeof(string))
.ToDictionary(f => f.Name,
f => (string) f.GetValue(null)).Values
f => (string) f.GetValue(null)!).Values
.Select(policyName => new AppRole() {Name = policyName})
.ToList();
foreach (var role in roles)
{
var exists = await roleManager.RoleExistsAsync(role.Name);
var exists = await roleManager.RoleExistsAsync(role.Name!);
if (!exists)
{
await roleManager.CreateAsync(role);

View file

@ -60,7 +60,7 @@ public class UnitOfWork : IUnitOfWork
public IGenreRepository GenreRepository => new GenreRepository(_context, _mapper);
public ITagRepository TagRepository => new TagRepository(_context, _mapper);
public ISiteThemeRepository SiteThemeRepository => new SiteThemeRepository(_context, _mapper);
public IMangaFileRepository MangaFileRepository => new MangaFileRepository(_context, _mapper);
public IMangaFileRepository MangaFileRepository => new MangaFileRepository(_context);
public IDeviceRepository DeviceRepository => new DeviceRepository(_context, _mapper);
/// <summary>