feat: enhance API response structure and error handling
This commit is contained in:
parent
bb8b293573
commit
1526dc4bc9
@ -2,12 +2,11 @@ package com.magamochi.mangamochi.controller;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.AuthenticationRequestDTO;
|
||||
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.repository.UserRepository;
|
||||
import com.magamochi.mangamochi.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@ -16,8 +15,6 @@ import org.springframework.web.bind.annotation.*;
|
||||
@RequiredArgsConstructor
|
||||
public class AuthenticationController {
|
||||
private final UserService userService;
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Operation(
|
||||
summary = "Authenticate user",
|
||||
@ -25,9 +22,9 @@ public class AuthenticationController {
|
||||
tags = {"Auth"},
|
||||
operationId = "authenticateUser")
|
||||
@PostMapping("/login")
|
||||
public AuthenticationResponseDTO authenticateUser(
|
||||
public DefaultResponseDTO<AuthenticationResponseDTO> authenticateUser(
|
||||
@RequestBody AuthenticationRequestDTO authenticationRequestDTO) {
|
||||
return userService.authenticate(authenticationRequestDTO);
|
||||
return DefaultResponseDTO.ok(userService.authenticate(authenticationRequestDTO));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@ -36,7 +33,10 @@ public class AuthenticationController {
|
||||
tags = {"Auth"},
|
||||
operationId = "registerUser")
|
||||
@PostMapping("/register")
|
||||
public void registerUser(@RequestBody RegistrationRequestDTO registrationRequestDTO) {
|
||||
public DefaultResponseDTO<Void> registerUser(
|
||||
@RequestBody RegistrationRequestDTO registrationRequestDTO) {
|
||||
userService.register(registrationRequestDTO);
|
||||
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.magamochi.mangamochi.controller;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.DefaultResponseDTO;
|
||||
import com.magamochi.mangamochi.model.dto.GenreDTO;
|
||||
import com.magamochi.mangamochi.service.GenreService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@ -19,7 +20,7 @@ public class GenreController {
|
||||
tags = {"Genre"},
|
||||
operationId = "getGenres")
|
||||
@GetMapping
|
||||
public List<GenreDTO> getGenres() {
|
||||
return genreService.getGenres();
|
||||
public DefaultResponseDTO<List<GenreDTO>> getGenres() {
|
||||
return DefaultResponseDTO.ok(genreService.getGenres());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -1,23 +1,15 @@
|
||||
package com.magamochi.mangamochi.controller;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.*;
|
||||
import com.magamochi.mangamochi.model.enumeration.ArchiveFileType;
|
||||
import com.magamochi.mangamochi.service.MangaService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
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 lombok.RequiredArgsConstructor;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.web.PageableDefault;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@ -32,19 +24,20 @@ public class MangaController {
|
||||
tags = {"Manga"},
|
||||
operationId = "getMangas")
|
||||
@GetMapping
|
||||
public Page<MangaListDTO> getMangas(
|
||||
public DefaultResponseDTO<Page<MangaListDTO>> getMangas(
|
||||
@ParameterObject MangaListFilterDTO filterDTO,
|
||||
@Parameter(hidden = true) @ParameterObject @PageableDefault Pageable pageable) {
|
||||
return mangaService.getMangas(filterDTO, pageable);
|
||||
return DefaultResponseDTO.ok(mangaService.getMangas(filterDTO, pageable));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Get the details of a manga",
|
||||
description = "Get the details of a manga by its ID",
|
||||
tags = {"Manga"},
|
||||
operationId = "getManga")
|
||||
@GetMapping("/{mangaId}")
|
||||
public MangaDTO getManga(@PathVariable Long mangaId) {
|
||||
return mangaService.getManga(mangaId);
|
||||
public DefaultResponseDTO<MangaDTO> getManga(@PathVariable Long mangaId) {
|
||||
return DefaultResponseDTO.ok(mangaService.getManga(mangaId));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@ -53,8 +46,9 @@ public class MangaController {
|
||||
tags = {"Manga"},
|
||||
operationId = "getMangaChapters")
|
||||
@GetMapping("/{mangaProviderId}/chapters")
|
||||
public List<MangaChapterDTO> getMangaChapters(@PathVariable Long mangaProviderId) {
|
||||
return mangaService.getMangaChapters(mangaProviderId);
|
||||
public DefaultResponseDTO<List<MangaChapterDTO>> getMangaChapters(
|
||||
@PathVariable Long mangaProviderId) {
|
||||
return DefaultResponseDTO.ok(mangaService.getMangaChapters(mangaProviderId));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@ -63,74 +57,9 @@ public class MangaController {
|
||||
tags = {"Manga"},
|
||||
operationId = "fetchMangaChapters")
|
||||
@PostMapping("/{mangaProviderId}/fetch-chapters")
|
||||
public void fetchMangaChapters(@PathVariable Long mangaProviderId) {
|
||||
public DefaultResponseDTO<Void> fetchMangaChapters(@PathVariable Long mangaProviderId) {
|
||||
mangaService.fetchMangaChapters(mangaProviderId);
|
||||
}
|
||||
|
||||
@Operation(summary = "Fetch chapter", operationId = "fetchChapter")
|
||||
@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();
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,9 +27,9 @@ public class MangaImportController {
|
||||
tags = {"Manga Import"},
|
||||
operationId = "importFromMangaDex")
|
||||
@PostMapping("/manga-dex")
|
||||
public ImportMangaDexResponseDTO importFromMangaDex(
|
||||
public DefaultResponseDTO<ImportMangaDexResponseDTO> importFromMangaDex(
|
||||
@RequestBody ImportMangaDexRequestDTO requestDTO) {
|
||||
return mangaDexProvider.importManga(requestDTO.id());
|
||||
return DefaultResponseDTO.ok(mangaDexProvider.importManga(requestDTO.id()));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@ -40,7 +40,7 @@ public class MangaImportController {
|
||||
@PostMapping(
|
||||
value = "/upload",
|
||||
consumes = {"multipart/form-data"})
|
||||
public void uploadMultipleFiles(
|
||||
public DefaultResponseDTO<Void> uploadMultipleFiles(
|
||||
@RequestPart("malId") @NotBlank String malId,
|
||||
@Parameter(
|
||||
description = "List of files to upload",
|
||||
@ -53,5 +53,7 @@ public class MangaImportController {
|
||||
@NotNull
|
||||
List<MultipartFile> files) {
|
||||
mangaImportService.importMangaFiles(malId, files);
|
||||
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package com.magamochi.mangamochi.controller;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.DefaultResponseDTO;
|
||||
import com.magamochi.mangamochi.service.UserFavoriteMangaService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -17,8 +18,10 @@ public class UserFavoriteMangaController {
|
||||
tags = {"Favorite Mangas"},
|
||||
operationId = "setFavorite")
|
||||
@PostMapping("/{id}/favorite")
|
||||
public void setFavorite(@PathVariable Long id) {
|
||||
public DefaultResponseDTO<Void> setFavorite(@PathVariable Long id) {
|
||||
userFavoriteMangaService.setFavorite(id);
|
||||
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@ -27,7 +30,9 @@ public class UserFavoriteMangaController {
|
||||
tags = {"Favorite Mangas"},
|
||||
operationId = "setUnfavorite")
|
||||
@PostMapping("/{id}/unfavorite")
|
||||
public void setUnfavorite(@PathVariable Long id) {
|
||||
public DefaultResponseDTO<Void> setUnfavorite(@PathVariable Long id) {
|
||||
userFavoriteMangaService.setUnfavorite(id);
|
||||
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
package com.magamochi.mangamochi.exception;
|
||||
|
||||
public class ConflictException extends RuntimeException {
|
||||
public ConflictException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.magamochi.mangamochi.exception;
|
||||
|
||||
public class NotFoundException extends RuntimeException {
|
||||
public NotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.magamochi.mangamochi.exception;
|
||||
|
||||
public class UnprocessableException extends RuntimeException {
|
||||
public UnprocessableException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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) {}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -46,6 +46,7 @@ public class MangaImportService {
|
||||
RateLimiter rateLimiter = RateLimiter.create(1);
|
||||
|
||||
public void importMangaFiles(String malId, List<MultipartFile> files) {
|
||||
log.info("Importing manga files for MAL ID {}", malId);
|
||||
var provider = providerService.getOrCreateProvider("Manual Import");
|
||||
|
||||
rateLimiter.acquire();
|
||||
@ -114,7 +115,7 @@ public class MangaImportService {
|
||||
mangaChapterRepository.save(chapter);
|
||||
});
|
||||
|
||||
log.warn("test");
|
||||
log.info("Import manga files for MAL ID {} completed.", malId);
|
||||
}
|
||||
|
||||
public void updateMangaData(Manga manga) {
|
||||
@ -135,30 +136,28 @@ public class MangaImportService {
|
||||
var authors =
|
||||
mangaData.data().authors().stream()
|
||||
.map(
|
||||
authorData -> {
|
||||
return authorRepository
|
||||
.findByMalId(authorData.mal_id())
|
||||
.orElseGet(
|
||||
() ->
|
||||
authorRepository.save(
|
||||
Author.builder()
|
||||
.malId(authorData.mal_id())
|
||||
.name(authorData.name())
|
||||
.build()));
|
||||
})
|
||||
authorData ->
|
||||
authorRepository
|
||||
.findByMalId(authorData.mal_id())
|
||||
.orElseGet(
|
||||
() ->
|
||||
authorRepository.save(
|
||||
Author.builder()
|
||||
.malId(authorData.mal_id())
|
||||
.name(authorData.name())
|
||||
.build())))
|
||||
.toList();
|
||||
|
||||
var mangaAuthors =
|
||||
authors.stream()
|
||||
.map(
|
||||
author -> {
|
||||
return mangaAuthorRepository
|
||||
.findByMangaAndAuthor(manga, author)
|
||||
.orElseGet(
|
||||
() ->
|
||||
mangaAuthorRepository.save(
|
||||
MangaAuthor.builder().manga(manga).author(author).build()));
|
||||
})
|
||||
author ->
|
||||
mangaAuthorRepository
|
||||
.findByMangaAndAuthor(manga, author)
|
||||
.orElseGet(
|
||||
() ->
|
||||
mangaAuthorRepository.save(
|
||||
MangaAuthor.builder().manga(manga).author(author).build())))
|
||||
.toList();
|
||||
|
||||
manga.setMangaAuthors(mangaAuthors);
|
||||
@ -166,30 +165,28 @@ public class MangaImportService {
|
||||
var genres =
|
||||
mangaData.data().genres().stream()
|
||||
.map(
|
||||
genreData -> {
|
||||
return genreRepository
|
||||
.findByMalId(genreData.mal_id())
|
||||
.orElseGet(
|
||||
() ->
|
||||
genreRepository.save(
|
||||
Genre.builder()
|
||||
.malId(genreData.mal_id())
|
||||
.name(genreData.name())
|
||||
.build()));
|
||||
})
|
||||
genreData ->
|
||||
genreRepository
|
||||
.findByMalId(genreData.mal_id())
|
||||
.orElseGet(
|
||||
() ->
|
||||
genreRepository.save(
|
||||
Genre.builder()
|
||||
.malId(genreData.mal_id())
|
||||
.name(genreData.name())
|
||||
.build())))
|
||||
.toList();
|
||||
|
||||
var mangaGenres =
|
||||
genres.stream()
|
||||
.map(
|
||||
genre -> {
|
||||
return mangaGenreRepository
|
||||
.findByMangaAndGenre(manga, genre)
|
||||
.orElseGet(
|
||||
() ->
|
||||
mangaGenreRepository.save(
|
||||
MangaGenre.builder().manga(manga).genre(genre).build()));
|
||||
})
|
||||
genre ->
|
||||
mangaGenreRepository
|
||||
.findByMangaAndGenre(manga, genre)
|
||||
.orElseGet(
|
||||
() ->
|
||||
mangaGenreRepository.save(
|
||||
MangaGenre.builder().manga(manga).genre(genre).build())))
|
||||
.toList();
|
||||
|
||||
manga.setMangaGenres(mangaGenres);
|
||||
|
||||
@ -2,30 +2,21 @@ package com.magamochi.mangamochi.service;
|
||||
|
||||
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.entity.Manga;
|
||||
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.entity.MangaProvider;
|
||||
import com.magamochi.mangamochi.model.repository.*;
|
||||
import com.magamochi.mangamochi.model.specification.MangaSpecification;
|
||||
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.URL;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
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.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -36,13 +27,8 @@ import org.springframework.stereotype.Service;
|
||||
public class MangaService {
|
||||
private final MangaImportService mangaImportService;
|
||||
private final UserService userService;
|
||||
private final MangaChapterRepository mangaChapterRepository;
|
||||
private final MangaRepository mangaRepository;
|
||||
private final MangaProviderRepository mangaProviderRepository;
|
||||
private final MangaChapterImageRepository mangaChapterImageRepository;
|
||||
private final ImageService imageService;
|
||||
|
||||
private final JikanClient jikanClient;
|
||||
|
||||
private final ContentProviderFactory contentProviderFactory;
|
||||
private final UserFavoriteMangaRepository userFavoriteMangaRepository;
|
||||
@ -69,148 +55,12 @@ public class MangaService {
|
||||
}
|
||||
|
||||
public List<MangaChapterDTO> getMangaChapters(Long mangaProviderId) {
|
||||
var mangaProvider =
|
||||
mangaProviderRepository
|
||||
.findById(mangaProviderId)
|
||||
.orElseThrow(() -> new RuntimeException("manga provider not found"));
|
||||
var mangaProvider = getMangaProviderThrowIfNotFound(mangaProviderId);
|
||||
|
||||
var chapters =
|
||||
mangaProvider.getMangaChapters().stream()
|
||||
.sorted(Comparator.comparing(MangaChapter::getId))
|
||||
.map(MangaChapterDTO::from)
|
||||
.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);
|
||||
return mangaProvider.getMangaChapters().stream()
|
||||
.sorted(Comparator.comparing(MangaChapter::getId))
|
||||
.map(MangaChapterDTO::from)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public MangaDTO getManga(Long mangaId) {
|
||||
@ -220,10 +70,7 @@ public class MangaService {
|
||||
}
|
||||
|
||||
public void fetchMangaChapters(Long mangaProviderId) {
|
||||
var mangaProvider =
|
||||
mangaProviderRepository
|
||||
.findById(mangaProviderId)
|
||||
.orElseThrow(() -> new RuntimeException("manga provider not found"));
|
||||
var mangaProvider = getMangaProviderThrowIfNotFound(mangaProviderId);
|
||||
|
||||
var contentProvider =
|
||||
contentProviderFactory.getContentProvider(mangaProvider.getProvider().getName());
|
||||
@ -233,28 +80,16 @@ public class MangaService {
|
||||
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) {
|
||||
return mangaRepository
|
||||
.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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
package com.magamochi.mangamochi.service;
|
||||
|
||||
import com.magamochi.mangamochi.exception.NotFoundException;
|
||||
import com.magamochi.mangamochi.model.entity.Manga;
|
||||
import com.magamochi.mangamochi.model.entity.UserFavoriteManga;
|
||||
import com.magamochi.mangamochi.model.repository.MangaRepository;
|
||||
import com.magamochi.mangamochi.model.repository.UserFavoriteMangaRepository;
|
||||
import java.util.NoSuchElementException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@ -33,7 +33,10 @@ public class UserFavoriteMangaService {
|
||||
var favoriteManga =
|
||||
userFavoriteMangaRepository
|
||||
.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);
|
||||
}
|
||||
@ -41,6 +44,6 @@ public class UserFavoriteMangaService {
|
||||
private Manga findMangaByIdThrowIfNotFound(Long mangaId) {
|
||||
return mangaRepository
|
||||
.findById(mangaId)
|
||||
.orElseThrow(() -> new RuntimeException("Manga not found"));
|
||||
.orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ package com.magamochi.mangamochi.service;
|
||||
|
||||
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.AuthenticationResponseDTO;
|
||||
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.util.JwtUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Log4j2
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserService {
|
||||
@ -31,8 +37,12 @@ public class UserService {
|
||||
|
||||
try {
|
||||
authenticationManager.authenticate(token);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Authentication failed", e);
|
||||
} catch (AuthenticationException e) {
|
||||
if (e.getMessage().equals("Bad credentials")) {
|
||||
throw new BadCredentialsException("Wrong email or password.");
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
var userDetails = userDetailsService.loadUserByUsername(request.email());
|
||||
@ -46,7 +56,7 @@ public class UserService {
|
||||
|
||||
public void register(RegistrationRequestDTO request) {
|
||||
if (userRepository.existsByEmail(request.email())) {
|
||||
throw new RuntimeException("Email is already taken");
|
||||
throw new ConflictException("An user with this email already exists.");
|
||||
}
|
||||
|
||||
userRepository.save(
|
||||
@ -61,7 +71,7 @@ public class UserService {
|
||||
public User getLoggedUserThrowIfNotFound() {
|
||||
var authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (isNull(authentication) || authentication.getName().equals("anonymousUser")) {
|
||||
throw new RuntimeException("No authenticated user found");
|
||||
throw new NotFoundException("User not found.");
|
||||
}
|
||||
|
||||
return findUserByEmailThrowIfNotFound(authentication.getName());
|
||||
@ -79,6 +89,6 @@ public class UserService {
|
||||
private User findUserByEmailThrowIfNotFound(String email) {
|
||||
return userRepository
|
||||
.findByEmail(email)
|
||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||
.orElseThrow(() -> new NotFoundException("User not found."));
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import static java.util.Objects.isNull;
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import com.magamochi.mangamochi.client.MangaDexAuthenticationClient;
|
||||
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.ContentProviderMangaInfoResponseDTO;
|
||||
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)
|
||||
.toList();
|
||||
|
||||
var map =
|
||||
IntStream.range(0, chapterImageHashes.size())
|
||||
.boxed()
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
i -> i,
|
||||
chapterImageHashes::get,
|
||||
(existing, replacement) -> existing,
|
||||
LinkedHashMap::new));
|
||||
|
||||
return map;
|
||||
return IntStream.range(0, chapterImageHashes.size())
|
||||
.boxed()
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
i -> i,
|
||||
chapterImageHashes::get,
|
||||
(existing, replacement) -> existing,
|
||||
LinkedHashMap::new));
|
||||
}
|
||||
|
||||
public ImportMangaDexResponseDTO importManga(UUID id) {
|
||||
@ -138,7 +137,7 @@ public class MangaDexProvider implements ContentProvider {
|
||||
var resultData = mangaDexClient.getManga(id, token).data();
|
||||
|
||||
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 =
|
||||
@ -149,7 +148,7 @@ public class MangaDexProvider implements ContentProvider {
|
||||
"en",
|
||||
resultData.attributes().title().values().stream()
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new NoSuchElementException("No title available")));
|
||||
.orElseThrow(() -> new UnprocessableException("No title available")));
|
||||
|
||||
var provider =
|
||||
providerRepository
|
||||
@ -162,7 +161,7 @@ public class MangaDexProvider implements ContentProvider {
|
||||
var manga = mangaCreationService.getOrCreateManga(mangaTitle, id.toString(), provider);
|
||||
|
||||
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(
|
||||
|
||||
@ -2,7 +2,6 @@ package com.magamochi.mangamochi.task;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import com.magamochi.mangamochi.model.repository.*;
|
||||
import com.magamochi.mangamochi.service.MangaImportService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -19,8 +18,6 @@ public class UpdateMangaDataTask {
|
||||
|
||||
@Scheduled(fixedDelayString = "1d")
|
||||
public void updateMangaData() {
|
||||
var rateLimiter = RateLimiter.create(1);
|
||||
|
||||
var mangas =
|
||||
mangaRepository.findAll().stream().filter(manga -> isNull(manga.getScore())).toList();
|
||||
|
||||
|
||||
@ -16,8 +16,8 @@ spring:
|
||||
default-schema: mangamochi
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 250MB
|
||||
max-request-size: 2GB
|
||||
max-file-size: 800MB
|
||||
max-request-size: 4GB
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user