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:
Joe Milazzo 2023-08-03 10:33:51 -05:00 committed by GitHub
parent 670bf82c38
commit 3b23d63234
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
389 changed files with 13652 additions and 7925 deletions

View file

@ -191,6 +191,7 @@
<ItemGroup>
<Folder Include="config\themes" />
<Folder Include="I18N\**" />
</ItemGroup>
<ItemGroup>

View file

@ -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"));
}

View file

@ -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));
}
}
}

View file

@ -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"));
}
}

View file

@ -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"));
}

View file

@ -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()!;

View file

@ -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));

View file

@ -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();

View file

@ -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();
}
}

View 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);
}
}

View file

@ -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);
}
}

View file

@ -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)

View file

@ -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()

View file

@ -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)
{

View file

@ -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}";

View file

@ -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));
}

View file

@ -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"));
}
}

View file

@ -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);

View file

@ -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"));
}

View file

@ -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));
}
}

View file

@ -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"));
}

View file

@ -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>()));
}

View file

@ -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()));
}

View file

@ -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));
}
}
}

View file

@ -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"));
}
}

View file

@ -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>

View file

@ -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"));
}
}

View file

@ -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; }
}

View file

@ -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)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class 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");
}
}
}

View file

@ -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");

View file

@ -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)

View file

@ -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);

View file

@ -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; }

View file

@ -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>();

View file

@ -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
View 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}"
}

View file

@ -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;

View file

@ -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,

View file

@ -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;

View file

@ -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();

View file

@ -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>

View 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();
}
}

View file

@ -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();

View file

@ -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;

View file

@ -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 =

View file

@ -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();

View file

@ -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>

View file

@ -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())
{