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.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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
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);
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user