Merged develop in

This commit is contained in:
Joseph Milazzo 2025-04-15 13:28:53 -05:00
commit 7c692a1b46
580 changed files with 21233 additions and 9031 deletions

View file

@ -11,6 +11,8 @@ using API.Entities.Enums.UserPreferences;
using API.Entities.History;
using API.Entities.Interfaces;
using API.Entities.Metadata;
using API.Entities.MetadataMatching;
using API.Entities.Person;
using API.Entities.Scrobble;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
@ -131,6 +133,9 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
builder.Entity<AppUserPreferences>()
.Property(b => b.WantToReadSync)
.HasDefaultValue(true);
builder.Entity<AppUserPreferences>()
.Property(b => b.AllowAutomaticWebtoonReaderDetection)
.HasDefaultValue(true);
builder.Entity<Library>()
.Property(b => b.AllowScrobbling)

View file

@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@ -35,7 +36,7 @@ public static class MigrateInitialInstallData
{
var fi = directoryService.FileSystem.FileInfo.New(dbFile);
var setting = settings.First(s => s.Key == ServerSettingKey.FirstInstallDate);
setting.Value = fi.CreationTimeUtc.ToString();
setting.Value = fi.CreationTimeUtc.ToString(CultureInfo.InvariantCulture);
await dataContext.SaveChangesAsync();
}

View file

@ -0,0 +1,55 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.6 - Manually check when a user triggers scrobble event generation
/// </summary>
public static class ManualMigrateScrobbleEventGen
{
public static async Task Migrate(DataContext context, ILogger<Program> logger)
{
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateScrobbleEventGen"))
{
return;
}
logger.LogCritical("Running ManualMigrateScrobbleEventGen migration - Please be patient, this may take some time. This is not an error");
var users = await context.Users
.Where(u => u.AniListAccessToken != null)
.ToListAsync();
foreach (var user in users)
{
if (await context.ScrobbleEvent.AnyAsync(se => se.AppUserId == user.Id))
{
user.HasRunScrobbleEventGeneration = true;
user.ScrobbleEventGenerationRan = DateTime.UtcNow;
context.AppUser.Update(user);
}
}
if (context.ChangeTracker.HasChanges())
{
await context.SaveChangesAsync();
}
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
{
Name = "ManualMigrateScrobbleEventGen",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await context.SaveChangesAsync();
logger.LogCritical("Running ManualMigrateScrobbleEventGen migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,49 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities.History;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.8.6 - Change to not scrobble specials as they will never process, this migration removes all existing scrobble events
/// </summary>
public static class ManualMigrateScrobbleSpecials
{
public static async Task Migrate(DataContext context, ILogger<Program> logger)
{
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateScrobbleSpecials"))
{
return;
}
logger.LogCritical("Running ManualMigrateScrobbleSpecials 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 events = await context.ScrobbleEvent
.Where(se => se.VolumeNumber == Parser.SpecialVolumeNumber)
.ToListAsync();
context.ScrobbleEvent.RemoveRange(events);
if (context.ChangeTracker.HasChanges())
{
await context.SaveChangesAsync();
logger.LogInformation("Removed {Count} scrobble events that were specials", events.Count);
}
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
{
Name = "ManualMigrateScrobbleSpecials",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await context.SaveChangesAsync();
logger.LogCritical("Running ManualMigrateScrobbleSpecials migration - Completed. This is not an error");
}
}

File diff suppressed because it is too large Load diff

View file

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class ScrobbleGenerationDbCapture : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "HasRunScrobbleEventGeneration",
table: "AspNetUsers",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<DateTime>(
name: "ScrobbleEventGenerationRan",
table: "AspNetUsers",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "HasRunScrobbleEventGeneration",
table: "AspNetUsers");
migrationBuilder.DropColumn(
name: "ScrobbleEventGenerationRan",
table: "AspNetUsers");
}
}
}

View file

@ -15,7 +15,7 @@ namespace API.Data.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.1");
modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
modelBuilder.Entity("API.Entities.AppRole", b =>
{
@ -85,6 +85,9 @@ namespace API.Data.Migrations
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("HasRunScrobbleEventGeneration")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastActive")
.HasColumnType("TEXT");
@ -124,6 +127,9 @@ namespace API.Data.Migrations
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<DateTime>("ScrobbleEventGenerationRan")
.HasColumnType("TEXT");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
@ -353,6 +359,11 @@ namespace API.Data.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("AllowAutomaticWebtoonReaderDetection")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<bool>("AniListScrobblingEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
@ -911,24 +922,6 @@ namespace API.Data.Migrations
b.ToTable("Chapter");
});
modelBuilder.Entity("API.Entities.ChapterPeople", b =>
{
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("PersonId")
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("INTEGER");
b.HasKey("ChapterId", "PersonId", "Role");
b.HasIndex("PersonId");
b.ToTable("ChapterPeople");
});
modelBuilder.Entity("API.Entities.CollectionTag", b =>
{
b.Property<int>("Id")
@ -1640,7 +1633,7 @@ namespace API.Data.Migrations
b.ToTable("MetadataFieldMapping");
});
modelBuilder.Entity("API.Entities.MetadataSettings", b =>
modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@ -1703,7 +1696,25 @@ namespace API.Data.Migrations
b.ToTable("MetadataSettings");
});
modelBuilder.Entity("API.Entities.Person", b =>
modelBuilder.Entity("API.Entities.Person.ChapterPeople", b =>
{
b.Property<int>("ChapterId")
.HasColumnType("INTEGER");
b.Property<int>("PersonId")
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("INTEGER");
b.HasKey("ChapterId", "PersonId", "Role");
b.HasIndex("PersonId");
b.ToTable("ChapterPeople");
});
modelBuilder.Entity("API.Entities.Person.Person", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@ -1747,6 +1758,32 @@ namespace API.Data.Migrations
b.ToTable("Person");
});
modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b =>
{
b.Property<int>("SeriesMetadataId")
.HasColumnType("INTEGER");
b.Property<int>("PersonId")
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("INTEGER");
b.Property<bool>("KavitaPlusConnection")
.HasColumnType("INTEGER");
b.Property<int>("OrderWeight")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0);
b.HasKey("SeriesMetadataId", "PersonId", "Role");
b.HasIndex("PersonId");
b.ToTable("SeriesMetadataPeople");
});
modelBuilder.Entity("API.Entities.ReadingList", b =>
{
b.Property<int>("Id")
@ -2111,32 +2148,6 @@ namespace API.Data.Migrations
b.ToTable("Series");
});
modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b =>
{
b.Property<int>("SeriesMetadataId")
.HasColumnType("INTEGER");
b.Property<int>("PersonId")
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("INTEGER");
b.Property<bool>("KavitaPlusConnection")
.HasColumnType("INTEGER");
b.Property<int>("OrderWeight")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0);
b.HasKey("SeriesMetadataId", "PersonId", "Role");
b.HasIndex("PersonId");
b.ToTable("SeriesMetadataPeople");
});
modelBuilder.Entity("API.Entities.ServerSetting", b =>
{
b.Property<int>("Key")
@ -2804,25 +2815,6 @@ namespace API.Data.Migrations
b.Navigation("Volume");
});
modelBuilder.Entity("API.Entities.ChapterPeople", b =>
{
b.HasOne("API.Entities.Chapter", "Chapter")
.WithMany("People")
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.Person", "Person")
.WithMany("ChapterPeople")
.HasForeignKey("PersonId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
b.Navigation("Person");
});
modelBuilder.Entity("API.Entities.Device", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
@ -2943,7 +2935,7 @@ namespace API.Data.Migrations
modelBuilder.Entity("API.Entities.MetadataFieldMapping", b =>
{
b.HasOne("API.Entities.MetadataSettings", "MetadataSettings")
b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings")
.WithMany("FieldMappings")
.HasForeignKey("MetadataSettingsId")
.OnDelete(DeleteBehavior.Cascade)
@ -2952,6 +2944,44 @@ namespace API.Data.Migrations
b.Navigation("MetadataSettings");
});
modelBuilder.Entity("API.Entities.Person.ChapterPeople", b =>
{
b.HasOne("API.Entities.Chapter", "Chapter")
.WithMany("People")
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.Person.Person", "Person")
.WithMany("ChapterPeople")
.HasForeignKey("PersonId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
b.Navigation("Person");
});
modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b =>
{
b.HasOne("API.Entities.Person.Person", "Person")
.WithMany("SeriesMetadataPeople")
.HasForeignKey("PersonId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata")
.WithMany("People")
.HasForeignKey("SeriesMetadataId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Person");
b.Navigation("SeriesMetadata");
});
modelBuilder.Entity("API.Entities.ReadingList", b =>
{
b.HasOne("API.Entities.AppUser", "AppUser")
@ -3072,25 +3102,6 @@ namespace API.Data.Migrations
b.Navigation("Library");
});
modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b =>
{
b.HasOne("API.Entities.Person", "Person")
.WithMany("SeriesMetadataPeople")
.HasForeignKey("PersonId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata")
.WithMany("People")
.HasForeignKey("SeriesMetadataId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Person");
b.Navigation("SeriesMetadata");
});
modelBuilder.Entity("API.Entities.Volume", b =>
{
b.HasOne("API.Entities.Series", "Series")
@ -3351,12 +3362,12 @@ namespace API.Data.Migrations
b.Navigation("People");
});
modelBuilder.Entity("API.Entities.MetadataSettings", b =>
modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b =>
{
b.Navigation("FieldMappings");
});
modelBuilder.Entity("API.Entities.Person", b =>
modelBuilder.Entity("API.Entities.Person.Person", b =>
{
b.Navigation("ChapterPeople");

View file

@ -16,6 +16,7 @@ using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface IAppUserProgressRepository
{
void Update(AppUserProgress userProgress);
@ -41,7 +42,7 @@ public interface IAppUserProgressRepository
Task UpdateAllProgressThatAreMoreThanChapterPages();
Task<IList<FullProgressDto>> GetUserProgressForChapter(int chapterId, int userId = 0);
}
#nullable disable
public class AppUserProgressRepository : IAppUserProgressRepository
{
private readonly DataContext _context;
@ -192,6 +193,7 @@ public class AppUserProgressRepository : IAppUserProgressRepository
.Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber)
.Select(p => p.chapter.Volume.MaxNumber)
.ToListAsync();
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max();
}

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using API.DTOs.CoverDb;
using API.Entities;
using API.Entities.Person;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

View file

@ -12,6 +12,7 @@ using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface IGenreRepository
{

View file

@ -9,15 +9,18 @@ using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface IMediaErrorRepository
{
void Attach(MediaError error);
void Remove(MediaError error);
void Remove(IList<MediaError> errors);
Task<MediaError> Find(string filename);
IEnumerable<MediaErrorDto> GetAllErrorDtosAsync();
Task<bool> ExistsAsync(MediaError error);
Task DeleteAll();
Task<List<MediaError>> GetAllErrorsAsync(IList<string> comments);
}
public class MediaErrorRepository : IMediaErrorRepository
@ -43,6 +46,11 @@ public class MediaErrorRepository : IMediaErrorRepository
_context.MediaError.Remove(error);
}
public void Remove(IList<MediaError> errors)
{
_context.MediaError.RemoveRange(errors);
}
public Task<MediaError?> Find(string filename)
{
return _context.MediaError.Where(e => e.FilePath == filename).SingleOrDefaultAsync();
@ -70,4 +78,11 @@ public class MediaErrorRepository : IMediaErrorRepository
_context.MediaError.RemoveRange(await _context.MediaError.ToListAsync());
await _context.SaveChangesAsync();
}
public Task<List<MediaError>> GetAllErrorsAsync(IList<string> comments)
{
return _context.MediaError
.Where(m => comments.Contains(m.Comment))
.ToListAsync();
}
}

View file

@ -1,11 +1,9 @@
using System.Collections;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.DTOs;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Person;
using API.Extensions;
using API.Extensions.QueryExtensions;
using API.Helpers;
@ -14,6 +12,7 @@ using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface IPersonRepository
{
@ -29,15 +28,13 @@ public interface IPersonRepository
Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role);
Task RemoveAllPeopleNoLongerAssociated();
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(int userId, List<int>? libraryIds = null);
Task<int> GetCountAsync();
Task<string> GetCoverImageAsync(int personId);
Task<string?> GetCoverImageAsync(int personId);
Task<string?> GetCoverImageByNameAsync(string name);
Task<IEnumerable<PersonRole>> GetRolesForPersonByName(int personId, int userId);
Task<PagedList<BrowsePersonDto>> GetAllWritersAndSeriesCount(int userId, UserParams userParams);
Task<Person?> GetPersonById(int personId);
Task<PersonDto?> GetPersonDtoByName(string name, int userId);
Task<Person> GetPersonByName(string name);
Task<bool> IsNameUnique(string name);
Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId);
@ -124,12 +121,8 @@ public class PersonRepository : IPersonRepository
.ToListAsync();
}
public async Task<int> GetCountAsync()
{
return await _context.Person.CountAsync();
}
public async Task<string> GetCoverImageAsync(int personId)
public async Task<string?> GetCoverImageAsync(int personId)
{
return await _context.Person
.Where(c => c.Id == personId)
@ -137,7 +130,7 @@ public class PersonRepository : IPersonRepository
.SingleOrDefaultAsync();
}
public async Task<string> GetCoverImageByNameAsync(string name)
public async Task<string?> GetCoverImageByNameAsync(string name)
{
var normalized = name.ToNormalized();
return await _context.Person
@ -206,7 +199,7 @@ public class PersonRepository : IPersonRepository
.FirstOrDefaultAsync();
}
public async Task<PersonDto> GetPersonDtoByName(string name, int userId)
public async Task<PersonDto?> GetPersonDtoByName(string name, int userId)
{
var normalized = name.ToNormalized();
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
@ -218,11 +211,6 @@ public class PersonRepository : IPersonRepository
.FirstOrDefaultAsync();
}
public async Task<Person> GetPersonByName(string name)
{
return await _context.Person.FirstOrDefaultAsync(p => p.NormalizedName == name.ToNormalized());
}
public async Task<bool> IsNameUnique(string name)
{
return !(await _context.Person.AnyAsync(p => p.Name == name));

View file

@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
[Flags]
public enum ReadingListIncludes
@ -48,11 +49,15 @@ public interface IReadingListRepository
Task<IList<string>> GetRandomCoverImagesAsync(int readingListId);
Task<IList<string>> GetAllCoverImagesAsync();
Task<bool> ReadingListExists(string name);
IEnumerable<PersonDto> GetReadingListCharactersAsync(int readingListId);
Task<bool> ReadingListExistsForUser(string name, int userId);
IEnumerable<PersonDto> GetReadingListPeopleAsync(int readingListId, PersonRole role);
Task<ReadingListCast> GetReadingListAllPeopleAsync(int readingListId);
Task<IList<ReadingList>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
Task<int> RemoveReadingListsWithoutSeries();
Task<ReadingList?> GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items);
Task<IEnumerable<ReadingList>> GetReadingListsByIds(IList<int> ids, ReadingListIncludes includes = ReadingListIncludes.Items);
Task<IEnumerable<ReadingList>> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items);
Task<ReadingListInfoDto?> GetReadingListInfoAsync(int readingListId);
}
public class ReadingListRepository : IReadingListRepository
@ -105,6 +110,7 @@ public class ReadingListRepository : IReadingListRepository
.SelectMany(r => r.Items.Select(ri => ri.Chapter.CoverImage))
.Where(t => !string.IsNullOrEmpty(t))
.ToListAsync();
return data
.OrderBy(_ => random.Next())
.Take(4)
@ -119,12 +125,19 @@ public class ReadingListRepository : IReadingListRepository
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized));
}
public IEnumerable<PersonDto> GetReadingListCharactersAsync(int readingListId)
public async Task<bool> ReadingListExistsForUser(string name, int userId)
{
var normalized = name.ToNormalized();
return await _context.ReadingList
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId);
}
public IEnumerable<PersonDto> GetReadingListPeopleAsync(int readingListId, PersonRole role)
{
return _context.ReadingListItem
.Where(item => item.ReadingListId == readingListId)
.SelectMany(item => item.Chapter.People)
.Where(p => p.Role == PersonRole.Character)
.Where(p => p.Role == role)
.OrderBy(p => p.Person.NormalizedName)
.Select(p => p.Person)
.Distinct()
@ -132,6 +145,77 @@ public class ReadingListRepository : IReadingListRepository
.AsEnumerable();
}
public async Task<ReadingListCast> GetReadingListAllPeopleAsync(int readingListId)
{
var allPeople = await _context.ReadingListItem
.Where(item => item.ReadingListId == readingListId)
.SelectMany(item => item.Chapter.People)
.OrderBy(p => p.Person.NormalizedName)
.Select(p => new
{
Role = p.Role,
Person = _mapper.Map<PersonDto>(p.Person)
})
.Distinct()
.ToListAsync();
// Create the ReadingListCast object
var cast = new ReadingListCast();
// Group people by role and populate the appropriate collections
foreach (var personGroup in allPeople.GroupBy(p => p.Role))
{
var people = personGroup.Select(pg => pg.Person).ToList();
switch (personGroup.Key)
{
case PersonRole.Writer:
cast.Writers = people;
break;
case PersonRole.CoverArtist:
cast.CoverArtists = people;
break;
case PersonRole.Publisher:
cast.Publishers = people;
break;
case PersonRole.Character:
cast.Characters = people;
break;
case PersonRole.Penciller:
cast.Pencillers = people;
break;
case PersonRole.Inker:
cast.Inkers = people;
break;
case PersonRole.Imprint:
cast.Imprints = people;
break;
case PersonRole.Colorist:
cast.Colorists = people;
break;
case PersonRole.Letterer:
cast.Letterers = people;
break;
case PersonRole.Editor:
cast.Editors = people;
break;
case PersonRole.Translator:
cast.Translators = people;
break;
case PersonRole.Team:
cast.Teams = people;
break;
case PersonRole.Location:
cast.Locations = people;
break;
case PersonRole.Other:
break;
}
}
return cast;
}
public async Task<IList<ReadingList>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
{
var extension = encodeFormat.GetExtension();
@ -170,7 +254,41 @@ public class ReadingListRepository : IReadingListRepository
.AsSplitQuery()
.ToListAsync();
}
public async Task<IEnumerable<ReadingList>> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items)
{
return await _context.ReadingList
.Where(rl => rl.Items.Any(rli => rli.SeriesId == seriesId))
.Includes(includes)
.AsSplitQuery()
.ToListAsync();
}
/// <summary>
/// Returns a Partial ReadingListInfoDto. The HourEstimate needs to be calculated outside the repo
/// </summary>
/// <param name="readingListId"></param>
/// <returns></returns>
public async Task<ReadingListInfoDto?> GetReadingListInfoAsync(int readingListId)
{
// Get sum of these across all ReadingListItems: long wordCount, int pageCount, bool isEpub (assume false if any ReadingListeItem.Series.Format is non-epub)
var readingList = await _context.ReadingList
.Where(rl => rl.Id == readingListId)
.Include(rl => rl.Items)
.ThenInclude(item => item.Series)
.Include(rl => rl.Items)
.ThenInclude(item => item.Volume)
.Include(rl => rl.Items)
.ThenInclude(item => item.Chapter)
.Select(rl => new ReadingListInfoDto()
{
WordCount = rl.Items.Sum(item => item.Chapter.WordCount),
Pages = rl.Items.Sum(item => item.Chapter.Pages),
IsAllEpub = rl.Items.All(item => item.Series.Format == MangaFormat.Epub),
})
.FirstOrDefaultAsync();
return readingList;
}
public void Remove(ReadingListItem item)
@ -343,8 +461,10 @@ public class ReadingListRepository : IReadingListRepository
public async Task<ReadingListDto?> GetReadingListDtoByIdAsync(int readingListId, int userId)
{
var user = await _context.AppUser.FirstAsync(u => u.Id == userId);
return await _context.ReadingList
.Where(r => r.Id == readingListId && (r.AppUserId == userId || r.Promoted))
.RestrictAgainstAgeRestriction(user.GetAgeRestriction())
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
.SingleOrDefaultAsync();
}

View file

@ -12,6 +12,7 @@ using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface IScrobbleRepository
{
@ -166,11 +167,11 @@ public class ScrobbleRepository : IScrobbleRepository
var query = _context.ScrobbleEvent
.Where(e => e.AppUserId == userId)
.Include(e => e.Series)
.SortBy(filter.Field, filter.IsDescending)
.WhereIf(!string.IsNullOrEmpty(filter.Query), s =>
EF.Functions.Like(s.Series.Name, $"%{filter.Query}%")
)
.WhereIf(!filter.IncludeReviews, e => e.ScrobbleEventType != ScrobbleEventType.Review)
.SortBy(filter.Field, filter.IsDescending)
.AsSplitQuery()
.ProjectTo<ScrobbleEventDto>(_mapper.ConfigurationProvider);

View file

@ -39,6 +39,7 @@ using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
[Flags]
public enum SeriesIncludes

View file

@ -7,11 +7,13 @@ using API.DTOs.Settings;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Metadata;
using API.Entities.MetadataMatching;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface ISettingsRepository
{

View file

@ -8,6 +8,7 @@ using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface ISiteThemeRepository
{

View file

@ -11,6 +11,7 @@ using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
public interface ITagRepository
{

View file

@ -23,6 +23,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
[Flags]
public enum AppUserIncludes
@ -56,7 +57,9 @@ public interface IUserRepository
void Delete(AppUser? user);
void Delete(AppUserBookmark bookmark);
void Delete(IEnumerable<AppUserDashboardStream> streams);
void Delete(AppUserDashboardStream stream);
void Delete(IEnumerable<AppUserSideNavStream> streams);
void Delete(AppUserSideNavStream stream);
Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true);
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
Task<bool> IsUserAdminAsync(AppUser? user);
@ -94,6 +97,7 @@ public interface IUserRepository
Task<IList<AppUserDashboardStream>> GetDashboardStreamWithFilter(int filterId);
Task<IList<SideNavStreamDto>> GetSideNavStreams(int userId, bool visibleOnly = false);
Task<AppUserSideNavStream?> GetSideNavStream(int streamId);
Task<AppUserSideNavStream?> GetSideNavStreamWithUser(int streamId);
Task<IList<AppUserSideNavStream>> GetSideNavStreamWithFilter(int filterId);
Task<IList<AppUserSideNavStream>> GetSideNavStreamsByLibraryId(int libraryId);
Task<IList<AppUserSideNavStream>> GetSideNavStreamWithExternalSource(int externalSourceId);
@ -166,11 +170,21 @@ public class UserRepository : IUserRepository
_context.AppUserDashboardStream.RemoveRange(streams);
}
public void Delete(AppUserDashboardStream stream)
{
_context.AppUserDashboardStream.Remove(stream);
}
public void Delete(IEnumerable<AppUserSideNavStream> streams)
{
_context.AppUserSideNavStream.RemoveRange(streams);
}
public void Delete(AppUserSideNavStream stream)
{
_context.AppUserSideNavStream.Remove(stream);
}
/// <summary>
/// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags.
/// </summary>
@ -395,6 +409,7 @@ public class UserRepository : IUserRepository
.FirstOrDefaultAsync(d => d.Id == streamId);
}
public async Task<IList<AppUserDashboardStream>> GetDashboardStreamWithFilter(int filterId)
{
return await _context.AppUserDashboardStream
@ -431,10 +446,10 @@ public class UserRepository : IUserRepository
.Select(d => d.LibraryId)
.ToList();
var libraryDtos = _context.Library
var libraryDtos = await _context.Library
.Where(l => libraryIds.Contains(l.Id))
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
.ToList();
.ToListAsync();
foreach (var dto in sideNavStreams.Where(dto => dto.StreamType == SideNavStreamType.Library))
{
@ -458,13 +473,21 @@ public class UserRepository : IUserRepository
return sideNavStreams;
}
public async Task<AppUserSideNavStream> GetSideNavStream(int streamId)
public async Task<AppUserSideNavStream?> GetSideNavStream(int streamId)
{
return await _context.AppUserSideNavStream
.Include(d => d.SmartFilter)
.FirstOrDefaultAsync(d => d.Id == streamId);
}
public async Task<AppUserSideNavStream?> GetSideNavStreamWithUser(int streamId)
{
return await _context.AppUserSideNavStream
.Include(d => d.SmartFilter)
.Include(d => d.AppUser)
.FirstOrDefaultAsync(d => d.Id == streamId);
}
public async Task<IList<AppUserSideNavStream>> GetSideNavStreamWithFilter(int filterId)
{
return await _context.AppUserSideNavStream
@ -546,7 +569,16 @@ public class UserRepository : IUserRepository
public async Task<IList<string>> GetRoles(int userId)
{
var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId);
if (user == null || _userManager == null) return ArraySegment<string>.Empty; // userManager is null on Unit Tests only
if (user == null) return ArraySegment<string>.Empty;
if (_userManager == null)
{
// userManager is null on Unit Tests only
return await _context.UserRoles
.Where(ur => ur.UserId == userId)
.Select(ur => ur.Role.Name)
.ToListAsync();
}
return await _userManager.GetRolesAsync(user);
}

View file

@ -15,6 +15,7 @@ using Kavita.Common;
using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
#nullable enable
[Flags]
public enum VolumeIncludes

View file

@ -11,6 +11,7 @@ using API.Data.Repositories;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Enums.Theme;
using API.Entities.MetadataMatching;
using API.Extensions;
using API.Services;
using Kavita.Common;