Localization - First Pass (#2174)
* Started designing the backend localization service * Worked in Transloco for initial PoC * Worked in Transloco for initial PoC * Translated the login screen * translated dashboard screen * Started work on the backend * Fixed a logic bug * translated edit-user screen * Hooked up the backend for having a locale property. * Hooked up the ability to view the available locales and switch to them. * Made the localization service languages be derived from what's in langs/ directory. * Fixed up localization switching * Switched when we check for a license on UI bootstrap * Tweaked some code * Fixed the bug where dashboard wasn't loading and made it so language switching is working. * Fixed a bug on dashboard with languagePath * Converted user-scrobble-history.component.html * Converted spoiler.component.html * Converted review-series-modal.component.html * Converted review-card-modal.component.html * Updated the readme * Translated using Weblate (English) Currently translated at 100.0% (54 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/en/ * Converted review-card.component.html * Deleted dead component * Converted want-to-read.component.html * Added translation using Weblate (Korean) * Translated using Weblate (Spanish) Currently translated at 40.7% (22 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/ * Translated using Weblate (Korean) Currently translated at 62.9% (34 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/ * Converted user-preferences.component.html * Translated using Weblate (Korean) Currently translated at 92.5% (50 of 54 strings) Translation: Kavita/ui Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/ * Converted user-holds.component.html * Converted theme-manager.component.html * Converted restriction-selector.component.html * Converted manage-devices.component.html * Converted edit-device.component.html * Converted change-password.component.html * Converted change-email.component.html * Converted change-age-restriction.component.html * Converted api-key.component.html * Converted anilist-key.component.html * Converted typeahead.component.html * Converted user-stats-info-cards.component.html * Converted user-stats.component.html * Converted top-readers.component.html * Converted some pipes and ensure translation is loaded before the app. * Finished all but one pipe for localization * Converted directory-picker.component.html * Converted library-access-modal.component.html * Converted a few components * Converted a few components * Converted a few components * Converted a few components * Converted a few components * Merged weblate in * ... -> … update * Updated the readme * Updateded all fonts to be woff2 * Cleaned up some strings to increase re-use * Removed an old flow (that doesn't exist in backend any longer) from when we introduced emails on Kavita. * Converted Series detail * Lots more converted * Lots more converted & hooked up the ability to flatten during prod build the language files. * Lots more converted * Lots more converted & fixed a bunch of broken pipes due to inject() * Lots more converted * Lots more converted * Lots more converted & fixed some bad keys * Lots more converted * Fixed some bugs with admin dasbhoard nested tabs not rendering on first load due to not using onpush change detection * Fixed up some localization errors and fixed forgot password error when the user doesn't have change password permission * Fixed a stupid build issue again * Started adding errors for interceptor and backend. * Finished off manga-reader * More translations * Few fixes * Fixed a bug where character tag badges weren't showing the name on chapter info * All components are translated * All toasts are translated * All confirm/alerts are translated * Trying something new for the backend * Migrated the localization strings for the backend into a new file. * Updated the localization service to be able to do backend localization with fallback to english. * Cleaned up some external reviews code to reduce looping * Localized AccountController.cs * 60% done with controllers * All controllers are done * All KavitaExceptions are covered * Some shakeout fixes * Prep for initial merge * Everything is done except options and basic shakeout proves response times are good. Unit tests are broken. * Fixed up the unit tests * All unit tests are now working * Removed some quantifier * I'm not sure I can support localization for some Volume/Chapter/Book strings within the codebase. --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: majora2007 <kavitareader@gmail.com> Co-authored-by: expertjun <jtrobin@naver.com> Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
This commit is contained in:
parent
670bf82c38
commit
3b23d63234
389 changed files with 13652 additions and 7925 deletions
|
@ -191,6 +191,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Folder Include="config\themes" />
|
||||
<Folder Include="I18N\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -14,12 +14,9 @@ using API.Entities.Enums;
|
|||
using API.Errors;
|
||||
using API.Extensions;
|
||||
using API.Helpers.Builders;
|
||||
using API.Middleware.RateLimit;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
using EasyCaching.Core;
|
||||
using Hangfire;
|
||||
using Kavita.Common;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
|
@ -46,6 +43,7 @@ public class AccountController : BaseApiController
|
|||
private readonly IAccountService _accountService;
|
||||
private readonly IEmailService _emailService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public AccountController(UserManager<AppUser> userManager,
|
||||
|
@ -53,7 +51,8 @@ public class AccountController : BaseApiController
|
|||
ITokenService tokenService, IUnitOfWork unitOfWork,
|
||||
ILogger<AccountController> logger,
|
||||
IMapper mapper, IAccountService accountService,
|
||||
IEmailService emailService, IEventHub eventHub)
|
||||
IEmailService emailService, IEventHub eventHub,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
|
@ -64,6 +63,7 @@ public class AccountController : BaseApiController
|
|||
_accountService = accountService;
|
||||
_emailService = emailService;
|
||||
_eventHub = eventHub;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -82,19 +82,21 @@ public class AccountController : BaseApiController
|
|||
var isAdmin = User.IsInRole(PolicyConstants.AdminRole);
|
||||
|
||||
if (resetPasswordDto.UserName == User.GetUsername() && !(User.IsInRole(PolicyConstants.ChangePasswordRole) || isAdmin))
|
||||
return Unauthorized("You are not permitted to this operation.");
|
||||
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
if (resetPasswordDto.UserName != User.GetUsername() && !isAdmin)
|
||||
return Unauthorized("You are not permitted to this operation.");
|
||||
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
if (string.IsNullOrEmpty(resetPasswordDto.OldPassword) && !isAdmin)
|
||||
return BadRequest(new ApiException(400, "You must enter your existing password to change your account unless you're an admin"));
|
||||
return BadRequest(
|
||||
new ApiException(400,
|
||||
await _localizationService.Translate(User.GetUserId(), "password-required")));
|
||||
|
||||
// If you're an admin and the username isn't yours, you don't need to validate the password
|
||||
var isResettingOtherUser = (resetPasswordDto.UserName != User.GetUsername() && isAdmin);
|
||||
if (!isResettingOtherUser && !await _userManager.CheckPasswordAsync(user, resetPasswordDto.OldPassword))
|
||||
{
|
||||
return BadRequest("Invalid Password");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-password"));
|
||||
}
|
||||
|
||||
var errors = await _accountService.ChangeUserPassword(user, resetPasswordDto.Password);
|
||||
|
@ -117,7 +119,7 @@ public class AccountController : BaseApiController
|
|||
public async Task<ActionResult<UserDto>> RegisterFirstUser(RegisterDto registerDto)
|
||||
{
|
||||
var admins = await _userManager.GetUsersInRoleAsync("Admin");
|
||||
if (admins.Count > 0) return BadRequest("Not allowed");
|
||||
if (admins.Count > 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "denied"));
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -135,8 +137,8 @@ public class AccountController : BaseApiController
|
|||
if (!result.Succeeded) return BadRequest(result.Errors);
|
||||
|
||||
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
if (string.IsNullOrEmpty(token)) return BadRequest("There was an issue generating a confirmation token.");
|
||||
if (!await ConfirmEmailToken(token, user)) return BadRequest($"There was an issue validating your email: {token}");
|
||||
if (string.IsNullOrEmpty(token)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "confirm-token-gen"));
|
||||
if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "validate-email", token));
|
||||
|
||||
|
||||
var roleResult = await _userManager.AddToRoleAsync(user, PolicyConstants.AdminRole);
|
||||
|
@ -163,7 +165,7 @@ public class AccountController : BaseApiController
|
|||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Something went wrong when registering user");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "register-user"));
|
||||
}
|
||||
|
||||
|
||||
|
@ -180,9 +182,9 @@ public class AccountController : BaseApiController
|
|||
.Include(u => u.UserPreferences)
|
||||
.SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper());
|
||||
|
||||
if (user == null) return Unauthorized("Your credentials are not correct");
|
||||
if (user == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "bad-credentials"));
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized("Your account is disabled. Contact the server admin.");
|
||||
if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "disabled-account"));
|
||||
|
||||
var result = await _signInManager
|
||||
.CheckPasswordSignInAsync(user, loginDto.Password, true);
|
||||
|
@ -190,12 +192,12 @@ public class AccountController : BaseApiController
|
|||
if (result.IsLockedOut)
|
||||
{
|
||||
await _userManager.UpdateSecurityStampAsync(user);
|
||||
return Unauthorized("You've been locked out from too many authorization attempts. Please wait 10 minutes.");
|
||||
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "locked-out"));
|
||||
}
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return Unauthorized(result.IsNotAllowed ? "You must confirm your email first" : "Your credentials are not correct");
|
||||
return Unauthorized(await _localizationService.Translate(User.GetUserId(), result.IsNotAllowed ? "confirm-email" : "bad-credentials"));
|
||||
}
|
||||
|
||||
// Update LastActive on account
|
||||
|
@ -256,7 +258,7 @@ public class AccountController : BaseApiController
|
|||
var token = await _tokenService.ValidateRefreshToken(tokenRequestDto);
|
||||
if (token == null)
|
||||
{
|
||||
return Unauthorized(new { message = "Invalid token" });
|
||||
return Unauthorized(new { message = await _localizationService.Translate(User.GetUserId(), "invalid-token") });
|
||||
}
|
||||
|
||||
return Ok(token);
|
||||
|
@ -295,7 +297,7 @@ public class AccountController : BaseApiController
|
|||
}
|
||||
|
||||
await _unitOfWork.RollbackAsync();
|
||||
return BadRequest("Something went wrong, unable to reset key");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "unable-to-reset-key"));
|
||||
|
||||
}
|
||||
|
||||
|
@ -310,26 +312,27 @@ public class AccountController : BaseApiController
|
|||
public async Task<ActionResult> UpdateEmail(UpdateEmailDto? dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized("You do not have permission");
|
||||
if (user == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
if (dto == null || string.IsNullOrEmpty(dto.Email) || string.IsNullOrEmpty(dto.Password)) return BadRequest("Invalid payload");
|
||||
if (dto == null || string.IsNullOrEmpty(dto.Email) || string.IsNullOrEmpty(dto.Password))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload"));
|
||||
|
||||
|
||||
// Validate this user's password
|
||||
if (! await _userManager.CheckPasswordAsync(user, dto.Password))
|
||||
{
|
||||
_logger.LogCritical("A user tried to change {UserName}'s email, but password didn't validate", user.UserName);
|
||||
return BadRequest("You do not have permission");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
}
|
||||
|
||||
// Validate no other users exist with this email
|
||||
if (user.Email!.Equals(dto.Email)) return Ok("Nothing to do");
|
||||
if (user.Email!.Equals(dto.Email)) return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
|
||||
// Check if email is used by another user
|
||||
var existingUserEmail = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
if (existingUserEmail != null)
|
||||
{
|
||||
return BadRequest("You cannot share emails across multiple accounts");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "share-multiple-emails"));
|
||||
}
|
||||
|
||||
// All validations complete, generate a new token and email it to the user at the new address. Confirm email link will update the email
|
||||
|
@ -337,7 +340,7 @@ public class AccountController : BaseApiController
|
|||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
_logger.LogError("There was an issue generating a token for the email");
|
||||
return BadRequest("There was an issue creating a confirmation email token. See logs.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generate-token"));
|
||||
}
|
||||
|
||||
user.EmailConfirmed = false;
|
||||
|
@ -392,10 +395,10 @@ public class AccountController : BaseApiController
|
|||
public async Task<ActionResult> UpdateAgeRestriction(UpdateAgeRestrictionDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized("You do not have permission");
|
||||
if (user == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
if (!await _accountService.HasChangeRestrictionRole(user)) return BadRequest("You do not have permission");
|
||||
if (!await _accountService.HasChangeRestrictionRole(user)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRating;
|
||||
user.AgeRestrictionIncludeUnknowns = isAdmin || dto.IncludeUnknowns;
|
||||
|
@ -410,7 +413,7 @@ public class AccountController : BaseApiController
|
|||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an error updating the age restriction");
|
||||
return BadRequest("There was an error updating the age restriction");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "age-restriction-update"));
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
|
||||
|
@ -429,17 +432,17 @@ public class AccountController : BaseApiController
|
|||
{
|
||||
var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (adminUser == null) return Unauthorized();
|
||||
if (!await _unitOfWork.UserRepository.IsUserAdminAsync(adminUser)) return Unauthorized("You do not have permission");
|
||||
if (!await _unitOfWork.UserRepository.IsUserAdminAsync(adminUser)) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(dto.UserId);
|
||||
if (user == null) return BadRequest("User does not exist");
|
||||
if (user == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-user"));
|
||||
|
||||
// Check if username is changing
|
||||
if (!user.UserName!.Equals(dto.Username))
|
||||
{
|
||||
// Validate username change
|
||||
var errors = await _accountService.ValidateUsername(dto.Username);
|
||||
if (errors.Any()) return BadRequest("Username already taken");
|
||||
if (errors.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "username-taken"));
|
||||
user.UserName = dto.Username;
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
}
|
||||
|
@ -504,7 +507,7 @@ public class AccountController : BaseApiController
|
|||
}
|
||||
|
||||
await _unitOfWork.RollbackAsync();
|
||||
return BadRequest("There was an exception when updating the user");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-user-update"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -520,9 +523,9 @@ public class AccountController : BaseApiController
|
|||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.EmailConfirmed)
|
||||
return BadRequest("User is already confirmed");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-confirmed"));
|
||||
if (string.IsNullOrEmpty(user.ConfirmationToken))
|
||||
return BadRequest("Manual setup is unable to be completed. Please cancel and recreate the invite.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "manual-setup-fail"));
|
||||
|
||||
return await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl);
|
||||
}
|
||||
|
@ -539,7 +542,7 @@ public class AccountController : BaseApiController
|
|||
public async Task<ActionResult<string>> InviteUser(InviteUserDto dto)
|
||||
{
|
||||
var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (adminUser == null) return Unauthorized("You are not permitted");
|
||||
if (adminUser == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
_logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email);
|
||||
|
||||
|
@ -552,8 +555,8 @@ public class AccountController : BaseApiController
|
|||
{
|
||||
var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
if (await _userManager.IsEmailConfirmedAsync(invitedUser!))
|
||||
return BadRequest($"User is already registered as {invitedUser!.UserName}");
|
||||
return BadRequest("User is already invited under this email and has yet to accepted invite.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-registered", invitedUser!.UserName));
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-invited"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -608,7 +611,7 @@ public class AccountController : BaseApiController
|
|||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
_logger.LogError("There was an issue generating a token for the email");
|
||||
return BadRequest("There was an creating the invite user");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-user"));
|
||||
}
|
||||
|
||||
user.ConfirmationToken = token;
|
||||
|
@ -650,7 +653,7 @@ public class AccountController : BaseApiController
|
|||
_logger.LogError(ex, "There was an error during invite user flow, unable to send an email");
|
||||
}
|
||||
|
||||
return BadRequest("There was an error setting up your account. Please check the logs");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-user"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -667,7 +670,7 @@ public class AccountController : BaseApiController
|
|||
if (user == null)
|
||||
{
|
||||
_logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email);
|
||||
return BadRequest("Invalid email confirmation");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-email-confirmation"));
|
||||
}
|
||||
|
||||
// Validate Password and Username
|
||||
|
@ -688,7 +691,7 @@ public class AccountController : BaseApiController
|
|||
if (!await ConfirmEmailToken(dto.Token, user))
|
||||
{
|
||||
_logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token);
|
||||
return BadRequest("Invalid email confirmation");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-email-confirmation"));
|
||||
}
|
||||
|
||||
user.UserName = dto.Username;
|
||||
|
@ -731,13 +734,13 @@ public class AccountController : BaseApiController
|
|||
if (user == null)
|
||||
{
|
||||
_logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email);
|
||||
return BadRequest("Invalid email confirmation");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-email-confirmation"));
|
||||
}
|
||||
|
||||
if (!await ConfirmEmailToken(dto.Token, user))
|
||||
{
|
||||
_logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token);
|
||||
return BadRequest("Invalid email confirmation");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-email-confirmation"));
|
||||
}
|
||||
|
||||
_logger.LogInformation("User is updating email from {OldEmail} to {NewEmail}", user.Email, dto.Email);
|
||||
|
@ -745,7 +748,7 @@ public class AccountController : BaseApiController
|
|||
if (!result.Succeeded)
|
||||
{
|
||||
_logger.LogError("Unable to update email for users: {Errors}", result.Errors.Select(e => e.Description));
|
||||
return BadRequest("Unable to update email for user. Check logs");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-user-email-update"));
|
||||
}
|
||||
user.ConfirmationToken = null;
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
@ -768,7 +771,7 @@ public class AccountController : BaseApiController
|
|||
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("Invalid credentials");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bad-credentials"));
|
||||
}
|
||||
|
||||
var result = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider,
|
||||
|
@ -776,16 +779,16 @@ public class AccountController : BaseApiController
|
|||
if (!result)
|
||||
{
|
||||
_logger.LogInformation("Unable to reset password, your email token is not correct: {@Dto}", dto);
|
||||
return BadRequest("Invalid credentials");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bad-credentials"));
|
||||
}
|
||||
|
||||
var errors = await _accountService.ChangeUserPassword(user, dto.Password);
|
||||
return errors.Any() ? BadRequest(errors) : Ok("Password updated");
|
||||
return errors.Any() ? BadRequest(errors) : Ok(await _localizationService.Translate(User.GetUserId(), "password-updated"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an unexpected error when confirming new password");
|
||||
return BadRequest("There was an unexpected error when confirming new password");
|
||||
return BadRequest("generic-password-update");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -804,15 +807,15 @@ public class AccountController : BaseApiController
|
|||
if (user == null)
|
||||
{
|
||||
_logger.LogError("There are no users with email: {Email} but user is requesting password reset", email);
|
||||
return Ok("An email will be sent to the email if it exists in our database");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "forgot-password-generic"));
|
||||
}
|
||||
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole))
|
||||
return Unauthorized("You are not permitted to this operation.");
|
||||
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
|
||||
if (string.IsNullOrEmpty(user.Email) || !user.EmailConfirmed)
|
||||
return BadRequest("You do not have an email on account or it has not been confirmed");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "confirm-email"));
|
||||
|
||||
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email);
|
||||
|
@ -825,10 +828,10 @@ public class AccountController : BaseApiController
|
|||
ServerConfirmationLink = emailLink,
|
||||
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
|
||||
});
|
||||
return Ok("Email sent");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "email-sent"));
|
||||
}
|
||||
|
||||
return Ok("Your server is not accessible. The Link to reset your password is in the logs.");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "not-accessible-password"));
|
||||
}
|
||||
|
||||
[HttpGet("email-confirmed")]
|
||||
|
@ -845,12 +848,12 @@ public class AccountController : BaseApiController
|
|||
public async Task<ActionResult<UserDto>> ConfirmMigrationEmail(ConfirmMigrationEmailDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
if (user == null) return BadRequest("Invalid credentials");
|
||||
if (user == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "bad-credentials"));
|
||||
|
||||
if (!await ConfirmEmailToken(dto.Token, user))
|
||||
{
|
||||
_logger.LogInformation("confirm-migration-email email token is invalid");
|
||||
return BadRequest("Invalid credentials");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bad-credentials"));
|
||||
}
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
@ -881,12 +884,12 @@ public class AccountController : BaseApiController
|
|||
public async Task<ActionResult<string>> ResendConfirmationSendEmail([FromQuery] int userId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user == null) return BadRequest("User does not exist");
|
||||
if (user == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-user"));
|
||||
|
||||
if (string.IsNullOrEmpty(user.Email))
|
||||
return BadRequest(
|
||||
"This user needs to migrate. Have them log out and login to trigger a migration flow");
|
||||
if (user.EmailConfirmed) return BadRequest("User already confirmed");
|
||||
await _localizationService.Translate(User.GetUserId(), "user-migration-needed"));
|
||||
if (user.EmailConfirmed) return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-confirmed"));
|
||||
|
||||
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-email", user.Email);
|
||||
|
@ -907,12 +910,12 @@ public class AccountController : BaseApiController
|
|||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an issue resending invite email");
|
||||
return BadRequest("There was an issue resending invite email");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-email"));
|
||||
}
|
||||
return Ok(emailLink);
|
||||
}
|
||||
|
||||
return Ok("The server is not accessible externally");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "not-accessible"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -926,7 +929,7 @@ public class AccountController : BaseApiController
|
|||
{
|
||||
// If there is an admin account already, return
|
||||
var users = await _unitOfWork.UserRepository.GetAdminUsersAsync();
|
||||
if (users.Any()) return BadRequest("Admin already exists");
|
||||
if (users.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "admin-already-exists"));
|
||||
|
||||
// Check if there is an existing invite
|
||||
var emailValidationErrors = await _accountService.ValidateEmail(dto.Email);
|
||||
|
@ -934,27 +937,27 @@ public class AccountController : BaseApiController
|
|||
{
|
||||
var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||
if (await _userManager.IsEmailConfirmedAsync(invitedUser!))
|
||||
return BadRequest($"User is already registered as {invitedUser!.UserName}");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-registered", invitedUser!.UserName));
|
||||
|
||||
_logger.LogInformation("A user is attempting to login, but hasn't accepted email invite");
|
||||
return BadRequest("User is already invited under this email and has yet to accepted invite.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-invited"));
|
||||
}
|
||||
|
||||
|
||||
var user = await _userManager.Users
|
||||
.Include(u => u.UserPreferences)
|
||||
.SingleOrDefaultAsync(x => x.NormalizedUserName == dto.Username.ToUpper());
|
||||
if (user == null) return BadRequest("Invalid username");
|
||||
if (user == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-username"));
|
||||
|
||||
var validPassword = await _signInManager.UserManager.CheckPasswordAsync(user, dto.Password);
|
||||
if (!validPassword) return BadRequest("Your credentials are not correct");
|
||||
if (!validPassword) return BadRequest(await _localizationService.Translate(User.GetUserId(), "bad-credentials"));
|
||||
|
||||
try
|
||||
{
|
||||
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
|
||||
user.Email = dto.Email;
|
||||
if (!await ConfirmEmailToken(token, user)) return BadRequest("There was a critical error during migration");
|
||||
if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "critical-email-migration"));
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
@ -968,7 +971,7 @@ public class AccountController : BaseApiController
|
|||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
return BadRequest("There was an error setting up your account. Please check the logs");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "critical-email-migration"));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||
using API.Data;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -18,13 +19,16 @@ public class BookController : BaseApiController
|
|||
private readonly IBookService _bookService;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public BookController(IBookService bookService,
|
||||
IUnitOfWork unitOfWork, ICacheService cacheService)
|
||||
IUnitOfWork unitOfWork, ICacheService cacheService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_bookService = bookService;
|
||||
_unitOfWork = unitOfWork;
|
||||
_cacheService = cacheService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -37,7 +41,7 @@ public class BookController : BaseApiController
|
|||
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
|
||||
{
|
||||
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||
if (dto == null) return BadRequest("Chapter does not exist");
|
||||
if (dto == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var bookTitle = string.Empty;
|
||||
switch (dto.SeriesFormat)
|
||||
{
|
||||
|
@ -92,14 +96,14 @@ public class BookController : BaseApiController
|
|||
[AllowAnonymous]
|
||||
public async Task<ActionResult> GetBookPageResources(int chapterId, [FromQuery] string file)
|
||||
{
|
||||
if (chapterId <= 0) return BadRequest("Chapter is not valid");
|
||||
if (chapterId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
if (chapter == null) return BadRequest("Chapter is not valid");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
|
||||
|
||||
var key = BookService.CoalesceKeyForAnyFile(book, file);
|
||||
|
||||
if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) return BadRequest("File was not found in book");
|
||||
if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "file-missing"));
|
||||
|
||||
var bookFile = book.Content.AllFiles.GetLocalFileRefByKey(key);
|
||||
var content = await bookFile.ReadContentAsBytesAsync();
|
||||
|
@ -118,9 +122,9 @@ public class BookController : BaseApiController
|
|||
[HttpGet("{chapterId}/chapters")]
|
||||
public async Task<ActionResult<ICollection<BookChapterItem>>> GetBookChapters(int chapterId)
|
||||
{
|
||||
if (chapterId <= 0) return BadRequest("Chapter is not valid");
|
||||
if (chapterId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
if (chapter == null) return BadRequest("Chapter is not valid");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -144,7 +148,7 @@ public class BookController : BaseApiController
|
|||
public async Task<ActionResult<string>> GetBookPage(int chapterId, [FromQuery] int page)
|
||||
{
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
if (chapter == null) return BadRequest("Could not find Chapter");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var path = _cacheService.GetCachedFile(chapter);
|
||||
|
||||
var baseUrl = "//" + Request.Host + Request.PathBase + "/api/";
|
||||
|
@ -155,7 +159,7 @@ public class BookController : BaseApiController
|
|||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,12 +20,15 @@ public class CollectionController : BaseApiController
|
|||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ICollectionTagService _collectionService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CollectionController(IUnitOfWork unitOfWork, ICollectionTagService collectionService)
|
||||
public CollectionController(IUnitOfWork unitOfWork, ICollectionTagService collectionService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_collectionService = collectionService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -87,14 +90,14 @@ public class CollectionController : BaseApiController
|
|||
{
|
||||
try
|
||||
{
|
||||
if (await _collectionService.UpdateTag(updatedTag)) return Ok("Tag updated successfully");
|
||||
if (await _collectionService.UpdateTag(updatedTag)) return Ok(await _localizationService.Translate(User.GetUserId(), "collection-updated-successfully"));
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
|
||||
return BadRequest("Something went wrong, please try again");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -111,7 +114,7 @@ public class CollectionController : BaseApiController
|
|||
|
||||
if (await _collectionService.AddTagToSeries(tag, dto.SeriesIds)) return Ok();
|
||||
|
||||
return BadRequest("There was an issue updating series with collection tag");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -126,18 +129,17 @@ public class CollectionController : BaseApiController
|
|||
try
|
||||
{
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updateSeriesForTagDto.Tag.Id, CollectionTagIncludes.SeriesMetadata);
|
||||
if (tag == null) return BadRequest("Not a valid Tag");
|
||||
if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist"));
|
||||
tag.SeriesMetadatas ??= new List<SeriesMetadata>();
|
||||
|
||||
if (await _collectionService.RemoveTagFromSeries(tag, updateSeriesForTagDto.SeriesIdsToRemove))
|
||||
return Ok("Tag updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "collection-updated"));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
|
||||
return BadRequest("Something went wrong. Please try again.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,13 +21,16 @@ public class DeviceController : BaseApiController
|
|||
private readonly IDeviceService _deviceService;
|
||||
private readonly IEmailService _emailService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public DeviceController(IUnitOfWork unitOfWork, IDeviceService deviceService, IEmailService emailService, IEventHub eventHub)
|
||||
public DeviceController(IUnitOfWork unitOfWork, IDeviceService deviceService,
|
||||
IEmailService emailService, IEventHub eventHub, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_deviceService = deviceService;
|
||||
_emailService = emailService;
|
||||
_eventHub = eventHub;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
|
||||
|
@ -36,9 +39,19 @@ public class DeviceController : BaseApiController
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
|
||||
if (user == null) return Unauthorized();
|
||||
var device = await _deviceService.Create(dto, user);
|
||||
try
|
||||
{
|
||||
var device = await _deviceService.Create(dto, user);
|
||||
if (device == null)
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-device-create"));
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (device == null) return BadRequest("There was an error when creating the device");
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
@ -50,7 +63,7 @@ public class DeviceController : BaseApiController
|
|||
if (user == null) return Unauthorized();
|
||||
var device = await _deviceService.Update(dto, user);
|
||||
|
||||
if (device == null) return BadRequest("There was an error when updating the device");
|
||||
if (device == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-device-update"));
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
@ -63,12 +76,12 @@ public class DeviceController : BaseApiController
|
|||
[HttpDelete]
|
||||
public async Task<ActionResult> DeleteDevice(int deviceId)
|
||||
{
|
||||
if (deviceId <= 0) return BadRequest("Not a valid deviceId");
|
||||
if (deviceId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "device-doesnt-exist"));
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
|
||||
if (user == null) return Unauthorized();
|
||||
if (await _deviceService.Delete(user, deviceId)) return Ok();
|
||||
|
||||
return BadRequest("Could not delete device");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-device-delete"));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
@ -81,15 +94,16 @@ public class DeviceController : BaseApiController
|
|||
[HttpPost("send-to")]
|
||||
public async Task<ActionResult> SendToDevice(SendToDeviceDto dto)
|
||||
{
|
||||
if (dto.ChapterIds.Any(i => i < 0)) return BadRequest("ChapterIds must be greater than 0");
|
||||
if (dto.DeviceId < 0) return BadRequest("DeviceId must be greater than 0");
|
||||
if (dto.ChapterIds.Any(i => i < 0)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "ChapterIds"));
|
||||
if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "DeviceId"));
|
||||
|
||||
if (await _emailService.IsDefaultEmailService())
|
||||
return BadRequest("Send to device cannot be used with Kavita's email service. Please configure your own.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email"));
|
||||
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.SendingToDeviceEvent($"Transferring files to your device", "started"), userId);
|
||||
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
|
||||
"started"), userId);
|
||||
try
|
||||
{
|
||||
var success = await _deviceService.SendTo(dto.ChapterIds, dto.DeviceId);
|
||||
|
@ -97,15 +111,16 @@ public class DeviceController : BaseApiController
|
|||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice,
|
||||
MessageFactory.SendingToDeviceEvent($"Transferring files to your device", "ended"), userId);
|
||||
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
|
||||
"ended"), userId);
|
||||
}
|
||||
|
||||
return BadRequest("There was an error sending the file to the device");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-send-to"));
|
||||
}
|
||||
|
||||
|
||||
|
@ -113,19 +128,21 @@ public class DeviceController : BaseApiController
|
|||
[HttpPost("send-series-to")]
|
||||
public async Task<ActionResult> SendSeriesToDevice(SendSeriesToDeviceDto dto)
|
||||
{
|
||||
if (dto.SeriesId <= 0) return BadRequest("SeriesId must be greater than 0");
|
||||
if (dto.DeviceId < 0) return BadRequest("DeviceId must be greater than 0");
|
||||
if (dto.SeriesId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "SeriesId"));
|
||||
if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "DeviceId"));
|
||||
|
||||
if (await _emailService.IsDefaultEmailService())
|
||||
return BadRequest("Send to device cannot be used with Kavita's email service. Please configure your own.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email"));
|
||||
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, MessageFactory.SendingToDeviceEvent($"Transferring files to your device", "started"), userId);
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
|
||||
"started"), userId);
|
||||
|
||||
var series =
|
||||
await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId,
|
||||
SeriesIncludes.Volumes | SeriesIncludes.Chapters);
|
||||
if (series == null) return BadRequest("Series doesn't Exist");
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist"));
|
||||
var chapterIds = series.Volumes.SelectMany(v => v.Chapters.Select(c => c.Id)).ToList();
|
||||
try
|
||||
{
|
||||
|
@ -134,14 +151,16 @@ public class DeviceController : BaseApiController
|
|||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice, MessageFactory.SendingToDeviceEvent($"Transferring files to your device", "ended"), userId);
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice,
|
||||
MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"),
|
||||
"ended"), userId);
|
||||
}
|
||||
|
||||
return BadRequest("There was an error sending the file(s) to the device");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-send-to"));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -30,11 +30,12 @@ public class DownloadController : BaseApiController
|
|||
private readonly ILogger<DownloadController> _logger;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private const string DefaultContentType = "application/octet-stream";
|
||||
|
||||
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService,
|
||||
IDownloadService downloadService, IEventHub eventHub, ILogger<DownloadController> logger, IBookmarkService bookmarkService,
|
||||
IAccountService accountService)
|
||||
IAccountService accountService, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_archiveService = archiveService;
|
||||
|
@ -44,6 +45,7 @@ public class DownloadController : BaseApiController
|
|||
_logger = logger;
|
||||
_bookmarkService = bookmarkService;
|
||||
_accountService = accountService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -92,9 +94,9 @@ public class DownloadController : BaseApiController
|
|||
[HttpGet("volume")]
|
||||
public async Task<ActionResult> DownloadVolume(int volumeId)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId);
|
||||
if (volume == null) return BadRequest("Volume doesn't exist");
|
||||
if (volume == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "volume-doesnt-exist"));
|
||||
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
|
||||
try
|
||||
|
@ -128,10 +130,10 @@ public class DownloadController : BaseApiController
|
|||
[HttpGet("chapter")]
|
||||
public async Task<ActionResult> DownloadChapter(int chapterId)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
if (chapter == null) return BadRequest("Invalid chapter");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume!.SeriesId);
|
||||
try
|
||||
|
@ -178,7 +180,7 @@ public class DownloadController : BaseApiController
|
|||
[HttpGet("series")]
|
||||
public async Task<ActionResult> DownloadSeries(int seriesId)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||
if (series == null) return BadRequest("Invalid Series");
|
||||
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
||||
|
@ -200,8 +202,8 @@ public class DownloadController : BaseApiController
|
|||
[HttpPost("bookmarks")]
|
||||
public async Task<ActionResult> DownloadBookmarkPages(DownloadBookmarkDto downloadBookmarkDto)
|
||||
{
|
||||
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
|
||||
if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest("Bookmarks cannot be empty");
|
||||
if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||
if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmarks-empty"));
|
||||
|
||||
// We know that all bookmarks will be for one single seriesId
|
||||
var userId = User.GetUserId()!;
|
||||
|
|
|
@ -22,13 +22,16 @@ public class ImageController : BaseApiController
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService, IImageService imageService)
|
||||
public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService,
|
||||
IImageService imageService, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_directoryService = directoryService;
|
||||
_imageService = imageService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -42,7 +45,7 @@ public class ImageController : BaseApiController
|
|||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-cover-image"));
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
|
@ -59,7 +62,7 @@ public class ImageController : BaseApiController
|
|||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-cover-image"));
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
|
@ -76,7 +79,7 @@ public class ImageController : BaseApiController
|
|||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-cover-image"));
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
|
@ -93,7 +96,7 @@ public class ImageController : BaseApiController
|
|||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId));
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-cover-image"));
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
Response.AddCacheHeader(path);
|
||||
|
@ -115,7 +118,7 @@ public class ImageController : BaseApiController
|
|||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
|
||||
{
|
||||
var destFile = await GenerateCollectionCoverImage(collectionTagId);
|
||||
if (string.IsNullOrEmpty(destFile)) return BadRequest("No cover image");
|
||||
if (string.IsNullOrEmpty(destFile)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-cover-image"));
|
||||
return PhysicalFile(destFile, MimeTypeMap.GetMimeType(_directoryService.FileSystem.Path.GetExtension(destFile)), _directoryService.FileSystem.Path.GetFileName(destFile));
|
||||
}
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
@ -137,7 +140,7 @@ public class ImageController : BaseApiController
|
|||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
|
||||
{
|
||||
var destFile = await GenerateReadingListCoverImage(readingListId);
|
||||
if (string.IsNullOrEmpty(destFile)) return BadRequest("No cover image");
|
||||
if (string.IsNullOrEmpty(destFile)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-cover-image"));
|
||||
return PhysicalFile(destFile, MimeTypeMap.GetMimeType(_directoryService.FileSystem.Path.GetExtension(destFile)), _directoryService.FileSystem.Path.GetFileName(destFile));
|
||||
}
|
||||
|
||||
|
@ -199,7 +202,7 @@ public class ImageController : BaseApiController
|
|||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
var bookmark = await _unitOfWork.UserRepository.GetBookmarkForPage(pageNum, chapterId, userId);
|
||||
if (bookmark == null) return BadRequest("Bookmark does not exist");
|
||||
if (bookmark == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-doesnt-exist"));
|
||||
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
|
@ -220,7 +223,7 @@ public class ImageController : BaseApiController
|
|||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
|
||||
if (userId == 0) return BadRequest();
|
||||
if (string.IsNullOrEmpty(url)) return BadRequest("Url cannot be null");
|
||||
if (string.IsNullOrEmpty(url)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "must-be-defined", "Url"));
|
||||
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
|
||||
// Check if the domain exists
|
||||
|
@ -235,7 +238,7 @@ public class ImageController : BaseApiController
|
|||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return BadRequest("There was an issue fetching favicon for domain");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-favicon"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -256,10 +259,11 @@ public class ImageController : BaseApiController
|
|||
public async Task<ActionResult> GetCoverUploadImage(string filename, string apiKey)
|
||||
{
|
||||
if (await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey) == 0) return BadRequest();
|
||||
if (filename.Contains("..")) return BadRequest("Invalid Filename");
|
||||
if (filename.Contains("..")) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-filename"));
|
||||
|
||||
var path = Path.Join(_directoryService.TempDirectory, filename);
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"File does not exist");
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "file-doesnt-exist"));
|
||||
var format = _directoryService.FileSystem.Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path));
|
||||
|
|
|
@ -36,13 +36,14 @@ public class LibraryController : BaseApiController
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILibraryWatcher _libraryWatcher;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IEasyCachingProvider _libraryCacheProvider;
|
||||
private const string CacheKey = "library_";
|
||||
|
||||
public LibraryController(IDirectoryService directoryService,
|
||||
ILogger<LibraryController> logger, IMapper mapper, ITaskScheduler taskScheduler,
|
||||
IUnitOfWork unitOfWork, IEventHub eventHub, ILibraryWatcher libraryWatcher,
|
||||
IEasyCachingProviderFactory cachingProviderFactory)
|
||||
IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService)
|
||||
{
|
||||
_directoryService = directoryService;
|
||||
_logger = logger;
|
||||
|
@ -51,6 +52,7 @@ public class LibraryController : BaseApiController
|
|||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
_libraryWatcher = libraryWatcher;
|
||||
_localizationService = localizationService;
|
||||
|
||||
_libraryCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.Library);
|
||||
}
|
||||
|
@ -66,7 +68,7 @@ public class LibraryController : BaseApiController
|
|||
{
|
||||
if (await _unitOfWork.LibraryRepository.LibraryExists(dto.Name))
|
||||
{
|
||||
return BadRequest("Library name already exists. Please choose a unique name to the server.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-name-exists"));
|
||||
}
|
||||
|
||||
var library = new LibraryBuilder(dto.Name, dto.Type)
|
||||
|
@ -96,7 +98,7 @@ public class LibraryController : BaseApiController
|
|||
}
|
||||
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue. Please try again.");
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library"));
|
||||
|
||||
_logger.LogInformation("Created a new library: {LibraryName}", library.Name);
|
||||
await _libraryWatcher.RestartWatching();
|
||||
|
@ -160,7 +162,8 @@ public class LibraryController : BaseApiController
|
|||
public async Task<ActionResult<IEnumerable<JumpKeyDto>>> GetJumpBar(int libraryId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, userId)) return BadRequest("User does not have access to library");
|
||||
if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, userId))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-library-access"));
|
||||
|
||||
return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId));
|
||||
}
|
||||
|
@ -175,9 +178,9 @@ public class LibraryController : BaseApiController
|
|||
public async Task<ActionResult<MemberDto>> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username);
|
||||
if (user == null) return BadRequest("Could not validate user");
|
||||
if (user == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-doesnt-exist"));
|
||||
|
||||
var libraryString = string.Join(",", updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name));
|
||||
var libraryString = string.Join(',', updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name));
|
||||
_logger.LogInformation("Granting user {UserName} access to: {Libraries}", updateLibraryForUserDto.Username, libraryString);
|
||||
|
||||
var allLibraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
|
||||
|
@ -195,7 +198,6 @@ public class LibraryController : BaseApiController
|
|||
{
|
||||
library.AppUsers.Add(user);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!_unitOfWork.HasChanges())
|
||||
|
@ -213,7 +215,7 @@ public class LibraryController : BaseApiController
|
|||
}
|
||||
|
||||
|
||||
return BadRequest("There was a critical issue. Please try again.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -224,9 +226,9 @@ public class LibraryController : BaseApiController
|
|||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("scan")]
|
||||
public ActionResult Scan(int libraryId, bool force = false)
|
||||
public async Task<ActionResult> Scan(int libraryId, bool force = false)
|
||||
{
|
||||
if (libraryId <= 0) return BadRequest("Invalid libraryId");
|
||||
if (libraryId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "libraryId"));
|
||||
_taskScheduler.ScanLibrary(libraryId, force);
|
||||
return Ok();
|
||||
}
|
||||
|
@ -277,7 +279,7 @@ public class LibraryController : BaseApiController
|
|||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
if (!isAdmin) return BadRequest("API key must belong to an admin");
|
||||
|
||||
if (dto.FolderPath.Contains("..")) return BadRequest("Invalid Path");
|
||||
if (dto.FolderPath.Contains("..")) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-path"));
|
||||
|
||||
dto.FolderPath = Services.Tasks.Scanner.Parser.Parser.NormalizePath(dto.FolderPath);
|
||||
|
||||
|
@ -310,12 +312,11 @@ public class LibraryController : BaseApiController
|
|||
if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId))
|
||||
{
|
||||
_logger.LogInformation("User is attempting to delete a library while a scan is in progress");
|
||||
return BadRequest(
|
||||
"You cannot delete a library while a scan is in progress. Please wait for scan to complete or restart Kavita then try to delete");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "delete-library-while-scan"));
|
||||
}
|
||||
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
|
||||
if (library == null) return BadRequest("Library no longer exists");
|
||||
if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist"));
|
||||
|
||||
// Due to a bad schema that I can't figure out how to fix, we need to erase all RelatedSeries before we delete the library
|
||||
// Aka SeriesRelation has an invalid foreign key
|
||||
|
@ -354,7 +355,7 @@ public class LibraryController : BaseApiController
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was a critical error trying to delete the library");
|
||||
_logger.LogError(ex, await _localizationService.Translate(User.GetUserId(), "generic-library"));
|
||||
await _unitOfWork.RollbackAsync();
|
||||
return Ok(false);
|
||||
}
|
||||
|
@ -384,11 +385,11 @@ public class LibraryController : BaseApiController
|
|||
public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto dto)
|
||||
{
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders);
|
||||
if (library == null) return BadRequest("Library doesn't exist");
|
||||
if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist"));
|
||||
|
||||
var newName = dto.Name.Trim();
|
||||
if (await _unitOfWork.LibraryRepository.LibraryExists(newName) && !library.Name.Equals(newName))
|
||||
return BadRequest("Library name already exists");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-name-exists"));
|
||||
|
||||
var originalFolders = library.Folders.Select(x => x.Path).ToList();
|
||||
|
||||
|
@ -416,7 +417,7 @@ public class LibraryController : BaseApiController
|
|||
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue updating the library.");
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library-update"));
|
||||
if (originalFolders.Count != dto.Folders.Count() || typeUpdate)
|
||||
{
|
||||
await _libraryWatcher.RestartWatching();
|
||||
|
|
|
@ -5,6 +5,8 @@ using API.Data;
|
|||
using API.DTOs.Account;
|
||||
using API.DTOs.License;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -18,13 +20,15 @@ public class LicenseController : BaseApiController
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILogger<LicenseController> _logger;
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public LicenseController(IUnitOfWork unitOfWork, ILogger<LicenseController> logger,
|
||||
ILicenseService licenseService)
|
||||
ILicenseService licenseService, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_logger = logger;
|
||||
_licenseService = licenseService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -73,7 +77,14 @@ public class LicenseController : BaseApiController
|
|||
[HttpPost]
|
||||
public async Task<ActionResult> UpdateLicense(UpdateLicenseDto dto)
|
||||
{
|
||||
await _licenseService.AddLicense(dto.License.Trim(), dto.Email.Trim());
|
||||
try
|
||||
{
|
||||
await _licenseService.AddLicense(dto.License.Trim(), dto.Email.Trim());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
|
30
API/Controllers/LocaleController.cs
Normal file
30
API/Controllers/LocaleController.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using API.DTOs.Filtering;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
public class LocaleController : BaseApiController
|
||||
{
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public LocaleController(ILocalizationService localizationService)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult<IEnumerable<string>> GetAllLocales()
|
||||
{
|
||||
var languages = _localizationService.GetLocales().Select(c => new CultureInfo(c)).Select(c =>
|
||||
new LanguageDto()
|
||||
{
|
||||
Title = c.DisplayName,
|
||||
IsoCode = c.IetfLanguageTag
|
||||
}).Where(l => !string.IsNullOrEmpty(l.IsoCode));
|
||||
return Ok(languages);
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ using API.DTOs.Filtering;
|
|||
using API.DTOs.Metadata;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using Kavita.Common.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
@ -19,10 +20,12 @@ namespace API.Controllers;
|
|||
public class MetadataController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public MetadataController(IUnitOfWork unitOfWork)
|
||||
public MetadataController(IUnitOfWork unitOfWork, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -35,7 +38,7 @@ public class MetadataController : BaseApiController
|
|||
public async Task<ActionResult<IList<GenreTagDto>>> GetAllGenres(string? libraryIds)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var ids = libraryIds?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids != null && ids.Count > 0)
|
||||
{
|
||||
return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, userId));
|
||||
|
@ -56,7 +59,7 @@ public class MetadataController : BaseApiController
|
|||
public async Task<ActionResult<IList<PersonDto>>> GetAllPeople(string? libraryIds)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var ids = libraryIds?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids != null && ids.Count > 0)
|
||||
{
|
||||
return Ok(await _unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, userId));
|
||||
|
@ -74,7 +77,7 @@ public class MetadataController : BaseApiController
|
|||
public async Task<ActionResult<IList<TagDto>>> GetAllTags(string? libraryIds)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var ids = libraryIds?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids != null && ids.Count > 0)
|
||||
{
|
||||
return Ok(await _unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, userId));
|
||||
|
@ -92,7 +95,7 @@ public class MetadataController : BaseApiController
|
|||
[HttpGet("age-ratings")]
|
||||
public async Task<ActionResult<IList<AgeRatingDto>>> GetAllAgeRatings(string? libraryIds)
|
||||
{
|
||||
var ids = libraryIds?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids != null && ids.Count > 0)
|
||||
{
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetAllAgeRatingsDtosForLibrariesAsync(ids));
|
||||
|
@ -115,7 +118,7 @@ public class MetadataController : BaseApiController
|
|||
[HttpGet("publication-status")]
|
||||
public ActionResult<IList<AgeRatingDto>> GetAllPublicationStatus(string? libraryIds)
|
||||
{
|
||||
var ids = libraryIds?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids is {Count: > 0})
|
||||
{
|
||||
return Ok(_unitOfWork.LibraryRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
|
||||
|
@ -138,7 +141,7 @@ public class MetadataController : BaseApiController
|
|||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})]
|
||||
public async Task<ActionResult<IList<LanguageDto>>> GetAllLanguages(string? libraryIds)
|
||||
{
|
||||
var ids = libraryIds?.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList();
|
||||
if (ids is {Count: > 0})
|
||||
{
|
||||
return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids));
|
||||
|
@ -168,9 +171,9 @@ public class MetadataController : BaseApiController
|
|||
[HttpGet("chapter-summary")]
|
||||
public async Task<ActionResult<string>> GetChapterSummary(int chapterId)
|
||||
{
|
||||
if (chapterId <= 0) return BadRequest("Chapter does not exist");
|
||||
if (chapterId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
if (chapter == null) return BadRequest("Chapter does not exist");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
return Ok(chapter.Summary);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ using API.Entities.Enums;
|
|||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using EasyCaching.Core;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -37,6 +36,7 @@ public class OpdsController : BaseApiController
|
|||
private readonly IReaderService _readerService;
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
|
||||
private readonly XmlSerializer _xmlSerializer;
|
||||
|
@ -71,7 +71,7 @@ public class OpdsController : BaseApiController
|
|||
public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
|
||||
IDirectoryService directoryService, ICacheService cacheService,
|
||||
IReaderService readerService, ISeriesService seriesService,
|
||||
IAccountService accountService, IEasyCachingProvider provider)
|
||||
IAccountService accountService, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_downloadService = downloadService;
|
||||
|
@ -80,6 +80,7 @@ public class OpdsController : BaseApiController
|
|||
_readerService = readerService;
|
||||
_seriesService = seriesService;
|
||||
_accountService = accountService;
|
||||
_localizationService = localizationService;
|
||||
|
||||
_xmlSerializer = new XmlSerializer(typeof(Feed));
|
||||
_xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription));
|
||||
|
@ -90,8 +91,9 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> Get(string apiKey)
|
||||
{
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
|
||||
|
@ -100,10 +102,10 @@ public class OpdsController : BaseApiController
|
|||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "onDeck",
|
||||
Title = "On Deck",
|
||||
Title = await _localizationService.Translate(userId, "on-deck"),
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = "Browse by On Deck"
|
||||
Text = await _localizationService.Translate(userId, "browse-on-deck")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
|
@ -113,10 +115,10 @@ public class OpdsController : BaseApiController
|
|||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "recentlyAdded",
|
||||
Title = "Recently Added",
|
||||
Title = await _localizationService.Translate(userId, "recently-added"),
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = "Browse by Recently Added"
|
||||
Text = await _localizationService.Translate(userId, "browse-recently-added")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
|
@ -126,10 +128,10 @@ public class OpdsController : BaseApiController
|
|||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "readingList",
|
||||
Title = "Reading Lists",
|
||||
Title = await _localizationService.Translate(userId, "reading-lists"),
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = "Browse by Reading Lists"
|
||||
Text = await _localizationService.Translate(userId, "browse-reading-lists")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
|
@ -139,10 +141,10 @@ public class OpdsController : BaseApiController
|
|||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "allLibraries",
|
||||
Title = "All Libraries",
|
||||
Title = await _localizationService.Translate(userId, "libraries"),
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = "Browse by Libraries"
|
||||
Text = await _localizationService.Translate(userId, "browse-libraries")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
|
@ -152,10 +154,10 @@ public class OpdsController : BaseApiController
|
|||
feed.Entries.Add(new FeedEntry()
|
||||
{
|
||||
Id = "allCollections",
|
||||
Title = "All Collections",
|
||||
Title = await _localizationService.Translate(userId, "collections"),
|
||||
Content = new FeedEntryContent()
|
||||
{
|
||||
Text = "Browse by Collections"
|
||||
Text = await _localizationService.Translate(userId, "browse-collections")
|
||||
},
|
||||
Links = new List<FeedLink>()
|
||||
{
|
||||
|
@ -183,12 +185,12 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetLibraries(string apiKey)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var libraries = await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId);
|
||||
var feed = CreateFeed("All Libraries", $"{prefix}{apiKey}/libraries", apiKey, prefix);
|
||||
var feed = CreateFeed(await _localizationService.Translate(userId, "libraries"), $"{prefix}{apiKey}/libraries", apiKey, prefix);
|
||||
SetFeedId(feed, "libraries");
|
||||
foreach (var library in libraries)
|
||||
{
|
||||
|
@ -210,10 +212,10 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetCollections(string apiKey)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user == null) return Unauthorized();
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
@ -222,7 +224,7 @@ public class OpdsController : BaseApiController
|
|||
: (await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(userId));
|
||||
|
||||
|
||||
var feed = CreateFeed("All Collections", $"{prefix}{apiKey}/collections", apiKey, prefix);
|
||||
var feed = CreateFeed(await _localizationService.Translate(userId, "collections"), $"{prefix}{apiKey}/collections", apiKey, prefix);
|
||||
SetFeedId(feed, "collections");
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
|
@ -248,10 +250,10 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetCollection(int collectionId, string apiKey, [FromQuery] int pageNumber = 0)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user == null) return Unauthorized();
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
@ -292,10 +294,10 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetReadingLists(string apiKey, [FromQuery] int pageNumber = 0)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
|
||||
var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId,
|
||||
true, GetUserParams(pageNumber), false);
|
||||
|
@ -333,10 +335,10 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetReadingListItems(int readingListId, string apiKey)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
|
||||
var userWithLists = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user!.UserName!, AppUserIncludes.ReadingListsWithItems);
|
||||
|
@ -344,10 +346,10 @@ public class OpdsController : BaseApiController
|
|||
var readingList = userWithLists.ReadingLists.SingleOrDefault(t => t.Id == readingListId);
|
||||
if (readingList == null)
|
||||
{
|
||||
return BadRequest("Reading list does not exist or you don't have access");
|
||||
return BadRequest(await _localizationService.Translate(userId, "reading-list-restricted"));
|
||||
}
|
||||
|
||||
var feed = CreateFeed(readingList.Title + " Reading List", $"{prefix}{apiKey}/reading-list/{readingListId}", apiKey, prefix);
|
||||
var feed = CreateFeed(readingList.Title + " " + await _localizationService.Translate(userId, "reading-list"), $"{prefix}{apiKey}/reading-list/{readingListId}", apiKey, prefix);
|
||||
SetFeedId(feed, $"reading-list-{readingListId}");
|
||||
|
||||
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId)).ToList();
|
||||
|
@ -364,16 +366,16 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetSeriesForLibrary(int libraryId, string apiKey, [FromQuery] int pageNumber = 0)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var library =
|
||||
(await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).SingleOrDefault(l =>
|
||||
l.Id == libraryId);
|
||||
if (library == null)
|
||||
{
|
||||
return BadRequest("User does not have access to this library");
|
||||
return BadRequest(await _localizationService.Translate(userId, "no-library-access"));
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, GetUserParams(pageNumber), _filterDto);
|
||||
|
@ -395,14 +397,14 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetRecentlyAdded(string apiKey, [FromQuery] int pageNumber = 1)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var recentlyAdded = await _unitOfWork.SeriesRepository.GetRecentlyAdded(0, userId, GetUserParams(pageNumber), _filterDto);
|
||||
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(recentlyAdded.Select(s => s.Id));
|
||||
|
||||
var feed = CreateFeed("Recently Added", $"{prefix}{apiKey}/recently-added", apiKey, prefix);
|
||||
var feed = CreateFeed(await _localizationService.Translate(userId, "recently-added"), $"{prefix}{apiKey}/recently-added", apiKey, prefix);
|
||||
SetFeedId(feed, "recently-added");
|
||||
AddPagination(feed, recentlyAdded, $"{prefix}{apiKey}/recently-added");
|
||||
|
||||
|
@ -418,19 +420,19 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetOnDeck(string apiKey, [FromQuery] int pageNumber = 1)
|
||||
{
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
|
||||
var userId = await GetUser(apiKey);
|
||||
var userParams = GetUserParams(pageNumber);
|
||||
var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, 0, userParams, _filterDto);
|
||||
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(pagedList.Select(s => s.Id));
|
||||
|
||||
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
|
||||
|
||||
var feed = CreateFeed("On Deck", $"{prefix}{apiKey}/on-deck", apiKey, prefix);
|
||||
var feed = CreateFeed(await _localizationService.Translate(userId, "on-deck"), $"{prefix}{apiKey}/on-deck", apiKey, prefix);
|
||||
SetFeedId(feed, "on-deck");
|
||||
AddPagination(feed, pagedList, $"{prefix}{apiKey}/on-deck");
|
||||
|
||||
|
@ -446,20 +448,20 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> SearchSeries(string apiKey, [FromQuery] string query)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
return BadRequest("You must pass a query parameter");
|
||||
return BadRequest(await _localizationService.Translate(userId, "query-required"));
|
||||
}
|
||||
query = query.Replace(@"%", string.Empty);
|
||||
// Get libraries user has access to
|
||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).ToList();
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
if (!libraries.Any()) return BadRequest(await _localizationService.Translate(userId, "libraries-restricted"));
|
||||
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
||||
|
@ -518,13 +520,14 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetSearchDescriptor(string apiKey)
|
||||
{
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (_, prefix) = await GetPrefix();
|
||||
var feed = new OpenSearchDescription()
|
||||
{
|
||||
ShortName = "Search",
|
||||
Description = "Search for Series, Collections, or Reading Lists",
|
||||
ShortName = await _localizationService.Translate(userId, "search"),
|
||||
Description = await _localizationService.Translate(userId, "search-description"),
|
||||
Url = new SearchLink()
|
||||
{
|
||||
Type = FeedLinkType.AtomAcquisition,
|
||||
|
@ -542,13 +545,13 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetSeries(string apiKey, int seriesId)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
|
||||
var feed = CreateFeed(series.Name + " - Storyline", $"{prefix}{apiKey}/series/{series.Id}", apiKey, prefix);
|
||||
var feed = CreateFeed(series!.Name + " - Storyline", $"{prefix}{apiKey}/series/{series.Id}", apiKey, prefix);
|
||||
SetFeedId(feed, $"series-{series.Id}");
|
||||
feed.Links.Add(CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"{baseUrl}api/image/series-cover?seriesId={seriesId}&apiKey={apiKey}"));
|
||||
|
||||
|
@ -564,7 +567,7 @@ public class OpdsController : BaseApiController
|
|||
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id);
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, volume.Id, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volume.Id, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -576,7 +579,7 @@ public class OpdsController : BaseApiController
|
|||
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(storylineChapter.Id);
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, storylineChapter.VolumeId, storylineChapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, storylineChapter.VolumeId, storylineChapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -586,7 +589,7 @@ public class OpdsController : BaseApiController
|
|||
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(special.Id);
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, special.VolumeId, special.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, special.VolumeId, special.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -597,26 +600,26 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetVolume(string apiKey, int seriesId, int volumeId)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId);
|
||||
var chapters =
|
||||
(await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId)).OrderBy(x => double.Parse(x.Number),
|
||||
_chapterSortComparer);
|
||||
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {SeriesService.FormatChapterName(libraryType)}s ",
|
||||
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s ",
|
||||
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix);
|
||||
SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}-{SeriesService.FormatChapterName(libraryType)}s");
|
||||
SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}-{_seriesService.FormatChapterName(userId, libraryType)}s");
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapter.Id);
|
||||
var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id);
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, volumeId, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volumeId, chapter.Id, mangaFile, series, chapterTest, apiKey, prefix, baseUrl));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -627,23 +630,23 @@ public class OpdsController : BaseApiController
|
|||
[Produces("application/xml")]
|
||||
public async Task<IActionResult> GetChapter(string apiKey, int seriesId, int volumeId, int chapterId)
|
||||
{
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var (baseUrl, prefix) = await GetPrefix();
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId);
|
||||
if (chapter == null) return BadRequest("Chapter doesn't exist");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "chapter-doesnt-exist"));
|
||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId);
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
|
||||
|
||||
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {SeriesService.FormatChapterName(libraryType)}s",
|
||||
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s",
|
||||
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey, prefix);
|
||||
SetFeedId(feed, $"series-{series.Id}-volume-{volumeId}-{SeriesService.FormatChapterName(libraryType)}-{chapterId}-files");
|
||||
SetFeedId(feed, $"series-{series.Id}-volume-{volumeId}-{_seriesService.FormatChapterName(userId, libraryType)}-{chapterId}-files");
|
||||
foreach (var mangaFile in files)
|
||||
{
|
||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, volumeId, chapterId, mangaFile, series, chapter, apiKey, prefix, baseUrl));
|
||||
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volumeId, chapterId, mangaFile, series, chapter, apiKey, prefix, baseUrl));
|
||||
}
|
||||
|
||||
return CreateXmlResult(SerializeXml(feed));
|
||||
|
@ -661,8 +664,9 @@ public class OpdsController : BaseApiController
|
|||
[HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}/download/{filename}")]
|
||||
public async Task<ActionResult> DownloadFile(string apiKey, int seriesId, int volumeId, int chapterId, string filename)
|
||||
{
|
||||
var userId = await GetUser(apiKey);
|
||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||
return BadRequest("OPDS is not enabled on this server");
|
||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey));
|
||||
if (!await _accountService.HasDownloadPermission(user))
|
||||
{
|
||||
|
@ -781,7 +785,7 @@ public class OpdsController : BaseApiController
|
|||
};
|
||||
}
|
||||
|
||||
private async Task<FeedEntry> CreateChapterWithFile(int seriesId, int volumeId, int chapterId, MangaFile mangaFile, SeriesDto series, ChapterDto chapter, string apiKey, string prefix, string baseUrl)
|
||||
private async Task<FeedEntry> CreateChapterWithFile(int userId, int seriesId, int volumeId, int chapterId, MangaFile mangaFile, SeriesDto series, ChapterDto chapter, string apiKey, string prefix, string baseUrl)
|
||||
{
|
||||
var fileSize =
|
||||
mangaFile.Bytes > 0 ? DirectoryService.GetHumanReadableBytes(mangaFile.Bytes) :
|
||||
|
@ -797,7 +801,8 @@ public class OpdsController : BaseApiController
|
|||
|
||||
if (volume!.Chapters.Count == 1)
|
||||
{
|
||||
SeriesService.RenameVolumeName(volume.Chapters.First(), volume, libraryType);
|
||||
var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty);
|
||||
SeriesService.RenameVolumeName(volume.Chapters.First(), volume, libraryType, volumeLabel);
|
||||
if (volume.Name != "0")
|
||||
{
|
||||
title += $" - {volume.Name}";
|
||||
|
@ -805,11 +810,11 @@ public class OpdsController : BaseApiController
|
|||
}
|
||||
else if (volume.Number != 0)
|
||||
{
|
||||
title = $"{series.Name} - Volume {volume.Name} - {SeriesService.FormatChapterTitle(chapter, libraryType)}";
|
||||
title = $"{series.Name} - Volume {volume.Name} - {await _seriesService.FormatChapterTitle(userId, chapter, libraryType)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
title = $"{series.Name} - {SeriesService.FormatChapterTitle(chapter, libraryType)}";
|
||||
title = $"{series.Name} - {await _seriesService.FormatChapterTitle(userId, chapter, libraryType)}";
|
||||
}
|
||||
|
||||
// Chunky requires a file at the end. Our API ignores this
|
||||
|
@ -857,14 +862,16 @@ public class OpdsController : BaseApiController
|
|||
[HttpGet("{apiKey}/image")]
|
||||
public async Task<ActionResult> GetPageStreamedImage(string apiKey, [FromQuery] int libraryId, [FromQuery] int seriesId, [FromQuery] int volumeId,[FromQuery] int chapterId, [FromQuery] int pageNumber)
|
||||
{
|
||||
if (pageNumber < 0) return BadRequest("Page cannot be less than 0");
|
||||
var userId = await GetUser(apiKey);
|
||||
if (pageNumber < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "Page"));
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
if (chapter == null) return BadRequest("There was an issue finding image file for reading");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "cache-file-find"));
|
||||
|
||||
try
|
||||
{
|
||||
var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber);
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {pageNumber}");
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path))
|
||||
return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", pageNumber));
|
||||
|
||||
var content = await _directoryService.ReadFileAsync(path);
|
||||
var format = Path.GetExtension(path);
|
||||
|
@ -895,8 +902,9 @@ public class OpdsController : BaseApiController
|
|||
[ResponseCache(Duration = 60 * 60, Location = ResponseCacheLocation.Client, NoStore = false)]
|
||||
public async Task<ActionResult> GetFavicon(string apiKey)
|
||||
{
|
||||
var userId = await GetUser(apiKey);
|
||||
var files = _directoryService.GetFilesWithExtension(Path.Join(Directory.GetCurrentDirectory(), ".."), @"\.ico");
|
||||
if (files.Length == 0) return BadRequest("Cannot find icon");
|
||||
if (files.Length == 0) return BadRequest(await _localizationService.Translate(userId, "favicon-doesnt-exist"));
|
||||
var path = files[0];
|
||||
var content = await _directoryService.ReadFileAsync(path);
|
||||
var format = Path.GetExtension(path);
|
||||
|
@ -919,7 +927,7 @@ public class OpdsController : BaseApiController
|
|||
{
|
||||
/* Do nothing */
|
||||
}
|
||||
throw new KavitaException("User does not exist");
|
||||
throw new KavitaException(await _localizationService.Get("en", "user-doesnt-exist"));
|
||||
}
|
||||
|
||||
private async Task<FeedLink> CreatePageStreamLink(int libraryId, int seriesId, int volumeId, int chapterId, MangaFile mangaFile, string apiKey, string prefix)
|
||||
|
|
|
@ -16,6 +16,7 @@ using API.Services;
|
|||
using API.Services.Plus;
|
||||
using API.SignalR;
|
||||
using Hangfire;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -37,13 +38,15 @@ public class ReaderController : BaseApiController
|
|||
private readonly IAccountService _accountService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IScrobblingService _scrobblingService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReaderController(ICacheService cacheService,
|
||||
IUnitOfWork unitOfWork, ILogger<ReaderController> logger,
|
||||
IReaderService readerService, IBookmarkService bookmarkService,
|
||||
IAccountService accountService, IEventHub eventHub,
|
||||
IScrobblingService scrobblingService)
|
||||
IScrobblingService scrobblingService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_cacheService = cacheService;
|
||||
_unitOfWork = unitOfWork;
|
||||
|
@ -53,6 +56,7 @@ public class ReaderController : BaseApiController
|
|||
_accountService = accountService;
|
||||
_eventHub = eventHub;
|
||||
_scrobblingService = scrobblingService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -71,13 +75,13 @@ public class ReaderController : BaseApiController
|
|||
// Validate the user has access to the PDF
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapter.Id,
|
||||
await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()));
|
||||
if (series == null) return BadRequest("Invalid Access");
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-access"));
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
var path = _cacheService.GetCachedFile(chapter);
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"Pdf doesn't exist when it should.");
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "pdf-doesnt-exist"));
|
||||
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(Path.GetExtension(path)), Path.GetFileName(path), true);
|
||||
}
|
||||
|
@ -110,7 +114,8 @@ public class ReaderController : BaseApiController
|
|||
try
|
||||
{
|
||||
var path = _cacheService.GetCachedPagePath(chapter.Id, page);
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}. Try refreshing to allow re-cache.");
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-image-for-page", page));
|
||||
var format = Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path), true);
|
||||
|
@ -170,7 +175,7 @@ public class ReaderController : BaseApiController
|
|||
try
|
||||
{
|
||||
var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page);
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
|
||||
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-image-for-page", page));
|
||||
var format = Path.GetExtension(path);
|
||||
|
||||
return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path));
|
||||
|
@ -217,7 +222,7 @@ public class ReaderController : BaseApiController
|
|||
if (chapter == null) return NoContent();
|
||||
|
||||
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
|
||||
if (dto == null) return BadRequest("Please perform a scan on this series or library and try again");
|
||||
if (dto == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "perform-scan"));
|
||||
var mangaFile = chapter.Files.First();
|
||||
|
||||
var info = new ChapterInfoDto()
|
||||
|
@ -256,7 +261,8 @@ public class ReaderController : BaseApiController
|
|||
}
|
||||
else
|
||||
{
|
||||
info.Subtitle = "Volume " + info.VolumeNumber;
|
||||
//info.Subtitle = await _localizationService.Translate(User.GetUserId(), "volume-num", info.VolumeNumber);
|
||||
info.Subtitle = $"Volume {info.VolumeNumber}";
|
||||
if (!info.ChapterNumber.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultChapter))
|
||||
{
|
||||
info.Subtitle += " " + ReaderService.FormatChapterName(info.LibraryType, true, true) +
|
||||
|
@ -309,9 +315,16 @@ public class ReaderController : BaseApiController
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
|
||||
if (user == null) return Unauthorized();
|
||||
await _readerService.MarkSeriesAsRead(user, markReadDto.SeriesId);
|
||||
try
|
||||
{
|
||||
await _readerService.MarkSeriesAsRead(user, markReadDto.SeriesId);
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId));
|
||||
BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(markReadDto.SeriesId, user.Id));
|
||||
|
@ -331,7 +344,7 @@ public class ReaderController : BaseApiController
|
|||
if (user == null) return Unauthorized();
|
||||
await _readerService.MarkSeriesAsUnread(user, markReadDto.SeriesId);
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId));
|
||||
return Ok();
|
||||
|
@ -357,7 +370,7 @@ public class ReaderController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -372,12 +385,19 @@ public class ReaderController : BaseApiController
|
|||
|
||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
||||
if (user == null) return Unauthorized();
|
||||
await _readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters);
|
||||
try
|
||||
{
|
||||
await _readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters);
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, markVolumeReadDto.SeriesId,
|
||||
markVolumeReadDto.VolumeId, 0, chapters.Sum(c => c.Pages)));
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("Could not save progress");
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId));
|
||||
BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(markVolumeReadDto.SeriesId, user.Id));
|
||||
|
@ -405,7 +425,7 @@ public class ReaderController : BaseApiController
|
|||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
||||
await _readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters.ToList());
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("Could not save progress");
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId));
|
||||
BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(dto.SeriesId, user.Id));
|
||||
return Ok();
|
||||
|
@ -439,7 +459,7 @@ public class ReaderController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -460,7 +480,7 @@ public class ReaderController : BaseApiController
|
|||
await _readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
||||
}
|
||||
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest("Could not save progress");
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
|
||||
foreach (var sId in dto.SeriesIds)
|
||||
{
|
||||
|
@ -497,7 +517,7 @@ public class ReaderController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("Could not save progress");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -529,7 +549,7 @@ public class ReaderController : BaseApiController
|
|||
{
|
||||
var userId = User.GetUserId();
|
||||
if (!await _readerService.SaveReadingProgress(progressDto, userId))
|
||||
return BadRequest("Could not save progress");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-read-progress"));
|
||||
|
||||
|
||||
return Ok(true);
|
||||
|
@ -589,7 +609,7 @@ public class ReaderController : BaseApiController
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.Bookmarks == null) return Ok("Nothing to remove");
|
||||
if (user.Bookmarks == null) return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -616,7 +636,7 @@ public class ReaderController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Could not clear bookmarks");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-clear-bookmarks"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -629,7 +649,7 @@ public class ReaderController : BaseApiController
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.Bookmarks == null) return Ok("Nothing to remove");
|
||||
if (user.Bookmarks == null) return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -653,7 +673,7 @@ public class ReaderController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Could not clear bookmarks");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-clear-bookmarks"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -692,15 +712,16 @@ public class ReaderController : BaseApiController
|
|||
if (user == null) return new UnauthorizedResult();
|
||||
|
||||
if (!await _accountService.HasBookmarkPermission(user))
|
||||
return BadRequest("You do not have permission to bookmark");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-permission"));
|
||||
|
||||
var chapter = await _cacheService.Ensure(bookmarkDto.ChapterId);
|
||||
if (chapter == null) return BadRequest("Could not find cached image. Reload and try again.");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "cache-file-find"));
|
||||
|
||||
bookmarkDto.Page = _readerService.CapPageToChapter(chapter, bookmarkDto.Page);
|
||||
var path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page);
|
||||
|
||||
if (!await _bookmarkService.BookmarkPage(user, bookmarkDto, path)) return BadRequest("Could not save bookmark");
|
||||
if (!await _bookmarkService.BookmarkPage(user, bookmarkDto, path))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-save"));
|
||||
|
||||
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
|
||||
return Ok();
|
||||
|
@ -719,10 +740,10 @@ public class ReaderController : BaseApiController
|
|||
if (user.Bookmarks.IsNullOrEmpty()) return Ok();
|
||||
|
||||
if (!await _accountService.HasBookmarkPermission(user))
|
||||
return BadRequest("You do not have permission to unbookmark");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-permission"));
|
||||
|
||||
if (!await _bookmarkService.RemoveBookmarkPage(user, bookmarkDto))
|
||||
return BadRequest("Could not remove bookmark");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-save"));
|
||||
BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
|
||||
return Ok();
|
||||
}
|
||||
|
@ -806,9 +827,10 @@ public class ReaderController : BaseApiController
|
|||
[HttpDelete("ptoc")]
|
||||
public async Task<ActionResult> DeletePersonalToc([FromQuery] int chapterId, [FromQuery] int pageNum, [FromQuery] string title)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title)) return BadRequest("Name cannot be empty");
|
||||
if (pageNum < 0) return BadRequest("Must be valid page number");
|
||||
var toc = await _unitOfWork.UserTableOfContentRepository.Get(User.GetUserId(), chapterId, pageNum, title);
|
||||
var userId = User.GetUserId();
|
||||
if (string.IsNullOrWhiteSpace(title)) return BadRequest(await _localizationService.Translate(userId, "name-required"));
|
||||
if (pageNum < 0) return BadRequest(await _localizationService.Translate(userId, "valid-number"));
|
||||
var toc = await _unitOfWork.UserTableOfContentRepository.Get(userId, chapterId, pageNum, title);
|
||||
if (toc == null) return Ok();
|
||||
_unitOfWork.UserTableOfContentRepository.Remove(toc);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
@ -825,13 +847,13 @@ public class ReaderController : BaseApiController
|
|||
public async Task<ActionResult> CreatePersonalToC(CreatePersonalToCDto dto)
|
||||
{
|
||||
// Validate there isn't already an existing page title combo?
|
||||
if (string.IsNullOrWhiteSpace(dto.Title)) return BadRequest("Name cannot be empty");
|
||||
if (dto.PageNumber < 0) return BadRequest("Must be valid page number");
|
||||
var userId = User.GetUserId();
|
||||
if (string.IsNullOrWhiteSpace(dto.Title)) return BadRequest(await _localizationService.Translate(userId, "name-required"));
|
||||
if (dto.PageNumber < 0) return BadRequest(await _localizationService.Translate(userId, "valid-number"));
|
||||
if (await _unitOfWork.UserTableOfContentRepository.IsUnique(userId, dto.ChapterId, dto.PageNumber,
|
||||
dto.Title.Trim()))
|
||||
{
|
||||
return BadRequest("Duplicate ToC entry already exists");
|
||||
return BadRequest(await _localizationService.Translate(userId, "duplicate-bookmark"));
|
||||
}
|
||||
|
||||
_unitOfWork.UserTableOfContentRepository.Attach(new AppUserTableOfContent()
|
||||
|
|
|
@ -21,11 +21,14 @@ public class ReadingListController : BaseApiController
|
|||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IReadingListService _readingListService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public ReadingListController(IUnitOfWork unitOfWork, IReadingListService readingListService)
|
||||
public ReadingListController(IUnitOfWork unitOfWork, IReadingListService readingListService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_readingListService = readingListService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -99,13 +102,13 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
|
||||
if (await _readingListService.UpdateReadingListItemPosition(dto)) return Ok("Updated");
|
||||
if (await _readingListService.UpdateReadingListItemPosition(dto)) return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
|
||||
|
||||
return BadRequest("Couldn't update position");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-position"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -119,15 +122,15 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
|
||||
if (await _readingListService.DeleteReadingListItem(dto))
|
||||
{
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
|
||||
return BadRequest("Couldn't delete item");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-item-delete"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -141,15 +144,15 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
|
||||
if (await _readingListService.RemoveFullyReadItems(readingListId, user))
|
||||
{
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
|
||||
return BadRequest("Could not remove read items");
|
||||
return BadRequest("Couldn't delete item(s)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -163,12 +166,13 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
|
||||
if (await _readingListService.DeleteReadingList(readingListId, user)) return Ok("List was deleted");
|
||||
if (await _readingListService.DeleteReadingList(readingListId, user))
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-deleted"));
|
||||
|
||||
return BadRequest("There was an issue deleting reading list");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-reading-list-delete"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -188,7 +192,7 @@ public class ReadingListController : BaseApiController
|
|||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
|
||||
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(user.Id, dto.Title));
|
||||
|
@ -203,12 +207,12 @@ public class ReadingListController : BaseApiController
|
|||
public async Task<ActionResult> UpdateList(UpdateReadingListDto dto)
|
||||
{
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("List does not exist");
|
||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||
|
||||
var user = await _readingListService.UserHasReadingListAccess(readingList.Id, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
|
||||
try
|
||||
|
@ -217,10 +221,10 @@ public class ReadingListController : BaseApiController
|
|||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -234,11 +238,11 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
|
||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||
var chapterIdsForSeries =
|
||||
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new [] {dto.SeriesId});
|
||||
|
||||
|
@ -253,7 +257,7 @@ public class ReadingListController : BaseApiController
|
|||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
@ -261,7 +265,7 @@ public class ReadingListController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
}
|
||||
|
||||
|
||||
|
@ -276,10 +280,10 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||
|
||||
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
|
||||
foreach (var chapterId in dto.ChapterIds)
|
||||
|
@ -298,7 +302,7 @@ public class ReadingListController : BaseApiController
|
|||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
@ -306,7 +310,7 @@ public class ReadingListController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -320,10 +324,10 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||
|
||||
var ids = await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(dto.SeriesIds.ToArray());
|
||||
|
||||
|
@ -341,7 +345,7 @@ public class ReadingListController : BaseApiController
|
|||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
@ -349,7 +353,7 @@ public class ReadingListController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
}
|
||||
|
||||
[HttpPost("update-by-volume")]
|
||||
|
@ -358,10 +362,10 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||
|
||||
var chapterIdsForVolume =
|
||||
(await _unitOfWork.ChapterRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList();
|
||||
|
@ -377,7 +381,7 @@ public class ReadingListController : BaseApiController
|
|||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
@ -385,7 +389,7 @@ public class ReadingListController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
}
|
||||
|
||||
[HttpPost("update-by-chapter")]
|
||||
|
@ -394,10 +398,10 @@ public class ReadingListController : BaseApiController
|
|||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-permission"));
|
||||
}
|
||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||
|
||||
// If there are adds, tell tracking this has been modified
|
||||
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, new List<int>() { dto.ChapterId }, readingList))
|
||||
|
@ -410,7 +414,7 @@ public class ReadingListController : BaseApiController
|
|||
if (_unitOfWork.HasChanges())
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "reading-list-updated"));
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
@ -418,7 +422,7 @@ public class ReadingListController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return Ok("Nothing to do");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -446,7 +450,7 @@ public class ReadingListController : BaseApiController
|
|||
{
|
||||
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
|
||||
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
|
||||
if (readingListItem == null) return BadRequest("Id does not exist");
|
||||
if (readingListItem == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var index = items.IndexOf(readingListItem) + 1;
|
||||
if (items.Count > index)
|
||||
{
|
||||
|
@ -467,7 +471,7 @@ public class ReadingListController : BaseApiController
|
|||
{
|
||||
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
|
||||
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
|
||||
if (readingListItem == null) return BadRequest("Id does not exist");
|
||||
if (readingListItem == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var index = items.IndexOf(readingListItem) - 1;
|
||||
if (0 <= index)
|
||||
{
|
||||
|
|
|
@ -8,6 +8,7 @@ using API.DTOs;
|
|||
using API.DTOs.Recommendation;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using EasyCaching.Core;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -21,15 +22,18 @@ public class RecommendedController : BaseApiController
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IRecommendationService _recommendationService;
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IEasyCachingProvider _cacheProvider;
|
||||
public const string CacheKey = "recommendation_";
|
||||
|
||||
public RecommendedController(IUnitOfWork unitOfWork, IRecommendationService recommendationService,
|
||||
ILicenseService licenseService, IEasyCachingProviderFactory cachingProviderFactory)
|
||||
ILicenseService licenseService, IEasyCachingProviderFactory cachingProviderFactory,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_recommendationService = recommendationService;
|
||||
_licenseService = licenseService;
|
||||
_localizationService = localizationService;
|
||||
_cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRecommendations);
|
||||
}
|
||||
|
||||
|
@ -50,7 +54,7 @@ public class RecommendedController : BaseApiController
|
|||
|
||||
if (!await _unitOfWork.UserRepository.HasAccessToSeries(userId, seriesId))
|
||||
{
|
||||
return BadRequest("User does not have access to this Series");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-restricted"));
|
||||
}
|
||||
|
||||
var cacheKey = $"{CacheKey}-{seriesId}-{userId}";
|
||||
|
|
|
@ -14,9 +14,7 @@ using AutoMapper;
|
|||
using EasyCaching.Core;
|
||||
using Hangfire;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
|
@ -65,7 +63,6 @@ public class ReviewController : BaseApiController
|
|||
|
||||
var cacheKey = CacheKey + seriesId;
|
||||
IEnumerable<UserReviewDto> externalReviews;
|
||||
var setCache = false;
|
||||
|
||||
var result = await _cacheProvider.GetAsync<IEnumerable<UserReviewDto>>(cacheKey);
|
||||
if (result.HasValue)
|
||||
|
@ -74,35 +71,15 @@ public class ReviewController : BaseApiController
|
|||
}
|
||||
else
|
||||
{
|
||||
externalReviews = await _reviewService.GetReviewsForSeries(userId, seriesId);
|
||||
setCache = true;
|
||||
}
|
||||
// if (_cache.TryGetValue(cacheKey, out string cachedData))
|
||||
// {
|
||||
// externalReviews = JsonConvert.DeserializeObject<IEnumerable<UserReviewDto>>(cachedData);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// externalReviews = await _reviewService.GetReviewsForSeries(userId, seriesId);
|
||||
// setCache = true;
|
||||
// }
|
||||
|
||||
// Fetch external reviews and splice them in
|
||||
foreach (var r in externalReviews)
|
||||
{
|
||||
userRatings.Add(r);
|
||||
}
|
||||
|
||||
if (setCache)
|
||||
{
|
||||
// var cacheEntryOptions = new MemoryCacheEntryOptions()
|
||||
// .SetSize(userRatings.Count)
|
||||
// .SetAbsoluteExpiration(TimeSpan.FromHours(10));
|
||||
//_cache.Set(cacheKey, JsonConvert.SerializeObject(externalReviews), cacheEntryOptions);
|
||||
externalReviews = (await _reviewService.GetReviewsForSeries(userId, seriesId)).ToList();
|
||||
await _cacheProvider.SetAsync(cacheKey, externalReviews, TimeSpan.FromHours(10));
|
||||
_logger.LogDebug("Caching external reviews for {Key}", cacheKey);
|
||||
}
|
||||
|
||||
|
||||
// Fetch external reviews and splice them in
|
||||
userRatings.AddRange(externalReviews);
|
||||
|
||||
return Ok(userRatings.Take(10));
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ using API.Entities.Scrobble;
|
|||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using Hangfire;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -26,12 +27,15 @@ public class ScrobblingController : BaseApiController
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IScrobblingService _scrobblingService;
|
||||
private readonly ILogger<ScrobblingController> _logger;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public ScrobblingController(IUnitOfWork unitOfWork, IScrobblingService scrobblingService, ILogger<ScrobblingController> logger)
|
||||
public ScrobblingController(IUnitOfWork unitOfWork, IScrobblingService scrobblingService,
|
||||
ILogger<ScrobblingController> logger, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_scrobblingService = scrobblingService;
|
||||
_logger = logger;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
[HttpGet("anilist-token")]
|
||||
|
@ -153,7 +157,8 @@ public class ScrobblingController : BaseApiController
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ScrobbleHolds);
|
||||
if (user == null) return Unauthorized();
|
||||
if (user.ScrobbleHolds.Any(s => s.SeriesId == seriesId)) return Ok("Nothing to do");
|
||||
if (user.ScrobbleHolds.Any(s => s.SeriesId == seriesId))
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
|
||||
var seriesHold = new ScrobbleHoldBuilder().WithSeriesId(seriesId).Build();
|
||||
user.ScrobbleHolds.Add(seriesHold);
|
||||
|
@ -181,7 +186,8 @@ public class ScrobblingController : BaseApiController
|
|||
{
|
||||
// Handle other exceptions or log the error
|
||||
_logger.LogError(ex, "An error occurred while adding the hold");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while adding the hold");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||
await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ using API.Data.Repositories;
|
|||
using API.DTOs;
|
||||
using API.DTOs.Search;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
@ -15,10 +16,12 @@ namespace API.Controllers;
|
|||
public class SearchController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public SearchController(IUnitOfWork unitOfWork)
|
||||
public SearchController(IUnitOfWork unitOfWork, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -55,7 +58,7 @@ public class SearchController : BaseApiController
|
|||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized();
|
||||
var libraries = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(user.Id, QueryContext.Search).ToList();
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
if (!libraries.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "libraries-restricted"));
|
||||
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ using API.Helpers;
|
|||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using EasyCaching.Core;
|
||||
using Kavita.Common;
|
||||
using Kavita.Common.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
|
@ -30,6 +31,7 @@ public class SeriesController : BaseApiController
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IEasyCachingProvider _ratingCacheProvider;
|
||||
private readonly IEasyCachingProvider _reviewCacheProvider;
|
||||
private readonly IEasyCachingProvider _recommendationCacheProvider;
|
||||
|
@ -37,13 +39,14 @@ public class SeriesController : BaseApiController
|
|||
|
||||
public SeriesController(ILogger<SeriesController> logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork,
|
||||
ISeriesService seriesService, ILicenseService licenseService,
|
||||
IEasyCachingProviderFactory cachingProviderFactory)
|
||||
IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService)
|
||||
{
|
||||
_logger = logger;
|
||||
_taskScheduler = taskScheduler;
|
||||
_unitOfWork = unitOfWork;
|
||||
_seriesService = seriesService;
|
||||
_licenseService = licenseService;
|
||||
_localizationService = localizationService;
|
||||
|
||||
_ratingCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRatings);
|
||||
_reviewCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusReviews);
|
||||
|
@ -58,7 +61,7 @@ public class SeriesController : BaseApiController
|
|||
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto);
|
||||
|
||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||
if (series == null) return BadRequest("Could not get series for library");
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series"));
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
|
@ -101,7 +104,7 @@ public class SeriesController : BaseApiController
|
|||
|
||||
if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok();
|
||||
|
||||
return BadRequest("There was an issue deleting the series requested");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-series-delete"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -149,7 +152,8 @@ public class SeriesController : BaseApiController
|
|||
public async Task<ActionResult> UpdateSeriesRating(UpdateSeriesRatingDto updateSeriesRatingDto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Ratings);
|
||||
if (!await _seriesService.UpdateRating(user!, updateSeriesRatingDto)) return BadRequest("There was a critical error.");
|
||||
if (!await _seriesService.UpdateRating(user!, updateSeriesRatingDto))
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -162,8 +166,8 @@ public class SeriesController : BaseApiController
|
|||
public async Task<ActionResult> UpdateSeries(UpdateSeriesDto updateSeries)
|
||||
{
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(updateSeries.Id);
|
||||
|
||||
if (series == null) return BadRequest("Series does not exist");
|
||||
if (series == null)
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist"));
|
||||
|
||||
series.NormalizedName = series.Name.ToNormalized();
|
||||
if (!string.IsNullOrEmpty(updateSeries.SortName?.Trim()))
|
||||
|
@ -199,7 +203,7 @@ public class SeriesController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("There was an error with updating the series");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-series-update"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -218,7 +222,7 @@ public class SeriesController : BaseApiController
|
|||
await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, userId, userParams, filterDto);
|
||||
|
||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||
if (series == null) return BadRequest("Could not get series");
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series"));
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
|
@ -254,7 +258,7 @@ public class SeriesController : BaseApiController
|
|||
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto);
|
||||
|
||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||
if (series == null) return BadRequest("Could not get series");
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series"));
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
|
@ -370,10 +374,10 @@ public class SeriesController : BaseApiController
|
|||
}
|
||||
}
|
||||
|
||||
return Ok("Successfully updated");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "series-updated"));
|
||||
}
|
||||
|
||||
return BadRequest("Could not update metadata");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "update-metadata-fail"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -390,7 +394,7 @@ public class SeriesController : BaseApiController
|
|||
await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, userParams);
|
||||
|
||||
// Apply progress/rating information (I can't work out how to do this in initial query)
|
||||
if (series == null) return BadRequest("Could not get series for collection");
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-series-collection"));
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
|
||||
|
||||
|
@ -407,7 +411,7 @@ public class SeriesController : BaseApiController
|
|||
[HttpPost("series-by-ids")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesById(SeriesByIdsDto dto)
|
||||
{
|
||||
if (dto.SeriesIds == null) return BadRequest("Must pass seriesIds");
|
||||
if (dto.SeriesIds == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload"));
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, userId));
|
||||
}
|
||||
|
@ -420,10 +424,11 @@ public class SeriesController : BaseApiController
|
|||
/// <remarks>This is cached for an hour</remarks>
|
||||
[ResponseCache(CacheProfileName = "Month", VaryByQueryKeys = new [] {"ageRating"})]
|
||||
[HttpGet("age-rating")]
|
||||
public ActionResult<string> GetAgeRating(int ageRating)
|
||||
public async Task<ActionResult<string>> GetAgeRating(int ageRating)
|
||||
{
|
||||
var val = (AgeRating) ageRating;
|
||||
if (val == AgeRating.NotApplicable) return "No Restriction";
|
||||
if (val == AgeRating.NotApplicable)
|
||||
return await _localizationService.Translate(User.GetUserId(), "age-restriction-not-applicable");
|
||||
|
||||
return Ok(val.ToDescription());
|
||||
}
|
||||
|
@ -439,7 +444,14 @@ public class SeriesController : BaseApiController
|
|||
public async Task<ActionResult<SeriesDetailDto>> GetSeriesDetailBreakdown(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return await _seriesService.GetSeriesDetail(seriesId, userId);
|
||||
try
|
||||
{
|
||||
return await _seriesService.GetSeriesDetail(seriesId, userId);
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -485,7 +497,7 @@ public class SeriesController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("There was an issue updating relationships");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-relationship"));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -41,11 +41,13 @@ public class ServerController : BaseApiController
|
|||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEasyCachingProviderFactory _cachingProviderFactory;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public ServerController(ILogger<ServerController> logger,
|
||||
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
|
||||
ICleanupService cleanupService, IScannerService scannerService, IAccountService accountService,
|
||||
ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IEasyCachingProviderFactory cachingProviderFactory)
|
||||
ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IEasyCachingProviderFactory cachingProviderFactory,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_logger = logger;
|
||||
_backupService = backupService;
|
||||
|
@ -58,6 +60,7 @@ public class ServerController : BaseApiController
|
|||
_taskScheduler = taskScheduler;
|
||||
_unitOfWork = unitOfWork;
|
||||
_cachingProviderFactory = cachingProviderFactory;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -103,12 +106,12 @@ public class ServerController : BaseApiController
|
|||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("analyze-files")]
|
||||
public ActionResult AnalyzeFiles()
|
||||
public async Task<ActionResult> AnalyzeFiles()
|
||||
{
|
||||
_logger.LogInformation("{UserName} is performing file analysis from admin dashboard", User.GetUsername());
|
||||
if (TaskScheduler.HasAlreadyEnqueuedTask(ScannerService.Name, "AnalyzeFiles",
|
||||
Array.Empty<object>(), TaskScheduler.DefaultQueue, true))
|
||||
return Ok("Job already running");
|
||||
return Ok(await _localizationService.Translate(User.GetUserId(), "job-already-running"));
|
||||
|
||||
BackgroundJob.Enqueue(() => _scannerService.AnalyzeFiles());
|
||||
return Ok();
|
||||
|
@ -127,7 +130,7 @@ public class ServerController : BaseApiController
|
|||
/// <summary>
|
||||
/// Returns non-sensitive information about the current system
|
||||
/// </summary>
|
||||
/// <remarks>This is just for the UI and is extremly lightweight</remarks>
|
||||
/// <remarks>This is just for the UI and is extremely lightweight</remarks>
|
||||
/// <returns></returns>
|
||||
[HttpGet("server-info-slim")]
|
||||
public async Task<ActionResult<ServerInfoDto>> GetSlimVersion()
|
||||
|
@ -146,8 +149,7 @@ public class ServerController : BaseApiController
|
|||
var encoding = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
if (encoding == EncodeFormat.PNG)
|
||||
{
|
||||
return BadRequest(
|
||||
"You cannot convert to PNG. For covers, use Refresh Covers. Bookmarks and favicons cannot be encoded back.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "encode-as-warning"));
|
||||
}
|
||||
|
||||
_taskScheduler.CovertAllCoversToEncoding();
|
||||
|
@ -160,7 +162,7 @@ public class ServerController : BaseApiController
|
|||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("logs")]
|
||||
public ActionResult GetLogs()
|
||||
public async Task<ActionResult> GetLogs()
|
||||
{
|
||||
var files = _backupService.GetLogFiles();
|
||||
try
|
||||
|
@ -171,7 +173,7 @@ public class ServerController : BaseApiController
|
|||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,9 +33,11 @@ public class SettingsController : BaseApiController
|
|||
private readonly IMapper _mapper;
|
||||
private readonly IEmailService _emailService;
|
||||
private readonly ILibraryWatcher _libraryWatcher;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler,
|
||||
IDirectoryService directoryService, IMapper mapper, IEmailService emailService, ILibraryWatcher libraryWatcher)
|
||||
IDirectoryService directoryService, IMapper mapper, IEmailService emailService, ILibraryWatcher libraryWatcher,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_logger = logger;
|
||||
_unitOfWork = unitOfWork;
|
||||
|
@ -44,6 +46,7 @@ public class SettingsController : BaseApiController
|
|||
_mapper = mapper;
|
||||
_emailService = emailService;
|
||||
_libraryWatcher = libraryWatcher;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
[HttpGet("base-url")]
|
||||
|
@ -224,7 +227,7 @@ public class SettingsController : BaseApiController
|
|||
foreach (var ipAddress in updateSettingsDto.IpAddresses.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (!IPAddress.TryParse(ipAddress.Trim(), out _)) {
|
||||
return BadRequest($"IP Address '{ipAddress}' is invalid");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "ip-address-invalid", ipAddress));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -279,7 +282,7 @@ public class SettingsController : BaseApiController
|
|||
// Validate new directory can be used
|
||||
if (!await _directoryService.CheckWriteAccess(bookmarkDirectory))
|
||||
{
|
||||
return BadRequest("Bookmark Directory does not have correct permissions for Kavita to use");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-dir-permissions"));
|
||||
}
|
||||
|
||||
originalBookmarkDirectory = setting.Value;
|
||||
|
@ -308,7 +311,7 @@ public class SettingsController : BaseApiController
|
|||
{
|
||||
if (updateSettingsDto.TotalBackups > 30 || updateSettingsDto.TotalBackups < 1)
|
||||
{
|
||||
return BadRequest("Total Backups must be between 1 and 30");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "total-backups"));
|
||||
}
|
||||
setting.Value = updateSettingsDto.TotalBackups + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
|
@ -318,7 +321,7 @@ public class SettingsController : BaseApiController
|
|||
{
|
||||
if (updateSettingsDto.TotalLogs > 30 || updateSettingsDto.TotalLogs < 1)
|
||||
{
|
||||
return BadRequest("Total Logs must be between 1 and 30");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "total-logs"));
|
||||
}
|
||||
setting.Value = updateSettingsDto.TotalLogs + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
|
@ -366,7 +369,7 @@ public class SettingsController : BaseApiController
|
|||
{
|
||||
_logger.LogError(ex, "There was an exception when updating server settings");
|
||||
await _unitOfWork.RollbackAsync();
|
||||
return BadRequest("There was a critical issue. Please try again.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -19,12 +19,15 @@ public class StatsController : BaseApiController
|
|||
private readonly IStatisticService _statService;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly UserManager<AppUser> _userManager;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public StatsController(IStatisticService statService, IUnitOfWork unitOfWork, UserManager<AppUser> userManager)
|
||||
public StatsController(IStatisticService statService, IUnitOfWork unitOfWork,
|
||||
UserManager<AppUser> userManager, ILocalizationService localizationService)
|
||||
{
|
||||
_statService = statService;
|
||||
_unitOfWork = unitOfWork;
|
||||
_userManager = userManager;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
[HttpGet("user/{userId}/read")]
|
||||
|
@ -33,7 +36,7 @@ public class StatsController : BaseApiController
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user!.Id != userId && !await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole))
|
||||
return Unauthorized("You are not authorized to view another user's statistics");
|
||||
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "stats-permission-denied"));
|
||||
|
||||
return Ok(await _statService.GetUserReadStatistics(userId, new List<int>()));
|
||||
}
|
||||
|
|
|
@ -16,11 +16,14 @@ public class TachiyomiController : BaseApiController
|
|||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ITachiyomiService _tachiyomiService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public TachiyomiController(IUnitOfWork unitOfWork, ITachiyomiService tachiyomiService)
|
||||
public TachiyomiController(IUnitOfWork unitOfWork, ITachiyomiService tachiyomiService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_tachiyomiService = tachiyomiService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -31,7 +34,7 @@ public class TachiyomiController : BaseApiController
|
|||
[HttpGet("latest-chapter")]
|
||||
public async Task<ActionResult<ChapterDto>> GetLatestChapter(int seriesId)
|
||||
{
|
||||
if (seriesId < 1) return BadRequest("seriesId must be greater than 0");
|
||||
if (seriesId < 1) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "SeriesId"));
|
||||
return Ok(await _tachiyomiService.GetLatestChapter(seriesId, User.GetUserId()));
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs.Theme;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using Kavita.Common;
|
||||
|
@ -15,12 +16,15 @@ public class ThemeController : BaseApiController
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public ThemeController(IUnitOfWork unitOfWork, IThemeService themeService, ITaskScheduler taskScheduler)
|
||||
public ThemeController(IUnitOfWork unitOfWork, IThemeService themeService, ITaskScheduler taskScheduler,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_themeService = themeService;
|
||||
_taskScheduler = taskScheduler;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
[ResponseCache(CacheProfileName = "10Minute")]
|
||||
|
@ -43,7 +47,15 @@ public class ThemeController : BaseApiController
|
|||
[HttpPost("update-default")]
|
||||
public async Task<ActionResult> UpdateDefault(UpdateDefaultThemeDto dto)
|
||||
{
|
||||
await _themeService.UpdateDefault(dto.ThemeId);
|
||||
try
|
||||
{
|
||||
await _themeService.UpdateDefault(dto.ThemeId);
|
||||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "theme-doesnt-exist"));
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -61,7 +73,7 @@ public class ThemeController : BaseApiController
|
|||
}
|
||||
catch (KavitaException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,10 +25,12 @@ public class UploadController : BaseApiController
|
|||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IReadingListService _readingListService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILogger<UploadController> logger,
|
||||
ITaskScheduler taskScheduler, IDirectoryService directoryService, IEventHub eventHub, IReadingListService readingListService)
|
||||
ITaskScheduler taskScheduler, IDirectoryService directoryService, IEventHub eventHub, IReadingListService readingListService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_imageService = imageService;
|
||||
|
@ -37,6 +39,7 @@ public class UploadController : BaseApiController
|
|||
_directoryService = directoryService;
|
||||
_eventHub = eventHub;
|
||||
_readingListService = readingListService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -57,9 +60,9 @@ public class UploadController : BaseApiController
|
|||
.DownloadFileAsync(_directoryService.TempDirectory, $"coverupload_{dateString}.{format}");
|
||||
|
||||
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
|
||||
return BadRequest($"Could not download file");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
|
||||
|
||||
if (!await _imageService.IsImage(path)) return BadRequest("Url does not return a valid image");
|
||||
if (!await _imageService.IsImage(path)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
|
||||
|
||||
return $"coverupload_{dateString}.{format}";
|
||||
}
|
||||
|
@ -67,10 +70,10 @@ public class UploadController : BaseApiController
|
|||
{
|
||||
// Unauthorized
|
||||
if (ex.StatusCode == 401)
|
||||
return BadRequest("The server requires authentication to load the url externally");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
|
||||
}
|
||||
|
||||
return BadRequest("Unable to download image, please use another url or upload by file");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-not-valid"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -87,13 +90,13 @@ public class UploadController : BaseApiController
|
|||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
return BadRequest("You must pass a url to use");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
|
||||
if (series == null) return BadRequest("Invalid Series");
|
||||
if (series == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-doesnt-exist"));
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetSeriesFormat(uploadFileDto.Id)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
|
@ -118,7 +121,7 @@ public class UploadController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Series");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-series-save"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -135,13 +138,13 @@ public class UploadController : BaseApiController
|
|||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
return BadRequest("You must pass a url to use");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(uploadFileDto.Id);
|
||||
if (tag == null) return BadRequest("Invalid Tag id");
|
||||
if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist"));
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
|
@ -166,7 +169,7 @@ public class UploadController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Collection Tag");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-collection-save"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -183,16 +186,16 @@ public class UploadController : BaseApiController
|
|||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
return BadRequest("You must pass a url to use");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required"));
|
||||
}
|
||||
|
||||
if (_readingListService.UserHasReadingListAccess(uploadFileDto.Id, User.GetUsername()) == null)
|
||||
return Unauthorized("You do not have access");
|
||||
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "access-denied"));
|
||||
|
||||
try
|
||||
{
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id);
|
||||
if (readingList == null) return BadRequest("Reading list is not valid");
|
||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
|
@ -217,7 +220,7 @@ public class UploadController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Reading List");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-reading-list-save"));
|
||||
}
|
||||
|
||||
private async Task<string> CreateThumbnail(UploadFileDto uploadFileDto, string filename, int thumbnailSize = 0)
|
||||
|
@ -247,13 +250,13 @@ public class UploadController : BaseApiController
|
|||
// See if we can do this all in memory without touching underlying system
|
||||
if (string.IsNullOrEmpty(uploadFileDto.Url))
|
||||
{
|
||||
return BadRequest("You must pass a url to use");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "url-required"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
if (chapter == null) return BadRequest("Invalid Chapter");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var filePath = await CreateThumbnail(uploadFileDto, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}");
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
|
@ -286,7 +289,7 @@ public class UploadController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Chapter");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-chapter-save"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -345,7 +348,7 @@ public class UploadController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to save cover image to Library");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-cover-library-save"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -360,7 +363,7 @@ public class UploadController : BaseApiController
|
|||
try
|
||||
{
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
|
||||
if (chapter == null) return BadRequest("Chapter no longer exists");
|
||||
if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
||||
var originalFile = chapter.CoverImage;
|
||||
chapter.CoverImage = string.Empty;
|
||||
chapter.CoverImageLocked = false;
|
||||
|
@ -385,7 +388,7 @@ public class UploadController : BaseApiController
|
|||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Unable to resetting cover lock for Chapter");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reset-chapter-lock"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ using API.Data;
|
|||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -18,12 +19,15 @@ public class UsersController : BaseApiController
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub)
|
||||
public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_mapper = mapper;
|
||||
_eventHub = eventHub;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
|
@ -38,7 +42,7 @@ public class UsersController : BaseApiController
|
|||
|
||||
if (await _unitOfWork.CommitAsync()) return Ok();
|
||||
|
||||
return BadRequest("Could not delete the user.");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-user-delete"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -66,7 +70,7 @@ public class UsersController : BaseApiController
|
|||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
|
||||
if (library == null) return BadRequest("Library does not exist");
|
||||
if (library == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "library-doesnt-exist"));
|
||||
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId));
|
||||
}
|
||||
|
||||
|
@ -113,16 +117,17 @@ public class UsersController : BaseApiController
|
|||
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
|
||||
existingPreferences.CollapseSeriesRelationships = preferencesDto.CollapseSeriesRelationships;
|
||||
existingPreferences.ShareReviews = preferencesDto.ShareReviews;
|
||||
if (_localizationService.GetLocales().Contains(preferencesDto.Locale))
|
||||
{
|
||||
existingPreferences.Locale = preferencesDto.Locale;
|
||||
}
|
||||
|
||||
_unitOfWork.UserRepository.Update(existingPreferences);
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
|
||||
return Ok(preferencesDto);
|
||||
}
|
||||
if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-user-pref"));
|
||||
|
||||
return BadRequest("There was an issue saving preferences.");
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id);
|
||||
return Ok(preferencesDto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -7,6 +7,7 @@ using API.DTOs.Filtering;
|
|||
using API.DTOs.WantToRead;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using API.Services.Plus;
|
||||
using Hangfire;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -21,11 +22,14 @@ public class WantToReadController : BaseApiController
|
|||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IScrobblingService _scrobblingService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public WantToReadController(IUnitOfWork unitOfWork, IScrobblingService scrobblingService)
|
||||
public WantToReadController(IUnitOfWork unitOfWork, IScrobblingService scrobblingService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_scrobblingService = scrobblingService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -85,7 +89,7 @@ public class WantToReadController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("There was an issue updating Read List");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-reading-list-update"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -113,6 +117,6 @@ public class WantToReadController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
return BadRequest("There was an issue updating Read List");
|
||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-reading-list-update"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,4 +147,9 @@ public class UserPreferencesDto
|
|||
/// </summary>
|
||||
[Required]
|
||||
public bool ShareReviews { get; set; } = false;
|
||||
/// <summary>
|
||||
/// UI Site Global Setting: The language locale that should be used for the user
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Locale { get; set; }
|
||||
}
|
||||
|
|
|
@ -100,6 +100,10 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.BookReaderWritingStyle)
|
||||
.HasDefaultValue(WritingStyle.Horizontal);
|
||||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.Locale)
|
||||
.IsRequired(true)
|
||||
.HasDefaultValue("en");
|
||||
|
||||
builder.Entity<Library>()
|
||||
.Property(b => b.AllowScrobbling)
|
||||
|
|
2275
API/Data/Migrations/20230727175518_AddLocaleOnPrefs.Designer.cs
generated
Normal file
2275
API/Data/Migrations/20230727175518_AddLocaleOnPrefs.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
29
API/Data/Migrations/20230727175518_AddLocaleOnPrefs.cs
Normal file
29
API/Data/Migrations/20230727175518_AddLocaleOnPrefs.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddLocaleOnPrefs : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Locale",
|
||||
table: "AppUserPreferences",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "en");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Locale",
|
||||
table: "AppUserPreferences");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -272,6 +272,12 @@ namespace API.Data.Migrations
|
|||
b.Property<int>("LayoutMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Locale")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValue("en");
|
||||
|
||||
b.Property<bool>("NoTransitions")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ public interface IAppUserProgressRepository
|
|||
Task<IEnumerable<AppUserProgress>> GetUserProgressForSeriesAsync(int seriesId, int userId);
|
||||
Task<IEnumerable<AppUserProgress>> GetAllProgress();
|
||||
Task<DateTime> GetLatestProgress();
|
||||
Task<ProgressDto> GetUserProgressDtoAsync(int chapterId, int userId);
|
||||
Task<ProgressDto?> GetUserProgressDtoAsync(int chapterId, int userId);
|
||||
Task<bool> AnyUserProgressForSeriesAsync(int seriesId, int userId);
|
||||
Task<int> GetHighestFullyReadChapterForSeries(int seriesId, int userId);
|
||||
Task<int> GetHighestFullyReadVolumeForSeries(int seriesId, int userId);
|
||||
|
@ -143,7 +143,7 @@ public class AppUserProgressRepository : IAppUserProgressRepository
|
|||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<ProgressDto> GetUserProgressDtoAsync(int chapterId, int userId)
|
||||
public async Task<ProgressDto?> GetUserProgressDtoAsync(int chapterId, int userId)
|
||||
{
|
||||
return await _context.AppUserProgresses
|
||||
.Where(p => p.AppUserId == userId && p.ChapterId == chapterId)
|
||||
|
|
|
@ -73,7 +73,7 @@ public interface IUserRepository
|
|||
Task<IEnumerable<AppUserRating>> GetSeriesWithReviews(int userId);
|
||||
Task<bool> HasHoldOnSeries(int userId, int seriesId);
|
||||
Task<IList<ScrobbleHoldDto>> GetHolds(int userId);
|
||||
|
||||
Task<string> GetLocale(int userId);
|
||||
}
|
||||
|
||||
public class UserRepository : IUserRepository
|
||||
|
@ -291,6 +291,13 @@ public class UserRepository : IUserRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<string> GetLocale(int userId)
|
||||
{
|
||||
return await _context.AppUserPreferences.Where(p => p.AppUserId == userId)
|
||||
.Select(p => p.Locale)
|
||||
.SingleAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
||||
{
|
||||
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
||||
|
|
|
@ -127,6 +127,10 @@ public class AppUserPreferences
|
|||
/// UI Site Global Setting: Should series reviews be shared with all users in the server
|
||||
/// </summary>
|
||||
public bool ShareReviews { get; set; } = false;
|
||||
/// <summary>
|
||||
/// UI Site Global Setting: The language locale that should be used for the user
|
||||
/// </summary>
|
||||
public string Locale { get; set; }
|
||||
|
||||
public AppUser AppUser { get; set; } = null!;
|
||||
public int AppUserId { get; set; }
|
||||
|
|
|
@ -66,6 +66,8 @@ public static class ApplicationServiceExtensions
|
|||
services.AddScoped<IPresenceTracker, PresenceTracker>();
|
||||
services.AddScoped<IImageService, ImageService>();
|
||||
|
||||
services.AddScoped<ILocalizationService, LocalizationService>();
|
||||
|
||||
|
||||
services.AddScoped<IScrobblingService, ScrobblingService>();
|
||||
services.AddScoped<ILicenseService, LicenseService>();
|
||||
|
|
|
@ -37,4 +37,11 @@ public class AppUserBuilder : IEntityBuilder<AppUser>
|
|||
_appUser.Libraries.Add(library);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AppUserBuilder WithLocale(string locale)
|
||||
{
|
||||
_appUser.UserPreferences.Locale = locale;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
185
API/I18N/en.json
Normal file
185
API/I18N/en.json
Normal file
|
@ -0,0 +1,185 @@
|
|||
{
|
||||
"confirm-email": "You must confirm your email first",
|
||||
"bad-credentials": "Your credentials are not correct",
|
||||
"locked-out": "You've been locked out from too many authorization attempts. Please wait 10 minutes.",
|
||||
"disabled-account": "Your account is disabled. Contact the server admin.",
|
||||
"register-user": "Something went wrong when registering user",
|
||||
"validate-email": "There was an issue validating your email: {0}",
|
||||
"confirm-token-gen": "There was an issue generating a confirmation token",
|
||||
"denied": "Not allowed",
|
||||
"permission-denied": "You are not permitted to this operation",
|
||||
"password-required": "You must enter your existing password to change your account unless you're an admin",
|
||||
"invalid-password": "Invalid Password",
|
||||
"invalid-token": "Invalid token",
|
||||
"unable-to-reset-key": "Something went wrong, unable to reset key",
|
||||
"invalid-payload": "Invalid payload",
|
||||
"nothing-to-do": "Nothing to do",
|
||||
"share-multiple-emails": "You cannot share emails across multiple accounts",
|
||||
"generate-token": "There was an issue generating a confirmation email token. See logs",
|
||||
"age-restriction-update": "There was an error updating the age restriction",
|
||||
"no-user": "User does not exist",
|
||||
"username-taken": "Username already taken",
|
||||
"user-already-confirmed": "User is already confirmed",
|
||||
"generic-user-update": "There was an exception when updating the user",
|
||||
"manual-setup-fail": "Manual setup is unable to be completed. Please cancel and recreate the invite",
|
||||
"user-already-registered": "User is already registered as {0}",
|
||||
"user-already-invited": "User is already invited under this email and has yet to accepted invite.",
|
||||
"generic-invite-user": "There was an issue inviting the user. Please check logs.",
|
||||
"invalid-email-confirmation": "Invalid email confirmation",
|
||||
"generic-user-email-update": "Unable to update email for user. Check logs.",
|
||||
"generic-password-update": "There was an unexpected error when confirming new password",
|
||||
"password-updated": "Password Updated",
|
||||
"forgot-password-generic": "An email will be sent to the email if it exists in our database",
|
||||
"not-accessible-password": "Your server is not accessible. The link to reset your password is in the logs",
|
||||
"not-accessible": "Your server is not accessible externally",
|
||||
"email-sent": "Email sent",
|
||||
"user-migration-needed": "This user needs to migrate. Have them log out and login to trigger a migration flow",
|
||||
"generic-invite-email": "There was an issue resending invite email",
|
||||
"admin-already-exists": "Admin already exists",
|
||||
"invalid-username": "Invalid username",
|
||||
"critical-email-migration": "There was an issue during email migration. Contact support",
|
||||
|
||||
"chapter-doesnt-exist": "Chapter does not exist",
|
||||
"file-missing": "File was not found in book",
|
||||
|
||||
"collection-updated": "Collection updated successfully",
|
||||
"generic-error": "Something went wrong, please try again",
|
||||
"collection-doesnt-exist": "Collection does not exist",
|
||||
|
||||
"device-doesnt-exist": "Device does not exist",
|
||||
"generic-device-create": "There was an error when creating the device",
|
||||
"generic-device-update": "There was an error when updating the device",
|
||||
"generic-device-delete": "There was an error when deleting the device",
|
||||
"greater-0": "{0} must be greater than 0",
|
||||
"send-to-kavita-email": "Send to device cannot be used with Kavita's email service. Please configure your own.",
|
||||
"send-to-device-status": "Transferring files to your device",
|
||||
"generic-send-to": "There was an error sending the file(s) to the device",
|
||||
"series-doesnt-exist": "Series does not exist",
|
||||
|
||||
"volume-doesnt-exist": "Volume does not exist",
|
||||
"bookmarks-empty": "Bookmarks cannot be empty",
|
||||
|
||||
"no-cover-image": "No cover image",
|
||||
"bookmark-doesnt-exist": "Bookmark does not exist",
|
||||
"must-be-defined": "{0} must be defined",
|
||||
"generic-favicon": "There was an issue fetching favicon for domain",
|
||||
"invalid-filename": "Invalid Filename",
|
||||
"file-doesnt-exist": "File does not exist",
|
||||
|
||||
"library-name-exists": "Library name already exists. Please choose a unique name to the server.",
|
||||
"generic-library": "There was a critical issue. Please try again.",
|
||||
"no-library-access": "User does not have access to this library",
|
||||
"user-doesnt-exist": "User does not exist",
|
||||
"library-doesnt-exist": "Library does not exist",
|
||||
"invalid-path": "Invalid Path",
|
||||
"delete-library-while-scan": "You cannot delete a library while a scan is in progress. Please wait for scan to complete or restart Kavita then try to delete",
|
||||
"generic-library-update": "There was a critical issue updating the library.",
|
||||
|
||||
"pdf-doesnt-exist": "PDF does not exist when it should",
|
||||
"invalid-access": "Invalid Access",
|
||||
"no-image-for-page": "No such image for page {0}. Try refreshing to allow re-cache.",
|
||||
"perform-scan": "Please perform a scan on this series or library and try again",
|
||||
"generic-read-progress": "There was an issue saving progress",
|
||||
"generic-clear-bookmarks": "Could not clear bookmarks",
|
||||
"bookmark-permission": "You do not have permission to bookmark/unbookmark",
|
||||
"bookmark-save": "Could not save bookmark",
|
||||
"cache-file-find": "Could not find cached image. Reload and try again.",
|
||||
"name-required": "Name cannot be empty",
|
||||
"valid-number": "Must be valid page number",
|
||||
"duplicate-bookmark": "Duplicate bookmark entry already exists",
|
||||
|
||||
"reading-list-permission": "You do not have permissions on this reading list or the list doesn't exist",
|
||||
"reading-list-position": "Couldn't update position",
|
||||
"reading-list-updated": "Updated",
|
||||
"reading-list-item-delete": "Couldn't delete item(s)",
|
||||
"reading-list-deleted": "Reading List was deleted",
|
||||
"generic-reading-list-delete": "There was an issue deleting the reading list",
|
||||
"generic-reading-list-update": "There was an issue updating the reading list",
|
||||
"generic-reading-list-create": "There was an issue creating the reading list",
|
||||
"reading-list-doesnt-exist": "Reading list does not exist",
|
||||
|
||||
"series-restricted": "User does not have access to this Series",
|
||||
|
||||
"generic-scrobble-hold": "An error occurred while adding the hold",
|
||||
|
||||
"libraries-restricted": "User does not have access to any libraries",
|
||||
|
||||
"no-series": "Could not get series for Library",
|
||||
"no-series-collection": "Could not get series for Collection",
|
||||
"generic-series-delete": "There was an issue deleting the series",
|
||||
"generic-series-update": "There was an error with updating the series",
|
||||
"series-updated": "Successfully updated",
|
||||
"update-metadata-fail": "Could not update metadata",
|
||||
"age-restriction-not-applicable": "No Restriction",
|
||||
"generic-relationship": "There was an issue updating relationships",
|
||||
|
||||
"job-already-running": "Job already running",
|
||||
"encode-as-warning": "You cannot convert to PNG. For covers, use Refresh Covers. Bookmarks and favicons cannot be encoded back.",
|
||||
|
||||
"ip-address-invalid": "IP Address '{0}' is invalid",
|
||||
"bookmark-dir-permissions": "Bookmark Directory does not have correct permissions for Kavita to use",
|
||||
"total-backups": "Total Backups must be between 1 and 30",
|
||||
"total-logs": "Total Logs must be between 1 and 30",
|
||||
|
||||
"stats-permission-denied": "You are not authorized to view another user's statistics",
|
||||
|
||||
"url-not-valid": "Url does not return a valid image or requires authorization",
|
||||
"url-required": "You must pass a url to use",
|
||||
"generic-cover-series-save": "Unable to save cover image to Series",
|
||||
"generic-cover-collection-save": "Unable to save cover image to Collection",
|
||||
"generic-cover-reading-list-save": "Unable to save cover image to Reading List",
|
||||
"generic-cover-chapter-save": "Unable to save cover image to Chapter",
|
||||
"generic-cover-library-save": "Unable to save cover image to Library",
|
||||
"access-denied": "You do not have access",
|
||||
"reset-chapter-lock": "Unable to resetting cover lock for Chapter",
|
||||
|
||||
"generic-user-delete": "Could not delete the user",
|
||||
"generic-user-pref": "There was an issue saving preferences",
|
||||
|
||||
"opds-disabled": "OPDS is not enabled on this server",
|
||||
"on-deck": "On Deck",
|
||||
"browse-on-deck": "Browse On Deck",
|
||||
"recently-added": "Recently Added",
|
||||
"browse-recently-added": "Browse Recently Added",
|
||||
"reading-lists": "Reading Lists",
|
||||
"browse-reading-lists": "Browse by Reading Lists",
|
||||
"libraries": "All Libraries",
|
||||
"browse-libraries": "Browse by Libraries",
|
||||
"collections": "All Collections",
|
||||
"browse-collections": "Browse by Collections",
|
||||
"reading-list-restricted": "Reading list does not exist or you don't have access",
|
||||
"query-required": "You must pass a query parameter",
|
||||
"search": "Search",
|
||||
"search-description": "Search for Series, Collections, or Reading Lists",
|
||||
"favicon-doesnt-exist": "Favicon does not exist",
|
||||
|
||||
"not-authenticated": "User is not authenticated",
|
||||
"unable-to-register-k+": "Unable to register license due to error. Reach out to Kavita+ Support",
|
||||
"anilist-cred-expired": "AniList Credentials have expired or not set",
|
||||
"scrobble-bad-payload": "Bad payload from Scrobble Provider",
|
||||
"theme-doesnt-exist": "Theme file missing or invalid",
|
||||
"bad-copy-files-for-download": "Unable to copy files to temp directory archive download.",
|
||||
"generic-create-temp-archive": "There was an issue creating temp archive",
|
||||
"epub-malformed": "The file is malformed! Cannot read.",
|
||||
"epub-html-missing": "Could not find the appropriate html for that page",
|
||||
"collection-tag-title-required": "Collection Title cannot be empty",
|
||||
"reading-list-title-required": "Reading List Title cannot be empty",
|
||||
"collection-tag-duplicate": "A collection with this name already exists",
|
||||
"device-duplicate": "A device with this name already exists",
|
||||
"device-not-created": "This device doesn't exist yet. Please create first",
|
||||
"send-to-permission": "Cannot Send non-EPUB or PDF to devices as not supported on Kindle",
|
||||
"progress-must-exist": "Progress must exist on user",
|
||||
"reading-list-name-exists": "A reading list of this name already exists",
|
||||
"user-no-access-library-from-series": "User does not have access to the library this series belongs to",
|
||||
"series-restricted-age-restriction": "User is not allowed to view this series due to age restrictions",
|
||||
|
||||
|
||||
|
||||
"volume-num": "Volume {0}",
|
||||
"book-num": "Book {0}",
|
||||
"issue-num": "Issue {0}{1}",
|
||||
"chapter-num": "Chapter {0}"
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -301,7 +301,7 @@ public class ArchiveService : IArchiveService
|
|||
|
||||
if (!_directoryService.CopyFilesToDirectory(files, tempLocation))
|
||||
{
|
||||
throw new KavitaException("Unable to copy files to temp directory archive download.");
|
||||
throw new KavitaException("bad-copy-files-for-download");
|
||||
}
|
||||
|
||||
var zipPath = Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip");
|
||||
|
@ -314,7 +314,7 @@ public class ArchiveService : IArchiveService
|
|||
catch (AggregateException ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an issue creating temp archive");
|
||||
throw new KavitaException("There was an issue creating temp archive");
|
||||
throw new KavitaException("generic-create-temp-archive");
|
||||
}
|
||||
|
||||
return zipPath;
|
||||
|
|
|
@ -1121,7 +1121,7 @@ public class BookService : IBookService
|
|||
if (doc.ParseErrors.Any())
|
||||
{
|
||||
LogBookErrors(book, contentFileRef, doc);
|
||||
throw new KavitaException("The file is malformed! Cannot read.");
|
||||
throw new KavitaException("epub-malformed");
|
||||
}
|
||||
_logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath);
|
||||
doc.DocumentNode.SelectSingleNode("/html").AppendChild(HtmlNode.CreateNode("<body></body>"));
|
||||
|
@ -1137,7 +1137,7 @@ public class BookService : IBookService
|
|||
"There was an issue reading one of the pages for", ex);
|
||||
}
|
||||
|
||||
throw new KavitaException("Could not find the appropriate html for that page");
|
||||
throw new KavitaException("epub-html-missing");
|
||||
}
|
||||
|
||||
private static void CreateToCChapter(EpubBookRef book, EpubNavigationItemRef navigationItem, IList<BookChapterItem> nestedChapters,
|
||||
|
|
|
@ -52,12 +52,12 @@ public class CollectionTagService : ICollectionTagService
|
|||
public async Task<bool> UpdateTag(CollectionTagDto dto)
|
||||
{
|
||||
var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(dto.Id);
|
||||
if (existingTag == null) throw new KavitaException("This tag does not exist");
|
||||
if (existingTag == null) throw new KavitaException("collection-doesnt-exist");
|
||||
|
||||
var title = dto.Title.Trim();
|
||||
if (string.IsNullOrEmpty(title)) throw new KavitaException("Title cannot be empty");
|
||||
if (string.IsNullOrEmpty(title)) throw new KavitaException("collection-tag-title-required");
|
||||
if (!title.Equals(existingTag.Title) && await TagExistsByName(dto.Title))
|
||||
throw new KavitaException("A tag with this name already exists");
|
||||
throw new KavitaException("collection-tag-duplicate");
|
||||
|
||||
existingTag.SeriesMetadatas ??= new List<SeriesMetadata>();
|
||||
existingTag.Title = title;
|
||||
|
|
|
@ -42,7 +42,7 @@ public class DeviceService : IDeviceService
|
|||
{
|
||||
userWithDevices.Devices ??= new List<Device>();
|
||||
var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Name!.Equals(dto.Name));
|
||||
if (existingDevice != null) throw new KavitaException("A device with this name already exists");
|
||||
if (existingDevice != null) throw new KavitaException("device-duplicate");
|
||||
|
||||
existingDevice = new DeviceBuilder(dto.Name)
|
||||
.WithPlatform(dto.Platform)
|
||||
|
@ -70,7 +70,7 @@ public class DeviceService : IDeviceService
|
|||
try
|
||||
{
|
||||
var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Id == dto.Id);
|
||||
if (existingDevice == null) throw new KavitaException("This device doesn't exist yet. Please create first");
|
||||
if (existingDevice == null) throw new KavitaException("device-not-created");
|
||||
|
||||
existingDevice.Name = dto.Name;
|
||||
existingDevice.Platform = dto.Platform;
|
||||
|
@ -108,11 +108,11 @@ public class DeviceService : IDeviceService
|
|||
public async Task<bool> SendTo(IReadOnlyList<int> chapterIds, int deviceId)
|
||||
{
|
||||
var device = await _unitOfWork.DeviceRepository.GetDeviceById(deviceId);
|
||||
if (device == null) throw new KavitaException("Device doesn't exist");
|
||||
if (device == null) throw new KavitaException("device-doesnt-exist");
|
||||
|
||||
var files = await _unitOfWork.ChapterRepository.GetFilesForChaptersAsync(chapterIds);
|
||||
if (files.Any(f => f.Format is not (MangaFormat.Epub or MangaFormat.Pdf)) && device.Platform == DevicePlatform.Kindle)
|
||||
throw new KavitaException("Cannot Send non Epub or Pdf to devices as not supported on Kindle");
|
||||
throw new KavitaException("send-to-permission");
|
||||
|
||||
|
||||
device.UpdateLastUsed();
|
||||
|
|
|
@ -25,6 +25,7 @@ public interface IDirectoryService
|
|||
string ConfigDirectory { get; }
|
||||
string SiteThemeDirectory { get; }
|
||||
string FaviconDirectory { get; }
|
||||
string LocalizationDirectory { get; }
|
||||
/// <summary>
|
||||
/// Original BookmarkDirectory. Only used for resetting directory. Use <see cref="ServerSettingKey.BackupDirectory"/> for actual path.
|
||||
/// </summary>
|
||||
|
@ -79,6 +80,7 @@ public class DirectoryService : IDirectoryService
|
|||
public string BookmarkDirectory { get; }
|
||||
public string SiteThemeDirectory { get; }
|
||||
public string FaviconDirectory { get; }
|
||||
public string LocalizationDirectory { get; }
|
||||
private readonly ILogger<DirectoryService> _logger;
|
||||
private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase;
|
||||
|
||||
|
@ -95,22 +97,23 @@ public class DirectoryService : IDirectoryService
|
|||
{
|
||||
_logger = logger;
|
||||
FileSystem = fileSystem;
|
||||
CoverImageDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "covers");
|
||||
CacheDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "cache");
|
||||
LogDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "logs");
|
||||
TempDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "temp");
|
||||
ConfigDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config");
|
||||
BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks");
|
||||
SiteThemeDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "themes");
|
||||
FaviconDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "favicons");
|
||||
|
||||
ExistOrCreate(SiteThemeDirectory);
|
||||
ExistOrCreate(ConfigDirectory);
|
||||
CoverImageDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "covers");
|
||||
ExistOrCreate(CoverImageDirectory);
|
||||
CacheDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "cache");
|
||||
ExistOrCreate(CacheDirectory);
|
||||
LogDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "logs");
|
||||
ExistOrCreate(LogDirectory);
|
||||
TempDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "temp");
|
||||
ExistOrCreate(TempDirectory);
|
||||
BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks");
|
||||
ExistOrCreate(BookmarkDirectory);
|
||||
SiteThemeDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "themes");
|
||||
ExistOrCreate(SiteThemeDirectory);
|
||||
FaviconDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "favicons");
|
||||
ExistOrCreate(FaviconDirectory);
|
||||
LocalizationDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "I18N");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
146
API/Services/LocalizationService.cs
Normal file
146
API/Services/LocalizationService.cs
Normal file
|
@ -0,0 +1,146 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace API.Services;
|
||||
#nullable enable
|
||||
|
||||
|
||||
public interface ILocalizationService
|
||||
{
|
||||
Task<string> Get(string locale, string key, params object[] args);
|
||||
Task<string> Translate(int userId, string key, params object[] args);
|
||||
IEnumerable<string> GetLocales();
|
||||
}
|
||||
|
||||
public class LocalizationService : ILocalizationService
|
||||
{
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
/// <summary>
|
||||
/// The locales for the UI
|
||||
/// </summary>
|
||||
private readonly string _localizationDirectoryUi;
|
||||
|
||||
private readonly MemoryCacheEntryOptions _cacheOptions;
|
||||
|
||||
|
||||
public LocalizationService(IDirectoryService directoryService,
|
||||
IHostEnvironment environment, IMemoryCache cache, IUnitOfWork unitOfWork)
|
||||
{
|
||||
_directoryService = directoryService;
|
||||
_cache = cache;
|
||||
_unitOfWork = unitOfWork;
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
_localizationDirectoryUi = directoryService.FileSystem.Path.Join(
|
||||
directoryService.FileSystem.Directory.GetCurrentDirectory(),
|
||||
"UI/Web/src/assets/langs");
|
||||
} else if (environment.EnvironmentName.Equals("Testing", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_localizationDirectoryUi = directoryService.FileSystem.Path.Join(
|
||||
directoryService.FileSystem.Directory.GetCurrentDirectory(),
|
||||
"/../../../../../UI/Web/src/assets/langs");
|
||||
}
|
||||
else
|
||||
{
|
||||
_localizationDirectoryUi = directoryService.FileSystem.Path.Join(
|
||||
directoryService.FileSystem.Directory.GetCurrentDirectory(),
|
||||
"wwwroot", "assets/langs");
|
||||
}
|
||||
|
||||
_cacheOptions = new MemoryCacheEntryOptions()
|
||||
.SetSize(1)
|
||||
.SetAbsoluteExpiration(TimeSpan.FromMinutes(15));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a language, if language is blank, falls back to english
|
||||
/// </summary>
|
||||
/// <param name="languageCode"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<Dictionary<string, string>?> LoadLanguage(string languageCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(languageCode)) languageCode = "en";
|
||||
var languageFile = _directoryService.FileSystem.Path.Join(_directoryService.LocalizationDirectory, languageCode + ".json");
|
||||
if (!_directoryService.FileSystem.FileInfo.New(languageFile).Exists)
|
||||
throw new ArgumentException($"Language {languageCode} does not exist");
|
||||
|
||||
var json = await _directoryService.FileSystem.File.ReadAllTextAsync(languageFile);
|
||||
return JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
}
|
||||
|
||||
public async Task<string> Get(string locale, string key, params object[] args)
|
||||
{
|
||||
|
||||
// Check if the translation for the given locale is cached
|
||||
var cacheKey = $"{locale}_{key}";
|
||||
if (!_cache.TryGetValue(cacheKey, out string? translatedString))
|
||||
{
|
||||
// Load the locale JSON file
|
||||
var translationData = await LoadLanguage(locale);
|
||||
|
||||
// Find the translation for the given key
|
||||
if (translationData != null && translationData.TryGetValue(key, out var value))
|
||||
{
|
||||
translatedString = value;
|
||||
|
||||
// Cache the translation for subsequent requests
|
||||
_cache.Set(cacheKey, translatedString, _cacheOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (string.IsNullOrEmpty(translatedString))
|
||||
{
|
||||
if (!locale.Equals("en"))
|
||||
{
|
||||
return await Get("en", key, args);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
// Format the translated string with arguments
|
||||
if (args.Length > 0)
|
||||
{
|
||||
translatedString = string.Format(translatedString, args);
|
||||
}
|
||||
|
||||
return translatedString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a translated string for a given user's locale, falling back to english or the key if missing
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> Translate(int userId, string key, params object[] args)
|
||||
{
|
||||
var userLocale = await _unitOfWork.UserRepository.GetLocale(userId);
|
||||
return await Get(userLocale, key, args);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns all available locales that exist on both the Frontend and the Backend
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<string> GetLocales()
|
||||
{
|
||||
return
|
||||
_directoryService.GetFilesWithExtension(_directoryService.FileSystem.Path.GetFullPath(_localizationDirectoryUi), @"\.json")
|
||||
.Select(f => _directoryService.FileSystem.Path.GetFileName(f).Replace(".json", string.Empty))
|
||||
.Union(_directoryService.GetFilesWithExtension(_directoryService.LocalizationDirectory, @"\.json")
|
||||
.Select(f => _directoryService.FileSystem.Path.GetFileName(f).Replace(".json", string.Empty)))
|
||||
.Distinct();
|
||||
}
|
||||
}
|
|
@ -164,7 +164,7 @@ public class LicenseService : ILicenseService
|
|||
var serverSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
|
||||
var lic = await RegisterLicense(license, email);
|
||||
if (string.IsNullOrWhiteSpace(lic))
|
||||
throw new KavitaException("Unable to register license due to error. Reach out to Kavita+ Support");
|
||||
throw new KavitaException("unable-to-register-k+");
|
||||
serverSetting.Value = lic;
|
||||
_unitOfWork.SettingsRepository.Update(serverSetting);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
|
|
@ -60,6 +60,7 @@ public class ScrobblingService : IScrobblingService
|
|||
private readonly IEventHub _eventHub;
|
||||
private readonly ILogger<ScrobblingService> _logger;
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public const string AniListWeblinkWebsite = "https://anilist.co/manga/";
|
||||
public const string MalWeblinkWebsite = "https://myanimelist.net/manga/";
|
||||
|
@ -87,13 +88,15 @@ public class ScrobblingService : IScrobblingService
|
|||
|
||||
|
||||
public ScrobblingService(IUnitOfWork unitOfWork, ITokenService tokenService,
|
||||
IEventHub eventHub, ILogger<ScrobblingService> logger, ILicenseService licenseService)
|
||||
IEventHub eventHub, ILogger<ScrobblingService> logger, ILicenseService licenseService,
|
||||
ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_tokenService = tokenService;
|
||||
_eventHub = eventHub;
|
||||
_logger = logger;
|
||||
_licenseService = licenseService;
|
||||
_localizationService = localizationService;
|
||||
|
||||
FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli =>
|
||||
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
|
||||
|
@ -184,11 +187,11 @@ public class ScrobblingService : IScrobblingService
|
|||
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
||||
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
|
||||
{
|
||||
throw new KavitaException("AniList Credentials have expired or not set");
|
||||
throw new KavitaException(await _localizationService.Translate(userId, "unable-to-register-k+"));
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
|
||||
if (series == null) throw new KavitaException("Series not found");
|
||||
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
|
||||
if (library is not {AllowScrobbling: true}) return;
|
||||
if (library.Type == LibraryType.Comic) return;
|
||||
|
@ -229,11 +232,11 @@ public class ScrobblingService : IScrobblingService
|
|||
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
||||
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
|
||||
{
|
||||
throw new KavitaException("AniList Credentials have expired or not set");
|
||||
throw new KavitaException(await _localizationService.Translate(userId, "anilist-cred-expired"));
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
|
||||
if (series == null) throw new KavitaException("Series not found");
|
||||
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
|
||||
if (library is not {AllowScrobbling: true}) return;
|
||||
if (library.Type == LibraryType.Comic) return;
|
||||
|
@ -273,11 +276,11 @@ public class ScrobblingService : IScrobblingService
|
|||
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
||||
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
|
||||
{
|
||||
throw new KavitaException("AniList Credentials have expired or not set");
|
||||
throw new KavitaException(await _localizationService.Translate(userId, "anilist-cred-expired"));
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
|
||||
if (series == null) throw new KavitaException("Series not found");
|
||||
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
|
||||
if (await _unitOfWork.UserRepository.HasHoldOnSeries(userId, seriesId))
|
||||
{
|
||||
_logger.LogInformation("Series {SeriesName} is on UserId {UserId}'s hold list. Not scrobbling", series.Name, userId);
|
||||
|
@ -338,11 +341,11 @@ public class ScrobblingService : IScrobblingService
|
|||
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
||||
if (await HasTokenExpired(token, ScrobbleProvider.AniList))
|
||||
{
|
||||
throw new KavitaException("AniList Credentials have expired or not set");
|
||||
throw new KavitaException(await _localizationService.Translate(userId, "anilist-cred-expired"));
|
||||
}
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
|
||||
if (series == null) throw new KavitaException("Series not found");
|
||||
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId);
|
||||
if (library is not {AllowScrobbling: true}) return;
|
||||
if (library.Type == LibraryType.Comic) return;
|
||||
|
|
|
@ -117,7 +117,7 @@ public class ReaderService : IReaderService
|
|||
{
|
||||
var seenVolume = new Dictionary<int, bool>();
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||
if (series == null) throw new KavitaException("Series suddenly doesn't exist, cannot mark as read");
|
||||
if (series == null) throw new KavitaException("series-doesnt-exist");
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
var userProgress = GetUserProgressForChapter(user, chapter);
|
||||
|
@ -202,8 +202,9 @@ public class ReaderService : IReaderService
|
|||
|
||||
if (user.Progresses == null)
|
||||
{
|
||||
throw new KavitaException("Progresses must exist on user");
|
||||
throw new KavitaException("progress-must-exist");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
userProgress =
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Serialization;
|
||||
using API.Comparators;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
|
@ -17,7 +18,6 @@ using API.Services.Tasks.Scanner.Parser;
|
|||
using API.SignalR;
|
||||
using Kavita.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
|
@ -49,7 +49,7 @@ public interface IReadingListService
|
|||
/// <summary>
|
||||
/// Methods responsible for management of Reading Lists
|
||||
/// </summary>
|
||||
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, String)"/> to be called beforehand</remarks>
|
||||
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, string)"/> to be called beforehand</remarks>
|
||||
public class ReadingListService : IReadingListService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
@ -69,13 +69,13 @@ public class ReadingListService : IReadingListService
|
|||
public static string FormatTitle(ReadingListItemDto item)
|
||||
{
|
||||
var title = string.Empty;
|
||||
if (item.ChapterNumber == Tasks.Scanner.Parser.Parser.DefaultChapter && item.VolumeNumber != Tasks.Scanner.Parser.Parser.DefaultVolume) {
|
||||
if (item.ChapterNumber == Parser.DefaultChapter && item.VolumeNumber != Parser.DefaultVolume) {
|
||||
title = $"Volume {item.VolumeNumber}";
|
||||
}
|
||||
|
||||
if (item.SeriesFormat == MangaFormat.Epub) {
|
||||
var specialTitle = Tasks.Scanner.Parser.Parser.CleanSpecialTitle(item.ChapterNumber);
|
||||
if (specialTitle == Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
var specialTitle = Parser.CleanSpecialTitle(item.ChapterNumber);
|
||||
if (specialTitle == Parser.DefaultChapter)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.ChapterTitleName))
|
||||
{
|
||||
|
@ -83,7 +83,7 @@ public class ReadingListService : IReadingListService
|
|||
}
|
||||
else
|
||||
{
|
||||
title = $"Volume {Tasks.Scanner.Parser.Parser.CleanSpecialTitle(item.VolumeNumber)}";
|
||||
title = $"Volume {Parser.CleanSpecialTitle(item.VolumeNumber)}";
|
||||
}
|
||||
} else {
|
||||
title = $"Volume {specialTitle}";
|
||||
|
@ -92,12 +92,12 @@ public class ReadingListService : IReadingListService
|
|||
|
||||
var chapterNum = item.ChapterNumber;
|
||||
if (!string.IsNullOrEmpty(chapterNum) && !JustNumbers.Match(item.ChapterNumber).Success) {
|
||||
chapterNum = Tasks.Scanner.Parser.Parser.CleanSpecialTitle(item.ChapterNumber);
|
||||
chapterNum = Parser.CleanSpecialTitle(item.ChapterNumber);
|
||||
}
|
||||
|
||||
if (title != string.Empty) return title;
|
||||
|
||||
if (item.ChapterNumber == Tasks.Scanner.Parser.Parser.DefaultChapter &&
|
||||
if (item.ChapterNumber == Parser.DefaultChapter &&
|
||||
!string.IsNullOrEmpty(item.ChapterTitleName))
|
||||
{
|
||||
title = item.ChapterTitleName;
|
||||
|
@ -124,13 +124,13 @@ public class ReadingListService : IReadingListService
|
|||
var hasExisting = userWithReadingList.ReadingLists.Any(l => l.Title.Equals(title));
|
||||
if (hasExisting)
|
||||
{
|
||||
throw new KavitaException("A list of this name already exists");
|
||||
throw new KavitaException("reading-list-name-exists");
|
||||
}
|
||||
|
||||
var readingList = new ReadingListBuilder(title).Build();
|
||||
userWithReadingList.ReadingLists.Add(readingList);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) throw new KavitaException("There was a problem creating list");
|
||||
if (!_unitOfWork.HasChanges()) throw new KavitaException("generic-reading-list-create");
|
||||
await _unitOfWork.CommitAsync();
|
||||
return readingList;
|
||||
}
|
||||
|
@ -144,10 +144,10 @@ public class ReadingListService : IReadingListService
|
|||
public async Task UpdateReadingList(ReadingList readingList, UpdateReadingListDto dto)
|
||||
{
|
||||
dto.Title = dto.Title.Trim();
|
||||
if (string.IsNullOrEmpty(dto.Title)) throw new KavitaException("Title must be set");
|
||||
if (string.IsNullOrEmpty(dto.Title)) throw new KavitaException("reading-list-title-required");
|
||||
|
||||
if (!dto.Title.Equals(readingList.Title) && await _unitOfWork.ReadingListRepository.ReadingListExists(dto.Title))
|
||||
throw new KavitaException("Reading list already exists");
|
||||
throw new KavitaException("reading-list-name-exists");
|
||||
|
||||
readingList.Summary = dto.Summary;
|
||||
readingList.Title = dto.Title.Trim();
|
||||
|
@ -192,7 +192,7 @@ public class ReadingListService : IReadingListService
|
|||
/// <summary>
|
||||
/// Removes all entries that are fully read from the reading list. This commits
|
||||
/// </summary>
|
||||
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, String)"/> to be called beforehand</remarks>
|
||||
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess(int, string)"/> to be called beforehand</remarks>
|
||||
/// <param name="readingListId">Reading List Id</param>
|
||||
/// <param name="user">User</param>
|
||||
/// <returns></returns>
|
||||
|
@ -404,7 +404,7 @@ public class ReadingListService : IReadingListService
|
|||
|
||||
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
|
||||
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds, ChapterIncludes.Volumes))
|
||||
.OrderBy(c => Tasks.Scanner.Parser.Parser.MinNumberFromRange(c.Volume.Name))
|
||||
.OrderBy(c => Parser.MinNumberFromRange(c.Volume.Name))
|
||||
.ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting)
|
||||
.ToList();
|
||||
|
||||
|
@ -529,7 +529,7 @@ public class ReadingListService : IReadingListService
|
|||
/// <param name="cblReading"></param>
|
||||
public async Task<CblImportSummaryDto> ValidateCblFile(int userId, CblReadingList cblReading)
|
||||
{
|
||||
var importSummary = new CblImportSummaryDto()
|
||||
var importSummary = new CblImportSummaryDto
|
||||
{
|
||||
CblName = cblReading.Name,
|
||||
Success = CblImportResult.Success,
|
||||
|
@ -542,20 +542,20 @@ public class ReadingListService : IReadingListService
|
|||
if (await _unitOfWork.ReadingListRepository.ReadingListExists(cblReading.Name))
|
||||
{
|
||||
importSummary.Success = CblImportResult.Fail;
|
||||
importSummary.Results.Add(new CblBookResult()
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
{
|
||||
Reason = CblImportReason.NameConflict,
|
||||
ReadingListName = cblReading.Name
|
||||
});
|
||||
}
|
||||
|
||||
var uniqueSeries = cblReading.Books.Book.Select(b => Tasks.Scanner.Parser.Parser.Normalize(b.Series)).Distinct().ToList();
|
||||
var uniqueSeries = cblReading.Books.Book.Select(b => Parser.Normalize(b.Series)).Distinct().ToList();
|
||||
var userSeries =
|
||||
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
|
||||
if (!userSeries.Any())
|
||||
{
|
||||
// Report that no series exist in the reading list
|
||||
importSummary.Results.Add(new CblBookResult()
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
{
|
||||
Reason = CblImportReason.AllSeriesMissing
|
||||
});
|
||||
|
@ -569,7 +569,7 @@ public class ReadingListService : IReadingListService
|
|||
importSummary.Success = CblImportResult.Fail;
|
||||
foreach (var conflict in conflicts)
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult()
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
{
|
||||
Reason = CblImportReason.SeriesCollision,
|
||||
Series = conflict.Name,
|
||||
|
@ -593,7 +593,7 @@ public class ReadingListService : IReadingListService
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.ReadingListsWithItems);
|
||||
_logger.LogDebug("Importing {ReadingListName} CBL for User {UserName}", cblReading.Name, user!.UserName);
|
||||
var importSummary = new CblImportSummaryDto()
|
||||
var importSummary = new CblImportSummaryDto
|
||||
{
|
||||
CblName = cblReading.Name,
|
||||
Success = CblImportResult.Success,
|
||||
|
@ -601,13 +601,13 @@ public class ReadingListService : IReadingListService
|
|||
SuccessfulInserts = new List<CblBookResult>()
|
||||
};
|
||||
|
||||
var uniqueSeries = cblReading.Books.Book.Select(b => Tasks.Scanner.Parser.Parser.Normalize(b.Series)).Distinct().ToList();
|
||||
var uniqueSeries = cblReading.Books.Book.Select(b => Parser.Normalize(b.Series)).Distinct().ToList();
|
||||
var userSeries =
|
||||
(await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList();
|
||||
var allSeries = userSeries.ToDictionary(s => Tasks.Scanner.Parser.Parser.Normalize(s.Name));
|
||||
var allSeriesLocalized = userSeries.ToDictionary(s => Tasks.Scanner.Parser.Parser.Normalize(s.LocalizedName));
|
||||
var allSeries = userSeries.ToDictionary(s => Parser.Normalize(s.Name));
|
||||
var allSeriesLocalized = userSeries.ToDictionary(s => Parser.Normalize(s.LocalizedName));
|
||||
|
||||
var readingListNameNormalized = Tasks.Scanner.Parser.Parser.Normalize(cblReading.Name);
|
||||
var readingListNameNormalized = Parser.Normalize(cblReading.Name);
|
||||
// Get all the user's reading lists
|
||||
var allReadingLists = (user.ReadingLists).ToDictionary(s => s.NormalizedTitle);
|
||||
if (!allReadingLists.TryGetValue(readingListNameNormalized, out var readingList))
|
||||
|
@ -620,7 +620,7 @@ public class ReadingListService : IReadingListService
|
|||
// Reading List exists, check if we own it
|
||||
if (user.ReadingLists.All(l => l.NormalizedTitle != readingListNameNormalized))
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult()
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
{
|
||||
Reason = CblImportReason.NameConflict
|
||||
});
|
||||
|
@ -632,7 +632,7 @@ public class ReadingListService : IReadingListService
|
|||
readingList.Items ??= new List<ReadingListItem>();
|
||||
foreach (var (book, i) in cblReading.Books.Book.Select((value, i) => ( value, i )))
|
||||
{
|
||||
var normalizedSeries = Tasks.Scanner.Parser.Parser.Normalize(book.Series);
|
||||
var normalizedSeries = Parser.Normalize(book.Series);
|
||||
if (!allSeries.TryGetValue(normalizedSeries, out var bookSeries) && !allSeriesLocalized.TryGetValue(normalizedSeries, out bookSeries))
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult(book)
|
||||
|
@ -644,7 +644,7 @@ public class ReadingListService : IReadingListService
|
|||
}
|
||||
// Prioritize lookup by Volume then Chapter, but allow fallback to just Chapter
|
||||
var bookVolume = string.IsNullOrEmpty(book.Volume)
|
||||
? Tasks.Scanner.Parser.Parser.DefaultVolume
|
||||
? Parser.DefaultVolume
|
||||
: book.Volume;
|
||||
var matchingVolume = bookSeries.Volumes.Find(v => bookVolume == v.Name) ?? bookSeries.Volumes.Find(v => v.Number == 0);
|
||||
if (matchingVolume == null)
|
||||
|
@ -660,7 +660,7 @@ public class ReadingListService : IReadingListService
|
|||
|
||||
// We need to handle chapter 0 or empty string when it's just a volume
|
||||
var bookNumber = string.IsNullOrEmpty(book.Number)
|
||||
? Tasks.Scanner.Parser.Parser.DefaultChapter
|
||||
? Parser.DefaultChapter
|
||||
: book.Number;
|
||||
var chapter = matchingVolume.Chapters.FirstOrDefault(c => c.Number == bookNumber);
|
||||
if (chapter == null)
|
||||
|
@ -720,7 +720,7 @@ public class ReadingListService : IReadingListService
|
|||
private static IList<Series> FindCblImportConflicts(IEnumerable<Series> userSeries)
|
||||
{
|
||||
var dict = new HashSet<string>();
|
||||
return userSeries.Where(series => !dict.Add(Tasks.Scanner.Parser.Parser.Normalize(series.Name))).ToList();
|
||||
return userSeries.Where(series => !dict.Add(Parser.Normalize(series.Name))).ToList();
|
||||
}
|
||||
|
||||
private static bool IsCblEmpty(CblReadingList cblReading, CblImportSummaryDto importSummary,
|
||||
|
@ -729,7 +729,7 @@ public class ReadingListService : IReadingListService
|
|||
readingListFromCbl = new CblImportSummaryDto();
|
||||
if (cblReading.Books == null || cblReading.Books.Book.Count == 0)
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult()
|
||||
importSummary.Results.Add(new CblBookResult
|
||||
{
|
||||
Reason = CblImportReason.EmptyFile
|
||||
});
|
||||
|
@ -755,7 +755,7 @@ public class ReadingListService : IReadingListService
|
|||
|
||||
public static CblReadingList LoadCblFromPath(string path)
|
||||
{
|
||||
var reader = new System.Xml.Serialization.XmlSerializer(typeof(CblReadingList));
|
||||
var reader = new XmlSerializer(typeof(CblReadingList));
|
||||
using var file = new StreamReader(path);
|
||||
var cblReadingList = (CblReadingList) reader.Deserialize(file);
|
||||
file.Close();
|
||||
|
|
|
@ -30,6 +30,12 @@ public interface ISeriesService
|
|||
Task<bool> DeleteMultipleSeries(IList<int> seriesIds);
|
||||
Task<bool> UpdateRelatedSeries(UpdateRelatedSeriesDto dto);
|
||||
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
||||
Task<string> FormatChapterTitle(int userId, ChapterDto chapter, LibraryType libraryType, bool withHash = true);
|
||||
Task<string> FormatChapterTitle(int userId, Chapter chapter, LibraryType libraryType, bool withHash = true);
|
||||
|
||||
Task<string> FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string? chapterTitle,
|
||||
bool withHash);
|
||||
Task<string> FormatChapterName(int userId, LibraryType libraryType, bool withHash = false);
|
||||
}
|
||||
|
||||
public class SeriesService : ISeriesService
|
||||
|
@ -39,15 +45,17 @@ public class SeriesService : ISeriesService
|
|||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly ILogger<SeriesService> _logger;
|
||||
private readonly IScrobblingService _scrobblingService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public SeriesService(IUnitOfWork unitOfWork, IEventHub eventHub, ITaskScheduler taskScheduler,
|
||||
ILogger<SeriesService> logger, IScrobblingService scrobblingService)
|
||||
ILogger<SeriesService> logger, IScrobblingService scrobblingService, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
_taskScheduler = taskScheduler;
|
||||
_logger = logger;
|
||||
_scrobblingService = scrobblingService;
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -382,16 +390,17 @@ public class SeriesService : ISeriesService
|
|||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
var libraryIds = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId);
|
||||
if (!libraryIds.Contains(series.LibraryId))
|
||||
throw new UnauthorizedAccessException("User does not have access to the library this series belongs to");
|
||||
throw new UnauthorizedAccessException("user-no-access-library-from-series");
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user!.AgeRestriction != AgeRating.NotApplicable)
|
||||
{
|
||||
var seriesMetadata = await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId);
|
||||
if (seriesMetadata!.AgeRating > user.AgeRestriction)
|
||||
throw new UnauthorizedAccessException("User is not allowed to view this series due to age restrictions");
|
||||
throw new UnauthorizedAccessException("series-restricted-age-restriction");
|
||||
}
|
||||
|
||||
|
||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId))
|
||||
.OrderBy(v => Tasks.Scanner.Parser.Parser.MinNumberFromRange(v.Name))
|
||||
|
@ -401,13 +410,14 @@ public class SeriesService : ISeriesService
|
|||
var processedVolumes = new List<VolumeDto>();
|
||||
if (libraryType == LibraryType.Book)
|
||||
{
|
||||
var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty);
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
volume.Chapters = volume.Chapters.OrderBy(d => double.Parse(d.Number), ChapterSortComparer.Default).ToList();
|
||||
var firstChapter = volume.Chapters.First();
|
||||
// On Books, skip volumes that are specials, since these will be shown
|
||||
if (firstChapter.IsSpecial) continue;
|
||||
RenameVolumeName(firstChapter, volume, libraryType);
|
||||
RenameVolumeName(firstChapter, volume, libraryType, volumeLabel);
|
||||
processedVolumes.Add(volume);
|
||||
}
|
||||
}
|
||||
|
@ -431,7 +441,7 @@ public class SeriesService : ISeriesService
|
|||
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
chapter.Title = FormatChapterTitle(chapter, libraryType);
|
||||
chapter.Title = await FormatChapterTitle(userId, chapter, libraryType);
|
||||
if (!chapter.IsSpecial) continue;
|
||||
|
||||
if (!string.IsNullOrEmpty(chapter.TitleName)) chapter.Title = chapter.TitleName;
|
||||
|
@ -481,7 +491,7 @@ public class SeriesService : ISeriesService
|
|||
return !chapter.IsSpecial && !chapter.Number.Equals(Tasks.Scanner.Parser.Parser.DefaultChapter);
|
||||
}
|
||||
|
||||
public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType)
|
||||
public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType, string volumeLabel = "Volume")
|
||||
{
|
||||
if (libraryType == LibraryType.Book)
|
||||
{
|
||||
|
@ -496,19 +506,19 @@ public class SeriesService : ISeriesService
|
|||
{
|
||||
volume.Name += $" - {firstChapter.TitleName}";
|
||||
}
|
||||
else
|
||||
{
|
||||
volume.Name += $"";
|
||||
}
|
||||
// else
|
||||
// {
|
||||
// volume.Name += $"";
|
||||
// }
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
volume.Name = $"Volume {volume.Name}";
|
||||
volume.Name = $"{volumeLabel} {volume.Name}".Trim();
|
||||
}
|
||||
|
||||
|
||||
private static string FormatChapterTitle(bool isSpecial, LibraryType libraryType, string? chapterTitle, bool withHash)
|
||||
public async Task<string> FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string? chapterTitle, bool withHash)
|
||||
{
|
||||
if (string.IsNullOrEmpty(chapterTitle)) throw new ArgumentException("Chapter Title cannot be null");
|
||||
|
||||
|
@ -520,32 +530,33 @@ public class SeriesService : ISeriesService
|
|||
var hashSpot = withHash ? "#" : string.Empty;
|
||||
return libraryType switch
|
||||
{
|
||||
LibraryType.Book => $"Book {chapterTitle}",
|
||||
LibraryType.Comic => $"Issue {hashSpot}{chapterTitle}",
|
||||
LibraryType.Manga => $"Chapter {chapterTitle}",
|
||||
_ => "Chapter "
|
||||
LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle),
|
||||
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterTitle),
|
||||
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterTitle),
|
||||
_ => await _localizationService.Translate(userId, "chapter-num", ' ')
|
||||
};
|
||||
}
|
||||
|
||||
public static string FormatChapterTitle(ChapterDto chapter, LibraryType libraryType, bool withHash = true)
|
||||
public async Task<string> FormatChapterTitle(int userId, ChapterDto chapter, LibraryType libraryType, bool withHash = true)
|
||||
{
|
||||
return FormatChapterTitle(chapter.IsSpecial, libraryType, chapter.Title, withHash);
|
||||
return await FormatChapterTitle(userId, chapter.IsSpecial, libraryType, chapter.Title, withHash);
|
||||
}
|
||||
|
||||
public static string FormatChapterTitle(Chapter chapter, LibraryType libraryType, bool withHash = true)
|
||||
public async Task<string> FormatChapterTitle(int userId, Chapter chapter, LibraryType libraryType, bool withHash = true)
|
||||
{
|
||||
return FormatChapterTitle(chapter.IsSpecial, libraryType, chapter.Title, withHash);
|
||||
return await FormatChapterTitle(userId, chapter.IsSpecial, libraryType, chapter.Title, withHash);
|
||||
}
|
||||
|
||||
public static string FormatChapterName(LibraryType libraryType, bool withHash = false)
|
||||
public async Task<string> FormatChapterName(int userId, LibraryType libraryType, bool withHash = false)
|
||||
{
|
||||
return libraryType switch
|
||||
var hashSpot = withHash ? "#" : string.Empty;
|
||||
return (libraryType switch
|
||||
{
|
||||
LibraryType.Manga => "Chapter",
|
||||
LibraryType.Comic => withHash ? "Issue #" : "Issue",
|
||||
LibraryType.Book => "Book",
|
||||
_ => "Chapter"
|
||||
};
|
||||
LibraryType.Book => await _localizationService.Translate(userId, "book-num", string.Empty),
|
||||
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, string.Empty),
|
||||
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", string.Empty),
|
||||
_ => await _localizationService.Translate(userId, "chapter-num", ' ')
|
||||
}).Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -40,10 +40,10 @@ public class ThemeService : IThemeService
|
|||
public async Task<string> GetContent(int themeId)
|
||||
{
|
||||
var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId);
|
||||
if (theme == null) throw new KavitaException("Theme file missing or invalid");
|
||||
if (theme == null) throw new KavitaException("theme-doesnt-exist");
|
||||
var themeFile = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, theme.FileName);
|
||||
if (string.IsNullOrEmpty(themeFile) || !_directoryService.FileSystem.File.Exists(themeFile))
|
||||
throw new KavitaException("Theme file missing or invalid");
|
||||
throw new KavitaException("theme-doesnt-exist");
|
||||
|
||||
return await _directoryService.FileSystem.File.ReadAllTextAsync(themeFile);
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ public class ThemeService : IThemeService
|
|||
try
|
||||
{
|
||||
var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId);
|
||||
if (theme == null) throw new KavitaException("Theme file missing or invalid");
|
||||
if (theme == null) throw new KavitaException("theme-doesnt-exist");
|
||||
|
||||
foreach (var siteTheme in await _unitOfWork.SiteThemeRepository.GetThemes())
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue