feat: enhance API response structure and error handling

This commit is contained in:
Rodrigo Verdiani 2025-10-25 10:37:11 -03:00
parent bb8b293573
commit 1526dc4bc9
23 changed files with 631 additions and 342 deletions

View File

@ -2,12 +2,11 @@ package com.magamochi.mangamochi.controller;
import com.magamochi.mangamochi.model.dto.AuthenticationRequestDTO; import com.magamochi.mangamochi.model.dto.AuthenticationRequestDTO;
import com.magamochi.mangamochi.model.dto.AuthenticationResponseDTO; import com.magamochi.mangamochi.model.dto.AuthenticationResponseDTO;
import com.magamochi.mangamochi.model.dto.DefaultResponseDTO;
import com.magamochi.mangamochi.model.dto.RegistrationRequestDTO; import com.magamochi.mangamochi.model.dto.RegistrationRequestDTO;
import com.magamochi.mangamochi.model.repository.UserRepository;
import com.magamochi.mangamochi.service.UserService; import com.magamochi.mangamochi.service.UserService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@RestController @RestController
@ -16,8 +15,6 @@ import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor @RequiredArgsConstructor
public class AuthenticationController { public class AuthenticationController {
private final UserService userService; private final UserService userService;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Operation( @Operation(
summary = "Authenticate user", summary = "Authenticate user",
@ -25,9 +22,9 @@ public class AuthenticationController {
tags = {"Auth"}, tags = {"Auth"},
operationId = "authenticateUser") operationId = "authenticateUser")
@PostMapping("/login") @PostMapping("/login")
public AuthenticationResponseDTO authenticateUser( public DefaultResponseDTO<AuthenticationResponseDTO> authenticateUser(
@RequestBody AuthenticationRequestDTO authenticationRequestDTO) { @RequestBody AuthenticationRequestDTO authenticationRequestDTO) {
return userService.authenticate(authenticationRequestDTO); return DefaultResponseDTO.ok(userService.authenticate(authenticationRequestDTO));
} }
@Operation( @Operation(
@ -36,7 +33,10 @@ public class AuthenticationController {
tags = {"Auth"}, tags = {"Auth"},
operationId = "registerUser") operationId = "registerUser")
@PostMapping("/register") @PostMapping("/register")
public void registerUser(@RequestBody RegistrationRequestDTO registrationRequestDTO) { public DefaultResponseDTO<Void> registerUser(
@RequestBody RegistrationRequestDTO registrationRequestDTO) {
userService.register(registrationRequestDTO); userService.register(registrationRequestDTO);
return DefaultResponseDTO.ok().build();
} }
} }

View File

@ -1,5 +1,6 @@
package com.magamochi.mangamochi.controller; package com.magamochi.mangamochi.controller;
import com.magamochi.mangamochi.model.dto.DefaultResponseDTO;
import com.magamochi.mangamochi.model.dto.GenreDTO; import com.magamochi.mangamochi.model.dto.GenreDTO;
import com.magamochi.mangamochi.service.GenreService; import com.magamochi.mangamochi.service.GenreService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@ -19,7 +20,7 @@ public class GenreController {
tags = {"Genre"}, tags = {"Genre"},
operationId = "getGenres") operationId = "getGenres")
@GetMapping @GetMapping
public List<GenreDTO> getGenres() { public DefaultResponseDTO<List<GenreDTO>> getGenres() {
return genreService.getGenres(); return DefaultResponseDTO.ok(genreService.getGenres());
} }
} }

View File

@ -0,0 +1,83 @@
package com.magamochi.mangamochi.controller;
import com.magamochi.mangamochi.model.dto.*;
import com.magamochi.mangamochi.model.enumeration.ArchiveFileType;
import com.magamochi.mangamochi.service.MangaChapterService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/mangas/chapters")
@RequiredArgsConstructor
public class MangaChapterController {
private final MangaChapterService mangaChapterService;
@Operation(
summary = "Fetch chapter",
description = "Fetch the chapter from the provider",
tags = {"Manga Chapter"},
operationId = "fetchChapter")
@PostMapping(value = "/{chapterId}/fetch")
public DefaultResponseDTO<Void> fetchChapter(@PathVariable Long chapterId) {
mangaChapterService.fetchChapter(chapterId);
return DefaultResponseDTO.ok().build();
}
@Operation(
summary = "Get the images for a specific manga/provider combination",
description =
"Retrieve a list of manga chapter images for a specific manga/provider combination.",
tags = {"Manga Chapter"},
operationId = "getMangaChapterImages")
@GetMapping("/{chapterId}/images")
public DefaultResponseDTO<MangaChapterImagesDTO> getMangaChapterImages(
@PathVariable Long chapterId) {
return DefaultResponseDTO.ok(mangaChapterService.getMangaChapterImages(chapterId));
}
@Operation(
summary = "Mark a chapter as read",
description = "Mark a chapter as read by its ID.",
tags = {"Manga Chapter"},
operationId = "markAsRead")
@PostMapping("/{chapterId}/mark-as-read")
public DefaultResponseDTO<Void> markAsRead(@PathVariable Long chapterId) {
mangaChapterService.markAsRead(chapterId);
return DefaultResponseDTO.ok().build();
}
@Operation(
summary = "Download chapter archive",
description = "Download a chapter as a compressed file by its ID.",
tags = {"Manga Chapter"},
operationId = "downloadChapterArchive")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "Successful download",
content =
@Content(
mediaType = "application/octet-stream",
schema = @Schema(type = "string", format = "binary"))),
})
@PostMapping(value = "/{chapterId}/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<byte[]> downloadChapterArchive(
@PathVariable Long chapterId, @RequestParam ArchiveFileType archiveFileType)
throws IOException {
var response = mangaChapterService.downloadChapter(chapterId, archiveFileType);
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=\"" + response.filename() + "\"")
.body(response.content());
}
}

View File

@ -1,23 +1,15 @@
package com.magamochi.mangamochi.controller; package com.magamochi.mangamochi.controller;
import com.magamochi.mangamochi.model.dto.*; import com.magamochi.mangamochi.model.dto.*;
import com.magamochi.mangamochi.model.enumeration.ArchiveFileType;
import com.magamochi.mangamochi.service.MangaService; import com.magamochi.mangamochi.service.MangaService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import java.io.IOException;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject; import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault; import org.springframework.data.web.PageableDefault;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@RestController @RestController
@ -32,19 +24,20 @@ public class MangaController {
tags = {"Manga"}, tags = {"Manga"},
operationId = "getMangas") operationId = "getMangas")
@GetMapping @GetMapping
public Page<MangaListDTO> getMangas( public DefaultResponseDTO<Page<MangaListDTO>> getMangas(
@ParameterObject MangaListFilterDTO filterDTO, @ParameterObject MangaListFilterDTO filterDTO,
@Parameter(hidden = true) @ParameterObject @PageableDefault Pageable pageable) { @Parameter(hidden = true) @ParameterObject @PageableDefault Pageable pageable) {
return mangaService.getMangas(filterDTO, pageable); return DefaultResponseDTO.ok(mangaService.getMangas(filterDTO, pageable));
} }
@Operation( @Operation(
summary = "Get the details of a manga", summary = "Get the details of a manga",
description = "Get the details of a manga by its ID",
tags = {"Manga"}, tags = {"Manga"},
operationId = "getManga") operationId = "getManga")
@GetMapping("/{mangaId}") @GetMapping("/{mangaId}")
public MangaDTO getManga(@PathVariable Long mangaId) { public DefaultResponseDTO<MangaDTO> getManga(@PathVariable Long mangaId) {
return mangaService.getManga(mangaId); return DefaultResponseDTO.ok(mangaService.getManga(mangaId));
} }
@Operation( @Operation(
@ -53,8 +46,9 @@ public class MangaController {
tags = {"Manga"}, tags = {"Manga"},
operationId = "getMangaChapters") operationId = "getMangaChapters")
@GetMapping("/{mangaProviderId}/chapters") @GetMapping("/{mangaProviderId}/chapters")
public List<MangaChapterDTO> getMangaChapters(@PathVariable Long mangaProviderId) { public DefaultResponseDTO<List<MangaChapterDTO>> getMangaChapters(
return mangaService.getMangaChapters(mangaProviderId); @PathVariable Long mangaProviderId) {
return DefaultResponseDTO.ok(mangaService.getMangaChapters(mangaProviderId));
} }
@Operation( @Operation(
@ -63,74 +57,9 @@ public class MangaController {
tags = {"Manga"}, tags = {"Manga"},
operationId = "fetchMangaChapters") operationId = "fetchMangaChapters")
@PostMapping("/{mangaProviderId}/fetch-chapters") @PostMapping("/{mangaProviderId}/fetch-chapters")
public void fetchMangaChapters(@PathVariable Long mangaProviderId) { public DefaultResponseDTO<Void> fetchMangaChapters(@PathVariable Long mangaProviderId) {
mangaService.fetchMangaChapters(mangaProviderId); mangaService.fetchMangaChapters(mangaProviderId);
}
@Operation(summary = "Fetch chapter", operationId = "fetchChapter") return DefaultResponseDTO.ok().build();
@PostMapping(value = "/chapter/{chapterId}/fetch")
public ResponseEntity<Void> fetchChapter(@PathVariable Long chapterId) {
mangaService.fetchChapter(chapterId);
return ResponseEntity.ok().build();
}
@Operation(
summary = "Get the available chapters for a specific manga/provider combination",
description = "Retrieve a list of manga chapters for a specific manga/provider combination.",
tags = {"Manga"},
operationId = "getMangaChapterImages")
@GetMapping("/{chapterId}/images")
public MangaChapterImagesDTO getMangaChapterImages(@PathVariable Long chapterId) {
return mangaService.getMangaChapterImages(chapterId);
}
@Operation(
summary = "Mark a chapter as read",
description = "Mark a chapter as read by its ID.",
tags = {"Manga"},
operationId = "markAsRead")
@PostMapping("/{chapterId}/mark-as-read")
public void markAsRead(@PathVariable Long chapterId) {
mangaService.markAsRead(chapterId);
}
@Operation(
summary = "Download all chapters for a manga provider",
operationId = "downloadAllChapters")
@PostMapping(value = "/chapter/{mangaProviderId}/download-all")
public ResponseEntity<Void> downloadAllChapters(@PathVariable Long mangaProviderId) {
mangaService.downloadAllChapters(mangaProviderId);
return ResponseEntity.ok().build();
}
@Operation(summary = "Download chapter archive", operationId = "downloadChapterArchive")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "Successful download",
content =
@Content(
mediaType = "application/octet-stream",
schema = @Schema(type = "string", format = "binary"))),
})
@PostMapping(
value = "/chapter/{chapterId}/download-archive",
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<byte[]> downloadChapterArchive(
@PathVariable Long chapterId, @RequestParam ArchiveFileType archiveFileType)
throws IOException {
var response = mangaService.downloadChapter(chapterId, archiveFileType);
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=\"" + response.filename() + "\"")
.body(response.content());
}
@Operation(summary = "Update manga info", operationId = "updateMangaInfo")
@PostMapping(value = "/manga/{mangaId}/info")
public ResponseEntity<Void> updateMangaInfo(@PathVariable Long mangaId) {
mangaService.updateInfo(mangaId);
return ResponseEntity.ok().build();
} }
} }

View File

@ -27,9 +27,9 @@ public class MangaImportController {
tags = {"Manga Import"}, tags = {"Manga Import"},
operationId = "importFromMangaDex") operationId = "importFromMangaDex")
@PostMapping("/manga-dex") @PostMapping("/manga-dex")
public ImportMangaDexResponseDTO importFromMangaDex( public DefaultResponseDTO<ImportMangaDexResponseDTO> importFromMangaDex(
@RequestBody ImportMangaDexRequestDTO requestDTO) { @RequestBody ImportMangaDexRequestDTO requestDTO) {
return mangaDexProvider.importManga(requestDTO.id()); return DefaultResponseDTO.ok(mangaDexProvider.importManga(requestDTO.id()));
} }
@Operation( @Operation(
@ -40,7 +40,7 @@ public class MangaImportController {
@PostMapping( @PostMapping(
value = "/upload", value = "/upload",
consumes = {"multipart/form-data"}) consumes = {"multipart/form-data"})
public void uploadMultipleFiles( public DefaultResponseDTO<Void> uploadMultipleFiles(
@RequestPart("malId") @NotBlank String malId, @RequestPart("malId") @NotBlank String malId,
@Parameter( @Parameter(
description = "List of files to upload", description = "List of files to upload",
@ -53,5 +53,7 @@ public class MangaImportController {
@NotNull @NotNull
List<MultipartFile> files) { List<MultipartFile> files) {
mangaImportService.importMangaFiles(malId, files); mangaImportService.importMangaFiles(malId, files);
return DefaultResponseDTO.ok().build();
} }
} }

View File

@ -0,0 +1,51 @@
package com.magamochi.mangamochi.controller;
import com.magamochi.mangamochi.model.dto.DefaultResponseDTO;
import com.magamochi.mangamochi.model.dto.ImportReviewDTO;
import com.magamochi.mangamochi.service.MangaImportReviewService;
import io.swagger.v3.oas.annotations.Operation;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/manga/import/review")
@RequiredArgsConstructor
public class MangaImportReviewController {
private final MangaImportReviewService mangaImportReviewService;
@Operation(
summary = "Get list of pending import reviews",
description = "Get list of pending import reviews.",
tags = {"Manga Import Review"},
operationId = "getImportReviews")
@GetMapping
public DefaultResponseDTO<List<ImportReviewDTO>> getImportReviews() {
return DefaultResponseDTO.ok(mangaImportReviewService.getImportReviews());
}
@Operation(
summary = "Delete pending import review",
description = "Delete pending import review by ID.",
tags = {"Manga Import Review"},
operationId = "deleteImportReview")
@DeleteMapping("/{id}")
public DefaultResponseDTO<Void> deleteImportReview(@PathVariable Long id) {
mangaImportReviewService.deleteImportReview(id);
return DefaultResponseDTO.ok().build();
}
@Operation(
summary = "Resolve import review",
description = "Resolve import review by ID.",
tags = {"Manga Import Review"},
operationId = "resolveImportReview")
@PostMapping
public DefaultResponseDTO<Void> resolveImportReview(
@RequestParam Long importReviewId, @RequestParam String malId) {
mangaImportReviewService.resolveImportReview(importReviewId, malId);
return DefaultResponseDTO.ok().build();
}
}

View File

@ -1,5 +1,6 @@
package com.magamochi.mangamochi.controller; package com.magamochi.mangamochi.controller;
import com.magamochi.mangamochi.model.dto.DefaultResponseDTO;
import com.magamochi.mangamochi.service.UserFavoriteMangaService; import com.magamochi.mangamochi.service.UserFavoriteMangaService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -17,8 +18,10 @@ public class UserFavoriteMangaController {
tags = {"Favorite Mangas"}, tags = {"Favorite Mangas"},
operationId = "setFavorite") operationId = "setFavorite")
@PostMapping("/{id}/favorite") @PostMapping("/{id}/favorite")
public void setFavorite(@PathVariable Long id) { public DefaultResponseDTO<Void> setFavorite(@PathVariable Long id) {
userFavoriteMangaService.setFavorite(id); userFavoriteMangaService.setFavorite(id);
return DefaultResponseDTO.ok().build();
} }
@Operation( @Operation(
@ -27,7 +30,9 @@ public class UserFavoriteMangaController {
tags = {"Favorite Mangas"}, tags = {"Favorite Mangas"},
operationId = "setUnfavorite") operationId = "setUnfavorite")
@PostMapping("/{id}/unfavorite") @PostMapping("/{id}/unfavorite")
public void setUnfavorite(@PathVariable Long id) { public DefaultResponseDTO<Void> setUnfavorite(@PathVariable Long id) {
userFavoriteMangaService.setUnfavorite(id); userFavoriteMangaService.setUnfavorite(id);
return DefaultResponseDTO.ok().build();
} }
} }

View File

@ -0,0 +1,7 @@
package com.magamochi.mangamochi.exception;
public class ConflictException extends RuntimeException {
public ConflictException(String message) {
super(message);
}
}

View File

@ -0,0 +1,65 @@
package com.magamochi.mangamochi.exception;
import com.magamochi.mangamochi.model.dto.ErrorResponseDTO;
import java.time.Instant;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponseDTO> handleRuntimeException() {
var error =
new ErrorResponseDTO(
Instant.now(), "An unexpected error occurred. Please try again later.");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ErrorResponseDTO> handleAuthenticationException() {
var error = new ErrorResponseDTO(Instant.now(), "An error occurred during authentication.");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
}
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ErrorResponseDTO> handleBadCredentialsException(BadCredentialsException e) {
var error = new ErrorResponseDTO(Instant.now(), e.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
}
@ExceptionHandler(UsernameNotFoundException.class)
public ResponseEntity<ErrorResponseDTO> handleUsernameNotFoundException() {
var error = new ErrorResponseDTO(Instant.now(), "Username not found.");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponseDTO> handleNotFoundException(NotFoundException e) {
var error = new ErrorResponseDTO(Instant.now(), e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(ConflictException.class)
public ResponseEntity<ErrorResponseDTO> handleConflictException(ConflictException e) {
var error = new ErrorResponseDTO(Instant.now(), e.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
@ExceptionHandler(UnprocessableException.class)
public ResponseEntity<ErrorResponseDTO> handleUnprocessableException(UnprocessableException e) {
var error = new ErrorResponseDTO(Instant.now(), e.getMessage());
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(error);
}
}

View File

@ -0,0 +1,7 @@
package com.magamochi.mangamochi.exception;
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,7 @@
package com.magamochi.mangamochi.exception;
public class UnprocessableException extends RuntimeException {
public UnprocessableException(String message) {
super(message);
}
}

View File

@ -0,0 +1,29 @@
package com.magamochi.mangamochi.model.dto;
import jakarta.annotation.Nullable;
import java.time.Instant;
import lombok.Builder;
import lombok.Data;
@Builder
@Data
public class DefaultResponseDTO<T> {
private Instant timestamp;
private T data;
private String message;
private static <T> DefaultResponseDTOBuilder<T> init() {
return DefaultResponseDTO.<T>builder().timestamp(Instant.now());
}
public static DefaultResponseDTOBuilder<Void> ok() {
return DefaultResponseDTO.init();
}
public static <T> DefaultResponseDTO<T> ok(@Nullable T data) {
DefaultResponseDTOBuilder<T> defaultResponseDTOBuilder = DefaultResponseDTO.init();
defaultResponseDTOBuilder.data(data);
return defaultResponseDTOBuilder.build();
}
}

View File

@ -0,0 +1,7 @@
package com.magamochi.mangamochi.model.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.Instant;
public record ErrorResponseDTO(@NotNull Instant timestamp, @NotBlank String message) {}

View File

@ -0,0 +1,24 @@
package com.magamochi.mangamochi.model.dto;
import com.magamochi.mangamochi.model.entity.MangaImportReview;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.Instant;
public record ImportReviewDTO(
@NotNull Long id,
@NotBlank String title,
@NotBlank String providerName,
String externalUrl,
@NotBlank String reason,
@NotNull Instant createdAt) {
public static ImportReviewDTO from(MangaImportReview review) {
return new ImportReviewDTO(
review.getId(),
review.getTitle(),
review.getProvider().getName(),
review.getUrl(),
"Title match not found",
review.getCreatedAt());
}
}

View File

@ -0,0 +1,154 @@
package com.magamochi.mangamochi.service;
import com.magamochi.mangamochi.exception.UnprocessableException;
import com.magamochi.mangamochi.model.dto.MangaChapterArchiveDTO;
import com.magamochi.mangamochi.model.dto.MangaChapterImagesDTO;
import com.magamochi.mangamochi.model.entity.MangaChapter;
import com.magamochi.mangamochi.model.entity.MangaChapterImage;
import com.magamochi.mangamochi.model.enumeration.ArchiveFileType;
import com.magamochi.mangamochi.model.repository.MangaChapterImageRepository;
import com.magamochi.mangamochi.model.repository.MangaChapterRepository;
import com.magamochi.mangamochi.service.providers.ContentProviderFactory;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Log4j2
@Service
@RequiredArgsConstructor
public class MangaChapterService {
private final MangaChapterRepository mangaChapterRepository;
private final MangaChapterImageRepository mangaChapterImageRepository;
private final ImageService imageService;
private final ContentProviderFactory contentProviderFactory;
@Transactional
public void fetchChapter(Long chapterId) {
var chapter = getMangaChapterThrowIfNotFound(chapterId);
var mangaProvider = chapter.getMangaProvider();
var provider = contentProviderFactory.getContentProvider(mangaProvider.getProvider().getName());
var chapterImagesUrls = provider.getChapterImagesUrls(chapter.getUrl());
if (chapterImagesUrls.isEmpty()) {
throw new UnprocessableException(
"No images found on provider for Manga Chapter ID: " + chapterId);
}
var chapterImages =
chapterImagesUrls.entrySet().stream()
.map(
entry -> {
try {
var inputStream =
new BufferedInputStream(
new URL(new URI(entry.getValue()).toASCIIString()).openStream());
var bytes = inputStream.readAllBytes();
inputStream.close();
var image =
imageService.uploadImage(bytes, "image/jpeg", "chapter/" + chapterId);
log.info(
"Downloaded image {}/{} for manga {} chapter {}: {}",
entry.getKey() + 1,
chapterImagesUrls.size(),
chapter.getMangaProvider().getManga().getTitle(),
chapterId,
entry.getValue());
return MangaChapterImage.builder()
.mangaChapter(chapter)
.position(entry.getKey())
.image(image)
.build();
} catch (IOException | URISyntaxException e) {
throw new UnprocessableException(
"Could not download image for chapter ID: " + chapterId);
}
})
.toList();
mangaChapterImageRepository.saveAll(chapterImages);
chapter.setDownloaded(true);
mangaChapterRepository.save(chapter);
}
public MangaChapterImagesDTO getMangaChapterImages(Long chapterId) {
var chapter = getMangaChapterThrowIfNotFound(chapterId);
return MangaChapterImagesDTO.from(chapter);
}
public void markAsRead(Long chapterId) {
var chapter = getMangaChapterThrowIfNotFound(chapterId);
chapter.setRead(true);
mangaChapterRepository.save(chapter);
}
public MangaChapterArchiveDTO downloadChapter(Long chapterId, ArchiveFileType archiveFileType)
throws IOException {
var chapter = getMangaChapterThrowIfNotFound(chapterId);
var chapterImages = mangaChapterImageRepository.findAllByMangaChapter(chapter);
var byteArrayOutputStream =
switch (archiveFileType) {
case CBZ -> getChapterCbzArchive(chapterImages);
default ->
throw new UnprocessableException(
"Unsupported archive file type: " + archiveFileType.name());
};
return new MangaChapterArchiveDTO(
chapter.getTitle() + ".cbz", byteArrayOutputStream.toByteArray());
}
private ByteArrayOutputStream getChapterCbzArchive(List<MangaChapterImage> chapterImages)
throws IOException {
var byteArrayOutputStream = new ByteArrayOutputStream();
var bufferedOutputStream = new BufferedOutputStream(byteArrayOutputStream);
var zipOutputStream = new ZipOutputStream(bufferedOutputStream);
var totalPages = chapterImages.size();
var paddingLength = String.valueOf(totalPages).length();
for (var pageNumber = 1; pageNumber <= totalPages; pageNumber++) {
var imgSrc = chapterImages.get(pageNumber - 1);
var paddedFileName = String.format("%0" + paddingLength + "d.jpg", imgSrc.getPosition());
zipOutputStream.putNextEntry(new ZipEntry(paddedFileName));
IOUtils.copy(imageService.getImageStream(imgSrc.getImage()), zipOutputStream);
zipOutputStream.closeEntry();
}
zipOutputStream.finish();
zipOutputStream.flush();
IOUtils.closeQuietly(zipOutputStream);
return byteArrayOutputStream;
}
private MangaChapter getMangaChapterThrowIfNotFound(Long chapterId) {
return mangaChapterRepository
.findById(chapterId)
.orElseThrow(() -> new RuntimeException("Manga Chapter not found for ID: " + chapterId));
}
}

View File

@ -0,0 +1,77 @@
package com.magamochi.mangamochi.service;
import static java.util.Objects.isNull;
import com.google.common.util.concurrent.RateLimiter;
import com.magamochi.mangamochi.client.JikanClient;
import com.magamochi.mangamochi.exception.NotFoundException;
import com.magamochi.mangamochi.model.dto.ImportReviewDTO;
import com.magamochi.mangamochi.model.entity.Manga;
import com.magamochi.mangamochi.model.entity.MangaImportReview;
import com.magamochi.mangamochi.model.entity.MangaProvider;
import com.magamochi.mangamochi.model.repository.MangaImportReviewRepository;
import com.magamochi.mangamochi.model.repository.MangaProviderRepository;
import com.magamochi.mangamochi.model.repository.MangaRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class MangaImportReviewService {
private final MangaImportReviewRepository mangaImportReviewRepository;
private final MangaRepository mangaRepository;
private final MangaProviderRepository mangaProviderRepository;
private final JikanClient jikanClient;
RateLimiter rateLimiter = RateLimiter.create(1);
public List<ImportReviewDTO> getImportReviews() {
return mangaImportReviewRepository.findAll().stream().map(ImportReviewDTO::from).toList();
}
public void deleteImportReview(Long id) {
var importReview = getImportReviewThrowIfNotFound(id);
mangaImportReviewRepository.delete(importReview);
}
public void resolveImportReview(Long id, String malId) {
var importReview = getImportReviewThrowIfNotFound(id);
rateLimiter.acquire();
var jikanResult = jikanClient.getMangaById(Long.parseLong(malId)).data();
if (isNull(jikanResult)) {
throw new NotFoundException("MyAnimeList manga not found for ID: " + id);
}
var manga =
mangaRepository
.findByTitleIgnoreCase(jikanResult.title())
.orElseGet(
() ->
mangaRepository.save(
Manga.builder()
.title(jikanResult.title())
.malId(Long.parseLong(malId))
.build()));
mangaProviderRepository.save(
MangaProvider.builder()
.manga(manga)
.mangaTitle(importReview.getTitle())
.provider(importReview.getProvider())
.url(importReview.getUrl())
.build());
mangaImportReviewRepository.delete(importReview);
}
private MangaImportReview getImportReviewThrowIfNotFound(Long id) {
return mangaImportReviewRepository
.findById(id)
.orElseThrow(() -> new NotFoundException("Import review not found for ID: " + id));
}
}

View File

@ -46,6 +46,7 @@ public class MangaImportService {
RateLimiter rateLimiter = RateLimiter.create(1); RateLimiter rateLimiter = RateLimiter.create(1);
public void importMangaFiles(String malId, List<MultipartFile> files) { public void importMangaFiles(String malId, List<MultipartFile> files) {
log.info("Importing manga files for MAL ID {}", malId);
var provider = providerService.getOrCreateProvider("Manual Import"); var provider = providerService.getOrCreateProvider("Manual Import");
rateLimiter.acquire(); rateLimiter.acquire();
@ -114,7 +115,7 @@ public class MangaImportService {
mangaChapterRepository.save(chapter); mangaChapterRepository.save(chapter);
}); });
log.warn("test"); log.info("Import manga files for MAL ID {} completed.", malId);
} }
public void updateMangaData(Manga manga) { public void updateMangaData(Manga manga) {
@ -135,30 +136,28 @@ public class MangaImportService {
var authors = var authors =
mangaData.data().authors().stream() mangaData.data().authors().stream()
.map( .map(
authorData -> { authorData ->
return authorRepository authorRepository
.findByMalId(authorData.mal_id()) .findByMalId(authorData.mal_id())
.orElseGet( .orElseGet(
() -> () ->
authorRepository.save( authorRepository.save(
Author.builder() Author.builder()
.malId(authorData.mal_id()) .malId(authorData.mal_id())
.name(authorData.name()) .name(authorData.name())
.build())); .build())))
})
.toList(); .toList();
var mangaAuthors = var mangaAuthors =
authors.stream() authors.stream()
.map( .map(
author -> { author ->
return mangaAuthorRepository mangaAuthorRepository
.findByMangaAndAuthor(manga, author) .findByMangaAndAuthor(manga, author)
.orElseGet( .orElseGet(
() -> () ->
mangaAuthorRepository.save( mangaAuthorRepository.save(
MangaAuthor.builder().manga(manga).author(author).build())); MangaAuthor.builder().manga(manga).author(author).build())))
})
.toList(); .toList();
manga.setMangaAuthors(mangaAuthors); manga.setMangaAuthors(mangaAuthors);
@ -166,30 +165,28 @@ public class MangaImportService {
var genres = var genres =
mangaData.data().genres().stream() mangaData.data().genres().stream()
.map( .map(
genreData -> { genreData ->
return genreRepository genreRepository
.findByMalId(genreData.mal_id()) .findByMalId(genreData.mal_id())
.orElseGet( .orElseGet(
() -> () ->
genreRepository.save( genreRepository.save(
Genre.builder() Genre.builder()
.malId(genreData.mal_id()) .malId(genreData.mal_id())
.name(genreData.name()) .name(genreData.name())
.build())); .build())))
})
.toList(); .toList();
var mangaGenres = var mangaGenres =
genres.stream() genres.stream()
.map( .map(
genre -> { genre ->
return mangaGenreRepository mangaGenreRepository
.findByMangaAndGenre(manga, genre) .findByMangaAndGenre(manga, genre)
.orElseGet( .orElseGet(
() -> () ->
mangaGenreRepository.save( mangaGenreRepository.save(
MangaGenre.builder().manga(manga).genre(genre).build())); MangaGenre.builder().manga(manga).genre(genre).build())))
})
.toList(); .toList();
manga.setMangaGenres(mangaGenres); manga.setMangaGenres(mangaGenres);

View File

@ -2,30 +2,21 @@ package com.magamochi.mangamochi.service;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
import com.magamochi.mangamochi.client.JikanClient; import com.magamochi.mangamochi.exception.NotFoundException;
import com.magamochi.mangamochi.model.dto.*; import com.magamochi.mangamochi.model.dto.*;
import com.magamochi.mangamochi.model.entity.Manga; import com.magamochi.mangamochi.model.entity.Manga;
import com.magamochi.mangamochi.model.entity.MangaChapter; import com.magamochi.mangamochi.model.entity.MangaChapter;
import com.magamochi.mangamochi.model.entity.MangaChapterImage; import com.magamochi.mangamochi.model.entity.MangaProvider;
import com.magamochi.mangamochi.model.enumeration.ArchiveFileType;
import com.magamochi.mangamochi.model.repository.*; import com.magamochi.mangamochi.model.repository.*;
import com.magamochi.mangamochi.model.specification.MangaSpecification; import com.magamochi.mangamochi.model.specification.MangaSpecification;
import com.magamochi.mangamochi.service.providers.ContentProviderFactory; import com.magamochi.mangamochi.service.providers.ContentProviderFactory;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.*; import java.net.*;
import java.net.URL;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -36,13 +27,8 @@ import org.springframework.stereotype.Service;
public class MangaService { public class MangaService {
private final MangaImportService mangaImportService; private final MangaImportService mangaImportService;
private final UserService userService; private final UserService userService;
private final MangaChapterRepository mangaChapterRepository;
private final MangaRepository mangaRepository; private final MangaRepository mangaRepository;
private final MangaProviderRepository mangaProviderRepository; private final MangaProviderRepository mangaProviderRepository;
private final MangaChapterImageRepository mangaChapterImageRepository;
private final ImageService imageService;
private final JikanClient jikanClient;
private final ContentProviderFactory contentProviderFactory; private final ContentProviderFactory contentProviderFactory;
private final UserFavoriteMangaRepository userFavoriteMangaRepository; private final UserFavoriteMangaRepository userFavoriteMangaRepository;
@ -69,148 +55,12 @@ public class MangaService {
} }
public List<MangaChapterDTO> getMangaChapters(Long mangaProviderId) { public List<MangaChapterDTO> getMangaChapters(Long mangaProviderId) {
var mangaProvider = var mangaProvider = getMangaProviderThrowIfNotFound(mangaProviderId);
mangaProviderRepository
.findById(mangaProviderId)
.orElseThrow(() -> new RuntimeException("manga provider not found"));
var chapters = return mangaProvider.getMangaChapters().stream()
mangaProvider.getMangaChapters().stream() .sorted(Comparator.comparing(MangaChapter::getId))
.sorted(Comparator.comparing(MangaChapter::getId)) .map(MangaChapterDTO::from)
.map(MangaChapterDTO::from) .toList();
.toList();
return chapters;
}
public void fetchChapter(Long chapterId) {
try {
var chapter =
mangaChapterRepository
.findById(chapterId)
.orElseThrow(() -> new RuntimeException("Chapter not found"));
var mangaProvider = chapter.getMangaProvider();
var provider =
contentProviderFactory.getContentProvider(mangaProvider.getProvider().getName());
var chapterImagesUrls = provider.getChapterImagesUrls(chapter.getUrl());
if (chapterImagesUrls.isEmpty()) {
throw new RuntimeException("Chapter image not found");
}
var chapterImages =
chapterImagesUrls.entrySet().stream()
.map(
entry -> {
try {
var inputStream =
new BufferedInputStream(
new URL(new URI(entry.getValue()).toASCIIString()).openStream());
var bytes = inputStream.readAllBytes();
inputStream.close();
var image =
imageService.uploadImage(bytes, "image/jpeg", "chapter/" + chapterId);
log.info(
"Downloaded image {}/{} for manga {} chapter {}: {}",
entry.getKey() + 1,
chapterImagesUrls.size(),
chapter.getMangaProvider().getManga().getTitle(),
chapterId,
entry.getValue());
return MangaChapterImage.builder()
.mangaChapter(chapter)
.position(entry.getKey())
.image(image)
.build();
} catch (IOException | URISyntaxException e) {
throw new RuntimeException(e);
}
})
.toList();
mangaChapterImageRepository.saveAll(chapterImages);
chapter.setDownloaded(true);
mangaChapterRepository.save(chapter);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public MangaChapterArchiveDTO downloadChapter(Long chapterId, ArchiveFileType archiveFileType)
throws IOException {
var chapter =
mangaChapterRepository
.findById(chapterId)
.orElseThrow(() -> new RuntimeException("Chapter not found"));
var chapterImages = mangaChapterImageRepository.findAllByMangaChapter(chapter);
var byteArrayOutputStream =
switch (archiveFileType) {
case CBZ -> getChapterCbzArchive(chapterImages);
default -> throw new RuntimeException("Unsupported archive file type");
};
return new MangaChapterArchiveDTO(
chapter.getTitle() + ".cbz", byteArrayOutputStream.toByteArray());
}
private ByteArrayOutputStream getChapterCbzArchive(List<MangaChapterImage> chapterImages)
throws IOException {
var byteArrayOutputStream = new ByteArrayOutputStream();
var bufferedOutputStream = new BufferedOutputStream(byteArrayOutputStream);
var zipOutputStream = new ZipOutputStream(bufferedOutputStream);
var totalPages = chapterImages.size();
var paddingLength = String.valueOf(totalPages).length();
for (var pageNumber = 1; pageNumber <= totalPages; pageNumber++) {
var imgSrc = chapterImages.get(pageNumber - 1);
var paddedFileName = String.format("%0" + paddingLength + "d.jpg", imgSrc.getPosition());
zipOutputStream.putNextEntry(new ZipEntry(paddedFileName));
IOUtils.copy(imageService.getImageStream(imgSrc.getImage()), zipOutputStream);
zipOutputStream.closeEntry();
}
zipOutputStream.finish();
zipOutputStream.flush();
IOUtils.closeQuietly(zipOutputStream);
return byteArrayOutputStream;
}
public void downloadAllChapters(Long mangaProviderId) {
var mangaProvider =
mangaProviderRepository
.findById(mangaProviderId)
.orElseThrow(() -> new RuntimeException("Manga provider not found"));
var mangaChapters = mangaChapterRepository.findByMangaProviderId(mangaProviderId);
mangaChapters.forEach(mangaChapter -> fetchChapter(mangaChapter.getId()));
}
public void updateInfo(Long mangaId) {
var manga = findMangaByIdThrowIfNotFound(mangaId);
var mangaSearchResponse = jikanClient.mangaSearch(manga.getTitle());
if (mangaSearchResponse.data().isEmpty()) {
throw new RuntimeException("Manga not found");
}
// TODO: create logic to select appropriate manga
var mangaResponse = mangaSearchResponse.data().getFirst();
manga.setTitle(mangaResponse.title());
manga.setMalId(mangaResponse.mal_id());
mangaRepository.save(manga);
} }
public MangaDTO getManga(Long mangaId) { public MangaDTO getManga(Long mangaId) {
@ -220,10 +70,7 @@ public class MangaService {
} }
public void fetchMangaChapters(Long mangaProviderId) { public void fetchMangaChapters(Long mangaProviderId) {
var mangaProvider = var mangaProvider = getMangaProviderThrowIfNotFound(mangaProviderId);
mangaProviderRepository
.findById(mangaProviderId)
.orElseThrow(() -> new RuntimeException("manga provider not found"));
var contentProvider = var contentProvider =
contentProviderFactory.getContentProvider(mangaProvider.getProvider().getName()); contentProviderFactory.getContentProvider(mangaProvider.getProvider().getName());
@ -233,28 +80,16 @@ public class MangaService {
chapter -> mangaImportService.persistMangaChapter(mangaProvider, chapter)); chapter -> mangaImportService.persistMangaChapter(mangaProvider, chapter));
} }
public MangaChapterImagesDTO getMangaChapterImages(Long chapterId) {
var chapter =
mangaChapterRepository
.findById(chapterId)
.orElseThrow(() -> new RuntimeException("Chapter not found"));
return MangaChapterImagesDTO.from(chapter);
}
public void markAsRead(Long chapterId) {
var chapter =
mangaChapterRepository
.findById(chapterId)
.orElseThrow(() -> new RuntimeException("Chapter not found"));
chapter.setRead(true);
mangaChapterRepository.save(chapter);
}
public Manga findMangaByIdThrowIfNotFound(Long mangaId) { public Manga findMangaByIdThrowIfNotFound(Long mangaId) {
return mangaRepository return mangaRepository
.findById(mangaId) .findById(mangaId)
.orElseThrow(() -> new RuntimeException("Manga not found")); .orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
}
private MangaProvider getMangaProviderThrowIfNotFound(Long mangaProviderId) {
return mangaProviderRepository
.findById(mangaProviderId)
.orElseThrow(
() -> new NotFoundException("Manga Provider not found for ID: " + mangaProviderId));
} }
} }

View File

@ -1,10 +1,10 @@
package com.magamochi.mangamochi.service; package com.magamochi.mangamochi.service;
import com.magamochi.mangamochi.exception.NotFoundException;
import com.magamochi.mangamochi.model.entity.Manga; import com.magamochi.mangamochi.model.entity.Manga;
import com.magamochi.mangamochi.model.entity.UserFavoriteManga; import com.magamochi.mangamochi.model.entity.UserFavoriteManga;
import com.magamochi.mangamochi.model.repository.MangaRepository; import com.magamochi.mangamochi.model.repository.MangaRepository;
import com.magamochi.mangamochi.model.repository.UserFavoriteMangaRepository; import com.magamochi.mangamochi.model.repository.UserFavoriteMangaRepository;
import java.util.NoSuchElementException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -33,7 +33,10 @@ public class UserFavoriteMangaService {
var favoriteManga = var favoriteManga =
userFavoriteMangaRepository userFavoriteMangaRepository
.findByUserAndManga(user, manga) .findByUserAndManga(user, manga)
.orElseThrow(() -> new NoSuchElementException("No manga found")); .orElseThrow(
() ->
new NotFoundException(
"Error while trying to unfavorite manga. Please try again later."));
userFavoriteMangaRepository.delete(favoriteManga); userFavoriteMangaRepository.delete(favoriteManga);
} }
@ -41,6 +44,6 @@ public class UserFavoriteMangaService {
private Manga findMangaByIdThrowIfNotFound(Long mangaId) { private Manga findMangaByIdThrowIfNotFound(Long mangaId) {
return mangaRepository return mangaRepository
.findById(mangaId) .findById(mangaId)
.orElseThrow(() -> new RuntimeException("Manga not found")); .orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
} }
} }

View File

@ -2,6 +2,8 @@ package com.magamochi.mangamochi.service;
import static java.util.Objects.isNull; import static java.util.Objects.isNull;
import com.magamochi.mangamochi.exception.ConflictException;
import com.magamochi.mangamochi.exception.NotFoundException;
import com.magamochi.mangamochi.model.dto.AuthenticationRequestDTO; import com.magamochi.mangamochi.model.dto.AuthenticationRequestDTO;
import com.magamochi.mangamochi.model.dto.AuthenticationResponseDTO; import com.magamochi.mangamochi.model.dto.AuthenticationResponseDTO;
import com.magamochi.mangamochi.model.dto.RegistrationRequestDTO; import com.magamochi.mangamochi.model.dto.RegistrationRequestDTO;
@ -10,13 +12,17 @@ import com.magamochi.mangamochi.model.enumeration.UserRole;
import com.magamochi.mangamochi.model.repository.UserRepository; import com.magamochi.mangamochi.model.repository.UserRepository;
import com.magamochi.mangamochi.util.JwtUtil; import com.magamochi.mangamochi.util.JwtUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Log4j2
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class UserService { public class UserService {
@ -31,8 +37,12 @@ public class UserService {
try { try {
authenticationManager.authenticate(token); authenticationManager.authenticate(token);
} catch (Exception e) { } catch (AuthenticationException e) {
throw new RuntimeException("Authentication failed", e); if (e.getMessage().equals("Bad credentials")) {
throw new BadCredentialsException("Wrong email or password.");
}
throw e;
} }
var userDetails = userDetailsService.loadUserByUsername(request.email()); var userDetails = userDetailsService.loadUserByUsername(request.email());
@ -46,7 +56,7 @@ public class UserService {
public void register(RegistrationRequestDTO request) { public void register(RegistrationRequestDTO request) {
if (userRepository.existsByEmail(request.email())) { if (userRepository.existsByEmail(request.email())) {
throw new RuntimeException("Email is already taken"); throw new ConflictException("An user with this email already exists.");
} }
userRepository.save( userRepository.save(
@ -61,7 +71,7 @@ public class UserService {
public User getLoggedUserThrowIfNotFound() { public User getLoggedUserThrowIfNotFound() {
var authentication = SecurityContextHolder.getContext().getAuthentication(); var authentication = SecurityContextHolder.getContext().getAuthentication();
if (isNull(authentication) || authentication.getName().equals("anonymousUser")) { if (isNull(authentication) || authentication.getName().equals("anonymousUser")) {
throw new RuntimeException("No authenticated user found"); throw new NotFoundException("User not found.");
} }
return findUserByEmailThrowIfNotFound(authentication.getName()); return findUserByEmailThrowIfNotFound(authentication.getName());
@ -79,6 +89,6 @@ public class UserService {
private User findUserByEmailThrowIfNotFound(String email) { private User findUserByEmailThrowIfNotFound(String email) {
return userRepository return userRepository
.findByEmail(email) .findByEmail(email)
.orElseThrow(() -> new RuntimeException("User not found")); .orElseThrow(() -> new NotFoundException("User not found."));
} }
} }

View File

@ -5,6 +5,8 @@ import static java.util.Objects.isNull;
import com.google.common.util.concurrent.RateLimiter; import com.google.common.util.concurrent.RateLimiter;
import com.magamochi.mangamochi.client.MangaDexAuthenticationClient; import com.magamochi.mangamochi.client.MangaDexAuthenticationClient;
import com.magamochi.mangamochi.client.MangaDexClient; import com.magamochi.mangamochi.client.MangaDexClient;
import com.magamochi.mangamochi.exception.NotFoundException;
import com.magamochi.mangamochi.exception.UnprocessableException;
import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO; import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO;
import com.magamochi.mangamochi.model.dto.ContentProviderMangaInfoResponseDTO; import com.magamochi.mangamochi.model.dto.ContentProviderMangaInfoResponseDTO;
import com.magamochi.mangamochi.model.dto.ImportMangaDexResponseDTO; import com.magamochi.mangamochi.model.dto.ImportMangaDexResponseDTO;
@ -118,17 +120,14 @@ public class MangaDexProvider implements ContentProvider {
.map(s -> chapter.baseUrl() + "/data/" + chapter.chapter().hash() + "/" + s) .map(s -> chapter.baseUrl() + "/data/" + chapter.chapter().hash() + "/" + s)
.toList(); .toList();
var map = return IntStream.range(0, chapterImageHashes.size())
IntStream.range(0, chapterImageHashes.size()) .boxed()
.boxed() .collect(
.collect( Collectors.toMap(
Collectors.toMap( i -> i,
i -> i, chapterImageHashes::get,
chapterImageHashes::get, (existing, replacement) -> existing,
(existing, replacement) -> existing, LinkedHashMap::new));
LinkedHashMap::new));
return map;
} }
public ImportMangaDexResponseDTO importManga(UUID id) { public ImportMangaDexResponseDTO importManga(UUID id) {
@ -138,7 +137,7 @@ public class MangaDexProvider implements ContentProvider {
var resultData = mangaDexClient.getManga(id, token).data(); var resultData = mangaDexClient.getManga(id, token).data();
if (resultData.attributes().title().isEmpty()) { if (resultData.attributes().title().isEmpty()) {
throw new NoSuchElementException("Manga title not found for ID: " + id); throw new UnprocessableException("Manga title not found for ID: " + id);
} }
var mangaTitle = var mangaTitle =
@ -149,7 +148,7 @@ public class MangaDexProvider implements ContentProvider {
"en", "en",
resultData.attributes().title().values().stream() resultData.attributes().title().values().stream()
.findFirst() .findFirst()
.orElseThrow(() -> new NoSuchElementException("No title available"))); .orElseThrow(() -> new UnprocessableException("No title available")));
var provider = var provider =
providerRepository providerRepository
@ -162,7 +161,7 @@ public class MangaDexProvider implements ContentProvider {
var manga = mangaCreationService.getOrCreateManga(mangaTitle, id.toString(), provider); var manga = mangaCreationService.getOrCreateManga(mangaTitle, id.toString(), provider);
if (isNull(manga)) { if (isNull(manga)) {
throw new NoSuchElementException("Manga not found for ID: " + id); throw new NotFoundException("Manga could not be found or created for ID: " + id);
} }
mangaProviderRepository.save( mangaProviderRepository.save(

View File

@ -2,7 +2,6 @@ package com.magamochi.mangamochi.task;
import static java.util.Objects.isNull; import static java.util.Objects.isNull;
import com.google.common.util.concurrent.RateLimiter;
import com.magamochi.mangamochi.model.repository.*; import com.magamochi.mangamochi.model.repository.*;
import com.magamochi.mangamochi.service.MangaImportService; import com.magamochi.mangamochi.service.MangaImportService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -19,8 +18,6 @@ public class UpdateMangaDataTask {
@Scheduled(fixedDelayString = "1d") @Scheduled(fixedDelayString = "1d")
public void updateMangaData() { public void updateMangaData() {
var rateLimiter = RateLimiter.create(1);
var mangas = var mangas =
mangaRepository.findAll().stream().filter(manga -> isNull(manga.getScore())).toList(); mangaRepository.findAll().stream().filter(manga -> isNull(manga.getScore())).toList();

View File

@ -16,8 +16,8 @@ spring:
default-schema: mangamochi default-schema: mangamochi
servlet: servlet:
multipart: multipart:
max-file-size: 250MB max-file-size: 800MB
max-request-size: 2GB max-request-size: 4GB
springdoc: springdoc:
api-docs: api-docs: