Merged develop in

This commit is contained in:
Joseph Milazzo 2025-06-20 12:35:57 -05:00
commit 2af01b654d
256 changed files with 17954 additions and 3884 deletions

View file

@ -153,6 +153,9 @@ public class AccountController : BaseApiController
// Assign default streams
AddDefaultStreamsToUser(user);
// Assign default reading profile
await AddDefaultReadingProfileToUser(user);
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
if (string.IsNullOrEmpty(token)) return BadRequest(await _localizationService.Get("en", "confirm-token-gen"));
if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Get("en", "validate-email", token));
@ -609,7 +612,7 @@ public class AccountController : BaseApiController
}
/// <summary>
/// Requests the Invite Url for the UserId. Will return error if user is already validated.
/// Requests the Invite Url for the AppUserId. Will return error if user is already validated.
/// </summary>
/// <param name="userId"></param>
/// <param name="withBaseUrl">Include the "https://ip:port/" in the generated link</param>
@ -669,6 +672,9 @@ public class AccountController : BaseApiController
// Assign default streams
AddDefaultStreamsToUser(user);
// Assign default reading profile
await AddDefaultReadingProfileToUser(user);
// Assign Roles
var roles = dto.Roles;
var hasAdminRole = dto.Roles.Contains(PolicyConstants.AdminRole);
@ -779,6 +785,16 @@ public class AccountController : BaseApiController
}
}
private async Task AddDefaultReadingProfileToUser(AppUser user)
{
var profile = new AppUserReadingProfileBuilder(user.Id)
.WithName("Default Profile")
.WithKind(ReadingProfileKind.Default)
.Build();
_unitOfWork.AppUserReadingProfileRepository.Add(profile);
await _unitOfWork.CommitAsync();
}
/// <summary>
/// Last step in authentication flow, confirms the email token for email
/// </summary>

View file

@ -6,8 +6,10 @@ using System.Threading.Tasks;
using API.Constants;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.DTOs.Filtering;
using API.DTOs.Metadata;
using API.DTOs.Metadata.Browse;
using API.DTOs.Person;
using API.DTOs.Recommendation;
using API.DTOs.SeriesDetail;
@ -46,6 +48,22 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(User.GetUserId(), ids, context));
}
/// <summary>
/// Returns a list of Genres with counts for counts when Genre is on Series/Chapter
/// </summary>
/// <returns></returns>
[HttpPost("genres-with-counts")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)]
public async Task<ActionResult<PagedList<BrowseGenreDto>>> GetBrowseGenres(UserParams? userParams = null)
{
userParams ??= UserParams.Default;
var list = await unitOfWork.GenreRepository.GetBrowseableGenre(User.GetUserId(), userParams);
Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages);
return Ok(list);
}
/// <summary>
/// Fetches people from the instance by role
/// </summary>
@ -95,6 +113,22 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
return Ok(await unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(User.GetUserId()));
}
/// <summary>
/// Returns a list of Tags with counts for counts when Tag is on Series/Chapter
/// </summary>
/// <returns></returns>
[HttpPost("tags-with-counts")]
[ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)]
public async Task<ActionResult<PagedList<BrowseTagDto>>> GetBrowseTags(UserParams? userParams = null)
{
userParams ??= UserParams.Default;
var list = await unitOfWork.TagRepository.GetBrowseableTag(User.GetUserId(), userParams);
Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages);
return Ok(list);
}
/// <summary>
/// Fetches all age ratings from the instance
/// </summary>

View file

@ -4,6 +4,9 @@ using System.Threading.Tasks;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.DTOs.Filtering.v2;
using API.DTOs.Metadata.Browse;
using API.DTOs.Metadata.Browse.Requests;
using API.DTOs.Person;
using API.Entities.Enums;
using API.Extensions;
@ -77,11 +80,13 @@ public class PersonController : BaseApiController
/// <param name="userParams"></param>
/// <returns></returns>
[HttpPost("all")]
public async Task<ActionResult<PagedList<BrowsePersonDto>>> GetAuthorsForBrowse([FromQuery] UserParams? userParams)
public async Task<ActionResult<PagedList<BrowsePersonDto>>> GetPeopleForBrowse(BrowsePersonFilterDto filter, [FromQuery] UserParams? userParams)
{
userParams ??= UserParams.Default;
var list = await _unitOfWork.PersonRepository.GetAllWritersAndSeriesCount(User.GetUserId(), userParams);
var list = await _unitOfWork.PersonRepository.GetBrowsePersonDtos(User.GetUserId(), filter, userParams);
Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages);
return Ok(list);
}
@ -112,6 +117,7 @@ public class PersonController : BaseApiController
person.Name = dto.Name?.Trim();
person.NormalizedName = person.Name.ToNormalized();
person.Description = dto.Description ?? string.Empty;
person.CoverImageLocked = dto.CoverImageLocked;

View file

@ -45,7 +45,7 @@ public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService
throw new KavitaUnauthenticatedUserException();
}
var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId);
logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName.Replace(Environment.NewLine, string.Empty), user!.UserName, userId);
logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({AppUserId})'s API Key", pluginName.Replace(Environment.NewLine, string.Empty), user!.UserName, userId);
return new UserDto
{

View file

@ -0,0 +1,198 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.Extensions;
using API.Services;
using AutoMapper;
using Kavita.Common;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace API.Controllers;
[Route("api/reading-profile")]
public class ReadingProfileController(ILogger<ReadingProfileController> logger, IUnitOfWork unitOfWork,
IReadingProfileService readingProfileService): BaseApiController
{
/// <summary>
/// Gets all non-implicit reading profiles for a user
/// </summary>
/// <returns></returns>
[HttpGet("all")]
public async Task<ActionResult<IList<UserReadingProfileDto>>> GetAllReadingProfiles()
{
return Ok(await unitOfWork.AppUserReadingProfileRepository.GetProfilesDtoForUser(User.GetUserId(), true));
}
/// <summary>
/// Returns the ReadingProfile that should be applied to the given series, walks up the tree.
/// Series -> Library -> Default
/// </summary>
/// <param name="seriesId"></param>
/// <param name="skipImplicit"></param>
/// <returns></returns>
[HttpGet("{seriesId:int}")]
public async Task<ActionResult<UserReadingProfileDto>> GetProfileForSeries(int seriesId, [FromQuery] bool skipImplicit)
{
return Ok(await readingProfileService.GetReadingProfileDtoForSeries(User.GetUserId(), seriesId, skipImplicit));
}
/// <summary>
/// Returns the (potential) Reading Profile bound to the library
/// </summary>
/// <param name="libraryId"></param>
/// <returns></returns>
[HttpGet("library")]
public async Task<ActionResult<UserReadingProfileDto?>> GetProfileForLibrary(int libraryId)
{
return Ok(await readingProfileService.GetReadingProfileDtoForLibrary(User.GetUserId(), libraryId));
}
/// <summary>
/// Creates a new reading profile for the current user
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("create")]
public async Task<ActionResult<UserReadingProfileDto>> CreateReadingProfile([FromBody] UserReadingProfileDto dto)
{
return Ok(await readingProfileService.CreateReadingProfile(User.GetUserId(), dto));
}
/// <summary>
/// Promotes the implicit profile to a user profile. Removes the series from other profiles
/// </summary>
/// <param name="profileId"></param>
/// <returns></returns>
[HttpPost("promote")]
public async Task<ActionResult<UserReadingProfileDto>> PromoteImplicitReadingProfile([FromQuery] int profileId)
{
return Ok(await readingProfileService.PromoteImplicitProfile(User.GetUserId(), profileId));
}
/// <summary>
/// Update the implicit reading profile for a series, creates one if none exists
/// </summary>
/// <remarks>Any modification to the reader settings during reading will create an implicit profile. Use "update-parent" to save to the bound series profile.</remarks>
/// <param name="dto"></param>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpPost("series")]
public async Task<ActionResult<UserReadingProfileDto>> UpdateReadingProfileForSeries([FromBody] UserReadingProfileDto dto, [FromQuery] int seriesId)
{
var updatedProfile = await readingProfileService.UpdateImplicitReadingProfile(User.GetUserId(), seriesId, dto);
return Ok(updatedProfile);
}
/// <summary>
/// Updates the non-implicit reading profile for the given series, and removes implicit profiles
/// </summary>
/// <param name="dto"></param>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpPost("update-parent")]
public async Task<ActionResult<UserReadingProfileDto>> UpdateParentProfileForSeries([FromBody] UserReadingProfileDto dto, [FromQuery] int seriesId)
{
var newParentProfile = await readingProfileService.UpdateParent(User.GetUserId(), seriesId, dto);
return Ok(newParentProfile);
}
/// <summary>
/// Updates the given reading profile, must belong to the current user
/// </summary>
/// <param name="dto"></param>
/// <returns>The updated reading profile</returns>
/// <remarks>
/// This does not update connected series and libraries.
/// </remarks>
[HttpPost]
public async Task<ActionResult<UserReadingProfileDto>> UpdateReadingProfile(UserReadingProfileDto dto)
{
return Ok(await readingProfileService.UpdateReadingProfile(User.GetUserId(), dto));
}
/// <summary>
/// Deletes the given profile, requires the profile to belong to the logged-in user
/// </summary>
/// <param name="profileId"></param>
/// <returns></returns>
/// <exception cref="KavitaException"></exception>
/// <exception cref="UnauthorizedAccessException"></exception>
[HttpDelete]
public async Task<IActionResult> DeleteReadingProfile([FromQuery] int profileId)
{
await readingProfileService.DeleteReadingProfile(User.GetUserId(), profileId);
return Ok();
}
/// <summary>
/// Sets the reading profile for a given series, removes the old one
/// </summary>
/// <param name="seriesId"></param>
/// <param name="profileId"></param>
/// <returns></returns>
[HttpPost("series/{seriesId:int}")]
public async Task<IActionResult> AddProfileToSeries(int seriesId, [FromQuery] int profileId)
{
await readingProfileService.AddProfileToSeries(User.GetUserId(), profileId, seriesId);
return Ok();
}
/// <summary>
/// Clears the reading profile for the given series for the currently logged-in user
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpDelete("series/{seriesId:int}")]
public async Task<IActionResult> ClearSeriesProfile(int seriesId)
{
await readingProfileService.ClearSeriesProfile(User.GetUserId(), seriesId);
return Ok();
}
/// <summary>
/// Sets the reading profile for a given library, removes the old one
/// </summary>
/// <param name="libraryId"></param>
/// <param name="profileId"></param>
/// <returns></returns>
[HttpPost("library/{libraryId:int}")]
public async Task<IActionResult> AddProfileToLibrary(int libraryId, [FromQuery] int profileId)
{
await readingProfileService.AddProfileToLibrary(User.GetUserId(), profileId, libraryId);
return Ok();
}
/// <summary>
/// Clears the reading profile for the given library for the currently logged-in user
/// </summary>
/// <param name="libraryId"></param>
/// <param name="profileId"></param>
/// <returns></returns>
[HttpDelete("library/{libraryId:int}")]
public async Task<IActionResult> ClearLibraryProfile(int libraryId)
{
await readingProfileService.ClearLibraryProfile(User.GetUserId(), libraryId);
return Ok();
}
/// <summary>
/// Assigns the reading profile to all passes series, and deletes their implicit profiles
/// </summary>
/// <param name="profileId"></param>
/// <param name="seriesIds"></param>
/// <returns></returns>
[HttpPost("bulk")]
public async Task<IActionResult> BulkAddReadingProfile([FromQuery] int profileId, [FromBody] IList<int> seriesIds)
{
await readingProfileService.BulkAddProfileToSeries(User.GetUserId(), profileId, seriesIds);
return Ok();
}
}

View file

@ -310,7 +310,7 @@ public class SeriesController : BaseApiController
/// </summary>
/// <param name="filterDto"></param>
/// <param name="userParams"></param>
/// <param name="libraryId"></param>
/// <param name="libraryId">This is not in use</param>
/// <returns></returns>
[HttpPost("all-v2")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesV2(FilterV2Dto filterDto, [FromQuery] UserParams userParams,
@ -321,8 +321,6 @@ public class SeriesController : BaseApiController
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto, context);
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series"));
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);

View file

@ -103,38 +103,13 @@ public class UsersController : BaseApiController
var existingPreferences = user!.UserPreferences;
existingPreferences.ReadingDirection = preferencesDto.ReadingDirection;
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
existingPreferences.AutoCloseMenu = preferencesDto.AutoCloseMenu;
existingPreferences.ShowScreenHints = preferencesDto.ShowScreenHints;
existingPreferences.EmulateBook = preferencesDto.EmulateBook;
existingPreferences.ReaderMode = preferencesDto.ReaderMode;
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
existingPreferences.BackgroundColor = string.IsNullOrEmpty(preferencesDto.BackgroundColor) ? "#000000" : preferencesDto.BackgroundColor;
existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin;
existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing;
existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily;
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
existingPreferences.BookReaderWritingStyle = preferencesDto.BookReaderWritingStyle;
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode;
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
existingPreferences.GlobalPageLayoutMode = preferencesDto.GlobalPageLayoutMode;
existingPreferences.BlurUnreadSummaries = preferencesDto.BlurUnreadSummaries;
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
existingPreferences.NoTransitions = preferencesDto.NoTransitions;
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships;
existingPreferences.ShareReviews = preferencesDto.ShareReviews;
existingPreferences.PdfTheme = preferencesDto.PdfTheme;
existingPreferences.PdfScrollMode = preferencesDto.PdfScrollMode;
existingPreferences.PdfSpreadMode = preferencesDto.PdfSpreadMode;
if (await _licenseService.HasActiveLicense())
{
existingPreferences.AniListScrobblingEnabled = preferencesDto.AniListScrobblingEnabled;