diff --git a/pom.xml b/pom.xml
index 84c3513..3df6343 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,14 +5,14 @@
org.springframework.boot
spring-boot-starter-parent
- 3.5.6
+ 4.0.3
- com.magamochi
+ com.mangamochi
mangamochi
0.0.1-SNAPSHOT
mangamochi
- Demo project for Spring Boot
+
@@ -27,7 +27,7 @@
- 21
+ 25
@@ -65,56 +65,49 @@
software.amazon.awssdk
s3
- 2.34.5
+ 2.42.14
compile
org.springdoc
springdoc-openapi-starter-webmvc-ui
- 2.8.13
+ 3.0.2
org.springframework.cloud
spring-cloud-starter-openfeign
- 4.3.0
+ 5.0.1
org.jsoup
jsoup
- 1.21.2
+ 1.22.1
io.hypersistence
- hypersistence-utils-hibernate-63
- 3.11.0
+ hypersistence-utils-hibernate-73
+ 3.15.2
-
-
com.google.guava
guava
33.5.0-jre
-
org.springframework.boot
spring-boot-starter-security
-
-
io.jsonwebtoken
jjwt-api
0.13.0
-
io.jsonwebtoken
jjwt-impl
0.13.0
runtime
-
io.jsonwebtoken
jjwt-jackson
@@ -125,12 +118,12 @@
org.springframework.boot
spring-boot-starter-amqp
-
-
- io.github.resilience4j
- resilience4j-spring-boot3
- 2.3.0
-
+
+ org.apache.tika
+ tika-core
+ 3.2.3
+ compile
+
@@ -162,7 +155,7 @@
com.diffplug.spotless
spotless-maven-plugin
- 2.46.1
+ 3.3.0
diff --git a/src/main/java/com/magamochi/mangamochi/MangamochiApplication.java b/src/main/java/com/magamochi/MangamochiApplication.java
similarity index 94%
rename from src/main/java/com/magamochi/mangamochi/MangamochiApplication.java
rename to src/main/java/com/magamochi/MangamochiApplication.java
index 8f40b76..f774fd7 100644
--- a/src/main/java/com/magamochi/mangamochi/MangamochiApplication.java
+++ b/src/main/java/com/magamochi/MangamochiApplication.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi;
+package com.magamochi;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.boot.SpringApplication;
diff --git a/src/main/java/com/magamochi/authentication/controller/AuthenticationController.java b/src/main/java/com/magamochi/authentication/controller/AuthenticationController.java
new file mode 100644
index 0000000..565e89e
--- /dev/null
+++ b/src/main/java/com/magamochi/authentication/controller/AuthenticationController.java
@@ -0,0 +1,40 @@
+package com.magamochi.authentication.controller;
+
+import com.magamochi.authentication.model.dto.AuthenticationRequestDTO;
+import com.magamochi.authentication.model.dto.AuthenticationResponseDTO;
+import com.magamochi.authentication.model.dto.RefreshTokenRequestDTO;
+import com.magamochi.authentication.service.AuthenticationService;
+import com.magamochi.common.model.dto.DefaultResponseDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/authentication")
+@CrossOrigin(origins = "*")
+@RequiredArgsConstructor
+public class AuthenticationController {
+ private final AuthenticationService authenticationService;
+
+ @Operation(
+ summary = "Authenticate an user",
+ description = "Authenticate an user with email and password.",
+ tags = {"Authentication"},
+ operationId = "login")
+ @PostMapping
+ public DefaultResponseDTO login(
+ @RequestBody AuthenticationRequestDTO authenticationRequestDTO) {
+ return DefaultResponseDTO.ok(authenticationService.authenticate(authenticationRequestDTO));
+ }
+
+ @Operation(
+ summary = "Refresh authentication token",
+ description = "Refresh the authentication token",
+ tags = {"Authentication"},
+ operationId = "refreshAuthToken")
+ @PostMapping("/refresh")
+ public DefaultResponseDTO refreshAuthToken(
+ @RequestBody RefreshTokenRequestDTO authenticationRequestDTO) {
+ return DefaultResponseDTO.ok(authenticationService.refreshAuthToken(authenticationRequestDTO));
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/AuthenticationRequestDTO.java b/src/main/java/com/magamochi/authentication/model/dto/AuthenticationRequestDTO.java
similarity index 74%
rename from src/main/java/com/magamochi/mangamochi/model/dto/AuthenticationRequestDTO.java
rename to src/main/java/com/magamochi/authentication/model/dto/AuthenticationRequestDTO.java
index f5fada2..de343e9 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/AuthenticationRequestDTO.java
+++ b/src/main/java/com/magamochi/authentication/model/dto/AuthenticationRequestDTO.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.authentication.model.dto;
import jakarta.validation.constraints.NotNull;
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/AuthenticationResponseDTO.java b/src/main/java/com/magamochi/authentication/model/dto/AuthenticationResponseDTO.java
similarity index 71%
rename from src/main/java/com/magamochi/mangamochi/model/dto/AuthenticationResponseDTO.java
rename to src/main/java/com/magamochi/authentication/model/dto/AuthenticationResponseDTO.java
index 0d5d1ec..f9c863d 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/AuthenticationResponseDTO.java
+++ b/src/main/java/com/magamochi/authentication/model/dto/AuthenticationResponseDTO.java
@@ -1,6 +1,6 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.authentication.model.dto;
-import com.magamochi.mangamochi.model.enumeration.UserRole;
+import com.magamochi.user.model.enumeration.UserRole;
import jakarta.validation.constraints.NotNull;
public record AuthenticationResponseDTO(
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/RefreshTokenRequestDTO.java b/src/main/java/com/magamochi/authentication/model/dto/RefreshTokenRequestDTO.java
similarity index 71%
rename from src/main/java/com/magamochi/mangamochi/model/dto/RefreshTokenRequestDTO.java
rename to src/main/java/com/magamochi/authentication/model/dto/RefreshTokenRequestDTO.java
index d93fc5d..5cb57e4 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/RefreshTokenRequestDTO.java
+++ b/src/main/java/com/magamochi/authentication/model/dto/RefreshTokenRequestDTO.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.authentication.model.dto;
import jakarta.validation.constraints.NotNull;
diff --git a/src/main/java/com/magamochi/mangamochi/security/JwtRequestFilter.java b/src/main/java/com/magamochi/authentication/security/JwtRequestFilter.java
similarity index 88%
rename from src/main/java/com/magamochi/mangamochi/security/JwtRequestFilter.java
rename to src/main/java/com/magamochi/authentication/security/JwtRequestFilter.java
index 719c8f2..7c8f717 100644
--- a/src/main/java/com/magamochi/mangamochi/security/JwtRequestFilter.java
+++ b/src/main/java/com/magamochi/authentication/security/JwtRequestFilter.java
@@ -1,17 +1,16 @@
-package com.magamochi.mangamochi.security;
+package com.magamochi.authentication.security;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
-import com.magamochi.mangamochi.service.CustomUserDetailsService;
-import com.magamochi.mangamochi.util.JwtUtil;
+import com.magamochi.user.service.CustomUserDetailsService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
-import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
@@ -26,7 +25,7 @@ public class JwtRequestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
- HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain chain)
+ HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
diff --git a/src/main/java/com/magamochi/mangamochi/util/JwtUtil.java b/src/main/java/com/magamochi/authentication/security/JwtUtil.java
similarity index 98%
rename from src/main/java/com/magamochi/mangamochi/util/JwtUtil.java
rename to src/main/java/com/magamochi/authentication/security/JwtUtil.java
index 4fc03b9..bb7c228 100644
--- a/src/main/java/com/magamochi/mangamochi/util/JwtUtil.java
+++ b/src/main/java/com/magamochi/authentication/security/JwtUtil.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.util;
+package com.magamochi.authentication.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
diff --git a/src/main/java/com/magamochi/mangamochi/config/SecurityConfig.java b/src/main/java/com/magamochi/authentication/security/SecurityConfig.java
similarity index 88%
rename from src/main/java/com/magamochi/mangamochi/config/SecurityConfig.java
rename to src/main/java/com/magamochi/authentication/security/SecurityConfig.java
index f4b92de..89943bf 100644
--- a/src/main/java/com/magamochi/mangamochi/config/SecurityConfig.java
+++ b/src/main/java/com/magamochi/authentication/security/SecurityConfig.java
@@ -1,8 +1,6 @@
-package com.magamochi.mangamochi.config;
+package com.magamochi.authentication.security;
-import com.magamochi.mangamochi.security.JwtRequestFilter;
-import com.magamochi.mangamochi.service.CustomUserDetailsService;
-import com.magamochi.mangamochi.util.JwtUtil;
+import com.magamochi.user.service.CustomUserDetailsService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
@@ -27,18 +25,17 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
-
- private final CustomUserDetailsService userDetailsService;
+ private final CustomUserDetailsService customUserDetailsService;
private final JwtUtil jwtUtil;
@Bean
public JwtRequestFilter jwtRequestFilter() {
- return new JwtRequestFilter(jwtUtil, userDetailsService);
+ return new JwtRequestFilter(jwtUtil, customUserDetailsService);
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
- var authProvider = new DaoAuthenticationProvider(userDetailsService);
+ var authProvider = new DaoAuthenticationProvider(customUserDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
diff --git a/src/main/java/com/magamochi/authentication/service/AuthenticationService.java b/src/main/java/com/magamochi/authentication/service/AuthenticationService.java
new file mode 100644
index 0000000..ec6defa
--- /dev/null
+++ b/src/main/java/com/magamochi/authentication/service/AuthenticationService.java
@@ -0,0 +1,68 @@
+package com.magamochi.authentication.service;
+
+import com.magamochi.authentication.model.dto.AuthenticationRequestDTO;
+import com.magamochi.authentication.model.dto.AuthenticationResponseDTO;
+import com.magamochi.authentication.model.dto.RefreshTokenRequestDTO;
+import com.magamochi.authentication.security.JwtUtil;
+import com.magamochi.user.service.UserService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class AuthenticationService {
+ private final AuthenticationManager authenticationManager;
+
+ private final UserDetailsService userDetailsService;
+ private final UserService userService;
+
+ private final JwtUtil jwtUtil;
+
+ public AuthenticationResponseDTO authenticate(AuthenticationRequestDTO request) {
+ authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(request.email(), request.password()));
+
+ var userDetails = userDetailsService.loadUserByUsername(request.email());
+
+ var accessToken = jwtUtil.generateAccessToken(userDetails);
+ var refreshToken = jwtUtil.generateRefreshToken(userDetails);
+
+ var user = userService.find(userDetails.getUsername());
+
+ return new AuthenticationResponseDTO(
+ user.getId(),
+ accessToken,
+ refreshToken,
+ userDetails.getUsername(),
+ user.getName(),
+ user.getRole());
+ }
+
+ public AuthenticationResponseDTO refreshAuthToken(
+ RefreshTokenRequestDTO authenticationRequestDTO) {
+ var username = jwtUtil.extractUsernameFromRefreshToken(authenticationRequestDTO.refreshToken());
+
+ var userDetails = userDetailsService.loadUserByUsername(username);
+
+ if (!jwtUtil.validateRefreshToken(authenticationRequestDTO.refreshToken(), userDetails)) {
+ throw new BadCredentialsException("Invalid refresh token");
+ }
+
+ var newAccessToken = jwtUtil.generateAccessToken(userDetails);
+ var newRefreshToken = jwtUtil.generateRefreshToken(userDetails);
+
+ var user = userService.find(userDetails.getUsername());
+
+ return new AuthenticationResponseDTO(
+ user.getId(),
+ newAccessToken,
+ newRefreshToken,
+ userDetails.getUsername(),
+ user.getName(),
+ user.getRole());
+ }
+}
diff --git a/src/main/java/com/magamochi/catalog/client/AniListClient.java b/src/main/java/com/magamochi/catalog/client/AniListClient.java
new file mode 100644
index 0000000..754fa8f
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/client/AniListClient.java
@@ -0,0 +1,64 @@
+package com.magamochi.catalog.client;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+@FeignClient(name = "aniList", url = "https://graphql.anilist.co")
+public interface AniListClient {
+ @PostMapping
+ MangaResponse getManga(@RequestBody GraphQLRequest request);
+
+ @PostMapping
+ MangaSearchResponse searchManga(@RequestBody SearchGraphQLRequest request);
+
+ record GraphQLRequest(String query, Variables variables) {
+ public record Variables(Long id) {}
+ }
+
+ record MangaResponse(Data data) {
+ public record Data(Manga Media) {}
+ }
+
+ record SearchGraphQLRequest(String query, SearchVariables variables) {
+ public record SearchVariables(String search) {}
+ }
+
+ record MangaSearchResponse(SearchData data) {
+ public record SearchData(@JsonProperty("Page") Page page) {}
+
+ public record Page(List media) {}
+ }
+
+ record Manga(
+ Long id,
+ Long idMal,
+ Title title,
+ String status,
+ String description,
+ Integer chapters,
+ Integer averageScore,
+ CoverImage coverImage,
+ List genres,
+ FuzzyDate startDate,
+ FuzzyDate endDate,
+ StaffConnection staff) {
+
+ public record Title(
+ String romaji, String english, @JsonProperty("native") String nativeTitle) {}
+
+ public record CoverImage(String large) {}
+
+ public record FuzzyDate(Integer year, Integer month, Integer day) {}
+
+ public record StaffConnection(List edges) {
+ public record StaffEdge(String role, Staff node) {
+ public record Staff(Name name) {
+ public record Name(String full) {}
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/client/JikanClient.java b/src/main/java/com/magamochi/catalog/client/JikanClient.java
similarity index 87%
rename from src/main/java/com/magamochi/mangamochi/client/JikanClient.java
rename to src/main/java/com/magamochi/catalog/client/JikanClient.java
index 5cba7f0..9c27e53 100644
--- a/src/main/java/com/magamochi/mangamochi/client/JikanClient.java
+++ b/src/main/java/com/magamochi/catalog/client/JikanClient.java
@@ -1,6 +1,5 @@
-package com.magamochi.mangamochi.client;
+package com.magamochi.catalog.client;
-import io.github.resilience4j.retry.annotation.Retry;
import java.time.OffsetDateTime;
import java.util.List;
import org.springframework.cloud.openfeign.FeignClient;
@@ -9,7 +8,6 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "jikan", url = "https://api.jikan.moe/v4/manga")
-@Retry(name = "JikanRetry")
public interface JikanClient {
@GetMapping
SearchResponse mangaSearch(@RequestParam String q);
@@ -30,10 +28,10 @@ public interface JikanClient {
String title,
List title_synonyms,
String status,
- boolean publishing,
+ Boolean publishing,
String synopsis,
- float score,
- int chapters,
+ Float score,
+ Integer chapters,
PublishData published,
List authors,
List genres) {
diff --git a/src/main/java/com/magamochi/catalog/controller/CatalogController.java b/src/main/java/com/magamochi/catalog/controller/CatalogController.java
new file mode 100644
index 0000000..7e1a21e
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/controller/CatalogController.java
@@ -0,0 +1,58 @@
+package com.magamochi.catalog.controller;
+
+import com.magamochi.catalog.model.dto.GenreDTO;
+import com.magamochi.catalog.model.dto.MangaDTO;
+import com.magamochi.catalog.model.dto.MangaListDTO;
+import com.magamochi.catalog.model.dto.MangaListFilterDTO;
+import com.magamochi.catalog.service.GenreService;
+import com.magamochi.catalog.service.MangaService;
+import com.magamochi.common.model.dto.DefaultResponseDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+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.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/catalog")
+@RequiredArgsConstructor
+public class CatalogController {
+ private final GenreService genreService;
+ private final MangaService mangaService;
+
+ @Operation(
+ summary = "Get a list of manga genres",
+ description = "Retrieve a list of manga genres.",
+ tags = {"Catalog"},
+ operationId = "getGenres")
+ @GetMapping("/genres")
+ public DefaultResponseDTO> getGenres() {
+ return DefaultResponseDTO.ok(genreService.getGenres());
+ }
+
+ @Operation(
+ summary = "Get a list of mangas",
+ description = "Retrieve a list of mangas with their details.",
+ tags = {"Catalog"},
+ operationId = "getMangas")
+ @GetMapping("/mangas")
+ public DefaultResponseDTO> getMangas(
+ @ParameterObject MangaListFilterDTO filterDTO,
+ @Parameter(hidden = true) @ParameterObject @PageableDefault Pageable pageable) {
+ return DefaultResponseDTO.ok(mangaService.get(filterDTO, pageable));
+ }
+
+ @Operation(
+ summary = "Get the details of a manga",
+ description = "Get the details of a manga by its ID",
+ tags = {"Catalog"},
+ operationId = "getManga")
+ @GetMapping("/mangas/{mangaId}")
+ public DefaultResponseDTO getManga(@PathVariable Long mangaId) {
+ return DefaultResponseDTO.ok(mangaService.get(mangaId));
+ }
+}
diff --git a/src/main/java/com/magamochi/catalog/controller/MangaIngestReviewController.java b/src/main/java/com/magamochi/catalog/controller/MangaIngestReviewController.java
new file mode 100644
index 0000000..da269bb
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/controller/MangaIngestReviewController.java
@@ -0,0 +1,51 @@
+package com.magamochi.catalog.controller;
+
+import com.magamochi.catalog.model.dto.MangaIngestReviewDTO;
+import com.magamochi.catalog.service.MangaIngestReviewService;
+import com.magamochi.common.model.dto.DefaultResponseDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/catalog")
+@RequiredArgsConstructor
+public class MangaIngestReviewController {
+ private final MangaIngestReviewService mangaIngestReviewService;
+
+ @Operation(
+ summary = "Get list of pending manga ingest reviews",
+ description = "Get list of pending manga ingest reviews.",
+ tags = {"Manga Ingest Review"},
+ operationId = "getMangaIngestReviews")
+ @GetMapping("/ingest-reviews")
+ public DefaultResponseDTO> getMangaIngestReviews() {
+ return DefaultResponseDTO.ok(mangaIngestReviewService.get());
+ }
+
+ @Operation(
+ summary = "Delete pending manga ingest review",
+ description = "Delete pending manga ingest review by ID.",
+ tags = {"Manga Ingest Review"},
+ operationId = "deleteMangaIngestReview")
+ @DeleteMapping("/ingest-reviews/{id}")
+ public DefaultResponseDTO deleteMangaIngestReview(@PathVariable Long id) {
+ mangaIngestReviewService.deleteIngestReview(id);
+
+ return DefaultResponseDTO.ok().build();
+ }
+
+ @Operation(
+ summary = "Resolve manga ingest review",
+ description = "Resolve manga ingest review by ID.",
+ tags = {"Manga Ingest Review"},
+ operationId = "resolveMangaIngestReview")
+ @PostMapping("/ingest-reviews")
+ public DefaultResponseDTO resolveMangaIngestReview(
+ @RequestParam Long id, @RequestParam String malId) {
+ mangaIngestReviewService.resolveImportReview(id, malId);
+
+ return DefaultResponseDTO.ok().build();
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/GenreDTO.java b/src/main/java/com/magamochi/catalog/model/dto/GenreDTO.java
similarity index 73%
rename from src/main/java/com/magamochi/mangamochi/model/dto/GenreDTO.java
rename to src/main/java/com/magamochi/catalog/model/dto/GenreDTO.java
index f6f50c1..94151c6 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/GenreDTO.java
+++ b/src/main/java/com/magamochi/catalog/model/dto/GenreDTO.java
@@ -1,6 +1,6 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.catalog.model.dto;
-import com.magamochi.mangamochi.model.entity.Genre;
+import com.magamochi.catalog.model.entity.Genre;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/MangaDTO.java b/src/main/java/com/magamochi/catalog/model/dto/MangaDTO.java
similarity index 65%
rename from src/main/java/com/magamochi/mangamochi/model/dto/MangaDTO.java
rename to src/main/java/com/magamochi/catalog/model/dto/MangaDTO.java
index 454a8e7..091186a 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/MangaDTO.java
+++ b/src/main/java/com/magamochi/catalog/model/dto/MangaDTO.java
@@ -1,12 +1,12 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.catalog.model.dto;
import static java.util.Objects.isNull;
-import com.magamochi.mangamochi.model.entity.Manga;
-import com.magamochi.mangamochi.model.entity.MangaAlternativeTitle;
-import com.magamochi.mangamochi.model.entity.MangaChapter;
-import com.magamochi.mangamochi.model.entity.MangaProvider;
-import com.magamochi.mangamochi.model.enumeration.ProviderStatus;
+import com.magamochi.catalog.model.entity.Manga;
+import com.magamochi.catalog.model.entity.MangaAlternativeTitle;
+import com.magamochi.catalog.model.entity.MangaContentProvider;
+import com.magamochi.catalog.model.enumeration.MangaStatus;
+import com.magamochi.content.model.entity.MangaContent;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.OffsetDateTime;
@@ -16,7 +16,7 @@ public record MangaDTO(
@NotNull Long id,
@NotBlank String title,
String coverImageKey,
- String status,
+ MangaStatus status,
OffsetDateTime publishedFrom,
OffsetDateTime publishedTo,
String synopsis,
@@ -33,43 +33,43 @@ public record MangaDTO(
return new MangaDTO(
manga.getId(),
manga.getTitle(),
- isNull(manga.getCoverImage()) ? null : manga.getCoverImage().getFileKey(),
+ isNull(manga.getCoverImage()) ? null : manga.getCoverImage().getObjectKey(),
manga.getStatus(),
manga.getPublishedFrom(),
manga.getPublishedTo(),
manga.getSynopsis(),
- manga.getMangaProviders().size(),
+ manga.getMangaContentProviders().size(),
manga.getAlternativeTitles().stream().map(MangaAlternativeTitle::getTitle).toList(),
manga.getMangaGenres().stream().map(mangaGenre -> mangaGenre.getGenre().getName()).toList(),
manga.getMangaAuthors().stream()
.map(mangaAuthor -> mangaAuthor.getAuthor().getName())
.toList(),
manga.getScore(),
- manga.getMangaProviders().stream().map(MangaProviderDTO::from).toList(),
+ manga.getMangaContentProviders().stream().map(MangaProviderDTO::from).toList(),
manga.getChapterCount(),
favorite,
following);
}
public record MangaProviderDTO(
- @NotNull long id,
+ long id,
@NotBlank String providerName,
- @NotNull ProviderStatus providerStatus,
+ boolean active,
@NotNull Integer chaptersAvailable,
@NotNull Integer chaptersDownloaded,
@NotNull Boolean supportsChapterFetch) {
- public static MangaProviderDTO from(MangaProvider mangaProvider) {
- var chapters = mangaProvider.getMangaChapters();
+ public static MangaProviderDTO from(MangaContentProvider mangaContentProvider) {
+ var chapters = mangaContentProvider.getMangaContents();
var chaptersAvailable = chapters.size();
- var chaptersDownloaded = (int) chapters.stream().filter(MangaChapter::getDownloaded).count();
+ var chaptersDownloaded = (int) chapters.stream().filter(MangaContent::getDownloaded).count();
return new MangaProviderDTO(
- mangaProvider.getId(),
- mangaProvider.getProvider().getName(),
- mangaProvider.getProvider().getStatus(),
+ mangaContentProvider.getId(),
+ mangaContentProvider.getContentProvider().getName(),
+ mangaContentProvider.getContentProvider().isActive(),
chaptersAvailable,
chaptersDownloaded,
- mangaProvider.getProvider().getSupportsChapterFetch());
+ mangaContentProvider.getContentProvider().getSupportsContentFetch());
}
}
}
diff --git a/src/main/java/com/magamochi/catalog/model/dto/MangaDataDTO.java b/src/main/java/com/magamochi/catalog/model/dto/MangaDataDTO.java
new file mode 100644
index 0000000..b8c7663
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/model/dto/MangaDataDTO.java
@@ -0,0 +1,20 @@
+package com.magamochi.catalog.model.dto;
+
+import com.magamochi.catalog.model.enumeration.MangaStatus;
+import java.time.OffsetDateTime;
+import java.util.List;
+import lombok.Builder;
+
+@Builder
+public record MangaDataDTO(
+ String title,
+ String synopsis,
+ MangaStatus status,
+ Double score,
+ OffsetDateTime publishedFrom,
+ OffsetDateTime publishedTo,
+ Integer chapterCount,
+ List authors,
+ List genres,
+ List alternativeTitles,
+ String coverImageUrl) {}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/ImportReviewDTO.java b/src/main/java/com/magamochi/catalog/model/dto/MangaIngestReviewDTO.java
similarity index 50%
rename from src/main/java/com/magamochi/mangamochi/model/dto/ImportReviewDTO.java
rename to src/main/java/com/magamochi/catalog/model/dto/MangaIngestReviewDTO.java
index b771d0c..c731389 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/ImportReviewDTO.java
+++ b/src/main/java/com/magamochi/catalog/model/dto/MangaIngestReviewDTO.java
@@ -1,22 +1,22 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.catalog.model.dto;
-import com.magamochi.mangamochi.model.entity.MangaImportReview;
+import com.magamochi.catalog.model.entity.MangaIngestReview;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.Instant;
-public record ImportReviewDTO(
+public record MangaIngestReviewDTO(
@NotNull Long id,
@NotBlank String title,
- @NotBlank String providerName,
+ @NotBlank String contentProviderName,
String externalUrl,
@NotBlank String reason,
@NotNull Instant createdAt) {
- public static ImportReviewDTO from(MangaImportReview review) {
- return new ImportReviewDTO(
+ public static MangaIngestReviewDTO from(MangaIngestReview review) {
+ return new MangaIngestReviewDTO(
review.getId(),
- review.getTitle(),
- review.getProvider().getName(),
+ review.getMangaTitle(),
+ review.getContentProvider().getName(),
review.getUrl(),
"Title match not found",
review.getCreatedAt());
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/MangaListDTO.java b/src/main/java/com/magamochi/catalog/model/dto/MangaListDTO.java
similarity index 81%
rename from src/main/java/com/magamochi/mangamochi/model/dto/MangaListDTO.java
rename to src/main/java/com/magamochi/catalog/model/dto/MangaListDTO.java
index e4cc66f..15943e1 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/MangaListDTO.java
+++ b/src/main/java/com/magamochi/catalog/model/dto/MangaListDTO.java
@@ -1,8 +1,9 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.catalog.model.dto;
import static java.util.Objects.nonNull;
-import com.magamochi.mangamochi.model.entity.Manga;
+import com.magamochi.catalog.model.entity.Manga;
+import com.magamochi.catalog.model.enumeration.MangaStatus;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.OffsetDateTime;
@@ -12,7 +13,7 @@ public record MangaListDTO(
@NotNull Long id,
@NotBlank String title,
String coverImageKey,
- String status,
+ MangaStatus status,
OffsetDateTime publishedFrom,
OffsetDateTime publishedTo,
Integer providerCount,
@@ -24,11 +25,11 @@ public record MangaListDTO(
return new MangaListDTO(
manga.getId(),
manga.getTitle(),
- nonNull(manga.getCoverImage()) ? manga.getCoverImage().getFileKey() : null,
+ nonNull(manga.getCoverImage()) ? manga.getCoverImage().getObjectKey() : null,
manga.getStatus(),
manga.getPublishedFrom(),
manga.getPublishedTo(),
- manga.getMangaProviders().size(),
+ manga.getMangaContentProviders().size(),
manga.getMangaGenres().stream().map(mangaGenre -> mangaGenre.getGenre().getName()).toList(),
manga.getMangaAuthors().stream()
.map(mangaAuthor -> mangaAuthor.getAuthor().getName())
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/MangaListFilterDTO.java b/src/main/java/com/magamochi/catalog/model/dto/MangaListFilterDTO.java
similarity index 80%
rename from src/main/java/com/magamochi/mangamochi/model/dto/MangaListFilterDTO.java
rename to src/main/java/com/magamochi/catalog/model/dto/MangaListFilterDTO.java
index f025d7b..c92bdb6 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/MangaListFilterDTO.java
+++ b/src/main/java/com/magamochi/catalog/model/dto/MangaListFilterDTO.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.catalog.model.dto;
import java.util.List;
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/Author.java b/src/main/java/com/magamochi/catalog/model/entity/Author.java
similarity index 89%
rename from src/main/java/com/magamochi/mangamochi/model/entity/Author.java
rename to src/main/java/com/magamochi/catalog/model/entity/Author.java
index e5217c6..fa43822 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/Author.java
+++ b/src/main/java/com/magamochi/catalog/model/entity/Author.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.catalog.model.entity;
import jakarta.persistence.*;
import java.time.Instant;
@@ -19,8 +19,6 @@ public class Author {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- private Long malId;
-
private String name;
@CreationTimestamp private Instant createdAt;
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/Genre.java b/src/main/java/com/magamochi/catalog/model/entity/Genre.java
similarity index 84%
rename from src/main/java/com/magamochi/mangamochi/model/entity/Genre.java
rename to src/main/java/com/magamochi/catalog/model/entity/Genre.java
index de1b80d..14decf8 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/Genre.java
+++ b/src/main/java/com/magamochi/catalog/model/entity/Genre.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.catalog.model.entity;
import jakarta.persistence.*;
import java.util.List;
@@ -16,8 +16,6 @@ public class Genre {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- private Long malId;
-
private String name;
@OneToMany(mappedBy = "genre")
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/Language.java b/src/main/java/com/magamochi/catalog/model/entity/Language.java
similarity index 86%
rename from src/main/java/com/magamochi/mangamochi/model/entity/Language.java
rename to src/main/java/com/magamochi/catalog/model/entity/Language.java
index e09c187..a5bae30 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/Language.java
+++ b/src/main/java/com/magamochi/catalog/model/entity/Language.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.catalog.model.entity;
import jakarta.persistence.*;
import lombok.*;
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/Manga.java b/src/main/java/com/magamochi/catalog/model/entity/Manga.java
similarity index 72%
rename from src/main/java/com/magamochi/mangamochi/model/entity/Manga.java
rename to src/main/java/com/magamochi/catalog/model/entity/Manga.java
index 6c75e62..8662417 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/Manga.java
+++ b/src/main/java/com/magamochi/catalog/model/entity/Manga.java
@@ -1,5 +1,9 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.catalog.model.entity;
+import com.magamochi.catalog.model.enumeration.MangaState;
+import com.magamochi.catalog.model.enumeration.MangaStatus;
+import com.magamochi.image.model.entity.Image;
+import com.magamochi.model.entity.UserFavoriteManga;
import jakarta.persistence.*;
import java.time.Instant;
import java.time.OffsetDateTime;
@@ -26,16 +30,13 @@ public class Manga {
private String title;
- private String status;
+ @Enumerated(EnumType.STRING)
+ private MangaStatus status;
private String synopsis;
- @CreationTimestamp private Instant createdAt;
-
- @UpdateTimestamp private Instant updatedAt;
-
@OneToMany(mappedBy = "manga")
- private List mangaProviders;
+ private List mangaContentProviders;
@ManyToOne
@JoinColumn(name = "cover_image_id")
@@ -47,6 +48,18 @@ public class Manga {
private OffsetDateTime publishedTo;
+ @Builder.Default private Integer chapterCount = 0;
+
+ @Builder.Default private Boolean follow = false;
+
+ @Enumerated(EnumType.STRING)
+ @Builder.Default
+ private MangaState state = MangaState.PENDING;
+
+ @CreationTimestamp private Instant createdAt;
+
+ @UpdateTimestamp private Instant updatedAt;
+
@OneToMany(mappedBy = "manga")
private List mangaAuthors;
@@ -58,8 +71,4 @@ public class Manga {
@OneToMany(mappedBy = "manga")
private List alternativeTitles;
-
- @Builder.Default private Integer chapterCount = 0;
-
- @Builder.Default private Boolean follow = false;
}
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/MangaAlternativeTitle.java b/src/main/java/com/magamochi/catalog/model/entity/MangaAlternativeTitle.java
similarity index 88%
rename from src/main/java/com/magamochi/mangamochi/model/entity/MangaAlternativeTitle.java
rename to src/main/java/com/magamochi/catalog/model/entity/MangaAlternativeTitle.java
index 61f1d19..a92cd0c 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/MangaAlternativeTitle.java
+++ b/src/main/java/com/magamochi/catalog/model/entity/MangaAlternativeTitle.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.catalog.model.entity;
import jakarta.persistence.*;
import lombok.*;
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/MangaAuthor.java b/src/main/java/com/magamochi/catalog/model/entity/MangaAuthor.java
similarity index 89%
rename from src/main/java/com/magamochi/mangamochi/model/entity/MangaAuthor.java
rename to src/main/java/com/magamochi/catalog/model/entity/MangaAuthor.java
index d85e216..0e6890a 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/MangaAuthor.java
+++ b/src/main/java/com/magamochi/catalog/model/entity/MangaAuthor.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.catalog.model.entity;
import jakarta.persistence.*;
import lombok.*;
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/MangaProvider.java b/src/main/java/com/magamochi/catalog/model/entity/MangaContentProvider.java
similarity index 58%
rename from src/main/java/com/magamochi/mangamochi/model/entity/MangaProvider.java
rename to src/main/java/com/magamochi/catalog/model/entity/MangaContentProvider.java
index fbcb97d..cd4e150 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/MangaProvider.java
+++ b/src/main/java/com/magamochi/catalog/model/entity/MangaContentProvider.java
@@ -1,5 +1,7 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.catalog.model.entity;
+import com.magamochi.content.model.entity.MangaContent;
+import com.magamochi.ingestion.model.entity.ContentProvider;
import jakarta.persistence.*;
import java.time.Instant;
import java.util.List;
@@ -8,13 +10,13 @@ import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
@Entity
-@Table(name = "manga_provider")
+@Table(name = "manga_content_provider")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
-public class MangaProvider {
+public class MangaContentProvider {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@@ -24,15 +26,15 @@ public class MangaProvider {
private Manga manga;
@ManyToOne
- @JoinColumn(name = "provider_id", nullable = false)
- private Provider provider;
+ @JoinColumn(name = "content_provider_id", nullable = false)
+ private ContentProvider contentProvider;
private String mangaTitle;
private String url;
- @OneToMany(mappedBy = "mangaProvider")
- List mangaChapters;
+ @OneToMany(mappedBy = "mangaContentProvider")
+ List mangaContents;
@CreationTimestamp private Instant createdAt;
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/MangaGenre.java b/src/main/java/com/magamochi/catalog/model/entity/MangaGenre.java
similarity index 89%
rename from src/main/java/com/magamochi/mangamochi/model/entity/MangaGenre.java
rename to src/main/java/com/magamochi/catalog/model/entity/MangaGenre.java
index 5f05135..4fcfa5f 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/MangaGenre.java
+++ b/src/main/java/com/magamochi/catalog/model/entity/MangaGenre.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.catalog.model.entity;
import jakarta.persistence.*;
import lombok.*;
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/MangaImportReview.java b/src/main/java/com/magamochi/catalog/model/entity/MangaIngestReview.java
similarity index 55%
rename from src/main/java/com/magamochi/mangamochi/model/entity/MangaImportReview.java
rename to src/main/java/com/magamochi/catalog/model/entity/MangaIngestReview.java
index 9321c84..83adb8e 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/MangaImportReview.java
+++ b/src/main/java/com/magamochi/catalog/model/entity/MangaIngestReview.java
@@ -1,29 +1,30 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.catalog.model.entity;
+import com.magamochi.ingestion.model.entity.ContentProvider;
import jakarta.persistence.*;
import java.time.Instant;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
@Entity
-@Table(name = "manga_import_reviews")
+@Table(name = "manga_ingest_reviews")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
-public class MangaImportReview {
+public class MangaIngestReview {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- private String title;
+ private String mangaTitle;
private String url;
@ManyToOne
- @JoinColumn(name = "provider_id")
- private Provider provider;
+ @JoinColumn(name = "content_provider_id")
+ private ContentProvider contentProvider;
@CreationTimestamp private Instant createdAt;
}
diff --git a/src/main/java/com/magamochi/catalog/model/enumeration/MangaState.java b/src/main/java/com/magamochi/catalog/model/enumeration/MangaState.java
new file mode 100644
index 0000000..a14e814
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/model/enumeration/MangaState.java
@@ -0,0 +1,6 @@
+package com.magamochi.catalog.model.enumeration;
+
+public enum MangaState {
+ PENDING,
+ AVAILABLE,
+}
diff --git a/src/main/java/com/magamochi/mangamochi/model/enumeration/MangaStatus.java b/src/main/java/com/magamochi/catalog/model/enumeration/MangaStatus.java
similarity index 62%
rename from src/main/java/com/magamochi/mangamochi/model/enumeration/MangaStatus.java
rename to src/main/java/com/magamochi/catalog/model/enumeration/MangaStatus.java
index d0dc22e..df32633 100644
--- a/src/main/java/com/magamochi/mangamochi/model/enumeration/MangaStatus.java
+++ b/src/main/java/com/magamochi/catalog/model/enumeration/MangaStatus.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.enumeration;
+package com.magamochi.catalog.model.enumeration;
public enum MangaStatus {
ONGOING,
diff --git a/src/main/java/com/magamochi/catalog/model/repository/AuthorRepository.java b/src/main/java/com/magamochi/catalog/model/repository/AuthorRepository.java
new file mode 100644
index 0000000..d763ac4
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/model/repository/AuthorRepository.java
@@ -0,0 +1,6 @@
+package com.magamochi.catalog.model.repository;
+
+import com.magamochi.catalog.model.entity.Author;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface AuthorRepository extends JpaRepository {}
diff --git a/src/main/java/com/magamochi/catalog/model/repository/GenreRepository.java b/src/main/java/com/magamochi/catalog/model/repository/GenreRepository.java
new file mode 100644
index 0000000..0fd67b3
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/model/repository/GenreRepository.java
@@ -0,0 +1,6 @@
+package com.magamochi.catalog.model.repository;
+
+import com.magamochi.catalog.model.entity.Genre;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface GenreRepository extends JpaRepository {}
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/LanguageRepository.java b/src/main/java/com/magamochi/catalog/model/repository/LanguageRepository.java
similarity index 67%
rename from src/main/java/com/magamochi/mangamochi/model/repository/LanguageRepository.java
rename to src/main/java/com/magamochi/catalog/model/repository/LanguageRepository.java
index 5411c94..6900355 100644
--- a/src/main/java/com/magamochi/mangamochi/model/repository/LanguageRepository.java
+++ b/src/main/java/com/magamochi/catalog/model/repository/LanguageRepository.java
@@ -1,6 +1,6 @@
-package com.magamochi.mangamochi.model.repository;
+package com.magamochi.catalog.model.repository;
-import com.magamochi.mangamochi.model.entity.Language;
+import com.magamochi.catalog.model.entity.Language;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/MangaAlternativeTitlesRepository.java b/src/main/java/com/magamochi/catalog/model/repository/MangaAlternativeTitlesRepository.java
similarity index 59%
rename from src/main/java/com/magamochi/mangamochi/model/repository/MangaAlternativeTitlesRepository.java
rename to src/main/java/com/magamochi/catalog/model/repository/MangaAlternativeTitlesRepository.java
index 150f704..3329058 100644
--- a/src/main/java/com/magamochi/mangamochi/model/repository/MangaAlternativeTitlesRepository.java
+++ b/src/main/java/com/magamochi/catalog/model/repository/MangaAlternativeTitlesRepository.java
@@ -1,6 +1,6 @@
-package com.magamochi.mangamochi.model.repository;
+package com.magamochi.catalog.model.repository;
-import com.magamochi.mangamochi.model.entity.MangaAlternativeTitle;
+import com.magamochi.catalog.model.entity.MangaAlternativeTitle;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MangaAlternativeTitlesRepository
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/MangaAuthorRepository.java b/src/main/java/com/magamochi/catalog/model/repository/MangaAuthorRepository.java
similarity index 53%
rename from src/main/java/com/magamochi/mangamochi/model/repository/MangaAuthorRepository.java
rename to src/main/java/com/magamochi/catalog/model/repository/MangaAuthorRepository.java
index 5020397..61f5425 100644
--- a/src/main/java/com/magamochi/mangamochi/model/repository/MangaAuthorRepository.java
+++ b/src/main/java/com/magamochi/catalog/model/repository/MangaAuthorRepository.java
@@ -1,8 +1,8 @@
-package com.magamochi.mangamochi.model.repository;
+package com.magamochi.catalog.model.repository;
-import com.magamochi.mangamochi.model.entity.Author;
-import com.magamochi.mangamochi.model.entity.Manga;
-import com.magamochi.mangamochi.model.entity.MangaAuthor;
+import com.magamochi.catalog.model.entity.Author;
+import com.magamochi.catalog.model.entity.Manga;
+import com.magamochi.catalog.model.entity.MangaAuthor;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
diff --git a/src/main/java/com/magamochi/catalog/model/repository/MangaContentProviderRepository.java b/src/main/java/com/magamochi/catalog/model/repository/MangaContentProviderRepository.java
new file mode 100644
index 0000000..7632849
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/model/repository/MangaContentProviderRepository.java
@@ -0,0 +1,10 @@
+package com.magamochi.catalog.model.repository;
+
+import com.magamochi.catalog.model.entity.MangaContentProvider;
+import jakarta.validation.constraints.NotBlank;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface MangaContentProviderRepository extends JpaRepository {
+ boolean existsByMangaTitleIgnoreCaseAndContentProvider_Id(
+ @NotBlank String mangaTitle, long contentProviderId);
+}
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/MangaGenreRepository.java b/src/main/java/com/magamochi/catalog/model/repository/MangaGenreRepository.java
similarity index 54%
rename from src/main/java/com/magamochi/mangamochi/model/repository/MangaGenreRepository.java
rename to src/main/java/com/magamochi/catalog/model/repository/MangaGenreRepository.java
index fd100fc..cfc89fe 100644
--- a/src/main/java/com/magamochi/mangamochi/model/repository/MangaGenreRepository.java
+++ b/src/main/java/com/magamochi/catalog/model/repository/MangaGenreRepository.java
@@ -1,6 +1,8 @@
-package com.magamochi.mangamochi.model.repository;
+package com.magamochi.catalog.model.repository;
-import com.magamochi.mangamochi.model.entity.*;
+import com.magamochi.catalog.model.entity.Genre;
+import com.magamochi.catalog.model.entity.Manga;
+import com.magamochi.catalog.model.entity.MangaGenre;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
diff --git a/src/main/java/com/magamochi/catalog/model/repository/MangaIngestReviewRepository.java b/src/main/java/com/magamochi/catalog/model/repository/MangaIngestReviewRepository.java
new file mode 100644
index 0000000..56b5cf3
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/model/repository/MangaIngestReviewRepository.java
@@ -0,0 +1,8 @@
+package com.magamochi.catalog.model.repository;
+
+import com.magamochi.catalog.model.entity.MangaIngestReview;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface MangaIngestReviewRepository extends JpaRepository {
+ boolean existsByMangaTitleIgnoreCaseAndContentProvider_Id(String mangaTitle, long providerId);
+}
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/MangaRepository.java b/src/main/java/com/magamochi/catalog/model/repository/MangaRepository.java
similarity index 82%
rename from src/main/java/com/magamochi/mangamochi/model/repository/MangaRepository.java
rename to src/main/java/com/magamochi/catalog/model/repository/MangaRepository.java
index 8e981e9..f423f81 100644
--- a/src/main/java/com/magamochi/mangamochi/model/repository/MangaRepository.java
+++ b/src/main/java/com/magamochi/catalog/model/repository/MangaRepository.java
@@ -1,6 +1,6 @@
-package com.magamochi.mangamochi.model.repository;
+package com.magamochi.catalog.model.repository;
-import com.magamochi.mangamochi.model.entity.Manga;
+import com.magamochi.catalog.model.entity.Manga;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
diff --git a/src/main/java/com/magamochi/catalog/queue/command/MangaUpdateCommand.java b/src/main/java/com/magamochi/catalog/queue/command/MangaUpdateCommand.java
new file mode 100644
index 0000000..43c67d6
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/queue/command/MangaUpdateCommand.java
@@ -0,0 +1,3 @@
+package com.magamochi.catalog.queue.command;
+
+public record MangaUpdateCommand(long mangaId) {}
diff --git a/src/main/java/com/magamochi/catalog/queue/consumer/MangaCoverUpdateConsumer.java b/src/main/java/com/magamochi/catalog/queue/consumer/MangaCoverUpdateConsumer.java
new file mode 100644
index 0000000..bd3b12c
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/queue/consumer/MangaCoverUpdateConsumer.java
@@ -0,0 +1,21 @@
+package com.magamochi.catalog.queue.consumer;
+
+import com.magamochi.catalog.service.MangaUpdateService;
+import com.magamochi.common.queue.command.ImageUpdateCommand;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class MangaCoverUpdateConsumer {
+ private final MangaUpdateService mangaUpdateService;
+
+ @RabbitListener(queues = "${queues.manga-cover-update}")
+ public void receiveImageUpdateCommand(ImageUpdateCommand command) {
+ log.info("Received manga cover image update command: {}", command);
+ mangaUpdateService.updateMangaCoverImage(command.entityId(), command.imageId());
+ }
+}
diff --git a/src/main/java/com/magamochi/catalog/queue/consumer/MangaIngestConsumer.java b/src/main/java/com/magamochi/catalog/queue/consumer/MangaIngestConsumer.java
new file mode 100644
index 0000000..e79cc69
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/queue/consumer/MangaIngestConsumer.java
@@ -0,0 +1,21 @@
+package com.magamochi.catalog.queue.consumer;
+
+import com.magamochi.catalog.service.MangaIngestService;
+import com.magamochi.common.queue.command.MangaIngestCommand;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class MangaIngestConsumer {
+ private final MangaIngestService mangaIngestService;
+
+ @RabbitListener(queues = "${queues.manga-ingest}")
+ public void receiveMangaIngestCommand(MangaIngestCommand command) {
+ log.info("Received manga ingest command: {}", command);
+ mangaIngestService.ingestManga(command.providerId(), command.mangaTitle(), command.url());
+ }
+}
diff --git a/src/main/java/com/magamochi/catalog/queue/consumer/MangaUpdateConsumer.java b/src/main/java/com/magamochi/catalog/queue/consumer/MangaUpdateConsumer.java
new file mode 100644
index 0000000..3ce0d0d
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/queue/consumer/MangaUpdateConsumer.java
@@ -0,0 +1,21 @@
+package com.magamochi.catalog.queue.consumer;
+
+import com.magamochi.catalog.queue.command.MangaUpdateCommand;
+import com.magamochi.catalog.service.MangaUpdateService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class MangaUpdateConsumer {
+ private final MangaUpdateService mangaUpdateService;
+
+ @RabbitListener(queues = "${queues.manga-update}")
+ public void receiveMangaUpdateCommand(MangaUpdateCommand command) {
+ log.info("Received manga update command: {}", command);
+ mangaUpdateService.update(command.mangaId());
+ }
+}
diff --git a/src/main/java/com/magamochi/catalog/queue/producer/MangaUpdateProducer.java b/src/main/java/com/magamochi/catalog/queue/producer/MangaUpdateProducer.java
new file mode 100644
index 0000000..2bd5d1e
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/queue/producer/MangaUpdateProducer.java
@@ -0,0 +1,23 @@
+package com.magamochi.catalog.queue.producer;
+
+import com.magamochi.catalog.queue.command.MangaUpdateCommand;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class MangaUpdateProducer {
+ private final RabbitTemplate rabbitTemplate;
+
+ @Value("${queues.manga-update}")
+ private String mangaUpdateQueue;
+
+ public void sendMangaUpdateCommand(MangaUpdateCommand command) {
+ rabbitTemplate.convertAndSend(mangaUpdateQueue, command);
+ log.info("Sent manga update command: {}", command);
+ }
+}
diff --git a/src/main/java/com/magamochi/catalog/service/AniListService.java b/src/main/java/com/magamochi/catalog/service/AniListService.java
new file mode 100644
index 0000000..fbd9589
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/service/AniListService.java
@@ -0,0 +1,179 @@
+package com.magamochi.catalog.service;
+
+import static java.util.Objects.isNull;
+import static java.util.Objects.nonNull;
+
+import com.google.common.util.concurrent.RateLimiter;
+import com.magamochi.catalog.client.AniListClient;
+import com.magamochi.catalog.model.dto.MangaDataDTO;
+import com.magamochi.catalog.model.enumeration.MangaStatus;
+import com.magamochi.catalog.util.DoubleUtil;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.jspecify.annotations.NonNull;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class AniListService {
+ private final AniListClient aniListClient;
+ private final RateLimiter aniListRateLimiter;
+
+ public Map searchMangaByTitle(String title) {
+ var request = getSearchGraphQLRequest(title);
+
+ aniListRateLimiter.acquire();
+ var response = aniListClient.searchManga(request);
+
+ if (nonNull(response) && nonNull(response.data()) && nonNull(response.data().page())) {
+ return response.data().page().media().stream()
+ .flatMap(
+ manga ->
+ Stream.of(
+ manga.title().romaji(),
+ manga.title().english(),
+ manga.title().nativeTitle())
+ .filter(Objects::nonNull)
+ .filter(t -> !t.isBlank())
+ .map(titleString -> Map.entry(titleString, manga)))
+ .collect(
+ Collectors.toMap(
+ Map.Entry::getKey, Map.Entry::getValue, (existingManga, manga) -> existingManga));
+ }
+
+ return Map.of();
+ }
+
+ public MangaDataDTO getMangaDataById(Long aniListId) {
+ var request = getGraphQLRequest(aniListId);
+
+ aniListRateLimiter.acquire();
+ var media = aniListClient.getManga(request).data().Media();
+
+ var authors =
+ media.staff().edges().stream()
+ .filter(edge -> isAuthorRole(edge.role()))
+ .map(edge -> edge.node().name().full())
+ .distinct()
+ .toList();
+
+ boolean hasRomajiTitle = nonNull(media.title().romaji());
+ return MangaDataDTO.builder()
+ .title(hasRomajiTitle ? media.title().romaji() : media.title().english())
+ .score(
+ nonNull(media.averageScore())
+ ? DoubleUtil.round((double) media.averageScore() / 10, 2)
+ : 0)
+ .synopsis(media.description())
+ .chapterCount(media.chapters())
+ .publishedFrom(convertFuzzyDate(media.startDate()))
+ .publishedTo(convertFuzzyDate(media.endDate()))
+ .authors(authors)
+ .genres(media.genres())
+ // TODO: improve this
+ .alternativeTitles(
+ hasRomajiTitle
+ ? nonNull(media.title().english())
+ ? List.of(media.title().english(), media.title().nativeTitle())
+ : List.of(media.title().nativeTitle())
+ : List.of(media.title().nativeTitle()))
+ .coverImageUrl(media.coverImage().large())
+ .status(mapStatus(media.status()))
+ .build();
+ }
+
+ private static AniListClient.@NonNull GraphQLRequest getGraphQLRequest(Long aniListId) {
+ var query =
+ """
+ query ($id: Int) {
+ Media (id: $id, type: MANGA) {
+ title {
+ romaji
+ english
+ native
+ }
+ startDate { year month day }
+ endDate { year month day }
+ description
+ status
+ averageScore
+ chapters
+ coverImage { large }
+ genres
+ staff {
+ edges {
+ role
+ node {
+ name {
+ full
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+ return new AniListClient.GraphQLRequest(
+ query, new AniListClient.GraphQLRequest.Variables(aniListId));
+ }
+
+ private static AniListClient.@NonNull SearchGraphQLRequest getSearchGraphQLRequest(String title) {
+ var query =
+ """
+ query ($search: String) {
+ Page(page: 1, perPage: 10) {
+ media(search: $search, type: MANGA) {
+ id
+ idMal
+ title {
+ romaji
+ english
+ native
+ }
+ status
+ }
+ }
+ }
+ """;
+
+ var variables = new AniListClient.SearchGraphQLRequest.SearchVariables(title);
+ return new AniListClient.SearchGraphQLRequest(query, variables);
+ }
+
+ private OffsetDateTime convertFuzzyDate(AniListClient.Manga.FuzzyDate date) {
+ if (isNull(date) || isNull(date.year())) {
+ return null;
+ }
+ return OffsetDateTime.of(
+ date.year(),
+ isNull(date.month()) ? 1 : date.month(),
+ isNull(date.day()) ? 1 : date.day(),
+ 0,
+ 0,
+ 0,
+ 0,
+ ZoneOffset.UTC);
+ }
+
+ private MangaStatus mapStatus(String aniListStatus) {
+ return switch (aniListStatus.toLowerCase()) {
+ case "releasing" -> MangaStatus.ONGOING;
+ case "finished" -> MangaStatus.COMPLETED;
+ default -> MangaStatus.UNKNOWN;
+ };
+ }
+
+ private boolean isAuthorRole(String role) {
+ return role.equalsIgnoreCase("Story & Art")
+ || role.equalsIgnoreCase("Story")
+ || role.equalsIgnoreCase("Art");
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/service/GenreService.java b/src/main/java/com/magamochi/catalog/service/GenreService.java
similarity index 69%
rename from src/main/java/com/magamochi/mangamochi/service/GenreService.java
rename to src/main/java/com/magamochi/catalog/service/GenreService.java
index bf6d4c2..d2ba420 100644
--- a/src/main/java/com/magamochi/mangamochi/service/GenreService.java
+++ b/src/main/java/com/magamochi/catalog/service/GenreService.java
@@ -1,7 +1,7 @@
-package com.magamochi.mangamochi.service;
+package com.magamochi.catalog.service;
-import com.magamochi.mangamochi.model.dto.GenreDTO;
-import com.magamochi.mangamochi.model.repository.GenreRepository;
+import com.magamochi.catalog.model.dto.GenreDTO;
+import com.magamochi.catalog.model.repository.GenreRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
diff --git a/src/main/java/com/magamochi/mangamochi/service/LanguageService.java b/src/main/java/com/magamochi/catalog/service/LanguageService.java
similarity index 58%
rename from src/main/java/com/magamochi/mangamochi/service/LanguageService.java
rename to src/main/java/com/magamochi/catalog/service/LanguageService.java
index 69d048c..fe8af57 100644
--- a/src/main/java/com/magamochi/mangamochi/service/LanguageService.java
+++ b/src/main/java/com/magamochi/catalog/service/LanguageService.java
@@ -1,8 +1,8 @@
-package com.magamochi.mangamochi.service;
+package com.magamochi.catalog.service;
-import com.magamochi.mangamochi.exception.NotFoundException;
-import com.magamochi.mangamochi.model.entity.Language;
-import com.magamochi.mangamochi.model.repository.LanguageRepository;
+import com.magamochi.catalog.model.entity.Language;
+import com.magamochi.catalog.model.repository.LanguageRepository;
+import com.magamochi.common.exception.NotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -11,7 +11,7 @@ import org.springframework.stereotype.Service;
public class LanguageService {
public final LanguageRepository languageRepository;
- public Language getOrThrow(String code) {
+ public Language find(String code) {
return languageRepository
.findByCodeIgnoreCase(code)
.orElseThrow(() -> new NotFoundException("Language with code " + code + " not found"));
diff --git a/src/main/java/com/magamochi/catalog/service/MangaContentProviderService.java b/src/main/java/com/magamochi/catalog/service/MangaContentProviderService.java
new file mode 100644
index 0000000..85aa949
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/service/MangaContentProviderService.java
@@ -0,0 +1,22 @@
+package com.magamochi.catalog.service;
+
+import com.magamochi.catalog.model.entity.MangaContentProvider;
+import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
+import com.magamochi.common.exception.NotFoundException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class MangaContentProviderService {
+ private final MangaContentProviderRepository mangaContentProviderRepository;
+
+ public MangaContentProvider find(long mangaContentProviderId) {
+ return mangaContentProviderRepository
+ .findById(mangaContentProviderId)
+ .orElseThrow(
+ () ->
+ new NotFoundException(
+ "MangaContentProvider not found - ID: " + mangaContentProviderId));
+ }
+}
diff --git a/src/main/java/com/magamochi/catalog/service/MangaIngestReviewService.java b/src/main/java/com/magamochi/catalog/service/MangaIngestReviewService.java
new file mode 100644
index 0000000..24d188f
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/service/MangaIngestReviewService.java
@@ -0,0 +1,68 @@
+package com.magamochi.catalog.service;
+
+import com.magamochi.catalog.model.dto.MangaIngestReviewDTO;
+import com.magamochi.catalog.model.entity.MangaIngestReview;
+import com.magamochi.catalog.model.repository.MangaIngestReviewRepository;
+import com.magamochi.common.exception.NotFoundException;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.NotImplementedException;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class MangaIngestReviewService {
+ private final MangaIngestReviewRepository mangaIngestReviewRepository;
+
+ public List get() {
+ return mangaIngestReviewRepository.findAll().stream().map(MangaIngestReviewDTO::from).toList();
+ }
+
+ public void deleteIngestReview(Long id) {
+ var importReview = getImportReviewThrowIfNotFound(id);
+
+ mangaIngestReviewRepository.delete(importReview);
+ }
+
+ public void resolveImportReview(Long id, String malId) {
+ throw new NotImplementedException();
+ // var importReview = getImportReviewThrowIfNotFound(id);
+ //
+ // jikanRateLimiter.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()));
+ //
+ // if (!mangaContentProviderRepository.existsByMangaAndContentProviderAndUrlIgnoreCase(
+ // manga, importReview.getContentProvider(), importReview.getUrl())) {
+ // mangaContentProviderRepository.save(
+ // MangaContentProvider.builder()
+ // .manga(manga)
+ // .mangaTitle(importReview.getMangaTitle())
+ // .contentProvider(importReview.getContentProvider())
+ // .url(importReview.getUrl())
+ // .build());
+ // }
+ //
+ // mangaIngestReviewRepository.delete(importReview);
+ }
+
+ private MangaIngestReview getImportReviewThrowIfNotFound(Long id) {
+ return mangaIngestReviewRepository
+ .findById(id)
+ .orElseThrow(() -> new NotFoundException("Import review not found for ID: " + id));
+ }
+}
diff --git a/src/main/java/com/magamochi/catalog/service/MangaIngestService.java b/src/main/java/com/magamochi/catalog/service/MangaIngestService.java
new file mode 100644
index 0000000..5f3d919
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/service/MangaIngestService.java
@@ -0,0 +1,101 @@
+package com.magamochi.catalog.service;
+
+import static java.util.Objects.isNull;
+
+import com.magamochi.catalog.model.entity.MangaContentProvider;
+import com.magamochi.catalog.model.entity.MangaIngestReview;
+import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
+import com.magamochi.catalog.model.repository.MangaIngestReviewRepository;
+import com.magamochi.ingestion.service.ContentProviderService;
+import jakarta.validation.constraints.NotBlank;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class MangaIngestService {
+ private final ContentProviderService contentProviderService;
+ private final MangaResolutionService mangaResolutionService;
+
+ private final MangaIngestReviewRepository mangaIngestReviewRepository;
+ private final MangaContentProviderRepository mangaContentProviderRepository;
+
+ public void ingestManga(
+ long contentProviderId, @NotBlank String mangaTitle, @NotBlank String url) {
+ log.info(
+ "Ingesting manga with mangaTitle '{}' from provider {}", mangaTitle, contentProviderId);
+
+ if (mangaContentProviderRepository.existsByMangaTitleIgnoreCaseAndContentProvider_Id(
+ mangaTitle, contentProviderId)) {
+ log.info(
+ "Manga with mangaTitle '{}' already exists for provider '{}', skipping ingest",
+ mangaTitle,
+ contentProviderId);
+ return;
+ }
+
+ var manga = mangaResolutionService.findOrCreateManga(mangaTitle);
+ if (isNull(manga)) {
+ createMangaIngestReview(mangaTitle, url, contentProviderId);
+ return;
+ }
+
+ try {
+ var contentProvider = contentProviderService.find(contentProviderId);
+
+ mangaContentProviderRepository.save(
+ MangaContentProvider.builder()
+ .manga(manga)
+ .mangaTitle(mangaTitle)
+ .contentProvider(contentProvider)
+ .url(url)
+ .build());
+ } catch (Exception e) {
+ log.error(
+ "Failed to ingest manga with mangaTitle '{}' from provider '{}'",
+ mangaTitle,
+ contentProviderId,
+ e);
+ }
+
+ log.info(
+ "Successfully ingested manga with mangaTitle '{}' from provider {}",
+ mangaTitle,
+ contentProviderId);
+ }
+
+ private void createMangaIngestReview(String mangaTitle, String url, long contentProviderId) {
+ log.info(
+ "Creating manga ingest review for manga with mangaTitle '{}' from provider {}",
+ mangaTitle,
+ contentProviderId);
+
+ if (mangaIngestReviewRepository.existsByMangaTitleIgnoreCaseAndContentProvider_Id(
+ mangaTitle, contentProviderId)) {
+ log.info(
+ "Manga ingest review already exists for manga with mangaTitle '{}' and provider {}, skipping review creation",
+ mangaTitle,
+ contentProviderId);
+ return;
+ }
+
+ try {
+ var contentProvider = contentProviderService.find(contentProviderId);
+
+ mangaIngestReviewRepository.save(
+ MangaIngestReview.builder()
+ .mangaTitle(mangaTitle)
+ .url(url)
+ .contentProvider(contentProvider)
+ .build());
+ } catch (Exception e) {
+ log.error(
+ "Failed to create manga ingest review for manga with mangaTitle '{}' from provider {}",
+ mangaTitle,
+ contentProviderId,
+ e);
+ }
+ }
+}
diff --git a/src/main/java/com/magamochi/catalog/service/MangaResolutionService.java b/src/main/java/com/magamochi/catalog/service/MangaResolutionService.java
new file mode 100644
index 0000000..51a6fcb
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/service/MangaResolutionService.java
@@ -0,0 +1,133 @@
+package com.magamochi.catalog.service;
+
+import static java.util.Objects.nonNull;
+
+import com.magamochi.catalog.model.entity.Manga;
+import com.magamochi.catalog.model.repository.MangaRepository;
+import com.magamochi.catalog.queue.command.MangaUpdateCommand;
+import com.magamochi.catalog.queue.producer.MangaUpdateProducer;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class MangaResolutionService {
+ private final AniListService aniListService;
+ private final MyAnimeListService myAnimeListService;
+ private final TitleMatcherService titleMatcherService;
+
+ private final MangaUpdateProducer mangaUpdateProducer;
+
+ private final MangaRepository mangaRepository;
+
+ public Manga findOrCreateManga(String searchTitle) {
+ var existingManga = mangaRepository.findByTitleIgnoreCase(searchTitle);
+ if (existingManga.isPresent()) {
+ return existingManga.get();
+ }
+
+ var aniListResult = searchMangaOnAniList(searchTitle);
+ var malResult = searchMangaOnMyAnimeList(searchTitle);
+
+ if (aniListResult.isEmpty() && malResult.isEmpty()) {
+ return null;
+ }
+
+ var canonicalTitle =
+ aniListResult
+ .map(ProviderResult::title)
+ .orElseGet(() -> malResult.map(ProviderResult::title).orElse(searchTitle));
+
+ var aniListId = aniListResult.map(ProviderResult::externalId).orElse(null);
+ var malId = malResult.map(ProviderResult::externalId).orElse(null);
+
+ return findOrCreateManga(canonicalTitle, aniListId, malId);
+ }
+
+ private Optional searchMangaOnAniList(String title) {
+ var searchResults = aniListService.searchMangaByTitle(title);
+ if (searchResults.isEmpty()) {
+ return Optional.empty();
+ }
+
+ var matchResponse =
+ titleMatcherService.findBestMatch(
+ TitleMatcherService.TitleMatchRequest.builder()
+ .title(title)
+ .options(searchResults.keySet())
+ .build());
+
+ if (!matchResponse.matchFound()) {
+ log.warn("No title match found for manga with title {} on AniList", title);
+ return Optional.empty();
+ }
+
+ var matchedManga = searchResults.get(matchResponse.bestMatch());
+
+ var bestTitle =
+ nonNull(matchedManga.title().romaji())
+ ? matchedManga.title().romaji()
+ : matchedManga.title().english();
+
+ return Optional.of(new ProviderResult(bestTitle, matchedManga.id()));
+ }
+
+ private Optional searchMangaOnMyAnimeList(String title) {
+ var searchResults = myAnimeListService.searchMangaByTitle(title);
+ if (searchResults.isEmpty()) {
+ return Optional.empty();
+ }
+
+ var matchResponse =
+ titleMatcherService.findBestMatch(
+ TitleMatcherService.TitleMatchRequest.builder()
+ .title(title)
+ .options(searchResults.keySet())
+ .build());
+ if (!matchResponse.matchFound()) {
+ log.warn("No title match found for manga with title {} on MyAnimeList", title);
+ return Optional.empty();
+ }
+
+ var bestTitle = matchResponse.bestMatch();
+ var malId = searchResults.get(bestTitle);
+
+ return Optional.of(new ProviderResult(bestTitle, malId));
+ }
+
+ private Manga findOrCreateManga(String canonicalTitle, Long aniListId, Long malId) {
+ if (nonNull(aniListId)) {
+ var existingByAniList = mangaRepository.findByAniListId(aniListId);
+ if (existingByAniList.isPresent()) {
+ return existingByAniList.get();
+ }
+ }
+
+ if (nonNull(malId)) {
+ var existingByMalId = mangaRepository.findByMalId(malId);
+ if (existingByMalId.isPresent()) {
+ return existingByMalId.get();
+ }
+ }
+
+ return mangaRepository
+ .findByTitleIgnoreCase(canonicalTitle)
+ .orElseGet(
+ () -> {
+ var newManga =
+ Manga.builder().title(canonicalTitle).malId(malId).aniListId(aniListId).build();
+
+ var savedManga = mangaRepository.save(newManga);
+
+ mangaUpdateProducer.sendMangaUpdateCommand(
+ new MangaUpdateCommand(savedManga.getId()));
+
+ return savedManga;
+ });
+ }
+
+ private record ProviderResult(String title, Long externalId) {}
+}
diff --git a/src/main/java/com/magamochi/catalog/service/MangaService.java b/src/main/java/com/magamochi/catalog/service/MangaService.java
new file mode 100644
index 0000000..0439c43
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/service/MangaService.java
@@ -0,0 +1,81 @@
+package com.magamochi.catalog.service;
+
+import static java.util.Objects.nonNull;
+
+import com.magamochi.catalog.model.dto.MangaDTO;
+import com.magamochi.catalog.model.dto.MangaListDTO;
+import com.magamochi.catalog.model.dto.MangaListFilterDTO;
+import com.magamochi.catalog.model.entity.Manga;
+import com.magamochi.catalog.model.repository.MangaRepository;
+import com.magamochi.common.exception.NotFoundException;
+import com.magamochi.model.repository.UserFavoriteMangaRepository;
+import com.magamochi.model.repository.UserMangaFollowRepository;
+import com.magamochi.model.specification.MangaSpecification;
+import com.magamochi.user.service.UserService;
+import java.util.Set;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class MangaService {
+ private final UserService userService;
+
+ private final MangaRepository mangaRepository;
+ private final UserFavoriteMangaRepository userFavoriteMangaRepository;
+ private final UserMangaFollowRepository userMangaFollowRepository;
+
+ public Manga find(long mangaId) {
+ return mangaRepository
+ .findById(mangaId)
+ .orElseThrow(() -> new NotFoundException("Manga with ID " + mangaId + " not found"));
+ }
+
+ public Page get(MangaListFilterDTO filterDTO, Pageable pageable) {
+ var user = userService.getLoggedUser();
+
+ var specification = MangaSpecification.getMangaListSpecification(filterDTO, user);
+
+ var favoriteMangasIds =
+ nonNull(user)
+ ? userFavoriteMangaRepository.findByUser(user).stream()
+ .map(ufm -> ufm.getManga().getId())
+ .collect(Collectors.toSet())
+ : Set.of();
+
+ return mangaRepository
+ .findAll(specification, pageable)
+ .map(
+ manga -> {
+ var favorite = favoriteMangasIds.contains(manga.getId());
+ return MangaListDTO.from(manga, favorite);
+ });
+ }
+
+ public MangaDTO get(Long mangaId) {
+ var manga = find(mangaId);
+ var user = userService.getLoggedUser();
+
+ var favoriteMangasIds =
+ nonNull(user)
+ ? userFavoriteMangaRepository.findByUser(user).stream()
+ .map(ufm -> ufm.getManga().getId())
+ .collect(Collectors.toSet())
+ : Set.of();
+
+ var followingMangaIds =
+ nonNull(user)
+ ? userMangaFollowRepository.findByUser(user).stream()
+ .map(umf -> umf.getManga().getId())
+ .collect(Collectors.toSet())
+ : Set.of();
+
+ return MangaDTO.from(
+ manga,
+ favoriteMangasIds.contains(manga.getId()),
+ followingMangaIds.contains(manga.getId()));
+ }
+}
diff --git a/src/main/java/com/magamochi/catalog/service/MangaUpdateService.java b/src/main/java/com/magamochi/catalog/service/MangaUpdateService.java
new file mode 100644
index 0000000..a133a3d
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/service/MangaUpdateService.java
@@ -0,0 +1,95 @@
+package com.magamochi.catalog.service;
+
+import static java.util.Objects.nonNull;
+
+import com.magamochi.catalog.model.dto.MangaDataDTO;
+import com.magamochi.catalog.model.entity.Manga;
+import com.magamochi.catalog.model.enumeration.MangaState;
+import com.magamochi.common.model.enumeration.ContentType;
+import com.magamochi.common.queue.command.ImageFetchCommand;
+import com.magamochi.common.queue.producer.ImageFetchProducer;
+import com.magamochi.image.service.ImageService;
+import jakarta.transaction.Transactional;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class MangaUpdateService {
+ private final AniListService aniListService;
+ private final MyAnimeListService myAnimeListService;
+ private final MangaService mangaService;
+ private final ImageService imageService;
+
+ private final ImageFetchProducer imageFetchProducer;
+
+ @Transactional
+ public void update(long mangaId) {
+ log.info("Updating manga with ID {}", mangaId);
+
+ var manga = mangaService.find(mangaId);
+ var mangaData = fetchExternalMangaData(manga);
+ applyUpdatesToDatabase(manga, mangaData);
+
+ imageFetchProducer.sendImageFetchCommand(
+ new ImageFetchCommand(manga.getId(), ContentType.MANGA_COVER, mangaData.coverImageUrl()));
+
+ log.info("Manga with ID {} updated successfully", mangaId);
+ }
+
+ @Transactional
+ public void updateMangaCoverImage(long mangaId, UUID imageId) {
+ log.info("Updating cover image for manga with ID {}", mangaId);
+
+ var manga = mangaService.find(mangaId);
+ var image = imageService.find(imageId);
+
+ manga.setCoverImage(image);
+
+ log.info("Manga with ID {} cover image updated successfully (Image ID {})", mangaId, imageId);
+ }
+
+ private MangaDataDTO fetchExternalMangaData(Manga manga) {
+ if (nonNull(manga.getAniListId())) {
+ return aniListService.getMangaDataById(manga.getAniListId());
+ }
+
+ if (nonNull(manga.getMalId())) {
+ return myAnimeListService.getMangaDataById(manga.getMalId());
+ }
+
+ throw new IllegalStateException(
+ "Cannot update manga: No external provider IDs found for Manga " + manga.getId());
+ }
+
+ private void applyUpdatesToDatabase(Manga manga, MangaDataDTO mangaData) {
+ manga.setTitle(mangaData.title());
+ manga.setSynopsis(mangaData.synopsis());
+ manga.setScore(mangaData.score());
+ manga.setStatus(mangaData.status());
+ manga.setPublishedFrom(mangaData.publishedFrom());
+ manga.setPublishedTo(mangaData.publishedTo());
+ manga.setChapterCount(mangaData.chapterCount());
+ manga.setState(MangaState.AVAILABLE);
+
+ // TODO: properly save these
+ //
+ // mangaAlternativeTitleService.saveOrUpdateMangaAlternativeTitles(
+ // manga.getId(), mangaData.alternativeTitles());
+ //
+ // var genreIds =
+ // mangaData.genres().stream()
+ // .map(genreService::findOrCreateGenre)
+ // .collect(Collectors.toSet());
+ // mangaGenreService.saveOrUpdateMangaGenres(manga.getId(), genreIds);
+ //
+ // var authorIds =
+ // mangaData.authors().stream()
+ // .map(authorService::findOrCreateAuthor)
+ // .collect(Collectors.toSet());
+ // mangaAuthorService.saveOrUpdateMangaAuthors(manga.getId(), authorIds);
+ }
+}
diff --git a/src/main/java/com/magamochi/catalog/service/MyAnimeListService.java b/src/main/java/com/magamochi/catalog/service/MyAnimeListService.java
new file mode 100644
index 0000000..530745f
--- /dev/null
+++ b/src/main/java/com/magamochi/catalog/service/MyAnimeListService.java
@@ -0,0 +1,88 @@
+package com.magamochi.catalog.service;
+
+import static java.util.Objects.isNull;
+import static java.util.Objects.nonNull;
+
+import com.google.common.util.concurrent.RateLimiter;
+import com.magamochi.catalog.client.JikanClient;
+import com.magamochi.catalog.model.dto.MangaDataDTO;
+import com.magamochi.catalog.model.enumeration.MangaStatus;
+import com.magamochi.catalog.util.DoubleUtil;
+import java.util.Map;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class MyAnimeListService {
+ private final JikanClient jikanClient;
+ private final RateLimiter jikanRateLimiter;
+
+ /// Searches for manga titles on MyAnimeList using the Jikan API and returns a map of title to MAL
+ // ID.
+ public Map searchMangaByTitle(String titleToSearch) {
+ jikanRateLimiter.acquire();
+ var results = jikanClient.mangaSearch(titleToSearch).data();
+ if (results.isEmpty()) {
+ log.warn("No manga found with title {}", titleToSearch);
+ return Map.of();
+ }
+
+ return results.stream()
+ .collect(
+ Collectors.toMap(
+ JikanClient.SearchResponse.MangaData::title,
+ JikanClient.SearchResponse.MangaData::mal_id,
+ (existing, second) -> existing));
+ }
+
+ public MangaDataDTO getMangaDataById(Long malId) {
+ jikanRateLimiter.acquire();
+ var response = jikanClient.getMangaById(malId);
+ if (isNull(response) || isNull(response.data())) {
+ log.warn("No manga found with MAL ID {}", malId);
+ return null;
+ }
+
+ var responseData = response.data();
+
+ var authors =
+ responseData.authors().stream()
+ .map(JikanClient.MangaResponse.MangaData.AuthorData::name)
+ .toList();
+
+ var genres =
+ responseData.genres().stream()
+ .map(JikanClient.MangaResponse.MangaData.GenreData::name)
+ .toList();
+
+ var alternativeTitles = responseData.title_synonyms();
+
+ return MangaDataDTO.builder()
+ .title(responseData.title())
+ .score(nonNull(responseData.score()) ? DoubleUtil.round(responseData.score(), 2) : 0)
+ .synopsis(responseData.synopsis())
+ .chapterCount(responseData.chapters())
+ .publishedFrom(responseData.published().from())
+ .publishedTo(responseData.published().to())
+ .authors(authors)
+ .genres(genres)
+ .alternativeTitles(alternativeTitles)
+ .coverImageUrl(responseData.images().jpg().large_image_url())
+ .status(mapStatus(responseData.status()))
+ .build();
+ }
+
+ private MangaStatus mapStatus(String malStatus) {
+ return switch (malStatus) {
+ case "finished" -> MangaStatus.COMPLETED;
+ case "publishing" -> MangaStatus.ONGOING;
+ case "on hiatus" -> MangaStatus.HIATUS;
+ case "discontinued" -> MangaStatus.CANCELLED;
+ default -> MangaStatus.UNKNOWN;
+ };
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/service/TitleMatcherService.java b/src/main/java/com/magamochi/catalog/service/TitleMatcherService.java
similarity index 55%
rename from src/main/java/com/magamochi/mangamochi/service/TitleMatcherService.java
rename to src/main/java/com/magamochi/catalog/service/TitleMatcherService.java
index de6ab3a..1a13d5e 100644
--- a/src/main/java/com/magamochi/mangamochi/service/TitleMatcherService.java
+++ b/src/main/java/com/magamochi/catalog/service/TitleMatcherService.java
@@ -1,10 +1,9 @@
-package com.magamochi.mangamochi.service;
+package com.magamochi.catalog.service;
+import static java.util.Objects.isNull;
import static org.apache.commons.lang3.StringUtils.isBlank;
-import static org.springframework.util.CollectionUtils.isEmpty;
-import com.magamochi.mangamochi.model.dto.TitleMatchRequestDTO;
-import com.magamochi.mangamochi.model.dto.TitleMatchResponseDTO;
+import lombok.Builder;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.text.similarity.LevenshteinDistance;
import org.springframework.stereotype.Service;
@@ -14,18 +13,24 @@ import org.springframework.stereotype.Service;
public class TitleMatcherService {
private final LevenshteinDistance levenshteinDistance = LevenshteinDistance.getDefaultInstance();
- public TitleMatchResponseDTO findBestMatch(TitleMatchRequestDTO request) {
- if (isBlank(request.getTitle()) || isEmpty(request.getOptions())) {
+ public TitleMatchResponse findBestMatch(TitleMatchRequest request) {
+ if (isBlank(request.title()) || !request.options().iterator().hasNext()) {
throw new IllegalArgumentException("Title and options are required");
}
- log.info("Finding best match for {}. Options: {}", request.getTitle(), request.getOptions());
+ // Set the default threshold if not specified
+ var threshold = request.threshold();
+ if (isNull(threshold) || threshold == 0) {
+ threshold = 85;
+ }
+
+ log.info("Finding best match for {}. Options: {}", request.title(), request.options());
String bestMatch = null;
double bestScore = 0.0;
- for (var option : request.getOptions()) {
- var score = calculateSimilarityScore(request.getTitle(), option);
+ for (var option : request.options()) {
+ var score = calculateSimilarityScore(request.title(), option);
if (score > bestScore) {
bestScore = score;
@@ -33,20 +38,20 @@ public class TitleMatcherService {
}
}
- if (bestScore >= request.getThreshold()) {
+ if (bestScore >= threshold) {
log.info(
- "Found best match for {}: {}. Similarity: {}", request.getTitle(), bestMatch, bestScore);
+ "Found best match for {}: {}. Similarity: {}", request.title(), bestMatch, bestScore);
- return TitleMatchResponseDTO.builder()
+ return TitleMatchResponse.builder()
.matchFound(true)
.bestMatch(bestMatch)
.similarity(bestScore)
.build();
}
- log.info("No match found for {}. Threshold: {}", request.getTitle(), request.getThreshold());
+ log.info("No match found for {}. Threshold: {}", request.title(), threshold);
- return TitleMatchResponseDTO.builder().matchFound(false).build();
+ return TitleMatchResponse.builder().matchFound(false).build();
}
private double calculateSimilarityScore(String title, String option) {
@@ -64,4 +69,10 @@ public class TitleMatcherService {
// Format to two decimal places for a cleaner result
return Math.round(similarity * 100.0) / 100.0;
}
+
+ @Builder
+ public record TitleMatchRequest(String title, Iterable options, Integer threshold) {}
+
+ @Builder
+ public record TitleMatchResponse(boolean matchFound, String bestMatch, Double similarity) {}
}
diff --git a/src/main/java/com/magamochi/mangamochi/util/DoubleUtil.java b/src/main/java/com/magamochi/catalog/util/DoubleUtil.java
similarity index 91%
rename from src/main/java/com/magamochi/mangamochi/util/DoubleUtil.java
rename to src/main/java/com/magamochi/catalog/util/DoubleUtil.java
index a809d25..cba0e28 100644
--- a/src/main/java/com/magamochi/mangamochi/util/DoubleUtil.java
+++ b/src/main/java/com/magamochi/catalog/util/DoubleUtil.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.util;
+package com.magamochi.catalog.util;
import java.math.BigDecimal;
import java.math.RoundingMode;
diff --git a/src/main/java/com/magamochi/mangamochi/client/MangaDexClient.java b/src/main/java/com/magamochi/client/MangaDexClient.java
similarity index 88%
rename from src/main/java/com/magamochi/mangamochi/client/MangaDexClient.java
rename to src/main/java/com/magamochi/client/MangaDexClient.java
index ffcd7ec..57ca8c5 100644
--- a/src/main/java/com/magamochi/mangamochi/client/MangaDexClient.java
+++ b/src/main/java/com/magamochi/client/MangaDexClient.java
@@ -1,14 +1,12 @@
-package com.magamochi.mangamochi.client;
+package com.magamochi.client;
-import com.magamochi.mangamochi.model.dto.MangaDexMangaDTO;
-import io.github.resilience4j.retry.annotation.Retry;
+import com.magamochi.model.dto.MangaDexMangaDTO;
import java.util.List;
import java.util.UUID;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(name = "mangaDex", url = "https://api.mangadex.org")
-@Retry(name = "MangaDexRetry")
public interface MangaDexClient {
@GetMapping("/manga/{id}")
MangaDexMangaDTO getManga(@PathVariable UUID id);
diff --git a/src/main/java/com/magamochi/mangamochi/client/NtfyClient.java b/src/main/java/com/magamochi/client/NtfyClient.java
similarity index 76%
rename from src/main/java/com/magamochi/mangamochi/client/NtfyClient.java
rename to src/main/java/com/magamochi/client/NtfyClient.java
index 64d7b1d..af6618b 100644
--- a/src/main/java/com/magamochi/mangamochi/client/NtfyClient.java
+++ b/src/main/java/com/magamochi/client/NtfyClient.java
@@ -1,12 +1,10 @@
-package com.magamochi.mangamochi.client;
+package com.magamochi.client;
-import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
@FeignClient(name = "ntfy", url = "${ntfy.endpoint}")
-@Retry(name = "JikanRetry")
public interface NtfyClient {
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
void notify(@RequestBody Request dto);
diff --git a/src/main/java/com/magamochi/common/config/RabbitConfig.java b/src/main/java/com/magamochi/common/config/RabbitConfig.java
new file mode 100644
index 0000000..912b9ba
--- /dev/null
+++ b/src/main/java/com/magamochi/common/config/RabbitConfig.java
@@ -0,0 +1,140 @@
+package com.magamochi.common.config;
+
+import com.magamochi.common.model.enumeration.ContentType;
+import org.springframework.amqp.core.Binding;
+import org.springframework.amqp.core.Queue;
+import org.springframework.amqp.core.TopicExchange;
+import org.springframework.amqp.rabbit.connection.ConnectionFactory;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class RabbitConfig {
+ @Value("${queues.manga-ingest}")
+ private String mangaIngestQueue;
+
+ @Value("${queues.manga-content-ingest}")
+ private String mangaContentIngestQueue;
+
+ @Value("${queues.manga-content-image-ingest}")
+ private String mangaContentImageIngestQueue;
+
+ @Value("${queues.provider-page-ingest}")
+ private String providerPageIngestQueue;
+
+ @Value("${queues.manga-update}")
+ private String mangaUpdateQueue;
+
+ @Value("${queues.manga-cover-update}")
+ private String mangaCoverUpdateQueue;
+
+ @Value("${queues.manga-content-image-update}")
+ private String mangaContentImageUpdateQueue;
+
+ @Value("${queues.image-fetch}")
+ private String imageFetchQueue;
+
+ @Value("${topics.image-updates}")
+ private String imageUpdatesTopic;
+
+ @Bean
+ public TopicExchange imageUpdatesExchange() {
+ return new TopicExchange(imageUpdatesTopic);
+ }
+
+ @Bean
+ public Queue imageFetchQueue() {
+ return new Queue(imageFetchQueue, false);
+ }
+
+ @Bean
+ public Queue mangaUpdateQueue() {
+ return new Queue(mangaUpdateQueue, false);
+ }
+
+ @Bean
+ public Queue mangaContentImageUpdateQueue() {
+ return new Queue(mangaContentImageUpdateQueue, false);
+ }
+
+ @Bean
+ public Queue mangaCoverUpdateQueue() {
+ return new Queue(mangaCoverUpdateQueue, false);
+ }
+
+ @Bean
+ public Binding bindingMangaCoverUpdateQueue(
+ Queue mangaCoverUpdateQueue, TopicExchange imageUpdatesExchange) {
+ return new Binding(
+ mangaCoverUpdateQueue.getName(),
+ Binding.DestinationType.QUEUE,
+ imageUpdatesExchange.getName(),
+ String.format("image.update.%s", ContentType.MANGA_COVER.name().toLowerCase()),
+ null);
+ }
+
+ @Bean
+ public Binding bindingMangaContentImageUpdateQueue(
+ Queue mangaContentImageUpdateQueue, TopicExchange imageUpdatesExchange) {
+ return new Binding(
+ mangaContentImageUpdateQueue.getName(),
+ Binding.DestinationType.QUEUE,
+ imageUpdatesExchange.getName(),
+ String.format("image.update.%s", ContentType.CONTENT_IMAGE.name().toLowerCase()),
+ null);
+ }
+
+ @Bean
+ public Queue mangaContentIngestQueue() {
+ return new Queue(mangaContentIngestQueue, false);
+ }
+
+ @Bean
+ public Queue mangaContentImageIngestQueue() {
+ return new Queue(mangaContentImageIngestQueue, false);
+ }
+
+ @Bean
+ public Queue mangaIngestQueue() {
+ return new Queue(mangaIngestQueue, false);
+ }
+
+ @Bean
+ public Queue providerPageIngestQueue() {
+ return new Queue(providerPageIngestQueue, false);
+ }
+
+ // TODO: remove unused queues
+
+ @Value("${rabbit-mq.queues.manga-chapter-download}")
+ private String mangaChapterDownloadQueue;
+
+ @Value("${rabbit-mq.queues.manga-follow-update-chapter}")
+ private String mangaFollowUpdateChapterQueue;
+
+ @Bean
+ public Queue mangaChapterDownloadQueue() {
+ return new Queue(mangaChapterDownloadQueue, false);
+ }
+
+ @Bean
+ public Queue mangaFollowUpdateChapterQueue() {
+ return new Queue(mangaFollowUpdateChapterQueue, false);
+ }
+
+ @Bean
+ public Jackson2JsonMessageConverter messageConverter() {
+ return new Jackson2JsonMessageConverter();
+ }
+
+ @Bean
+ public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
+ var rabbitTemplate = new RabbitTemplate(connectionFactory);
+ rabbitTemplate.setMessageConverter(messageConverter());
+
+ return rabbitTemplate;
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/config/RateLimiterConfig.java b/src/main/java/com/magamochi/common/config/RateLimiterConfig.java
similarity index 79%
rename from src/main/java/com/magamochi/mangamochi/config/RateLimiterConfig.java
rename to src/main/java/com/magamochi/common/config/RateLimiterConfig.java
index 1120da5..ade59f9 100644
--- a/src/main/java/com/magamochi/mangamochi/config/RateLimiterConfig.java
+++ b/src/main/java/com/magamochi/common/config/RateLimiterConfig.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.config;
+package com.magamochi.common.config;
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.context.annotation.Bean;
@@ -16,6 +16,11 @@ public class RateLimiterConfig {
return RateLimiter.create(1);
}
+ @Bean
+ public RateLimiter aniListRateLimiter() {
+ return RateLimiter.create(0.5);
+ }
+
@Bean
public RateLimiter imageDownloadRateLimiter() {
return RateLimiter.create(10);
diff --git a/src/main/java/com/magamochi/mangamochi/config/WebConfig.java b/src/main/java/com/magamochi/common/config/WebConfig.java
similarity index 92%
rename from src/main/java/com/magamochi/mangamochi/config/WebConfig.java
rename to src/main/java/com/magamochi/common/config/WebConfig.java
index 7f0df7a..06666d7 100644
--- a/src/main/java/com/magamochi/mangamochi/config/WebConfig.java
+++ b/src/main/java/com/magamochi/common/config/WebConfig.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.config;
+package com.magamochi.common.config;
import lombok.NonNull;
import org.springframework.context.annotation.Configuration;
diff --git a/src/main/java/com/magamochi/mangamochi/exception/ConflictException.java b/src/main/java/com/magamochi/common/exception/ConflictException.java
similarity index 74%
rename from src/main/java/com/magamochi/mangamochi/exception/ConflictException.java
rename to src/main/java/com/magamochi/common/exception/ConflictException.java
index 2b1a357..0c543ec 100644
--- a/src/main/java/com/magamochi/mangamochi/exception/ConflictException.java
+++ b/src/main/java/com/magamochi/common/exception/ConflictException.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.exception;
+package com.magamochi.common.exception;
public class ConflictException extends RuntimeException {
public ConflictException(String message) {
diff --git a/src/main/java/com/magamochi/mangamochi/exception/GlobalExceptionHandler.java b/src/main/java/com/magamochi/common/exception/GlobalExceptionHandler.java
similarity index 96%
rename from src/main/java/com/magamochi/mangamochi/exception/GlobalExceptionHandler.java
rename to src/main/java/com/magamochi/common/exception/GlobalExceptionHandler.java
index 5c8c79f..093cd65 100644
--- a/src/main/java/com/magamochi/mangamochi/exception/GlobalExceptionHandler.java
+++ b/src/main/java/com/magamochi/common/exception/GlobalExceptionHandler.java
@@ -1,6 +1,6 @@
-package com.magamochi.mangamochi.exception;
+package com.magamochi.common.exception;
-import com.magamochi.mangamochi.model.dto.ErrorResponseDTO;
+import com.magamochi.common.model.dto.ErrorResponseDTO;
import java.time.Instant;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
diff --git a/src/main/java/com/magamochi/mangamochi/exception/NotFoundException.java b/src/main/java/com/magamochi/common/exception/NotFoundException.java
similarity index 74%
rename from src/main/java/com/magamochi/mangamochi/exception/NotFoundException.java
rename to src/main/java/com/magamochi/common/exception/NotFoundException.java
index d9ce138..9fc1025 100644
--- a/src/main/java/com/magamochi/mangamochi/exception/NotFoundException.java
+++ b/src/main/java/com/magamochi/common/exception/NotFoundException.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.exception;
+package com.magamochi.common.exception;
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
diff --git a/src/main/java/com/magamochi/mangamochi/exception/UnprocessableException.java b/src/main/java/com/magamochi/common/exception/UnprocessableException.java
similarity index 84%
rename from src/main/java/com/magamochi/mangamochi/exception/UnprocessableException.java
rename to src/main/java/com/magamochi/common/exception/UnprocessableException.java
index 2db5e45..b2bdb69 100644
--- a/src/main/java/com/magamochi/mangamochi/exception/UnprocessableException.java
+++ b/src/main/java/com/magamochi/common/exception/UnprocessableException.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.exception;
+package com.magamochi.common.exception;
public class UnprocessableException extends RuntimeException {
public UnprocessableException(String message) {
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/DefaultResponseDTO.java b/src/main/java/com/magamochi/common/model/dto/DefaultResponseDTO.java
similarity index 94%
rename from src/main/java/com/magamochi/mangamochi/model/dto/DefaultResponseDTO.java
rename to src/main/java/com/magamochi/common/model/dto/DefaultResponseDTO.java
index 36d2ac0..0c7b8a4 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/DefaultResponseDTO.java
+++ b/src/main/java/com/magamochi/common/model/dto/DefaultResponseDTO.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.common.model.dto;
import jakarta.annotation.Nullable;
import java.time.Instant;
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/ErrorResponseDTO.java b/src/main/java/com/magamochi/common/model/dto/ErrorResponseDTO.java
similarity index 82%
rename from src/main/java/com/magamochi/mangamochi/model/dto/ErrorResponseDTO.java
rename to src/main/java/com/magamochi/common/model/dto/ErrorResponseDTO.java
index d6533f4..d20979b 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/ErrorResponseDTO.java
+++ b/src/main/java/com/magamochi/common/model/dto/ErrorResponseDTO.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.common.model.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
diff --git a/src/main/java/com/magamochi/common/model/enumeration/ContentType.java b/src/main/java/com/magamochi/common/model/enumeration/ContentType.java
new file mode 100644
index 0000000..a4ce0d7
--- /dev/null
+++ b/src/main/java/com/magamochi/common/model/enumeration/ContentType.java
@@ -0,0 +1,8 @@
+package com.magamochi.common.model.enumeration;
+
+public enum ContentType {
+ MANGA_COVER,
+ CHAPTER,
+ VOLUME,
+ CONTENT_IMAGE,
+}
diff --git a/src/main/java/com/magamochi/common/queue/command/ImageFetchCommand.java b/src/main/java/com/magamochi/common/queue/command/ImageFetchCommand.java
new file mode 100644
index 0000000..5f470c8
--- /dev/null
+++ b/src/main/java/com/magamochi/common/queue/command/ImageFetchCommand.java
@@ -0,0 +1,5 @@
+package com.magamochi.common.queue.command;
+
+import com.magamochi.common.model.enumeration.ContentType;
+
+public record ImageFetchCommand(long entityId, ContentType contentType, String url) {}
diff --git a/src/main/java/com/magamochi/common/queue/command/ImageUpdateCommand.java b/src/main/java/com/magamochi/common/queue/command/ImageUpdateCommand.java
new file mode 100644
index 0000000..e5cf71a
--- /dev/null
+++ b/src/main/java/com/magamochi/common/queue/command/ImageUpdateCommand.java
@@ -0,0 +1,5 @@
+package com.magamochi.common.queue.command;
+
+import java.util.UUID;
+
+public record ImageUpdateCommand(long entityId, UUID imageId) {}
diff --git a/src/main/java/com/magamochi/common/queue/command/MangaContentImageIngestCommand.java b/src/main/java/com/magamochi/common/queue/command/MangaContentImageIngestCommand.java
new file mode 100644
index 0000000..42ffb0b
--- /dev/null
+++ b/src/main/java/com/magamochi/common/queue/command/MangaContentImageIngestCommand.java
@@ -0,0 +1,6 @@
+package com.magamochi.common.queue.command;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record MangaContentImageIngestCommand(
+ long mangaContentId, @NotBlank String url, int position, boolean isLast) {}
diff --git a/src/main/java/com/magamochi/common/queue/command/MangaContentIngestCommand.java b/src/main/java/com/magamochi/common/queue/command/MangaContentIngestCommand.java
new file mode 100644
index 0000000..b04bd82
--- /dev/null
+++ b/src/main/java/com/magamochi/common/queue/command/MangaContentIngestCommand.java
@@ -0,0 +1,9 @@
+package com.magamochi.common.queue.command;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record MangaContentIngestCommand(
+ long mangaContentProviderId,
+ @NotBlank String title,
+ @NotBlank String url,
+ @NotBlank String languageCode) {}
diff --git a/src/main/java/com/magamochi/common/queue/command/MangaIngestCommand.java b/src/main/java/com/magamochi/common/queue/command/MangaIngestCommand.java
new file mode 100644
index 0000000..e29223c
--- /dev/null
+++ b/src/main/java/com/magamochi/common/queue/command/MangaIngestCommand.java
@@ -0,0 +1,6 @@
+package com.magamochi.common.queue.command;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record MangaIngestCommand(
+ long providerId, @NotBlank String mangaTitle, @NotBlank String url) {}
diff --git a/src/main/java/com/magamochi/common/queue/producer/ImageFetchProducer.java b/src/main/java/com/magamochi/common/queue/producer/ImageFetchProducer.java
new file mode 100644
index 0000000..9674a70
--- /dev/null
+++ b/src/main/java/com/magamochi/common/queue/producer/ImageFetchProducer.java
@@ -0,0 +1,23 @@
+package com.magamochi.common.queue.producer;
+
+import com.magamochi.common.queue.command.ImageFetchCommand;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class ImageFetchProducer {
+ private final RabbitTemplate rabbitTemplate;
+
+ @Value("${queues.image-fetch}")
+ private String imageFetchQueue;
+
+ public void sendImageFetchCommand(ImageFetchCommand command) {
+ rabbitTemplate.convertAndSend(imageFetchQueue, command);
+ log.info("Sent image fetch command: {}", command);
+ }
+}
diff --git a/src/main/java/com/magamochi/content/controller/ContentController.java b/src/main/java/com/magamochi/content/controller/ContentController.java
new file mode 100644
index 0000000..c035ddf
--- /dev/null
+++ b/src/main/java/com/magamochi/content/controller/ContentController.java
@@ -0,0 +1,44 @@
+package com.magamochi.content.controller;
+
+import com.magamochi.common.model.dto.DefaultResponseDTO;
+import com.magamochi.content.model.dto.MangaContentDTO;
+import com.magamochi.content.service.ContentService;
+import com.magamochi.model.dto.MangaContentImagesDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import jakarta.validation.constraints.NotNull;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/content")
+@RequiredArgsConstructor
+public class ContentController {
+ private final ContentService contentService;
+
+ @Operation(
+ summary = "Get the content for a specific manga/content provider combination",
+ description = "Retrieve the content for a specific manga/content provider combination.",
+ tags = {"Content"},
+ operationId = "getMangaProviderContent")
+ @GetMapping("/{mangaContentProviderId}")
+ public DefaultResponseDTO> getMangaProviderContent(
+ @PathVariable @NotNull Long mangaContentProviderId) {
+ return DefaultResponseDTO.ok(contentService.getContent(mangaContentProviderId));
+ }
+
+ @Operation(
+ summary = "Get the content images for a specific manga/provider combination",
+ description =
+ "Retrieve a list of manga content images for a specific manga/provider combination.",
+ tags = {"Content"},
+ operationId = "getMangaContentImages")
+ @GetMapping("/{mangaContentId}/images")
+ public DefaultResponseDTO getMangaContentImages(
+ @PathVariable Long mangaContentId) {
+ return DefaultResponseDTO.ok(contentService.getContentImages(mangaContentId));
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/LanguageDTO.java b/src/main/java/com/magamochi/content/model/dto/LanguageDTO.java
similarity index 81%
rename from src/main/java/com/magamochi/mangamochi/model/dto/LanguageDTO.java
rename to src/main/java/com/magamochi/content/model/dto/LanguageDTO.java
index e2d9163..3f70990 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/LanguageDTO.java
+++ b/src/main/java/com/magamochi/content/model/dto/LanguageDTO.java
@@ -1,8 +1,8 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.content.model.dto;
import static java.util.Objects.isNull;
-import com.magamochi.mangamochi.model.entity.Language;
+import com.magamochi.catalog.model.entity.Language;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
diff --git a/src/main/java/com/magamochi/content/model/dto/MangaContentDTO.java b/src/main/java/com/magamochi/content/model/dto/MangaContentDTO.java
new file mode 100644
index 0000000..86a52ca
--- /dev/null
+++ b/src/main/java/com/magamochi/content/model/dto/MangaContentDTO.java
@@ -0,0 +1,21 @@
+package com.magamochi.content.model.dto;
+
+import com.magamochi.content.model.entity.MangaContent;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+
+public record MangaContentDTO(
+ @NotNull Long id,
+ @NotBlank String title,
+ @NotNull Boolean downloaded,
+ @NotNull Boolean isRead,
+ LanguageDTO language) {
+ public static MangaContentDTO from(MangaContent mangaContent) {
+ return new MangaContentDTO(
+ mangaContent.getId(),
+ mangaContent.getTitle(),
+ mangaContent.getDownloaded(),
+ false,
+ LanguageDTO.from(mangaContent.getLanguage()));
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/MangaChapter.java b/src/main/java/com/magamochi/content/model/entity/MangaContent.java
similarity index 54%
rename from src/main/java/com/magamochi/mangamochi/model/entity/MangaChapter.java
rename to src/main/java/com/magamochi/content/model/entity/MangaContent.java
index 0427f9e..000683e 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/MangaChapter.java
+++ b/src/main/java/com/magamochi/content/model/entity/MangaContent.java
@@ -1,5 +1,8 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.content.model.entity;
+import com.magamochi.catalog.model.entity.Language;
+import com.magamochi.catalog.model.entity.MangaContentProvider;
+import com.magamochi.common.model.enumeration.ContentType;
import jakarta.persistence.*;
import java.time.Instant;
import java.util.List;
@@ -8,20 +11,22 @@ import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
@Entity
-@Table(name = "manga_chapters")
+@Table(name = "manga_contents")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
-public class MangaChapter {
+public class MangaContent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
- @JoinColumn(name = "manga_provider_id")
- private MangaProvider mangaProvider;
+ @JoinColumn(name = "manga_content_provider_id")
+ private MangaContentProvider mangaContentProvider;
+
+ @Builder.Default private ContentType type = ContentType.CHAPTER;
private String title;
@@ -29,16 +34,12 @@ public class MangaChapter {
@Builder.Default private Boolean downloaded = false;
- @Builder.Default private Boolean read = false;
-
@CreationTimestamp private Instant createdAt;
@UpdateTimestamp private Instant updatedAt;
- @OneToMany(mappedBy = "mangaChapter")
- private List mangaChapterImages;
-
- private Integer chapterNumber;
+ @OneToMany(mappedBy = "mangaContent")
+ private List mangaContentImages;
@ManyToOne
@JoinColumn(name = "language_id")
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/MangaChapterImage.java b/src/main/java/com/magamochi/content/model/entity/MangaContentImage.java
similarity index 68%
rename from src/main/java/com/magamochi/mangamochi/model/entity/MangaChapterImage.java
rename to src/main/java/com/magamochi/content/model/entity/MangaContentImage.java
index abcfd00..f0fc463 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/MangaChapterImage.java
+++ b/src/main/java/com/magamochi/content/model/entity/MangaContentImage.java
@@ -1,5 +1,6 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.content.model.entity;
+import com.magamochi.image.model.entity.Image;
import jakarta.persistence.*;
import java.time.Instant;
import lombok.*;
@@ -7,20 +8,20 @@ import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
@Entity
-@Table(name = "manga_chapter_images")
+@Table(name = "manga_content_images")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
-public class MangaChapterImage {
+public class MangaContentImage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
- @JoinColumn(name = "manga_chapter_id")
- private MangaChapter mangaChapter;
+ @JoinColumn(name = "manga_content_id")
+ private MangaContent mangaContent;
@OneToOne
@JoinColumn(name = "image_id")
diff --git a/src/main/java/com/magamochi/content/model/repository/MangaContentImageRepository.java b/src/main/java/com/magamochi/content/model/repository/MangaContentImageRepository.java
new file mode 100644
index 0000000..5ba885f
--- /dev/null
+++ b/src/main/java/com/magamochi/content/model/repository/MangaContentImageRepository.java
@@ -0,0 +1,12 @@
+package com.magamochi.content.model.repository;
+
+import com.magamochi.content.model.entity.MangaContent;
+import com.magamochi.content.model.entity.MangaContentImage;
+import java.util.List;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface MangaContentImageRepository extends JpaRepository {
+ List findAllByMangaContent(MangaContent mangaContent);
+
+ boolean existsByMangaContent_IdAndPosition(Long mangaContentId, int position);
+}
diff --git a/src/main/java/com/magamochi/content/model/repository/MangaContentRepository.java b/src/main/java/com/magamochi/content/model/repository/MangaContentRepository.java
new file mode 100644
index 0000000..2aadd67
--- /dev/null
+++ b/src/main/java/com/magamochi/content/model/repository/MangaContentRepository.java
@@ -0,0 +1,8 @@
+package com.magamochi.content.model.repository;
+
+import com.magamochi.content.model.entity.MangaContent;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface MangaContentRepository extends JpaRepository {
+ boolean existsByMangaContentProvider_IdAndUrlIgnoreCase(Long mangaContentProviderId, String url);
+}
diff --git a/src/main/java/com/magamochi/content/queue/consumer/MangaContentImageIngestConsumer.java b/src/main/java/com/magamochi/content/queue/consumer/MangaContentImageIngestConsumer.java
new file mode 100644
index 0000000..feedc13
--- /dev/null
+++ b/src/main/java/com/magamochi/content/queue/consumer/MangaContentImageIngestConsumer.java
@@ -0,0 +1,22 @@
+package com.magamochi.content.queue.consumer;
+
+import com.magamochi.common.queue.command.MangaContentImageIngestCommand;
+import com.magamochi.content.service.ContentIngestService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class MangaContentImageIngestConsumer {
+ private final ContentIngestService contentIngestService;
+
+ @RabbitListener(queues = "${queues.manga-content-image-ingest}")
+ public void receiveMangaContentImageIngestCommand(MangaContentImageIngestCommand command) {
+ log.info("Received manga content ingest command: {}", command);
+ contentIngestService.ingestImages(
+ command.mangaContentId(), command.url(), command.position(), command.isLast());
+ }
+}
diff --git a/src/main/java/com/magamochi/content/queue/consumer/MangaContentImageUpdateConsumer.java b/src/main/java/com/magamochi/content/queue/consumer/MangaContentImageUpdateConsumer.java
new file mode 100644
index 0000000..96a0e6d
--- /dev/null
+++ b/src/main/java/com/magamochi/content/queue/consumer/MangaContentImageUpdateConsumer.java
@@ -0,0 +1,22 @@
+package com.magamochi.content.queue.consumer;
+
+import com.magamochi.common.queue.command.ImageUpdateCommand;
+import com.magamochi.content.service.ContentIngestService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class MangaContentImageUpdateConsumer {
+ private final ContentIngestService contentIngestService;
+
+ @RabbitListener(queues = "${queues.manga-content-image-update}")
+ public void receiveMangaContentImageUpdateCommand(ImageUpdateCommand command) {
+ log.info("Received manga content image update command: {}", command);
+
+ contentIngestService.updateMangaContentImage(command.entityId(), command.imageId());
+ }
+}
diff --git a/src/main/java/com/magamochi/content/queue/consumer/MangaContentIngestConsumer.java b/src/main/java/com/magamochi/content/queue/consumer/MangaContentIngestConsumer.java
new file mode 100644
index 0000000..17df5bd
--- /dev/null
+++ b/src/main/java/com/magamochi/content/queue/consumer/MangaContentIngestConsumer.java
@@ -0,0 +1,22 @@
+package com.magamochi.content.queue.consumer;
+
+import com.magamochi.common.queue.command.MangaContentIngestCommand;
+import com.magamochi.content.service.ContentIngestService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class MangaContentIngestConsumer {
+ private final ContentIngestService contentIngestService;
+
+ @RabbitListener(queues = "${queues.manga-content-ingest}")
+ public void receiveMangaContentIngestCommand(MangaContentIngestCommand command) {
+ log.info("Received manga content ingest command: {}", command);
+ contentIngestService.ingest(
+ command.mangaContentProviderId(), command.title(), command.url(), command.languageCode());
+ }
+}
diff --git a/src/main/java/com/magamochi/content/service/ContentIngestService.java b/src/main/java/com/magamochi/content/service/ContentIngestService.java
new file mode 100644
index 0000000..3d2f13b
--- /dev/null
+++ b/src/main/java/com/magamochi/content/service/ContentIngestService.java
@@ -0,0 +1,113 @@
+package com.magamochi.content.service;
+
+import static java.util.Objects.isNull;
+
+import com.magamochi.catalog.service.LanguageService;
+import com.magamochi.catalog.service.MangaContentProviderService;
+import com.magamochi.common.exception.NotFoundException;
+import com.magamochi.common.model.enumeration.ContentType;
+import com.magamochi.common.queue.command.ImageFetchCommand;
+import com.magamochi.common.queue.producer.ImageFetchProducer;
+import com.magamochi.content.model.entity.MangaContent;
+import com.magamochi.content.model.entity.MangaContentImage;
+import com.magamochi.content.model.repository.MangaContentImageRepository;
+import com.magamochi.content.model.repository.MangaContentRepository;
+import com.magamochi.image.service.ImageService;
+import jakarta.validation.constraints.NotBlank;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class ContentIngestService {
+ private final ContentService contentService;
+ private final MangaContentProviderService mangaContentProviderService;
+ private final LanguageService languageService;
+
+ private final MangaContentRepository mangaContentRepository;
+ private final MangaContentImageRepository mangaContentImageRepository;
+
+ private final ImageFetchProducer imageFetchProducer;
+ private final ImageService imageService;
+
+ public void ingest(
+ long mangaContentProviderId,
+ @NotBlank String title,
+ @NotBlank String url,
+ @NotBlank String languageCode) {
+ log.info("Ingesting Manga Content ({}) for provider {}", title, mangaContentProviderId);
+
+ var mangaContentProvider = mangaContentProviderService.find(mangaContentProviderId);
+
+ if (mangaContentRepository.existsByMangaContentProvider_IdAndUrlIgnoreCase(
+ mangaContentProvider.getId(), url)) {
+ log.info(
+ "Manga Content ({}) for provider {} already exists. Skipped.",
+ title,
+ mangaContentProviderId);
+ return;
+ }
+
+ var language = languageService.find(languageCode);
+
+ var mangaContent =
+ mangaContentRepository.save(
+ MangaContent.builder()
+ .mangaContentProvider(mangaContentProvider)
+ .title(title)
+ .url(url)
+ .language(language)
+ .build());
+
+ log.info(
+ "Ingested Manga Content ({}) for provider {}: {}",
+ title,
+ mangaContentProviderId,
+ mangaContent.getId());
+ }
+
+ @Transactional
+ public void ingestImages(
+ long mangaContentId, @NotBlank String url, int position, boolean isLast) {
+ log.info(
+ "Ingesting Manga Content Image for MangaContent {}, position {}", mangaContentId, position);
+
+ var mangaContent = contentService.find(mangaContentId);
+
+ if (mangaContentImageRepository.existsByMangaContent_IdAndPosition(mangaContentId, position)) {
+ return;
+ }
+
+ var mangaContentImage =
+ mangaContentImageRepository.save(
+ MangaContentImage.builder().mangaContent(mangaContent).position(position).build());
+
+ imageFetchProducer.sendImageFetchCommand(
+ new ImageFetchCommand(mangaContentImage.getId(), ContentType.CONTENT_IMAGE, url));
+
+ if (isLast) {
+ mangaContent.setDownloaded(true);
+ }
+ }
+
+ @Transactional
+ public void updateMangaContentImage(long mangaContentImageId, UUID imageId) {
+ if (isNull(imageId)) {
+ log.error("Null imageID received!");
+ return;
+ }
+
+ var mangaContentImage =
+ mangaContentImageRepository
+ .findById(mangaContentImageId)
+ .orElseThrow(
+ () -> new NotFoundException("Image not found for ID: " + mangaContentImageId));
+
+ var image = imageService.find(imageId);
+ mangaContentImage.setImage(image);
+ }
+}
diff --git a/src/main/java/com/magamochi/content/service/ContentService.java b/src/main/java/com/magamochi/content/service/ContentService.java
new file mode 100644
index 0000000..0cd4e79
--- /dev/null
+++ b/src/main/java/com/magamochi/content/service/ContentService.java
@@ -0,0 +1,64 @@
+package com.magamochi.content.service;
+
+import com.magamochi.catalog.service.MangaContentProviderService;
+import com.magamochi.common.exception.NotFoundException;
+import com.magamochi.content.model.dto.MangaContentDTO;
+import com.magamochi.content.model.entity.MangaContent;
+import com.magamochi.content.model.repository.MangaContentRepository;
+import com.magamochi.model.dto.MangaContentImagesDTO;
+import jakarta.validation.constraints.NotNull;
+import java.util.Comparator;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class ContentService {
+ private final MangaContentProviderService mangaContentProviderService;
+
+ private final MangaContentRepository mangaContentRepository;
+
+ public List getContent(@NotNull Long mangaContentProviderId) {
+ var mangaContentProvider = mangaContentProviderService.find(mangaContentProviderId);
+
+ return mangaContentProvider.getMangaContents().stream()
+ .sorted(Comparator.comparing(MangaContent::getId))
+ .map(MangaContentDTO::from)
+ .toList();
+ }
+
+ public MangaContent find(Long id) {
+ return mangaContentRepository
+ .findById(id)
+ .orElseThrow(() -> new NotFoundException("MangaContent not found for ID: " + id));
+ }
+
+ public MangaContentImagesDTO getContentImages(Long mangaContentId) {
+ var mangaContent = find(mangaContentId);
+
+ var chapters =
+ mangaContent.getMangaContentProvider().getMangaContents().stream()
+ .sorted(Comparator.comparing(MangaContent::getId))
+ .toList();
+ Long prevId = null;
+ Long nextId = null;
+
+ // TODO: this doesn't perform well for large datasets
+ for (var i = 0; i < chapters.size(); i++) {
+ if (chapters.get(i).getId().equals(mangaContent.getId())) {
+ if (i > 0) {
+ prevId = chapters.get(i - 1).getId();
+ }
+
+ if (i < chapters.size() - 1) {
+ nextId = chapters.get(i + 1).getId();
+ }
+
+ break;
+ }
+ }
+
+ return MangaContentImagesDTO.from(mangaContent, prevId, nextId);
+ }
+}
diff --git a/src/main/java/com/magamochi/controller/ManagementController.java b/src/main/java/com/magamochi/controller/ManagementController.java
new file mode 100644
index 0000000..ef17ac8
--- /dev/null
+++ b/src/main/java/com/magamochi/controller/ManagementController.java
@@ -0,0 +1,66 @@
+package com.magamochi.controller;
+
+import com.magamochi.client.NtfyClient;
+import com.magamochi.common.model.dto.DefaultResponseDTO;
+import com.magamochi.image.task.ImageCleanupTask;
+import com.magamochi.ingestion.task.IngestFromContentProvidersTask;
+import com.magamochi.task.MangaFollowUpdateTask;
+import com.magamochi.user.repository.UserRepository;
+import io.swagger.v3.oas.annotations.Operation;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/management")
+@RequiredArgsConstructor
+public class ManagementController {
+ private final IngestFromContentProvidersTask ingestFromContentProvidersTask;
+ private final ImageCleanupTask imageCleanupTask;
+ private final MangaFollowUpdateTask mangaFollowUpdateTask;
+ private final UserRepository userRepository;
+ private final NtfyClient ntfyClient;
+
+ @Operation(
+ summary = "Cleanup unused S3 images",
+ description = "Triggers the cleanup of untracked S3 images",
+ tags = {"Management"},
+ operationId = "imageCleanup")
+ @PostMapping("image-cleanup")
+ public DefaultResponseDTO imageCleanup() {
+ imageCleanupTask.cleanupImages();
+
+ return DefaultResponseDTO.ok().build();
+ }
+
+ @Operation(
+ summary = "Trigger user follow update",
+ description = "Trigger user follow update",
+ tags = {"Management"},
+ operationId = "userFollowUpdate")
+ @PostMapping("user-follow")
+ public DefaultResponseDTO triggerUserFollowUpdate() {
+ mangaFollowUpdateTask.updateMangaList();
+
+ return DefaultResponseDTO.ok().build();
+ }
+
+ @Operation(
+ summary = "Test notification",
+ description = "Sends a test notification to all users",
+ tags = {"Management"},
+ operationId = "testNotification")
+ @PostMapping("test-notification")
+ public DefaultResponseDTO testNotification() {
+ var users = userRepository.findAll();
+
+ users.forEach(
+ user ->
+ ntfyClient.notify(
+ new NtfyClient.Request(
+ "mangamochi-" + user.getId().toString(),
+ "Mangamochi",
+ "This is a test notification :)")));
+
+ return DefaultResponseDTO.ok().build();
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/controller/MangaChapterController.java b/src/main/java/com/magamochi/controller/MangaChapterController.java
similarity index 64%
rename from src/main/java/com/magamochi/mangamochi/controller/MangaChapterController.java
rename to src/main/java/com/magamochi/controller/MangaChapterController.java
index b67eba3..4d1cdf9 100644
--- a/src/main/java/com/magamochi/mangamochi/controller/MangaChapterController.java
+++ b/src/main/java/com/magamochi/controller/MangaChapterController.java
@@ -1,8 +1,8 @@
-package com.magamochi.mangamochi.controller;
+package com.magamochi.controller;
-import com.magamochi.mangamochi.model.dto.*;
-import com.magamochi.mangamochi.model.enumeration.ArchiveFileType;
-import com.magamochi.mangamochi.service.MangaChapterService;
+import com.magamochi.common.model.dto.DefaultResponseDTO;
+import com.magamochi.model.enumeration.ArchiveFileType;
+import com.magamochi.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;
@@ -20,30 +20,6 @@ import org.springframework.web.bind.annotation.*;
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 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 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.",
diff --git a/src/main/java/com/magamochi/controller/MangaController.java b/src/main/java/com/magamochi/controller/MangaController.java
new file mode 100644
index 0000000..5c17b85
--- /dev/null
+++ b/src/main/java/com/magamochi/controller/MangaController.java
@@ -0,0 +1,50 @@
+package com.magamochi.controller;
+
+import com.magamochi.common.model.dto.DefaultResponseDTO;
+import com.magamochi.service.OldMangaService;
+import io.swagger.v3.oas.annotations.Operation;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/mangas")
+@RequiredArgsConstructor
+public class MangaController {
+ private final OldMangaService oldMangaService;
+
+ @Operation(
+ summary = "Fetch all chapters",
+ description = "Fetch all not yet downloaded chapters from the provider",
+ tags = {"Manga Chapter"},
+ operationId = "fetchAllChapters")
+ @PostMapping(value = "/{mangaProviderId}/fetch-all-chapters")
+ public DefaultResponseDTO fetchAllChapters(@PathVariable Long mangaProviderId) {
+ oldMangaService.fetchAllNotDownloadedChapters(mangaProviderId);
+
+ return DefaultResponseDTO.ok().build();
+ }
+
+ @Operation(
+ summary = "Follow the manga specified by its ID",
+ description = "Follow the manga specified by its ID.",
+ tags = {"Manga"},
+ operationId = "followManga")
+ @PostMapping("/{mangaId}/followManga")
+ public DefaultResponseDTO followManga(@PathVariable Long mangaId) {
+ oldMangaService.follow(mangaId);
+
+ return DefaultResponseDTO.ok().build();
+ }
+
+ @Operation(
+ summary = "Unfollow the manga specified by its ID",
+ description = "Unfollow the manga specified by its ID.",
+ tags = {"Manga"},
+ operationId = "unfollowManga")
+ @PostMapping("/{mangaId}/unfollowManga")
+ public DefaultResponseDTO unfollowManga(@PathVariable Long mangaId) {
+ oldMangaService.unfollow(mangaId);
+
+ return DefaultResponseDTO.ok().build();
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/controller/MangaImportController.java b/src/main/java/com/magamochi/controller/MangaImportController.java
similarity index 82%
rename from src/main/java/com/magamochi/mangamochi/controller/MangaImportController.java
rename to src/main/java/com/magamochi/controller/MangaImportController.java
index 9a4fae7..06c9e19 100644
--- a/src/main/java/com/magamochi/mangamochi/controller/MangaImportController.java
+++ b/src/main/java/com/magamochi/controller/MangaImportController.java
@@ -1,8 +1,10 @@
-package com.magamochi.mangamochi.controller;
+package com.magamochi.controller;
-import com.magamochi.mangamochi.model.dto.*;
-import com.magamochi.mangamochi.service.MangaImportService;
-import com.magamochi.mangamochi.service.ProviderManualMangaImportService;
+import com.magamochi.common.model.dto.DefaultResponseDTO;
+import com.magamochi.model.dto.ImportMangaResponseDTO;
+import com.magamochi.model.dto.ImportRequestDTO;
+// import com.magamochi.service.MangaImportService;
+import com.magamochi.service.ProviderManualMangaImportService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
@@ -18,7 +20,7 @@ import org.springframework.web.multipart.MultipartFile;
@RequestMapping("/manga/import")
@RequiredArgsConstructor
public class MangaImportController {
- private final MangaImportService mangaImportService;
+ // private final MangaImportService mangaImportService;
private final ProviderManualMangaImportService providerManualMangaImportService;
@Operation(
@@ -53,7 +55,7 @@ public class MangaImportController {
@RequestPart("files")
@NotNull
List files) {
- mangaImportService.importMangaFiles(malId, files);
+ // mangaImportService.importMangaFiles(malId, files);
return DefaultResponseDTO.ok().build();
}
diff --git a/src/main/java/com/magamochi/mangamochi/controller/UserFavoriteMangaController.java b/src/main/java/com/magamochi/controller/UserFavoriteMangaController.java
similarity index 86%
rename from src/main/java/com/magamochi/mangamochi/controller/UserFavoriteMangaController.java
rename to src/main/java/com/magamochi/controller/UserFavoriteMangaController.java
index 63c0d57..1c45802 100644
--- a/src/main/java/com/magamochi/mangamochi/controller/UserFavoriteMangaController.java
+++ b/src/main/java/com/magamochi/controller/UserFavoriteMangaController.java
@@ -1,7 +1,7 @@
-package com.magamochi.mangamochi.controller;
+package com.magamochi.controller;
-import com.magamochi.mangamochi.model.dto.DefaultResponseDTO;
-import com.magamochi.mangamochi.service.UserFavoriteMangaService;
+import com.magamochi.common.model.dto.DefaultResponseDTO;
+import com.magamochi.service.UserFavoriteMangaService;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
diff --git a/src/main/java/com/magamochi/mangamochi/config/S3ClientConfig.java b/src/main/java/com/magamochi/image/config/S3ClientConfig.java
similarity index 96%
rename from src/main/java/com/magamochi/mangamochi/config/S3ClientConfig.java
rename to src/main/java/com/magamochi/image/config/S3ClientConfig.java
index bcc3f26..64dbb19 100644
--- a/src/main/java/com/magamochi/mangamochi/config/S3ClientConfig.java
+++ b/src/main/java/com/magamochi/image/config/S3ClientConfig.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.config;
+package com.magamochi.image.config;
import java.net.URI;
import org.springframework.beans.factory.annotation.Value;
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/Image.java b/src/main/java/com/magamochi/image/model/entity/Image.java
similarity index 83%
rename from src/main/java/com/magamochi/mangamochi/model/entity/Image.java
rename to src/main/java/com/magamochi/image/model/entity/Image.java
index c13ce49..3829341 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/Image.java
+++ b/src/main/java/com/magamochi/image/model/entity/Image.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.image.model.entity;
import jakarta.persistence.*;
import java.time.Instant;
@@ -19,7 +19,9 @@ public class Image {
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
- private String fileKey;
+ private String objectKey;
+
+ private String fileHash;
@CreationTimestamp private Instant createdAt;
diff --git a/src/main/java/com/magamochi/image/model/repository/ImageRepository.java b/src/main/java/com/magamochi/image/model/repository/ImageRepository.java
new file mode 100644
index 0000000..342a464
--- /dev/null
+++ b/src/main/java/com/magamochi/image/model/repository/ImageRepository.java
@@ -0,0 +1,10 @@
+package com.magamochi.image.model.repository;
+
+import com.magamochi.image.model.entity.Image;
+import java.util.Optional;
+import java.util.UUID;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ImageRepository extends JpaRepository {
+ Optional findByFileHash(String fileHash);
+}
diff --git a/src/main/java/com/magamochi/image/queue/consumer/ImageFetchConsumer.java b/src/main/java/com/magamochi/image/queue/consumer/ImageFetchConsumer.java
new file mode 100644
index 0000000..7a17062
--- /dev/null
+++ b/src/main/java/com/magamochi/image/queue/consumer/ImageFetchConsumer.java
@@ -0,0 +1,28 @@
+package com.magamochi.image.queue.consumer;
+
+import com.magamochi.common.queue.command.ImageFetchCommand;
+import com.magamochi.common.queue.command.ImageUpdateCommand;
+import com.magamochi.image.queue.producer.ImageUpdateProducer;
+import com.magamochi.image.service.ImageFetchService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class ImageFetchConsumer {
+ private final ImageFetchService imageFetchService;
+ private final ImageUpdateProducer imageUpdateProducer;
+
+ @RabbitListener(queues = "${queues.image-fetch}")
+ public void receiveImageFetchCommand(ImageFetchCommand command) {
+ log.info("Received image fetch command: {}", command);
+
+ var imageId = imageFetchService.fetchImage(command.url(), command.contentType());
+
+ imageUpdateProducer.publishImageUpdateCommand(
+ new ImageUpdateCommand(command.entityId(), imageId), command.contentType());
+ }
+}
diff --git a/src/main/java/com/magamochi/image/queue/producer/ImageUpdateProducer.java b/src/main/java/com/magamochi/image/queue/producer/ImageUpdateProducer.java
new file mode 100644
index 0000000..89964f1
--- /dev/null
+++ b/src/main/java/com/magamochi/image/queue/producer/ImageUpdateProducer.java
@@ -0,0 +1,25 @@
+package com.magamochi.image.queue.producer;
+
+import com.magamochi.common.model.enumeration.ContentType;
+import com.magamochi.common.queue.command.ImageUpdateCommand;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class ImageUpdateProducer {
+ private final RabbitTemplate rabbitTemplate;
+
+ @Value("${topics.image-updates}")
+ private String imageUpdatesTopic;
+
+ public void publishImageUpdateCommand(ImageUpdateCommand command, ContentType contentType) {
+ var routingKey = String.format("image.update.%s", contentType.name().toLowerCase());
+
+ rabbitTemplate.convertAndSend(imageUpdatesTopic, routingKey, command);
+ }
+}
diff --git a/src/main/java/com/magamochi/image/service/ImageFetchService.java b/src/main/java/com/magamochi/image/service/ImageFetchService.java
new file mode 100644
index 0000000..0b7c7eb
--- /dev/null
+++ b/src/main/java/com/magamochi/image/service/ImageFetchService.java
@@ -0,0 +1,84 @@
+package com.magamochi.image.service;
+
+import static java.util.Objects.nonNull;
+
+import com.google.common.util.concurrent.RateLimiter;
+import com.magamochi.common.model.enumeration.ContentType;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.apache.tika.Tika;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class ImageFetchService {
+ private final ImageService imageManagerService;
+
+ private final RateLimiter imageDownloadRateLimiter;
+
+ private final HttpClient httpClient =
+ HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build();
+ private final Tika tika = new Tika();
+
+ public UUID fetchImage(String imageUrl, ContentType contentType) {
+ try {
+ var request = HttpRequest.newBuilder(URI.create(imageUrl.trim())).GET().build();
+
+ imageDownloadRateLimiter.acquire();
+ var response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
+
+ var imageBytes = response.body();
+
+ var fileContentType = resolveContentType(response, imageBytes);
+
+ var fileHash = computeHash(imageBytes);
+
+ return imageManagerService.upload(
+ imageBytes, fileContentType, contentType.name().toLowerCase(), fileHash);
+ } catch (Exception e) {
+ log.error("Failed to fetch image from URL: {}", imageUrl, e);
+ return null;
+ }
+ }
+
+ private String resolveContentType(HttpResponse response, byte[] fileBytes) {
+ var headerType =
+ response
+ .headers()
+ .firstValue("Content-Type")
+ .map(val -> val.split(";")[0].trim().toLowerCase())
+ .orElse(null);
+
+ if (nonNull(headerType) && headerType.startsWith("image/")) {
+ return headerType;
+ }
+
+ return tika.detect(fileBytes);
+ }
+
+ private String computeHash(byte[] content) throws NoSuchAlgorithmException {
+ var digest = MessageDigest.getInstance("SHA-256");
+ var hashBytes = digest.digest(content);
+ var hexString = new StringBuilder(2 * hashBytes.length);
+
+ for (byte b : hashBytes) {
+ var hex = Integer.toHexString(0xff & b);
+
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+
+ hexString.append(hex);
+ }
+
+ return hexString.toString();
+ }
+}
diff --git a/src/main/java/com/magamochi/image/service/ImageService.java b/src/main/java/com/magamochi/image/service/ImageService.java
new file mode 100644
index 0000000..e3c19a8
--- /dev/null
+++ b/src/main/java/com/magamochi/image/service/ImageService.java
@@ -0,0 +1,54 @@
+package com.magamochi.image.service;
+
+import com.magamochi.common.exception.NotFoundException;
+import com.magamochi.image.model.entity.Image;
+import com.magamochi.image.model.repository.ImageRepository;
+import java.util.List;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.apache.tika.mime.MimeTypeException;
+import org.apache.tika.mime.MimeTypes;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class ImageService {
+ private final S3Service s3Service;
+ private final ImageRepository imageRepository;
+
+ public UUID upload(byte[] data, String contentType, String path, String fileHash) {
+ var existingImage = imageRepository.findByFileHash(fileHash);
+ if (existingImage.isPresent()) {
+ log.info("Image already exists with hash {}, returning existing ID", fileHash);
+ return existingImage.get().getId();
+ }
+
+ log.info("Uploading new image {} to S3", path);
+
+ String extension = "";
+ try {
+ extension = MimeTypes.getDefaultMimeTypes().forName(contentType).getExtension();
+ } catch (MimeTypeException e) {
+ log.warn("Could not determine extension for content type: {}", contentType);
+ }
+
+ var filename = "manga/" + path + "/" + UUID.randomUUID() + extension;
+ var objectKey = s3Service.uploadFile(data, contentType, filename);
+
+ return imageRepository
+ .save(Image.builder().objectKey(objectKey).fileHash(fileHash).build())
+ .getId();
+ }
+
+ public Image find(UUID id) {
+ return imageRepository
+ .findById(id)
+ .orElseThrow(() -> new NotFoundException("Image not found with ID " + id));
+ }
+
+ public List findAll() {
+ return imageRepository.findAll();
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/service/S3Service.java b/src/main/java/com/magamochi/image/service/S3Service.java
similarity index 88%
rename from src/main/java/com/magamochi/mangamochi/service/S3Service.java
rename to src/main/java/com/magamochi/image/service/S3Service.java
index 1d15bd7..9cebd2f 100644
--- a/src/main/java/com/magamochi/mangamochi/service/S3Service.java
+++ b/src/main/java/com/magamochi/image/service/S3Service.java
@@ -1,12 +1,11 @@
-package com.magamochi.mangamochi.service;
+package com.magamochi.image.service;
import static java.util.Objects.nonNull;
-import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
-import java.util.UUID;
+import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@@ -21,11 +20,13 @@ public class S3Service {
@Value("${minio.bucket}")
private String bucket;
+ @Value("${storage.base-url}")
+ @Getter
+ private String baseUrl;
+
private final S3Client s3Client;
- public String uploadFile(byte[] data, String contentType, String path) {
- var filename = "manga/" + path + "/" + UUID.randomUUID();
-
+ public String uploadFile(byte[] data, String contentType, String filename) {
var request =
PutObjectRequest.builder().bucket(bucket).key(filename).contentType(contentType).build();
@@ -34,10 +35,26 @@ public class S3Service {
return filename;
}
- public InputStream getFile(String key) {
- var request = GetObjectRequest.builder().bucket(bucket).key(key).build();
+ public List listAllObjectKeys() {
+ var keys = new ArrayList();
+ String continuationToken = null;
- return s3Client.getObject(request);
+ do {
+ var requestBuilder = ListObjectsV2Request.builder().bucket(bucket).maxKeys(1000);
+
+ if (nonNull(continuationToken)) {
+ requestBuilder.continuationToken(continuationToken);
+ }
+
+ var response = s3Client.listObjectsV2(requestBuilder.build());
+
+ response.contents().forEach(s3Object -> keys.add(s3Object.key()));
+
+ continuationToken = response.isTruncated() ? response.nextContinuationToken() : null;
+
+ } while (nonNull(continuationToken));
+
+ return keys;
}
public void deleteObjects(Set objectKeys) {
@@ -50,7 +67,7 @@ public class S3Service {
objectKeys.stream().map(key -> ObjectIdentifier.builder().key(key).build()).toList();
for (int i = 0; i < allObjects.size(); i += BATCH_SIZE) {
- int end = Math.min(i + BATCH_SIZE, allObjects.size());
+ var end = Math.min(i + BATCH_SIZE, allObjects.size());
List batch = allObjects.subList(i, end);
DeleteObjectsRequest deleteRequest =
@@ -77,7 +94,6 @@ public class S3Service {
+ (i / BATCH_SIZE + 1)
+ ")");
}
-
} catch (S3Exception e) {
System.err.println(
"Failed to delete batch starting at index "
@@ -87,26 +103,4 @@ public class S3Service {
}
}
}
-
- public List listAllObjectKeys() {
- var keys = new ArrayList();
- String continuationToken = null;
-
- do {
- var requestBuilder = ListObjectsV2Request.builder().bucket(bucket).maxKeys(1000);
-
- if (nonNull(continuationToken)) {
- requestBuilder.continuationToken(continuationToken);
- }
-
- var response = s3Client.listObjectsV2(requestBuilder.build());
-
- response.contents().forEach(s3Object -> keys.add(s3Object.key()));
-
- continuationToken = response.isTruncated() ? response.nextContinuationToken() : null;
-
- } while (nonNull(continuationToken));
-
- return keys;
- }
}
diff --git a/src/main/java/com/magamochi/mangamochi/task/ImageCleanupTask.java b/src/main/java/com/magamochi/image/task/ImageCleanupTask.java
similarity index 78%
rename from src/main/java/com/magamochi/mangamochi/task/ImageCleanupTask.java
rename to src/main/java/com/magamochi/image/task/ImageCleanupTask.java
index f9756be..6360ea4 100644
--- a/src/main/java/com/magamochi/mangamochi/task/ImageCleanupTask.java
+++ b/src/main/java/com/magamochi/image/task/ImageCleanupTask.java
@@ -1,8 +1,8 @@
-package com.magamochi.mangamochi.task;
+package com.magamochi.image.task;
-import com.magamochi.mangamochi.model.entity.Image;
-import com.magamochi.mangamochi.model.repository.*;
-import com.magamochi.mangamochi.service.S3Service;
+import com.magamochi.image.model.entity.Image;
+import com.magamochi.image.service.ImageService;
+import com.magamochi.image.service.S3Service;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@@ -18,7 +18,7 @@ public class ImageCleanupTask {
private Boolean cleanUpEnabled;
private final S3Service s3Service;
- private final ImageRepository imageRepository;
+ private final ImageService imageService;
@Scheduled(cron = "${image-service.cron-expression}")
public void cleanUpImagesScheduled() {
@@ -36,8 +36,8 @@ public class ImageCleanupTask {
var imageKeys = s3Service.listAllObjectKeys();
var existingImages =
- imageRepository.findAll().parallelStream()
- .map(Image::getFileKey)
+ imageService.findAll().parallelStream()
+ .map(Image::getObjectKey)
.collect(Collectors.toSet());
var keysToRemove =
diff --git a/src/main/java/com/magamochi/mangamochi/client/FlareClient.java b/src/main/java/com/magamochi/ingestion/client/FlareClient.java
similarity index 94%
rename from src/main/java/com/magamochi/mangamochi/client/FlareClient.java
rename to src/main/java/com/magamochi/ingestion/client/FlareClient.java
index df4dc3a..5756b26 100644
--- a/src/main/java/com/magamochi/mangamochi/client/FlareClient.java
+++ b/src/main/java/com/magamochi/ingestion/client/FlareClient.java
@@ -1,6 +1,5 @@
-package com.magamochi.mangamochi.client;
+package com.magamochi.ingestion.client;
-import io.github.resilience4j.retry.annotation.Retry;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
@@ -10,7 +9,6 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(name = "flare-solverr", url = "${flare-solverr.endpoint}/v1")
-@Retry(name = "FlareSolverrRetry")
public interface FlareClient {
@PostMapping(
consumes = MediaType.APPLICATION_JSON_VALUE,
diff --git a/src/main/java/com/magamochi/ingestion/controller/IngestionController.java b/src/main/java/com/magamochi/ingestion/controller/IngestionController.java
new file mode 100644
index 0000000..f8304bf
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/controller/IngestionController.java
@@ -0,0 +1,80 @@
+package com.magamochi.ingestion.controller;
+
+import com.magamochi.common.model.dto.DefaultResponseDTO;
+import com.magamochi.ingestion.model.dto.ContentProviderListDTO;
+import com.magamochi.ingestion.service.ContentProviderService;
+import com.magamochi.ingestion.service.IngestionService;
+import io.swagger.v3.oas.annotations.Operation;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/ingestion")
+@RequiredArgsConstructor
+public class IngestionController {
+ private final ContentProviderService contentProviderService;
+ private final IngestionService ingestionService;
+
+ @Operation(
+ summary = "Get a list of content providers",
+ description = "Retrieve a list of content providers",
+ tags = {"Ingestion"},
+ operationId = "getContentProviders")
+ @GetMapping("/providers")
+ public DefaultResponseDTO getContentProviders(
+ @RequestParam(name = "manualImport", required = false) Boolean manualImport) {
+ return DefaultResponseDTO.ok(contentProviderService.getProviders(manualImport));
+ }
+
+ @Operation(
+ summary = "Fetch mangas from a content provider",
+ description =
+ "Triggers the ingestion process for a specific content provider, fetching manga data and queuing it for processing.",
+ tags = {"Ingestion"},
+ operationId = "fetchContentProviderMangas")
+ @PostMapping("/providers/{providerId}/fetch")
+ public DefaultResponseDTO fetchContentProviderMangas(@PathVariable Long providerId) {
+ ingestionService.fetchContentProviderMangas(providerId);
+
+ return DefaultResponseDTO.ok().build();
+ }
+
+ @Operation(
+ summary = "Fetch mangas from all content providers",
+ description =
+ "Triggers the ingestion process for all content providers, fetching manga data and queuing them for processing.",
+ tags = {"Ingestion"},
+ operationId = "fetchAllContentProviderMangas")
+ @PostMapping("/providers/fetch")
+ public DefaultResponseDTO fetchAllContentProviderMangas() {
+ ingestionService.fetchAllContentProviderMangas();
+
+ return DefaultResponseDTO.ok().build();
+ }
+
+ @Operation(
+ summary = "Fetch content list from a content provider",
+ description =
+ "Triggers the ingestion process for a specific content provider, fetching content list and queuing it for processing.",
+ tags = {"Ingestion"},
+ operationId = "fetchContentProviderContentList")
+ @PostMapping("/manga-content-providers/{mangaContentProviderId}/fetch")
+ public DefaultResponseDTO fetchContentProviderContentList(
+ @PathVariable Long mangaContentProviderId) {
+ ingestionService.fetchMangaContentProviderContentList(mangaContentProviderId);
+
+ return DefaultResponseDTO.ok().build();
+ }
+
+ @Operation(
+ summary = "Fetch content from a content provider",
+ description = "Fetch the content (images) from the content provider",
+ tags = {"Ingestion"},
+ operationId = "fetchContentProviderContent")
+ @PostMapping(value = "/manga-content/{mangaContentId}/fetch")
+ public DefaultResponseDTO fetchContentProviderContent(@PathVariable Long mangaContentId) {
+ ingestionService.fetchContent(mangaContentId);
+
+ return DefaultResponseDTO.ok().build();
+ }
+}
diff --git a/src/main/java/com/magamochi/ingestion/model/dto/ContentImageInfoDTO.java b/src/main/java/com/magamochi/ingestion/model/dto/ContentImageInfoDTO.java
new file mode 100644
index 0000000..33ea729
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/model/dto/ContentImageInfoDTO.java
@@ -0,0 +1,5 @@
+package com.magamochi.ingestion.model.dto;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record ContentImageInfoDTO(int position, @NotBlank String url) {}
diff --git a/src/main/java/com/magamochi/ingestion/model/dto/ContentInfoDTO.java b/src/main/java/com/magamochi/ingestion/model/dto/ContentInfoDTO.java
new file mode 100644
index 0000000..d967cbf
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/model/dto/ContentInfoDTO.java
@@ -0,0 +1,6 @@
+package com.magamochi.ingestion.model.dto;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record ContentInfoDTO(
+ @NotBlank String title, @NotBlank String url, @NotBlank String languageCode) {}
diff --git a/src/main/java/com/magamochi/ingestion/model/dto/ContentProviderListDTO.java b/src/main/java/com/magamochi/ingestion/model/dto/ContentProviderListDTO.java
new file mode 100644
index 0000000..b78d549
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/model/dto/ContentProviderListDTO.java
@@ -0,0 +1,19 @@
+package com.magamochi.ingestion.model.dto;
+
+import com.magamochi.ingestion.model.entity.ContentProvider;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import java.util.List;
+
+public record ContentProviderListDTO(@NotNull List providers) {
+ public static ContentProviderListDTO from(List contentProviders) {
+ return new ContentProviderListDTO(
+ contentProviders.stream().map(ContentProviderDTO::from).toList());
+ }
+
+ public record ContentProviderDTO(long id, @NotBlank String name) {
+ public static ContentProviderDTO from(ContentProvider contentProvider) {
+ return new ContentProviderDTO(contentProvider.getId(), contentProvider.getName());
+ }
+ }
+}
diff --git a/src/main/java/com/magamochi/ingestion/model/dto/MangaInfoDTO.java b/src/main/java/com/magamochi/ingestion/model/dto/MangaInfoDTO.java
new file mode 100644
index 0000000..609e160
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/model/dto/MangaInfoDTO.java
@@ -0,0 +1,5 @@
+package com.magamochi.ingestion.model.dto;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record MangaInfoDTO(@NotBlank String title, @NotBlank String url) {}
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/Provider.java b/src/main/java/com/magamochi/ingestion/model/entity/ContentProvider.java
similarity index 52%
rename from src/main/java/com/magamochi/mangamochi/model/entity/Provider.java
rename to src/main/java/com/magamochi/ingestion/model/entity/ContentProvider.java
index bbea2ac..26ab447 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/Provider.java
+++ b/src/main/java/com/magamochi/ingestion/model/entity/ContentProvider.java
@@ -1,6 +1,6 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.ingestion.model.entity;
-import com.magamochi.mangamochi.model.enumeration.ProviderStatus;
+import com.magamochi.catalog.model.entity.MangaContentProvider;
import jakarta.persistence.*;
import java.time.Instant;
import java.util.List;
@@ -9,30 +9,29 @@ import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
@Entity
-@Table(name = "providers")
+@Table(name = "content_providers")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
-public class Provider {
+public class ContentProvider {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
- @Enumerated(EnumType.STRING)
- private ProviderStatus status;
+ private boolean active;
+
+ private Boolean supportsContentFetch;
+
+ private Boolean manualImport;
@CreationTimestamp private Instant createdAt;
@UpdateTimestamp private Instant updatedAt;
- @OneToMany(mappedBy = "provider")
- private List mangaProviders;
-
- @Builder.Default private Boolean supportsChapterFetch = true;
-
- @Builder.Default private Boolean manualImport = false;
+ @OneToMany(mappedBy = "contentProvider")
+ private List mangaContentProviders;
}
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/FlareSession.java b/src/main/java/com/magamochi/ingestion/model/entity/FlareSession.java
similarity index 83%
rename from src/main/java/com/magamochi/mangamochi/model/entity/FlareSession.java
rename to src/main/java/com/magamochi/ingestion/model/entity/FlareSession.java
index af02dba..e9730c4 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/FlareSession.java
+++ b/src/main/java/com/magamochi/ingestion/model/entity/FlareSession.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.ingestion.model.entity;
import java.time.Instant;
import lombok.Builder;
diff --git a/src/main/java/com/magamochi/ingestion/model/repository/ContentProviderRepository.java b/src/main/java/com/magamochi/ingestion/model/repository/ContentProviderRepository.java
new file mode 100644
index 0000000..8f1e612
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/model/repository/ContentProviderRepository.java
@@ -0,0 +1,6 @@
+package com.magamochi.ingestion.model.repository;
+
+import com.magamochi.ingestion.model.entity.ContentProvider;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ContentProviderRepository extends JpaRepository {}
diff --git a/src/main/java/com/magamochi/ingestion/providers/ContentProvider.java b/src/main/java/com/magamochi/ingestion/providers/ContentProvider.java
new file mode 100644
index 0000000..aec1b8d
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/providers/ContentProvider.java
@@ -0,0 +1,12 @@
+package com.magamochi.ingestion.providers;
+
+import com.magamochi.catalog.model.entity.MangaContentProvider;
+import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
+import com.magamochi.ingestion.model.dto.ContentInfoDTO;
+import java.util.List;
+
+public interface ContentProvider {
+ List getAvailableChapters(MangaContentProvider provider);
+
+ List getContentImages(String chapterUrl);
+}
diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/ContentProviderFactory.java b/src/main/java/com/magamochi/ingestion/providers/ContentProviderFactory.java
similarity index 91%
rename from src/main/java/com/magamochi/mangamochi/service/providers/ContentProviderFactory.java
rename to src/main/java/com/magamochi/ingestion/providers/ContentProviderFactory.java
index c3b6ef9..71fd653 100644
--- a/src/main/java/com/magamochi/mangamochi/service/providers/ContentProviderFactory.java
+++ b/src/main/java/com/magamochi/ingestion/providers/ContentProviderFactory.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.service.providers;
+package com.magamochi.ingestion.providers;
import java.util.Map;
import java.util.Objects;
diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/ContentProviders.java b/src/main/java/com/magamochi/ingestion/providers/ContentProviders.java
similarity index 74%
rename from src/main/java/com/magamochi/mangamochi/service/providers/ContentProviders.java
rename to src/main/java/com/magamochi/ingestion/providers/ContentProviders.java
index c7f347a..ad860e6 100644
--- a/src/main/java/com/magamochi/mangamochi/service/providers/ContentProviders.java
+++ b/src/main/java/com/magamochi/ingestion/providers/ContentProviders.java
@@ -1,9 +1,8 @@
-package com.magamochi.mangamochi.service.providers;
+package com.magamochi.ingestion.providers;
public class ContentProviders {
- public static final String MANGA_LIVRE_TO = "Manga Livre.to";
public static final String MANGA_LIVRE_BLOG = "Manga Livre Blog";
- public static final String MANGA_DEX = "MangaDex";
+ public static final String MANGA_LIVRE_TO = "Manga Livre.to";
public static final String PINK_ROSA_SCAN = "Pink Rosa Scan";
- public static final String BATO = "Bato";
+ public static final String MANGA_DEX = "MangaDex";
}
diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/ManualImportContentProvider.java b/src/main/java/com/magamochi/ingestion/providers/ManualImportContentProvider.java
similarity index 62%
rename from src/main/java/com/magamochi/mangamochi/service/providers/ManualImportContentProvider.java
rename to src/main/java/com/magamochi/ingestion/providers/ManualImportContentProvider.java
index a96c2c7..1a36387 100644
--- a/src/main/java/com/magamochi/mangamochi/service/providers/ManualImportContentProvider.java
+++ b/src/main/java/com/magamochi/ingestion/providers/ManualImportContentProvider.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.service.providers;
+package com.magamochi.ingestion.providers;
public interface ManualImportContentProvider {
String getMangaTitle(String value);
diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/ManualImportContentProviderFactory.java b/src/main/java/com/magamochi/ingestion/providers/ManualImportContentProviderFactory.java
similarity index 92%
rename from src/main/java/com/magamochi/mangamochi/service/providers/ManualImportContentProviderFactory.java
rename to src/main/java/com/magamochi/ingestion/providers/ManualImportContentProviderFactory.java
index e0d4654..009c19a 100644
--- a/src/main/java/com/magamochi/mangamochi/service/providers/ManualImportContentProviderFactory.java
+++ b/src/main/java/com/magamochi/ingestion/providers/ManualImportContentProviderFactory.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.service.providers;
+package com.magamochi.ingestion.providers;
import java.util.Map;
import java.util.Objects;
diff --git a/src/main/java/com/magamochi/ingestion/providers/PagedContentProvider.java b/src/main/java/com/magamochi/ingestion/providers/PagedContentProvider.java
new file mode 100644
index 0000000..593724c
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/providers/PagedContentProvider.java
@@ -0,0 +1,10 @@
+package com.magamochi.ingestion.providers;
+
+import com.magamochi.ingestion.model.dto.MangaInfoDTO;
+import java.util.List;
+
+public interface PagedContentProvider {
+ int getTotalPages();
+
+ List getMangasFromPage(int page);
+}
diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/PagedContentProviderFactory.java b/src/main/java/com/magamochi/ingestion/providers/PagedContentProviderFactory.java
similarity index 91%
rename from src/main/java/com/magamochi/mangamochi/service/providers/PagedContentProviderFactory.java
rename to src/main/java/com/magamochi/ingestion/providers/PagedContentProviderFactory.java
index aa0dfba..bdddc6f 100644
--- a/src/main/java/com/magamochi/mangamochi/service/providers/PagedContentProviderFactory.java
+++ b/src/main/java/com/magamochi/ingestion/providers/PagedContentProviderFactory.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.service.providers;
+package com.magamochi.ingestion.providers;
import java.util.Map;
import java.util.Objects;
diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaDexProvider.java b/src/main/java/com/magamochi/ingestion/providers/impl/MangaDexProvider.java
similarity index 78%
rename from src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaDexProvider.java
rename to src/main/java/com/magamochi/ingestion/providers/impl/MangaDexProvider.java
index 2980be5..bdc7765 100644
--- a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaDexProvider.java
+++ b/src/main/java/com/magamochi/ingestion/providers/impl/MangaDexProvider.java
@@ -1,17 +1,17 @@
-package com.magamochi.mangamochi.service.providers.impl;
+package com.magamochi.ingestion.providers.impl;
import static java.util.Objects.isNull;
import com.google.common.util.concurrent.RateLimiter;
-import com.magamochi.mangamochi.client.MangaDexClient;
-import com.magamochi.mangamochi.exception.UnprocessableException;
-import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO;
-import com.magamochi.mangamochi.model.entity.MangaProvider;
-import com.magamochi.mangamochi.service.providers.ContentProvider;
-import com.magamochi.mangamochi.service.providers.ContentProviders;
-import com.magamochi.mangamochi.service.providers.ManualImportContentProvider;
+import com.magamochi.catalog.model.entity.MangaContentProvider;
+import com.magamochi.client.MangaDexClient;
+import com.magamochi.common.exception.UnprocessableException;
+import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
+import com.magamochi.ingestion.model.dto.ContentInfoDTO;
+import com.magamochi.ingestion.providers.ContentProvider;
+import com.magamochi.ingestion.providers.ContentProviders;
+import com.magamochi.ingestion.providers.ManualImportContentProvider;
import java.util.*;
-import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@@ -26,7 +26,7 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro
private final RateLimiter mangaDexRateLimiter;
@Override
- public List getAvailableChapters(MangaProvider provider) {
+ public List getAvailableChapters(MangaContentProvider provider) {
try {
mangaDexRateLimiter.acquire();
var response =
@@ -84,10 +84,9 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro
})
.map(
c ->
- new ContentProviderMangaChapterResponseDTO(
+ new ContentInfoDTO(
c.attributes().chapter() + " - " + c.attributes().title(),
c.id().toString(),
- c.attributes().chapter(),
languagesToImport.get(c.attributes().translatedLanguage())))
.toList();
} catch (Exception e) {
@@ -97,7 +96,7 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro
}
@Override
- public Map getChapterImagesUrls(String chapterUrl) {
+ public List getContentImages(String chapterUrl) {
mangaDexRateLimiter.acquire();
var chapter = mangaDexClient.getMangaChapter(UUID.fromString(chapterUrl));
@@ -108,12 +107,8 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro
return IntStream.range(0, chapterImageHashes.size())
.boxed()
- .collect(
- Collectors.toMap(
- i -> i,
- chapterImageHashes::get,
- (existing, replacement) -> existing,
- LinkedHashMap::new));
+ .map(position -> new ContentImageInfoDTO(position, chapterImageHashes.get(position)))
+ .toList();
}
@Override
diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreBlogProvider.java b/src/main/java/com/magamochi/ingestion/providers/impl/MangaLivreBlogProvider.java
similarity index 69%
rename from src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreBlogProvider.java
rename to src/main/java/com/magamochi/ingestion/providers/impl/MangaLivreBlogProvider.java
index 648edb4..3c566de 100644
--- a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreBlogProvider.java
+++ b/src/main/java/com/magamochi/ingestion/providers/impl/MangaLivreBlogProvider.java
@@ -1,16 +1,15 @@
-package com.magamochi.mangamochi.service.providers.impl;
+package com.magamochi.ingestion.providers.impl;
-import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO;
-import com.magamochi.mangamochi.model.dto.ContentProviderMangaInfoResponseDTO;
-import com.magamochi.mangamochi.model.entity.MangaProvider;
-import com.magamochi.mangamochi.model.enumeration.MangaStatus;
-import com.magamochi.mangamochi.service.providers.ContentProvider;
-import com.magamochi.mangamochi.service.providers.ContentProviders;
-import com.magamochi.mangamochi.service.providers.PagedContentProvider;
+import com.magamochi.catalog.model.entity.MangaContentProvider;
+import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
+import com.magamochi.ingestion.model.dto.ContentInfoDTO;
+import com.magamochi.ingestion.model.dto.MangaInfoDTO;
+import com.magamochi.ingestion.providers.ContentProvider;
+import com.magamochi.ingestion.providers.ContentProviders;
+import com.magamochi.ingestion.providers.PagedContentProvider;
import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;
-import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@@ -28,15 +27,14 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
private final String url = "https://mangalivre.blog/manga/";
@Override
- public List getAvailableChapters(
- MangaProvider mangaProvider) {
+ public List getAvailableChapters(MangaContentProvider mangaContentProvider) {
log.info(
"Getting available chapters from {}, manga {}",
ContentProviders.MANGA_LIVRE_BLOG,
- mangaProvider.getManga().getTitle());
+ mangaContentProvider.getManga().getTitle());
try {
- var document = Jsoup.connect(mangaProvider.getUrl()).get();
+ var document = Jsoup.connect(mangaContentProvider.getUrl()).get();
var chapterList = document.getElementsByClass("chapters-list").getFirst();
var chapterItems = chapterList.getElementsByClass("chapter-item");
@@ -50,8 +48,8 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
var chapterNumberElement =
linkElement.getElementsByClass("chapter-number").getFirst();
- return new ContentProviderMangaChapterResponseDTO(
- chapterNumberElement.text(), linkElement.attr("href"), null, "pt-BR");
+ return new ContentInfoDTO(
+ chapterNumberElement.text(), linkElement.attr("href"), "pt-BR");
})
.toList();
} catch (IOException | NoSuchElementException e) {
@@ -61,7 +59,7 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
}
@Override
- public Map getChapterImagesUrls(String chapterUrl) {
+ public List getContentImages(String chapterUrl) {
log.info("Getting images from {}, url {}", ContentProviders.MANGA_LIVRE_BLOG, chapterUrl);
try {
@@ -91,17 +89,16 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
return IntStream.range(0, imageUrls.size())
.boxed()
- .collect(
- Collectors.toMap(
- i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new));
+ .map(position -> new ContentImageInfoDTO(position, imageUrls.get(position)))
+ .toList();
} catch (IOException | NoSuchElementException e) {
log.error("Error fetching mangas from MangaLivre", e);
- return Map.of();
+ return List.of();
}
}
@Override
- public List getMangasFromPage(Integer page) {
+ public List getMangasFromPage(int page) {
log.info("Getting mangas from {}, page {}", ContentProviders.MANGA_LIVRE_BLOG, page);
try {
@@ -116,25 +113,13 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
try {
var linkElement = element.getElementsByTag("a").getFirst();
- var imageContainer =
- linkElement.getElementsByClass("manga-card-image").getFirst();
var contentContainer =
linkElement.getElementsByClass("manga-card-content").getFirst();
var title = contentContainer.getElementsByTag("h3").text();
var url = linkElement.attr("href");
- var status =
- switch (imageContainer
- .getElementsByClass("manga-status")
- .text()
- .toLowerCase()) {
- case "em andamento" -> MangaStatus.ONGOING;
- case "completo" -> MangaStatus.COMPLETED;
- case "hiato" -> MangaStatus.HIATUS;
- default -> MangaStatus.UNKNOWN;
- };
- return new ContentProviderMangaInfoResponseDTO(title, url, status);
+ return new MangaInfoDTO(title, url);
} catch (Exception e) {
return null;
}
@@ -148,7 +133,7 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
}
@Override
- public Integer getTotalPages() {
+ public int getTotalPages() {
log.info("Getting total pages for {}", ContentProviders.MANGA_LIVRE_BLOG);
try {
@@ -166,7 +151,7 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
return pageNumbers.stream().max(Integer::compareTo).orElse(null);
} catch (IOException | NoSuchElementException e) {
log.error("Error fetching total pages from MangaLivre", e);
- return null;
+ return 0;
}
}
}
diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreProvider.java b/src/main/java/com/magamochi/ingestion/providers/impl/MangaLivreProvider.java
similarity index 73%
rename from src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreProvider.java
rename to src/main/java/com/magamochi/ingestion/providers/impl/MangaLivreProvider.java
index d133ac5..3ec9263 100644
--- a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreProvider.java
+++ b/src/main/java/com/magamochi/ingestion/providers/impl/MangaLivreProvider.java
@@ -1,17 +1,16 @@
-package com.magamochi.mangamochi.service.providers.impl;
+package com.magamochi.ingestion.providers.impl;
import static java.util.Objects.nonNull;
-import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO;
-import com.magamochi.mangamochi.model.dto.ContentProviderMangaInfoResponseDTO;
-import com.magamochi.mangamochi.model.entity.MangaProvider;
-import com.magamochi.mangamochi.model.enumeration.MangaStatus;
-import com.magamochi.mangamochi.service.FlareService;
-import com.magamochi.mangamochi.service.providers.ContentProvider;
-import com.magamochi.mangamochi.service.providers.ContentProviders;
-import com.magamochi.mangamochi.service.providers.PagedContentProvider;
+import com.magamochi.catalog.model.entity.MangaContentProvider;
+import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
+import com.magamochi.ingestion.model.dto.ContentInfoDTO;
+import com.magamochi.ingestion.model.dto.MangaInfoDTO;
+import com.magamochi.ingestion.providers.ContentProvider;
+import com.magamochi.ingestion.providers.ContentProviders;
+import com.magamochi.ingestion.providers.PagedContentProvider;
+import com.magamochi.ingestion.service.FlareService;
import java.util.*;
-import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@@ -27,7 +26,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
private final FlareService flareService;
@Override
- public List getAvailableChapters(MangaProvider provider) {
+ public List getAvailableChapters(MangaContentProvider provider) {
log.info(
"Getting available chapters from {}, manga {}",
ContentProviders.MANGA_LIVRE_TO,
@@ -49,8 +48,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
var url = linkElement.attr("href");
var title = linkElement.text();
- return new ContentProviderMangaChapterResponseDTO(
- title.trim(), url.trim(), null, "pt-BR");
+ return new ContentInfoDTO(title.trim(), url.trim(), "pt-BR");
})
.toList();
} catch (NoSuchElementException e) {
@@ -60,7 +58,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
}
@Override
- public Map getChapterImagesUrls(String chapterUrl) {
+ public List getContentImages(String chapterUrl) {
log.info("Getting images from {}, url {}", ContentProviders.MANGA_LIVRE_TO, chapterUrl);
try {
@@ -79,17 +77,16 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
return IntStream.range(0, imageUrls.size())
.boxed()
- .collect(
- Collectors.toMap(
- i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new));
+ .map(position -> new ContentImageInfoDTO(position, imageUrls.get(position)))
+ .toList();
} catch (NoSuchElementException e) {
log.error("Error parsing manga images from MangaLivre", e);
- return Map.of();
+ return List.of();
}
}
@Override
- public List getMangasFromPage(Integer page) {
+ public List getMangasFromPage(int page) {
log.info("Getting mangas from {}, page {}", ContentProviders.MANGA_LIVRE_TO, page);
try {
@@ -115,8 +112,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
}
}
- return new ContentProviderMangaInfoResponseDTO(
- title.trim(), url.trim(), MangaStatus.UNKNOWN);
+ return new MangaInfoDTO(title.trim(), url.trim());
})
.toList();
} catch (NoSuchElementException e) {
@@ -126,7 +122,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
}
@Override
- public Integer getTotalPages() {
+ public int getTotalPages() {
log.info("Getting total pages for {}", ContentProviders.MANGA_LIVRE_TO);
try {
@@ -139,7 +135,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
return (int) Math.ceil((double) totalMangas / MANGAS_PER_PAGE);
} catch (NoSuchElementException e) {
log.error("Error parsing total pages from MangaLivre", e);
- return null;
+ return 0;
}
}
}
diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/impl/PinkRosaScanProvider.java b/src/main/java/com/magamochi/ingestion/providers/impl/PinkRosaScanProvider.java
similarity index 74%
rename from src/main/java/com/magamochi/mangamochi/service/providers/impl/PinkRosaScanProvider.java
rename to src/main/java/com/magamochi/ingestion/providers/impl/PinkRosaScanProvider.java
index 6e30086..2e4ef56 100644
--- a/src/main/java/com/magamochi/mangamochi/service/providers/impl/PinkRosaScanProvider.java
+++ b/src/main/java/com/magamochi/ingestion/providers/impl/PinkRosaScanProvider.java
@@ -1,18 +1,17 @@
-package com.magamochi.mangamochi.service.providers.impl;
+package com.magamochi.ingestion.providers.impl;
import static java.util.Objects.isNull;
import static org.apache.commons.lang3.StringUtils.isBlank;
-import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO;
-import com.magamochi.mangamochi.model.dto.ContentProviderMangaInfoResponseDTO;
-import com.magamochi.mangamochi.model.entity.MangaProvider;
-import com.magamochi.mangamochi.model.enumeration.MangaStatus;
-import com.magamochi.mangamochi.service.FlareService;
-import com.magamochi.mangamochi.service.providers.ContentProvider;
-import com.magamochi.mangamochi.service.providers.ContentProviders;
-import com.magamochi.mangamochi.service.providers.PagedContentProvider;
+import com.magamochi.catalog.model.entity.MangaContentProvider;
+import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
+import com.magamochi.ingestion.model.dto.ContentInfoDTO;
+import com.magamochi.ingestion.model.dto.MangaInfoDTO;
+import com.magamochi.ingestion.providers.ContentProvider;
+import com.magamochi.ingestion.providers.ContentProviders;
+import com.magamochi.ingestion.providers.PagedContentProvider;
+import com.magamochi.ingestion.service.FlareService;
import java.util.*;
-import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@@ -28,7 +27,7 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
private final FlareService flareService;
@Override
- public List getAvailableChapters(MangaProvider provider) {
+ public List getAvailableChapters(MangaContentProvider provider) {
log.info(
"Getting available chapters from {}, manga {}",
ContentProviders.PINK_ROSA_SCAN,
@@ -57,11 +56,8 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
.getFirst()
.getElementsByClass("text-sm truncate")
.getFirst();
- return new ContentProviderMangaChapterResponseDTO(
- chapterTitleElement.text().trim(),
- chapterItemElement.attr("href"),
- null,
- "pt-BR");
+ return new ContentInfoDTO(
+ chapterTitleElement.text().trim(), chapterItemElement.attr("href"), "pt-BR");
})
.toList();
} catch (NoSuchElementException e) {
@@ -71,7 +67,7 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
}
@Override
- public Map getChapterImagesUrls(String chapterUrl) {
+ public List getContentImages(String chapterUrl) {
log.info("Getting images from {}, url {}", ContentProviders.PINK_ROSA_SCAN, chapterUrl);
try {
@@ -99,22 +95,21 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
return IntStream.range(0, imageUrls.size())
.boxed()
- .collect(
- Collectors.toMap(
- i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new));
+ .map(position -> new ContentImageInfoDTO(position, imageUrls.get(position)))
+ .toList();
} catch (NoSuchElementException e) {
log.error("Error parsing mangas from Pink Rosa Scan", e);
- return Map.of();
+ return List.of();
}
}
@Override
- public Integer getTotalPages() {
+ public int getTotalPages() {
return 1;
}
@Override
- public List getMangasFromPage(Integer page) {
+ public List getMangasFromPage(int page) {
log.info("Getting mangas from {}", ContentProviders.PINK_ROSA_SCAN);
try {
@@ -141,7 +136,7 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
var textElement = linkElement.getElementsByTag("h3");
var title = textElement.text().trim();
- return new ContentProviderMangaInfoResponseDTO(title, url, MangaStatus.UNKNOWN);
+ return new MangaInfoDTO(title, url);
})
.toList();
} catch (NoSuchElementException e) {
diff --git a/src/main/java/com/magamochi/ingestion/queue/command/ProviderPageIngestCommand.java b/src/main/java/com/magamochi/ingestion/queue/command/ProviderPageIngestCommand.java
new file mode 100644
index 0000000..8f6a395
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/queue/command/ProviderPageIngestCommand.java
@@ -0,0 +1,3 @@
+package com.magamochi.ingestion.queue.command;
+
+public record ProviderPageIngestCommand(long providerId, int page) {}
diff --git a/src/main/java/com/magamochi/ingestion/queue/consumer/ProviderPageIngestConsumer.java b/src/main/java/com/magamochi/ingestion/queue/consumer/ProviderPageIngestConsumer.java
new file mode 100644
index 0000000..1fef3cb
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/queue/consumer/ProviderPageIngestConsumer.java
@@ -0,0 +1,21 @@
+package com.magamochi.ingestion.queue.consumer;
+
+import com.magamochi.ingestion.queue.command.ProviderPageIngestCommand;
+import com.magamochi.ingestion.service.IngestionService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class ProviderPageIngestConsumer {
+ private final IngestionService ingestionService;
+
+ @RabbitListener(queues = "${queues.provider-page-ingest}")
+ public void receiveProviderPageIngestCommand(ProviderPageIngestCommand command) {
+ log.info("Received provider page ingest command: {}", command);
+ ingestionService.fetchProviderPageMangas(command.providerId(), command.page());
+ }
+}
diff --git a/src/main/java/com/magamochi/ingestion/queue/producer/MangaContentImageIngestProducer.java b/src/main/java/com/magamochi/ingestion/queue/producer/MangaContentImageIngestProducer.java
new file mode 100644
index 0000000..38d16df
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/queue/producer/MangaContentImageIngestProducer.java
@@ -0,0 +1,23 @@
+package com.magamochi.ingestion.queue.producer;
+
+import com.magamochi.common.queue.command.MangaContentImageIngestCommand;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class MangaContentImageIngestProducer {
+ private final RabbitTemplate rabbitTemplate;
+
+ @Value("${queues.manga-content-image-ingest}")
+ private String mangaContentImageIngestQueue;
+
+ public void sendMangaContentImageIngestCommand(MangaContentImageIngestCommand command) {
+ rabbitTemplate.convertAndSend(mangaContentImageIngestQueue, command);
+ log.info("Sent manga content image ingest command: {}", command);
+ }
+}
diff --git a/src/main/java/com/magamochi/ingestion/queue/producer/MangaContentIngestProducer.java b/src/main/java/com/magamochi/ingestion/queue/producer/MangaContentIngestProducer.java
new file mode 100644
index 0000000..68702c8
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/queue/producer/MangaContentIngestProducer.java
@@ -0,0 +1,23 @@
+package com.magamochi.ingestion.queue.producer;
+
+import com.magamochi.common.queue.command.MangaContentIngestCommand;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class MangaContentIngestProducer {
+ private final RabbitTemplate rabbitTemplate;
+
+ @Value("${queues.manga-content-ingest}")
+ private String mangaContentIngestQueue;
+
+ public void sendMangaContentIngestCommand(MangaContentIngestCommand command) {
+ rabbitTemplate.convertAndSend(mangaContentIngestQueue, command);
+ log.info("Sent manga content ingest command: {}", command);
+ }
+}
diff --git a/src/main/java/com/magamochi/ingestion/queue/producer/MangaIngestProducer.java b/src/main/java/com/magamochi/ingestion/queue/producer/MangaIngestProducer.java
new file mode 100644
index 0000000..b67f334
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/queue/producer/MangaIngestProducer.java
@@ -0,0 +1,23 @@
+package com.magamochi.ingestion.queue.producer;
+
+import com.magamochi.common.queue.command.MangaIngestCommand;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class MangaIngestProducer {
+ private final RabbitTemplate rabbitTemplate;
+
+ @Value("${queues.manga-ingest}")
+ private String mangaIngestQueue;
+
+ public void sendMangaIngestCommand(MangaIngestCommand command) {
+ rabbitTemplate.convertAndSend(mangaIngestQueue, command);
+ log.info("Sent manga ingest command: {}", command);
+ }
+}
diff --git a/src/main/java/com/magamochi/ingestion/queue/producer/ProviderPageIngestProducer.java b/src/main/java/com/magamochi/ingestion/queue/producer/ProviderPageIngestProducer.java
new file mode 100644
index 0000000..2bacfca
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/queue/producer/ProviderPageIngestProducer.java
@@ -0,0 +1,23 @@
+package com.magamochi.ingestion.queue.producer;
+
+import com.magamochi.ingestion.queue.command.ProviderPageIngestCommand;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class ProviderPageIngestProducer {
+ private final RabbitTemplate rabbitTemplate;
+
+ @Value("${queues.provider-page-ingest}")
+ private String providerPageIngestQueue;
+
+ public void sendProviderPageIngestCommand(ProviderPageIngestCommand command) {
+ rabbitTemplate.convertAndSend(providerPageIngestQueue, command);
+ log.info("Sent provider page ingest command: {}", command);
+ }
+}
diff --git a/src/main/java/com/magamochi/ingestion/service/ContentProviderService.java b/src/main/java/com/magamochi/ingestion/service/ContentProviderService.java
new file mode 100644
index 0000000..026a31c
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/service/ContentProviderService.java
@@ -0,0 +1,35 @@
+package com.magamochi.ingestion.service;
+
+import static java.util.Objects.nonNull;
+
+import com.magamochi.common.exception.NotFoundException;
+import com.magamochi.ingestion.model.dto.ContentProviderListDTO;
+import com.magamochi.ingestion.model.entity.ContentProvider;
+import com.magamochi.ingestion.model.repository.ContentProviderRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class ContentProviderService {
+ private final ContentProviderRepository contentProviderRepository;
+
+ public ContentProviderListDTO getProviders(Boolean manualImport) {
+ var providers = contentProviderRepository.findAll();
+
+ if (nonNull(manualImport) && manualImport) {
+ providers = providers.stream().filter(ContentProvider::getManualImport).toList();
+ }
+
+ return ContentProviderListDTO.from(providers);
+ }
+
+ public ContentProvider find(Long contentProviderId) {
+ return contentProviderRepository
+ .findById(contentProviderId)
+ .orElseThrow(
+ () ->
+ new NotFoundException(
+ "Content Provider not found (ID: " + contentProviderId + ")."));
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/service/FlareService.java b/src/main/java/com/magamochi/ingestion/service/FlareService.java
similarity index 88%
rename from src/main/java/com/magamochi/mangamochi/service/FlareService.java
rename to src/main/java/com/magamochi/ingestion/service/FlareService.java
index bac79a2..f0ab713 100644
--- a/src/main/java/com/magamochi/mangamochi/service/FlareService.java
+++ b/src/main/java/com/magamochi/ingestion/service/FlareService.java
@@ -1,6 +1,6 @@
-package com.magamochi.mangamochi.service;
+package com.magamochi.ingestion.service;
-import com.magamochi.mangamochi.client.FlareClient;
+import com.magamochi.ingestion.client.FlareClient;
import lombok.RequiredArgsConstructor;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
diff --git a/src/main/java/com/magamochi/mangamochi/service/FlareSessionManager.java b/src/main/java/com/magamochi/ingestion/service/FlareSessionManager.java
similarity index 88%
rename from src/main/java/com/magamochi/mangamochi/service/FlareSessionManager.java
rename to src/main/java/com/magamochi/ingestion/service/FlareSessionManager.java
index 97c16d8..251f01a 100644
--- a/src/main/java/com/magamochi/mangamochi/service/FlareSessionManager.java
+++ b/src/main/java/com/magamochi/ingestion/service/FlareSessionManager.java
@@ -1,11 +1,10 @@
-package com.magamochi.mangamochi.service;
+package com.magamochi.ingestion.service;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
-import com.magamochi.mangamochi.client.FlareClient;
-import com.magamochi.mangamochi.model.entity.FlareSession;
-import com.magamochi.mangamochi.registry.FlareSessionRegistry;
+import com.magamochi.ingestion.client.FlareClient;
+import com.magamochi.ingestion.model.entity.FlareSession;
import java.time.Duration;
import java.time.Instant;
import lombok.RequiredArgsConstructor;
diff --git a/src/main/java/com/magamochi/mangamochi/registry/FlareSessionRegistry.java b/src/main/java/com/magamochi/ingestion/service/FlareSessionRegistry.java
similarity index 83%
rename from src/main/java/com/magamochi/mangamochi/registry/FlareSessionRegistry.java
rename to src/main/java/com/magamochi/ingestion/service/FlareSessionRegistry.java
index 33f6b68..4242d99 100644
--- a/src/main/java/com/magamochi/mangamochi/registry/FlareSessionRegistry.java
+++ b/src/main/java/com/magamochi/ingestion/service/FlareSessionRegistry.java
@@ -1,6 +1,6 @@
-package com.magamochi.mangamochi.registry;
+package com.magamochi.ingestion.service;
-import com.magamochi.mangamochi.model.entity.FlareSession;
+import com.magamochi.ingestion.model.entity.FlareSession;
import java.util.concurrent.ConcurrentHashMap;
import lombok.Getter;
import org.springframework.stereotype.Component;
diff --git a/src/main/java/com/magamochi/ingestion/service/IngestionService.java b/src/main/java/com/magamochi/ingestion/service/IngestionService.java
new file mode 100644
index 0000000..4591499
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/service/IngestionService.java
@@ -0,0 +1,106 @@
+package com.magamochi.ingestion.service;
+
+import com.magamochi.catalog.service.MangaContentProviderService;
+import com.magamochi.common.queue.command.MangaContentImageIngestCommand;
+import com.magamochi.common.queue.command.MangaContentIngestCommand;
+import com.magamochi.common.queue.command.MangaIngestCommand;
+import com.magamochi.content.service.ContentService;
+import com.magamochi.ingestion.providers.ContentProviderFactory;
+import com.magamochi.ingestion.providers.PagedContentProviderFactory;
+import com.magamochi.ingestion.queue.command.ProviderPageIngestCommand;
+import com.magamochi.ingestion.queue.producer.MangaContentImageIngestProducer;
+import com.magamochi.ingestion.queue.producer.MangaContentIngestProducer;
+import com.magamochi.ingestion.queue.producer.MangaIngestProducer;
+import com.magamochi.ingestion.queue.producer.ProviderPageIngestProducer;
+import java.util.stream.IntStream;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class IngestionService {
+ private final ContentProviderService contentProviderService;
+ private final ContentService contentService;
+ private final MangaContentProviderService mangaContentProviderService;
+
+ private final ContentProviderFactory contentProviderFactory;
+ private final PagedContentProviderFactory pagedContentProviderFactory;
+
+ private final ProviderPageIngestProducer providerPageIngestProducer;
+ private final MangaIngestProducer mangaIngestProducer;
+ private final MangaContentIngestProducer mangaContentIngestProducer;
+ private final MangaContentImageIngestProducer mangaContentImageIngestProducer;
+
+ public void fetchContentProviderMangas(long contentProviderId) {
+ var contentProvider = contentProviderService.find(contentProviderId);
+ var pagedContentProvider =
+ pagedContentProviderFactory.getPagedContentProvider(contentProvider.getName());
+ var pages = pagedContentProvider.getTotalPages();
+
+ IntStream.rangeClosed(1, pages)
+ .forEach(
+ page ->
+ providerPageIngestProducer.sendProviderPageIngestCommand(
+ new ProviderPageIngestCommand(contentProvider.getId(), page)));
+ }
+
+ public void fetchProviderPageMangas(long providerId, int page) {
+ var contentProvider = contentProviderService.find(providerId);
+ var pagedContentProvider =
+ pagedContentProviderFactory.getPagedContentProvider(contentProvider.getName());
+
+ var mangas = pagedContentProvider.getMangasFromPage(page);
+
+ mangas.forEach(
+ manga ->
+ mangaIngestProducer.sendMangaIngestCommand(
+ new MangaIngestCommand(contentProvider.getId(), manga.title(), manga.url())));
+ }
+
+ public void fetchAllContentProviderMangas() {
+ var contentProviders = contentProviderService.getProviders(null);
+ contentProviders.providers().forEach(dto -> fetchContentProviderMangas(dto.id()));
+ }
+
+ public void fetchMangaContentProviderContentList(Long mangaContentProviderId) {
+ var mangaContentProvider = mangaContentProviderService.find(mangaContentProviderId);
+
+ var contentProvider =
+ contentProviderFactory.getContentProvider(
+ mangaContentProvider.getContentProvider().getName());
+
+ var availableChapters = contentProvider.getAvailableChapters(mangaContentProvider);
+
+ availableChapters.forEach(
+ content ->
+ mangaContentIngestProducer.sendMangaContentIngestCommand(
+ new MangaContentIngestCommand(
+ mangaContentProvider.getId(),
+ content.title(),
+ content.url(),
+ content.languageCode())));
+ }
+
+ public void fetchContent(Long mangaContentId) {
+ var mangaContent = contentService.find(mangaContentId);
+ var mangaContentProvider = mangaContent.getMangaContentProvider();
+
+ var contentProvider =
+ contentProviderFactory.getContentProvider(
+ mangaContentProvider.getContentProvider().getName());
+
+ var chapterImagesUrl = contentProvider.getContentImages(mangaContent.getUrl());
+
+ IntStream.range(0, chapterImagesUrl.size())
+ .forEach(
+ i -> {
+ var item = chapterImagesUrl.get(i);
+
+ var isLast = i == chapterImagesUrl.size() - 1;
+
+ mangaContentImageIngestProducer.sendMangaContentImageIngestCommand(
+ new MangaContentImageIngestCommand(
+ mangaContent.getId(), item.url(), item.position(), isLast));
+ });
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/task/FlareSessionCleanupTask.java b/src/main/java/com/magamochi/ingestion/task/FlareSessionCleanupTask.java
similarity index 85%
rename from src/main/java/com/magamochi/mangamochi/task/FlareSessionCleanupTask.java
rename to src/main/java/com/magamochi/ingestion/task/FlareSessionCleanupTask.java
index bf0efc9..26610c5 100644
--- a/src/main/java/com/magamochi/mangamochi/task/FlareSessionCleanupTask.java
+++ b/src/main/java/com/magamochi/ingestion/task/FlareSessionCleanupTask.java
@@ -1,7 +1,7 @@
-package com.magamochi.mangamochi.task;
+package com.magamochi.ingestion.task;
-import com.magamochi.mangamochi.client.FlareClient;
-import com.magamochi.mangamochi.registry.FlareSessionRegistry;
+import com.magamochi.ingestion.client.FlareClient;
+import com.magamochi.ingestion.service.FlareSessionRegistry;
import java.time.Duration;
import java.time.Instant;
import lombok.RequiredArgsConstructor;
diff --git a/src/main/java/com/magamochi/mangamochi/task/FlareStartupCleanupTask.java b/src/main/java/com/magamochi/ingestion/task/FlareStartupCleanupTask.java
similarity index 89%
rename from src/main/java/com/magamochi/mangamochi/task/FlareStartupCleanupTask.java
rename to src/main/java/com/magamochi/ingestion/task/FlareStartupCleanupTask.java
index 5ceb54d..6e0ba9b 100644
--- a/src/main/java/com/magamochi/mangamochi/task/FlareStartupCleanupTask.java
+++ b/src/main/java/com/magamochi/ingestion/task/FlareStartupCleanupTask.java
@@ -1,6 +1,6 @@
-package com.magamochi.mangamochi.task;
+package com.magamochi.ingestion.task;
-import com.magamochi.mangamochi.client.FlareClient;
+import com.magamochi.ingestion.client.FlareClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.context.event.ApplicationReadyEvent;
diff --git a/src/main/java/com/magamochi/ingestion/task/IngestFromContentProvidersTask.java b/src/main/java/com/magamochi/ingestion/task/IngestFromContentProvidersTask.java
new file mode 100644
index 0000000..aa7d823
--- /dev/null
+++ b/src/main/java/com/magamochi/ingestion/task/IngestFromContentProvidersTask.java
@@ -0,0 +1,31 @@
+package com.magamochi.ingestion.task;
+
+import com.magamochi.ingestion.service.IngestionService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Log4j2
+@Component
+@RequiredArgsConstructor
+public class IngestFromContentProvidersTask {
+ @Value("${content-providers.update-enabled}")
+ private Boolean updateEnabled;
+
+ private final IngestionService ingestionService;
+
+ @Scheduled(cron = "${content-providers.cron-expression}")
+ public void updateMangaListScheduled() {
+ if (!updateEnabled) {
+ return;
+ }
+
+ log.info("Starting scheduling ingest from Content Providers.");
+
+ ingestionService.fetchAllContentProviderMangas();
+
+ log.info("Finished scheduling ingest from Content Providers.");
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/client/AniListClient.java b/src/main/java/com/magamochi/mangamochi/client/AniListClient.java
deleted file mode 100644
index b5c5b86..0000000
--- a/src/main/java/com/magamochi/mangamochi/client/AniListClient.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.magamochi.mangamochi.client;
-
-import io.github.resilience4j.retry.annotation.Retry;
-import java.util.List;
-import org.springframework.cloud.openfeign.FeignClient;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-
-@FeignClient(name = "aniList", url = "https://graphql.anilist.co")
-@Retry(name = "AniListRetry")
-public interface AniListClient {
-
- @PostMapping
- MangaResponse getManga(@RequestBody GraphQLRequest request);
-
- record GraphQLRequest(String query, Variables variables) {
- public record Variables(Long id) {}
- }
-
- record MangaResponse(Data data) {
- public record Data(Manga Media) {}
-
- public record Manga(
- Long id,
- Long idMal,
- Title title,
- String status,
- String description, // synopsis
- int chapters,
- int averageScore, // score (0-100)
- CoverImage coverImage,
- List genres,
- FuzzyDate startDate,
- FuzzyDate endDate,
- StaffConnection staff) {
- public record Title(String romaji, String english, String nativeTitle) {}
-
- public record CoverImage(String large) {}
-
- public record FuzzyDate(Integer year, Integer month, Integer day) {}
-
- public record StaffConnection(List edges) {
- public record StaffEdge(String role, Staff node) {
- public record Staff(Name name) {
- public record Name(String full) {}
- }
- }
- }
- }
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/config/RabbitConfig.java b/src/main/java/com/magamochi/mangamochi/config/RabbitConfig.java
deleted file mode 100644
index 09e13f0..0000000
--- a/src/main/java/com/magamochi/mangamochi/config/RabbitConfig.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package com.magamochi.mangamochi.config;
-
-import org.springframework.amqp.core.Queue;
-import org.springframework.amqp.rabbit.connection.ConnectionFactory;
-import org.springframework.amqp.rabbit.core.RabbitTemplate;
-import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-public class RabbitConfig {
- @Value("${rabbit-mq.queues.manga-data-update}")
- private String mangaDataUpdateQueue;
-
- @Value("${rabbit-mq.queues.manga-chapter-download}")
- private String mangaChapterDownloadQueue;
-
- @Value("${rabbit-mq.queues.manga-list-update}")
- private String mangaListUpdateQueue;
-
- @Value("${rabbit-mq.queues.manga-follow-update-chapter}")
- private String mangaFollowUpdateChapterQueue;
-
- @Bean
- public Queue mangaDataUpdateQueue() {
- return new Queue(mangaDataUpdateQueue, false);
- }
-
- @Bean
- public Queue mangaChapterDownloadQueue() {
- return new Queue(mangaChapterDownloadQueue, false);
- }
-
- @Bean
- public Queue mangaListUpdateQueue() {
- return new Queue(mangaListUpdateQueue, false);
- }
-
- @Bean
- public Queue mangaFollowUpdateChapterQueue() {
- return new Queue(mangaFollowUpdateChapterQueue, false);
- }
-
- @Bean
- public Jackson2JsonMessageConverter messageConverter() {
- return new Jackson2JsonMessageConverter();
- }
-
- @Bean
- public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
- var rabbitTemplate = new RabbitTemplate(connectionFactory);
- rabbitTemplate.setMessageConverter(messageConverter());
-
- return rabbitTemplate;
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/controller/AuthenticationController.java b/src/main/java/com/magamochi/mangamochi/controller/AuthenticationController.java
deleted file mode 100644
index eeec11f..0000000
--- a/src/main/java/com/magamochi/mangamochi/controller/AuthenticationController.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.magamochi.mangamochi.controller;
-
-import com.magamochi.mangamochi.model.dto.*;
-import com.magamochi.mangamochi.service.UserService;
-import io.swagger.v3.oas.annotations.Operation;
-import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.*;
-
-@RestController
-@RequestMapping("/auth")
-@CrossOrigin(origins = "*")
-@RequiredArgsConstructor
-public class AuthenticationController {
- private final UserService userService;
-
- @Operation(
- summary = "Authenticate user",
- description = "Authenticate user with email and password.",
- tags = {"Auth"},
- operationId = "authenticateUser")
- @PostMapping("/login")
- public DefaultResponseDTO authenticateUser(
- @RequestBody AuthenticationRequestDTO authenticationRequestDTO) {
- return DefaultResponseDTO.ok(userService.authenticate(authenticationRequestDTO));
- }
-
- @Operation(
- summary = "Refresh authentication token",
- description = "Refresh the authentication token",
- tags = {"Auth"},
- operationId = "refreshAuthToken")
- @PostMapping("/refresh")
- public DefaultResponseDTO refreshAuthToken(
- @RequestBody RefreshTokenRequestDTO authenticationRequestDTO) {
- return DefaultResponseDTO.ok(userService.refreshAuthToken(authenticationRequestDTO));
- }
-
- @Operation(
- summary = "Register user",
- description = "Register a new user.",
- tags = {"Auth"},
- operationId = "registerUser")
- @PostMapping("/register")
- public DefaultResponseDTO registerUser(
- @RequestBody RegistrationRequestDTO registrationRequestDTO) {
- userService.register(registrationRequestDTO);
-
- return DefaultResponseDTO.ok().build();
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/controller/GenreController.java b/src/main/java/com/magamochi/mangamochi/controller/GenreController.java
deleted file mode 100644
index 1335dbc..0000000
--- a/src/main/java/com/magamochi/mangamochi/controller/GenreController.java
+++ /dev/null
@@ -1,26 +0,0 @@
-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;
-import java.util.List;
-import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.*;
-
-@RestController
-@RequestMapping("/genres")
-@RequiredArgsConstructor
-public class GenreController {
- private final GenreService genreService;
-
- @Operation(
- summary = "Get a list of genres",
- description = "Retrieve a list of genres.",
- tags = {"Genre"},
- operationId = "getGenres")
- @GetMapping
- public DefaultResponseDTO> getGenres() {
- return DefaultResponseDTO.ok(genreService.getGenres());
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/controller/ManagementController.java b/src/main/java/com/magamochi/mangamochi/controller/ManagementController.java
deleted file mode 100644
index e4da1e5..0000000
--- a/src/main/java/com/magamochi/mangamochi/controller/ManagementController.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package com.magamochi.mangamochi.controller;
-
-import com.magamochi.mangamochi.client.NtfyClient;
-import com.magamochi.mangamochi.model.dto.DefaultResponseDTO;
-import com.magamochi.mangamochi.model.dto.UpdateMangaDataCommand;
-import com.magamochi.mangamochi.model.repository.UserRepository;
-import com.magamochi.mangamochi.queue.UpdateMangaDataProducer;
-import com.magamochi.mangamochi.task.ImageCleanupTask;
-import com.magamochi.mangamochi.task.MangaFollowUpdateTask;
-import com.magamochi.mangamochi.task.UpdateMangaListTask;
-import io.swagger.v3.oas.annotations.Operation;
-import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.*;
-
-@RestController
-@RequestMapping("/management")
-@RequiredArgsConstructor
-public class ManagementController {
- private final UpdateMangaListTask updateMangaListTask;
- private final ImageCleanupTask imageCleanupTask;
- private final MangaFollowUpdateTask mangaFollowUpdateTask;
- private final UserRepository userRepository;
- private final NtfyClient ntfyClient;
- private final UpdateMangaDataProducer updateMangaDataProducer;
-
- @Operation(
- summary = "Trigger manga data update",
- description = "Triggers the update of the metadata for a manga by its ID",
- tags = {"Management"},
- operationId = "triggerUpdateMangaData")
- @PostMapping("update-manga-data/{mangaId}")
- public DefaultResponseDTO triggerUpdateMangaData(@PathVariable Long mangaId) {
- updateMangaDataProducer.sendUpdateMangaDataCommand(new UpdateMangaDataCommand(mangaId));
-
- return DefaultResponseDTO.ok().build();
- }
-
- @Operation(
- summary = "Queue update manga list",
- description = "Queue the retrieval of the manga lists from the content providers",
- tags = {"Management"},
- operationId = "updateMangaList")
- @PostMapping("update-manga-list")
- public DefaultResponseDTO updateMangaList() {
- updateMangaListTask.updateMangaList();
-
- return DefaultResponseDTO.ok().build();
- }
-
- @Operation(
- summary = "Queue update provider manga list",
- description = "Queue the retrieval of the manga list for a specific provider",
- tags = {"Management"},
- operationId = "updateProviderMangaList")
- @PostMapping("update-provider-manga-list")
- public DefaultResponseDTO updateProviderMangaList(@RequestParam Long providerId) {
- updateMangaListTask.updateProviderMangaList(providerId);
-
- return DefaultResponseDTO.ok().build();
- }
-
- @Operation(
- summary = "Cleanup unused S3 images",
- description = "Triggers the cleanup of untracked S3 images",
- tags = {"Management"},
- operationId = "imageCleanup")
- @PostMapping("image-cleanup")
- public DefaultResponseDTO imageCleanup() {
- imageCleanupTask.cleanupImages();
-
- return DefaultResponseDTO.ok().build();
- }
-
- @Operation(
- summary = "Trigger user follow update",
- description = "Trigger user follow update",
- tags = {"Management"},
- operationId = "userFollowUpdate")
- @PostMapping("user-follow")
- public DefaultResponseDTO triggerUserFollowUpdate() {
- mangaFollowUpdateTask.updateMangaList();
-
- return DefaultResponseDTO.ok().build();
- }
-
- @Operation(
- summary = "Test notification",
- description = "Sends a test notification to all users",
- tags = {"Management"},
- operationId = "testNotification")
- @PostMapping("test-notification")
- public DefaultResponseDTO testNotification() {
- var users = userRepository.findAll();
-
- users.forEach(
- user ->
- ntfyClient.notify(
- new NtfyClient.Request(
- "mangamochi-" + user.getId().toString(),
- "Mangamochi",
- "This is a test notification :)")));
-
- return DefaultResponseDTO.ok().build();
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/controller/MangaController.java b/src/main/java/com/magamochi/mangamochi/controller/MangaController.java
deleted file mode 100644
index eeb0dcc..0000000
--- a/src/main/java/com/magamochi/mangamochi/controller/MangaController.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package com.magamochi.mangamochi.controller;
-
-import com.magamochi.mangamochi.model.dto.*;
-import com.magamochi.mangamochi.service.MangaService;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
-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.web.bind.annotation.*;
-
-@RestController
-@RequestMapping("/mangas")
-@RequiredArgsConstructor
-public class MangaController {
- private final MangaService mangaService;
-
- @Operation(
- summary = "Get a list of mangas",
- description = "Retrieve a list of mangas with their details.",
- tags = {"Manga"},
- operationId = "getMangas")
- @GetMapping
- public DefaultResponseDTO> getMangas(
- @ParameterObject MangaListFilterDTO filterDTO,
- @Parameter(hidden = true) @ParameterObject @PageableDefault Pageable 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 DefaultResponseDTO getManga(@PathVariable Long mangaId) {
- return DefaultResponseDTO.ok(mangaService.getManga(mangaId));
- }
-
- @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 = "getMangaChapters")
- @GetMapping("/{mangaProviderId}/chapters")
- public DefaultResponseDTO> getMangaChapters(
- @PathVariable Long mangaProviderId) {
- return DefaultResponseDTO.ok(mangaService.getMangaChapters(mangaProviderId));
- }
-
- @Operation(
- summary = "Fetch all chapters",
- description = "Fetch all not yet downloaded chapters from the provider",
- tags = {"Manga Chapter"},
- operationId = "fetchAllChapters")
- @PostMapping(value = "/{mangaProviderId}/fetch-all-chapters")
- public DefaultResponseDTO fetchAllChapters(@PathVariable Long mangaProviderId) {
- mangaService.fetchAllNotDownloadedChapters(mangaProviderId);
-
- return DefaultResponseDTO.ok().build();
- }
-
- @Operation(
- summary = "Fetch the available chapters for a specific manga/provider combination",
- description = "Fetch a list of manga chapters for a specific manga/provider combination.",
- tags = {"Manga"},
- operationId = "fetchMangaChapters")
- @PostMapping("/{mangaProviderId}/fetch-chapters")
- public DefaultResponseDTO fetchMangaChapters(@PathVariable Long mangaProviderId) {
- mangaService.fetchMangaChapters(mangaProviderId);
-
- return DefaultResponseDTO.ok().build();
- }
-
- @Operation(
- summary = "Follow the manga specified by its ID",
- description = "Follow the manga specified by its ID.",
- tags = {"Manga"},
- operationId = "followManga")
- @PostMapping("/{mangaId}/followManga")
- public DefaultResponseDTO followManga(@PathVariable Long mangaId) {
- mangaService.follow(mangaId);
-
- return DefaultResponseDTO.ok().build();
- }
-
- @Operation(
- summary = "Unfollow the manga specified by its ID",
- description = "Unfollow the manga specified by its ID.",
- tags = {"Manga"},
- operationId = "unfollowManga")
- @PostMapping("/{mangaId}/unfollowManga")
- public DefaultResponseDTO unfollowManga(@PathVariable Long mangaId) {
- mangaService.unfollow(mangaId);
-
- return DefaultResponseDTO.ok().build();
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/controller/MangaImportReviewController.java b/src/main/java/com/magamochi/mangamochi/controller/MangaImportReviewController.java
deleted file mode 100644
index a3d1500..0000000
--- a/src/main/java/com/magamochi/mangamochi/controller/MangaImportReviewController.java
+++ /dev/null
@@ -1,51 +0,0 @@
-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> 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 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 resolveImportReview(
- @RequestParam Long importReviewId, @RequestParam String malId) {
- mangaImportReviewService.resolveImportReview(importReviewId, malId);
-
- return DefaultResponseDTO.ok().build();
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/controller/ProviderController.java b/src/main/java/com/magamochi/mangamochi/controller/ProviderController.java
deleted file mode 100644
index 300e7cb..0000000
--- a/src/main/java/com/magamochi/mangamochi/controller/ProviderController.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.magamochi.mangamochi.controller;
-
-import com.magamochi.mangamochi.model.dto.*;
-import com.magamochi.mangamochi.service.ProviderService;
-import io.swagger.v3.oas.annotations.Operation;
-import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.*;
-
-@RestController
-@RequestMapping("/providers")
-@RequiredArgsConstructor
-public class ProviderController {
- private final ProviderService providerService;
-
- @Operation(
- summary = "Get a list of providers",
- description = "Retrieve a list of content providers",
- tags = {"Provider"},
- operationId = "getProviders")
- @GetMapping
- public DefaultResponseDTO getMangas(
- @RequestParam(name = "manualImport", required = false) Boolean manualImport) {
- return DefaultResponseDTO.ok(providerService.getProviders(manualImport));
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/ContentProviderMangaChapterResponseDTO.java b/src/main/java/com/magamochi/mangamochi/model/dto/ContentProviderMangaChapterResponseDTO.java
deleted file mode 100644
index 5fe280f..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/dto/ContentProviderMangaChapterResponseDTO.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.magamochi.mangamochi.model.dto;
-
-import jakarta.validation.constraints.NotBlank;
-
-public record ContentProviderMangaChapterResponseDTO(
- @NotBlank String chapterTitle,
- @NotBlank String chapterUrl,
- String chapter,
- String languageCode) {}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/ContentProviderMangaInfoResponseDTO.java b/src/main/java/com/magamochi/mangamochi/model/dto/ContentProviderMangaInfoResponseDTO.java
deleted file mode 100644
index f91ee58..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/dto/ContentProviderMangaInfoResponseDTO.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.magamochi.mangamochi.model.dto;
-
-import com.magamochi.mangamochi.model.enumeration.MangaStatus;
-import jakarta.validation.constraints.NotBlank;
-
-public record ContentProviderMangaInfoResponseDTO(
- @NotBlank String title, @NotBlank String url, MangaStatus status) {}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterDTO.java b/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterDTO.java
deleted file mode 100644
index 9da55db..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterDTO.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.magamochi.mangamochi.model.dto;
-
-import com.magamochi.mangamochi.model.entity.MangaChapter;
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
-
-public record MangaChapterDTO(
- @NotNull Long id,
- @NotBlank String title,
- @NotNull Boolean downloaded,
- @NotNull Boolean isRead,
- LanguageDTO language) {
- public static MangaChapterDTO from(MangaChapter mangaChapter) {
- return new MangaChapterDTO(
- mangaChapter.getId(),
- mangaChapter.getTitle(),
- mangaChapter.getDownloaded(),
- mangaChapter.getRead(),
- LanguageDTO.from(mangaChapter.getLanguage()));
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterImagesDTO.java b/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterImagesDTO.java
deleted file mode 100644
index f13bff6..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterImagesDTO.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.magamochi.mangamochi.model.dto;
-
-import com.magamochi.mangamochi.model.entity.MangaChapter;
-import com.magamochi.mangamochi.model.entity.MangaChapterImage;
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
-import java.util.Comparator;
-import java.util.List;
-
-public record MangaChapterImagesDTO(
- @NotNull Long id,
- @NotBlank String mangaTitle,
- Long previousChapterId,
- Long nextChapterId,
- @NotNull List<@NotBlank String> chapterImageKeys) {
- public static MangaChapterImagesDTO from(MangaChapter mangaChapter, Long prevId, Long nextId) {
- return new MangaChapterImagesDTO(
- mangaChapter.getId(),
- mangaChapter.getTitle(),
- prevId,
- nextId,
- mangaChapter.getMangaChapterImages().stream()
- .sorted(Comparator.comparing(MangaChapterImage::getPosition))
- .map(mangaChapterImage -> mangaChapterImage.getImage().getFileKey())
- .toList());
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterResponseDTO.java b/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterResponseDTO.java
deleted file mode 100644
index 2d7890d..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterResponseDTO.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.magamochi.mangamochi.model.dto;
-
-import jakarta.validation.constraints.NotNull;
-import java.util.UUID;
-
-public record MangaChapterResponseDTO(
- @NotNull UUID id,
- // @NotNull Long mangaProviderId,
- @NotNull String chapterTitle,
- @NotNull String chapterUrl) {}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/MangaListUpdateCommand.java b/src/main/java/com/magamochi/mangamochi/model/dto/MangaListUpdateCommand.java
deleted file mode 100644
index 28f714e..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/dto/MangaListUpdateCommand.java
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.magamochi.mangamochi.model.dto;
-
-public record MangaListUpdateCommand(String contentProviderName, Integer page) {}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/MangaMessageDTO.java b/src/main/java/com/magamochi/mangamochi/model/dto/MangaMessageDTO.java
deleted file mode 100644
index 910d99b..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/dto/MangaMessageDTO.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.magamochi.mangamochi.model.dto;
-
-import java.util.List;
-
-public record MangaMessageDTO(
- String contentProviderName, List mangaInfoResponseDTOs) {}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/ProviderListDTO.java b/src/main/java/com/magamochi/mangamochi/model/dto/ProviderListDTO.java
deleted file mode 100644
index 6cb226f..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/dto/ProviderListDTO.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.magamochi.mangamochi.model.dto;
-
-import com.magamochi.mangamochi.model.entity.*;
-import jakarta.validation.constraints.NotNull;
-import java.util.List;
-
-public record ProviderListDTO(@NotNull List providers) {
- public static ProviderListDTO from(List providers) {
- return new ProviderListDTO(providers.stream().map(ProviderDTO::from).toList());
- }
-
- record ProviderDTO(@NotNull Long id, @NotNull String name) {
- public static ProviderDTO from(Provider provider) {
- return new ProviderDTO(provider.getId(), provider.getName());
- }
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/TitleMatchRequestDTO.java b/src/main/java/com/magamochi/mangamochi/model/dto/TitleMatchRequestDTO.java
deleted file mode 100644
index a31b88c..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/dto/TitleMatchRequestDTO.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.magamochi.mangamochi.model.dto;
-
-import java.util.List;
-import lombok.Builder;
-import lombok.Getter;
-
-@Getter
-@Builder
-public class TitleMatchRequestDTO {
- private String title;
-
- private List options;
-
- @Builder.Default private int threshold = 85;
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/TitleMatchResponseDTO.java b/src/main/java/com/magamochi/mangamochi/model/dto/TitleMatchResponseDTO.java
deleted file mode 100644
index c2a3362..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/dto/TitleMatchResponseDTO.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.magamochi.mangamochi.model.dto;
-
-import lombok.Builder;
-import lombok.Getter;
-
-@Getter
-@Builder
-public class TitleMatchResponseDTO {
- boolean matchFound;
- String bestMatch;
- Double similarity;
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/UpdateMangaDataCommand.java b/src/main/java/com/magamochi/mangamochi/model/dto/UpdateMangaDataCommand.java
deleted file mode 100644
index f37aac6..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/dto/UpdateMangaDataCommand.java
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.magamochi.mangamochi.model.dto;
-
-public record UpdateMangaDataCommand(Long mangaId) {}
diff --git a/src/main/java/com/magamochi/mangamochi/model/enumeration/ArchiveFileType.java b/src/main/java/com/magamochi/mangamochi/model/enumeration/ArchiveFileType.java
deleted file mode 100644
index 9982b20..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/enumeration/ArchiveFileType.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.magamochi.mangamochi.model.enumeration;
-
-public enum ArchiveFileType {
- CBZ,
- CBR
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/enumeration/ProviderStatus.java b/src/main/java/com/magamochi/mangamochi/model/enumeration/ProviderStatus.java
deleted file mode 100644
index cb09460..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/enumeration/ProviderStatus.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.magamochi.mangamochi.model.enumeration;
-
-public enum ProviderStatus {
- ACTIVE,
- INACTIVE
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/enumeration/UserRole.java b/src/main/java/com/magamochi/mangamochi/model/enumeration/UserRole.java
deleted file mode 100644
index 0823d20..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/enumeration/UserRole.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.magamochi.mangamochi.model.enumeration;
-
-public enum UserRole {
- USER
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/AuthorRepository.java b/src/main/java/com/magamochi/mangamochi/model/repository/AuthorRepository.java
deleted file mode 100644
index 4ab61ff..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/repository/AuthorRepository.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.magamochi.mangamochi.model.repository;
-
-import com.magamochi.mangamochi.model.entity.Author;
-import java.util.Optional;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface AuthorRepository extends JpaRepository {
- Optional findByMalId(Long aLong);
-
- Optional findByName(String name);
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/GenreRepository.java b/src/main/java/com/magamochi/mangamochi/model/repository/GenreRepository.java
deleted file mode 100644
index 438148e..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/repository/GenreRepository.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.magamochi.mangamochi.model.repository;
-
-import com.magamochi.mangamochi.model.entity.Genre;
-import java.util.Optional;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface GenreRepository extends JpaRepository {
- Optional findByMalId(Long malId);
-
- Optional findByName(String name);
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/ImageRepository.java b/src/main/java/com/magamochi/mangamochi/model/repository/ImageRepository.java
deleted file mode 100644
index 688fa9b..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/repository/ImageRepository.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.magamochi.mangamochi.model.repository;
-
-import com.magamochi.mangamochi.model.entity.Image;
-import java.util.UUID;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface ImageRepository extends JpaRepository {}
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/MangaChapterImageRepository.java b/src/main/java/com/magamochi/mangamochi/model/repository/MangaChapterImageRepository.java
deleted file mode 100644
index a312418..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/repository/MangaChapterImageRepository.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.magamochi.mangamochi.model.repository;
-
-import com.magamochi.mangamochi.model.entity.MangaChapter;
-import com.magamochi.mangamochi.model.entity.MangaChapterImage;
-import java.util.List;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface MangaChapterImageRepository extends JpaRepository {
- List findAllByMangaChapter(MangaChapter mangaChapter);
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/MangaChapterRepository.java b/src/main/java/com/magamochi/mangamochi/model/repository/MangaChapterRepository.java
deleted file mode 100644
index 7e104f0..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/repository/MangaChapterRepository.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.magamochi.mangamochi.model.repository;
-
-import com.magamochi.mangamochi.model.entity.MangaChapter;
-import com.magamochi.mangamochi.model.entity.MangaProvider;
-import jakarta.validation.constraints.NotBlank;
-import java.util.List;
-import java.util.Optional;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface MangaChapterRepository extends JpaRepository {
- Optional findByMangaProviderAndUrlIgnoreCase(
- MangaProvider mangaProvider, @NotBlank String url);
-
- List findByMangaProviderId(Long mangaProvider_id);
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/MangaImportReviewRepository.java b/src/main/java/com/magamochi/mangamochi/model/repository/MangaImportReviewRepository.java
deleted file mode 100644
index 8114803..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/repository/MangaImportReviewRepository.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.magamochi.mangamochi.model.repository;
-
-import com.magamochi.mangamochi.model.entity.MangaImportReview;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface MangaImportReviewRepository extends JpaRepository {
- boolean existsByTitleIgnoreCaseAndUrlIgnoreCase(String title, String url);
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/MangaProviderRepository.java b/src/main/java/com/magamochi/mangamochi/model/repository/MangaProviderRepository.java
deleted file mode 100644
index 814d6d5..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/repository/MangaProviderRepository.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.magamochi.mangamochi.model.repository;
-
-import com.magamochi.mangamochi.model.entity.Manga;
-import com.magamochi.mangamochi.model.entity.MangaProvider;
-import com.magamochi.mangamochi.model.entity.Provider;
-import java.util.Optional;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface MangaProviderRepository extends JpaRepository {
- boolean existsByMangaAndProviderAndUrlIgnoreCase(Manga manga, Provider provider, String url);
-
- Optional findByMangaTitleIgnoreCaseAndProvider(
- String mangaTitle, Provider provider);
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/ProviderRepository.java b/src/main/java/com/magamochi/mangamochi/model/repository/ProviderRepository.java
deleted file mode 100644
index 762bc93..0000000
--- a/src/main/java/com/magamochi/mangamochi/model/repository/ProviderRepository.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.magamochi.mangamochi.model.repository;
-
-import com.magamochi.mangamochi.model.entity.Provider;
-import java.util.Optional;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface ProviderRepository extends JpaRepository {
- Optional findByNameIgnoreCase(String name);
-}
diff --git a/src/main/java/com/magamochi/mangamochi/queue/MangaChapterDownloadConsumer.java b/src/main/java/com/magamochi/mangamochi/queue/MangaChapterDownloadConsumer.java
deleted file mode 100644
index 3c286c2..0000000
--- a/src/main/java/com/magamochi/mangamochi/queue/MangaChapterDownloadConsumer.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.magamochi.mangamochi.queue;
-
-import com.magamochi.mangamochi.model.dto.MangaChapterDownloadCommand;
-import com.magamochi.mangamochi.service.MangaChapterService;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.log4j.Log4j2;
-import org.springframework.amqp.rabbit.annotation.RabbitListener;
-import org.springframework.stereotype.Service;
-
-@Log4j2
-@Service
-@RequiredArgsConstructor
-public class MangaChapterDownloadConsumer {
- private final MangaChapterService mangaChapterService;
-
- @RabbitListener(queues = "${rabbit-mq.queues.manga-chapter-download}")
- public void receiveMangaChapterDownloadCommand(MangaChapterDownloadCommand command) {
- log.info("Received manga chapter download command: {}", command);
-
- try {
- mangaChapterService.fetchChapter(command.chapterId());
- } catch (Exception e) {
- log.error("Couldn't download chapter {}. {}", command.chapterId(), e.getMessage());
- }
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaDataConsumer.java b/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaDataConsumer.java
deleted file mode 100644
index 519c5bc..0000000
--- a/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaDataConsumer.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.magamochi.mangamochi.queue;
-
-import com.magamochi.mangamochi.model.dto.UpdateMangaDataCommand;
-import com.magamochi.mangamochi.service.MangaImportService;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.log4j.Log4j2;
-import org.springframework.amqp.rabbit.annotation.RabbitListener;
-import org.springframework.stereotype.Service;
-
-@Log4j2
-@Service
-@RequiredArgsConstructor
-public class UpdateMangaDataConsumer {
- private final MangaImportService mangaImportService;
-
- @RabbitListener(queues = "${rabbit-mq.queues.manga-data-update}")
- public void receiveUpdateMangaDataCommand(UpdateMangaDataCommand command) {
- log.info("Received update manga data command: {}", command);
- mangaImportService.updateMangaData(command.mangaId());
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaDataProducer.java b/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaDataProducer.java
deleted file mode 100644
index 12f67a9..0000000
--- a/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaDataProducer.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.magamochi.mangamochi.queue;
-
-import com.magamochi.mangamochi.model.dto.UpdateMangaDataCommand;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.log4j.Log4j2;
-import org.springframework.amqp.rabbit.core.RabbitTemplate;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Service;
-
-@Log4j2
-@Service
-@RequiredArgsConstructor
-public class UpdateMangaDataProducer {
- private final RabbitTemplate rabbitTemplate;
-
- @Value("${rabbit-mq.queues.manga-data-update}")
- private String mangaDataUpdateQueue;
-
- public void sendUpdateMangaDataCommand(UpdateMangaDataCommand command) {
- rabbitTemplate.convertAndSend(mangaDataUpdateQueue, command);
- log.info("Sent update manga data command: {}", command);
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaListConsumer.java b/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaListConsumer.java
deleted file mode 100644
index e8e3a0b..0000000
--- a/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaListConsumer.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.magamochi.mangamochi.queue;
-
-import com.magamochi.mangamochi.model.dto.MangaListUpdateCommand;
-import com.magamochi.mangamochi.service.MangaListService;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.log4j.Log4j2;
-import org.springframework.amqp.rabbit.annotation.RabbitListener;
-import org.springframework.stereotype.Service;
-
-@Log4j2
-@Service
-@RequiredArgsConstructor
-public class UpdateMangaListConsumer {
- private final MangaListService mangaListService;
-
- @RabbitListener(queues = "${rabbit-mq.queues.manga-list-update}")
- public void receiveUpdateMangaListCommand(MangaListUpdateCommand command) {
- log.info("Received update manga list command: {}", command);
- mangaListService.updateMangaList(command.contentProviderName(), command.page());
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaListProducer.java b/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaListProducer.java
deleted file mode 100644
index e154d0d..0000000
--- a/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaListProducer.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.magamochi.mangamochi.queue;
-
-import com.magamochi.mangamochi.model.dto.MangaListUpdateCommand;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.log4j.Log4j2;
-import org.springframework.amqp.rabbit.core.RabbitTemplate;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Service;
-
-@Log4j2
-@Service
-@RequiredArgsConstructor
-public class UpdateMangaListProducer {
- private final RabbitTemplate rabbitTemplate;
-
- @Value("${rabbit-mq.queues.manga-list-update}")
- private String mangaListUpdateQueue;
-
- public void sendUpdateMangaListCommand(MangaListUpdateCommand command) {
- rabbitTemplate.convertAndSend(mangaListUpdateQueue, command);
- log.info("Sent update manga list command: {}", command);
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/service/ImageService.java b/src/main/java/com/magamochi/mangamochi/service/ImageService.java
deleted file mode 100644
index 8162b06..0000000
--- a/src/main/java/com/magamochi/mangamochi/service/ImageService.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.magamochi.mangamochi.service;
-
-import com.magamochi.mangamochi.model.entity.Image;
-import com.magamochi.mangamochi.model.repository.ImageRepository;
-import java.io.InputStream;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.log4j.Log4j2;
-import org.springframework.stereotype.Service;
-
-@Log4j2
-@Service
-@RequiredArgsConstructor
-public class ImageService {
- private final S3Service s3Service;
- private final ImageRepository imageRepository;
-
- public Image uploadImage(byte[] data, String contentType, String path) {
- log.info("Uploading image {} to S3", path);
- var fileKey = s3Service.uploadFile(data, contentType, path);
-
- return imageRepository.save(Image.builder().fileKey(fileKey).build());
- }
-
- public InputStream getImageStream(Image image) {
- return s3Service.getFile(image.getFileKey());
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/service/MangaChapterService.java b/src/main/java/com/magamochi/mangamochi/service/MangaChapterService.java
deleted file mode 100644
index 42cbe59..0000000
--- a/src/main/java/com/magamochi/mangamochi/service/MangaChapterService.java
+++ /dev/null
@@ -1,209 +0,0 @@
-package com.magamochi.mangamochi.service;
-
-import com.google.common.util.concurrent.RateLimiter;
-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 io.github.resilience4j.retry.Retry;
-import io.github.resilience4j.retry.RetryRegistry;
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URL;
-import java.util.Comparator;
-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;
-
- private final RateLimiter imageDownloadRateLimiter;
- private final RetryRegistry retryRegistry;
-
- @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 retryConfig = retryRegistry.retry("ImageDownloadRetry").getRetryConfig();
-
- var chapterImages =
- chapterImagesUrls.entrySet().parallelStream()
- .map(
- entry -> {
- imageDownloadRateLimiter.acquire();
-
- try {
- var finalUrl = new URI(entry.getValue().trim()).toASCIIString().trim();
- var retry =
- Retry.of("image-download-" + chapterId + "-" + entry.getKey(), retryConfig);
-
- retry
- .getEventPublisher()
- .onRetry(
- event ->
- log.warn(
- "Retrying image download {}/{} for chapter {}. Attempt #{}. Error: {}",
- entry.getKey() + 1,
- chapterImagesUrls.size(),
- chapterId,
- event.getNumberOfRetryAttempts(),
- event.getLastThrowable().getMessage()));
-
- return retry.executeCheckedSupplier(
- () -> {
- var url = new URL(finalUrl);
- var connection = url.openConnection();
- connection.setConnectTimeout(5000);
- connection.setReadTimeout(5000);
-
- try (var inputStream =
- new BufferedInputStream(connection.getInputStream())) {
- var bytes = inputStream.readAllBytes();
-
- 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 (Throwable e) {
- throw new UnprocessableException(
- "Could not download image for chapter ID: " + chapterId, e);
- }
- })
- .toList();
-
- mangaChapterImageRepository.saveAll(chapterImages);
-
- chapter.setDownloaded(true);
- mangaChapterRepository.save(chapter);
- }
-
- public MangaChapterImagesDTO getMangaChapterImages(Long chapterId) {
- var chapter = getMangaChapterThrowIfNotFound(chapterId);
-
- var chapters =
- chapter.getMangaProvider().getMangaChapters().stream()
- .sorted(Comparator.comparing(MangaChapter::getId))
- .toList();
- Long prevId = null;
- Long nextId = null;
-
- // TODO: this doesn't perform well for large datasets
- for (var i = 0; i < chapters.size(); i++) {
- if (chapters.get(i).getId().equals(chapterId)) {
- if (i > 0) {
- prevId = chapters.get(i - 1).getId();
- }
-
- if (i < chapters.size() - 1) {
- nextId = chapters.get(i + 1).getId();
- }
-
- break;
- }
- }
-
- return MangaChapterImagesDTO.from(chapter, prevId, nextId);
- }
-
- 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 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));
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/service/MangaCreationService.java b/src/main/java/com/magamochi/mangamochi/service/MangaCreationService.java
deleted file mode 100644
index 6e22b1e..0000000
--- a/src/main/java/com/magamochi/mangamochi/service/MangaCreationService.java
+++ /dev/null
@@ -1,163 +0,0 @@
-package com.magamochi.mangamochi.service;
-
-import static java.util.Objects.nonNull;
-
-import com.google.common.util.concurrent.RateLimiter;
-import com.magamochi.mangamochi.client.AniListClient;
-import com.magamochi.mangamochi.client.JikanClient;
-import com.magamochi.mangamochi.model.dto.TitleMatchRequestDTO;
-import com.magamochi.mangamochi.model.dto.UpdateMangaDataCommand;
-import com.magamochi.mangamochi.model.entity.Manga;
-import com.magamochi.mangamochi.model.entity.MangaImportReview;
-import com.magamochi.mangamochi.model.entity.Provider;
-import com.magamochi.mangamochi.model.repository.MangaImportReviewRepository;
-import com.magamochi.mangamochi.model.repository.MangaRepository;
-import com.magamochi.mangamochi.queue.UpdateMangaDataProducer;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.log4j.Log4j2;
-import org.springframework.stereotype.Service;
-
-@Log4j2
-@Service
-@RequiredArgsConstructor
-public class MangaCreationService {
- private final MangaRepository mangaRepository;
- private final MangaImportReviewRepository mangaImportReviewRepository;
-
- private final TitleMatcherService titleMatcherService;
-
- private final JikanClient jikanClient;
- private final AniListClient aniListClient;
-
- private final RateLimiter jikanRateLimiter;
-
- private final UpdateMangaDataProducer updateMangaDataProducer;
-
- public Manga getOrCreateManga(String title, String url, Provider provider) {
- var existingManga = mangaRepository.findByTitleIgnoreCase(title);
- if (existingManga.isPresent()) {
- return existingManga.get();
- }
-
- jikanRateLimiter.acquire();
- var jikanResults = jikanClient.mangaSearch(title).data();
- if (jikanResults.isEmpty()) {
- createMangaImportReview(title, url, provider);
- log.warn("No manga found with title {}", title);
- return null;
- }
-
- var titleMatchResponse =
- titleMatcherService.findBestMatch(
- TitleMatchRequestDTO.builder()
- .title(title)
- .options(
- jikanResults.stream()
- .flatMap(
- results ->
- results.titles().stream()
- .map(JikanClient.SearchResponse.MangaData.TitleData::title))
- .toList())
- .build());
-
- if (!titleMatchResponse.isMatchFound()) {
- createMangaImportReview(title, url, provider);
- log.warn("No match found for manga with title {}", title);
- return null;
- }
-
- var resultOptional =
- jikanResults.stream()
- .filter(
- results ->
- results.titles().stream()
- .map(JikanClient.SearchResponse.MangaData.TitleData::title)
- .toList()
- .contains(titleMatchResponse.getBestMatch()))
- .findFirst();
- if (resultOptional.isEmpty()) {
- createMangaImportReview(title, url, provider);
- log.warn("No match found for manga with title {}", title);
- return null;
- }
-
- var result = resultOptional.get();
- return getOrCreateManga(result.mal_id(), null, result.title());
- }
-
- public Manga getOrCreateManga(Long malId, Long aniListId) {
- if (nonNull(malId)) {
- try {
- jikanRateLimiter.acquire();
- var data = jikanClient.getMangaById(malId);
- return getOrCreateManga(data.data().mal_id(), aniListId, data.data().title());
- } catch (feign.FeignException.NotFound e) {
- log.warn("Manga not found on MyAnimeList for ID: {}", malId);
- }
- }
-
- if (nonNull(aniListId)) {
- try {
- var query =
- """
- query ($id: Int) {
- Media (id: $id, type: MANGA) {
- id
- idMal
- title {
- romaji
- english
- native
- }
- }
- }
- """;
- var request =
- new AniListClient.GraphQLRequest(
- query, new AniListClient.GraphQLRequest.Variables(aniListId));
- var data = aniListClient.getManga(request).data().Media();
-
- String title =
- nonNull(data.title().english()) ? data.title().english() : data.title().romaji();
- return getOrCreateManga(data.idMal(), data.id(), title);
- } catch (feign.FeignException.NotFound e) {
- log.warn("Manga not found on AniList for ID: {}", aniListId);
- }
- }
-
- throw new RuntimeException("Could not find manga on any provider");
- }
-
- private Manga getOrCreateManga(Long malId, Long aniListId, String title) {
- var mangaOptional = java.util.Optional.empty();
-
- if (nonNull(malId)) {
- mangaOptional = mangaRepository.findByMalId(malId);
- }
-
- if (mangaOptional.isEmpty() && nonNull(aniListId)) {
- mangaOptional = mangaRepository.findByAniListId(aniListId);
- }
-
- return mangaOptional.orElseGet(
- () -> {
- var manga =
- mangaRepository.save(
- Manga.builder().title(title).malId(malId).aniListId(aniListId).build());
-
- updateMangaDataProducer.sendUpdateMangaDataCommand(
- new UpdateMangaDataCommand(manga.getId()));
-
- return manga;
- });
- }
-
- private void createMangaImportReview(String title, String url, Provider provider) {
- if (mangaImportReviewRepository.existsByTitleIgnoreCaseAndUrlIgnoreCase(title, url)) {
- return;
- }
-
- mangaImportReviewRepository.save(
- MangaImportReview.builder().title(title).url(url).provider(provider).build());
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/service/MangaImportReviewService.java b/src/main/java/com/magamochi/mangamochi/service/MangaImportReviewService.java
deleted file mode 100644
index 6cd21a3..0000000
--- a/src/main/java/com/magamochi/mangamochi/service/MangaImportReviewService.java
+++ /dev/null
@@ -1,86 +0,0 @@
-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.dto.UpdateMangaDataCommand;
-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 com.magamochi.mangamochi.queue.UpdateMangaDataProducer;
-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;
-
- private final RateLimiter jikanRateLimiter;
-
- private final UpdateMangaDataProducer updateMangaDataProducer;
-
- public List 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);
-
- jikanRateLimiter.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()));
-
- if (!mangaProviderRepository.existsByMangaAndProviderAndUrlIgnoreCase(
- manga, importReview.getProvider(), importReview.getUrl())) {
- mangaProviderRepository.save(
- MangaProvider.builder()
- .manga(manga)
- .mangaTitle(importReview.getTitle())
- .provider(importReview.getProvider())
- .url(importReview.getUrl())
- .build());
- }
-
- mangaImportReviewRepository.delete(importReview);
-
- updateMangaDataProducer.sendUpdateMangaDataCommand(new UpdateMangaDataCommand(manga.getId()));
- }
-
- private MangaImportReview getImportReviewThrowIfNotFound(Long id) {
- return mangaImportReviewRepository
- .findById(id)
- .orElseThrow(() -> new NotFoundException("Import review not found for ID: " + id));
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/service/MangaImportService.java b/src/main/java/com/magamochi/mangamochi/service/MangaImportService.java
deleted file mode 100644
index 1948bfa..0000000
--- a/src/main/java/com/magamochi/mangamochi/service/MangaImportService.java
+++ /dev/null
@@ -1,430 +0,0 @@
-package com.magamochi.mangamochi.service;
-
-import static java.util.Objects.isNull;
-import static java.util.Objects.nonNull;
-
-import com.google.common.util.concurrent.RateLimiter;
-import com.magamochi.mangamochi.client.AniListClient;
-import com.magamochi.mangamochi.client.JikanClient;
-import com.magamochi.mangamochi.exception.NotFoundException;
-import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO;
-import com.magamochi.mangamochi.model.entity.*;
-import com.magamochi.mangamochi.model.repository.*;
-import com.magamochi.mangamochi.util.DoubleUtil;
-import java.io.*;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.time.OffsetDateTime;
-import java.time.ZoneOffset;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.List;
-import java.util.stream.IntStream;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.log4j.Log4j2;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.stereotype.Service;
-import org.springframework.web.multipart.MultipartFile;
-
-@Log4j2
-@Service
-@RequiredArgsConstructor
-public class MangaImportService {
- private final ProviderService providerService;
- private final MangaCreationService mangaCreationService;
- private final ImageService imageService;
- private final LanguageService languageService;
-
- private final GenreRepository genreRepository;
- private final MangaGenreRepository mangaGenreRepository;
- private final MangaProviderRepository mangaProviderRepository;
- private final AuthorRepository authorRepository;
- private final MangaAuthorRepository mangaAuthorRepository;
- private final MangaChapterRepository mangaChapterRepository;
- private final MangaRepository mangaRepository;
-
- private final JikanClient jikanClient;
- private final AniListClient aniListClient;
- private final MangaChapterImageRepository mangaChapterImageRepository;
- private final MangaAlternativeTitlesRepository mangaAlternativeTitlesRepository;
-
- private final RateLimiter jikanRateLimiter;
-
- public void importMangaFiles(String malId, List files) {
- log.info("Importing manga files for MAL ID {}", malId);
- var provider = providerService.getOrCreateProvider("Manual Import", false);
-
- jikanRateLimiter.acquire();
- var mangaData = jikanClient.getMangaById(Long.parseLong(malId));
-
- var mangaProvider = getOrCreateMangaProvider(mangaData.data().title(), provider);
-
- var sortedFiles = files.stream().sorted(Comparator.comparing(MultipartFile::getName)).toList();
-
- IntStream.rangeClosed(1, sortedFiles.size())
- .forEach(
- fileIndex -> {
- var file = sortedFiles.get(fileIndex - 1);
- log.info(
- "Importing file {}/{}: {}, for Mangá {}",
- fileIndex,
- sortedFiles.size(),
- file.getOriginalFilename(),
- mangaProvider.getManga().getTitle());
-
- var chapter =
- persistMangaChapter(
- mangaProvider,
- new ContentProviderMangaChapterResponseDTO(
- removeFileExtension(file.getOriginalFilename()),
- "manual_" + file.getOriginalFilename(),
- file.getOriginalFilename(),
- "en-US"));
-
- List allChapterImages = new ArrayList<>();
- try (InputStream is = file.getInputStream();
- ZipInputStream zis = new ZipInputStream(is)) {
- ZipEntry entry;
- var position = 0;
-
- while ((entry = zis.getNextEntry()) != null) {
- if (entry.isDirectory()) {
- continue;
- }
-
- var os = new ByteArrayOutputStream();
- zis.transferTo(os);
- var bytes = os.toByteArray();
-
- var image =
- imageService.uploadImage(bytes, "image/jpeg", "chapter/" + chapter.getId());
-
- var chapterImage =
- MangaChapterImage.builder()
- .position(position++)
- .image(image)
- .mangaChapter(chapter)
- .build();
-
- allChapterImages.add(chapterImage);
- zis.closeEntry();
- }
-
- log.info("Chapter images added for chapter {}", chapter.getTitle());
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
-
- mangaChapterImageRepository.saveAll(allChapterImages);
- chapter.setDownloaded(true);
- mangaChapterRepository.save(chapter);
- });
-
- log.info("Import manga files for MAL ID {} completed.", malId);
- }
-
- public void updateMangaData(Long mangaId) {
- var manga =
- mangaRepository
- .findById(mangaId)
- .orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
-
- updateMangaData(manga);
- }
-
- public void updateMangaData(Manga manga) {
- log.info("Updating manga {}", manga.getTitle());
-
- if (nonNull(manga.getMalId())) {
- try {
- updateFromJikan(manga);
- return;
- } catch (Exception e) {
- log.warn(
- "Error updating manga data from Jikan for manga {}. Trying AniList... Error: {}",
- manga.getTitle(),
- e.getMessage());
- }
- }
-
- if (nonNull(manga.getAniListId())) {
- try {
- updateFromAniList(manga);
- return;
- } catch (Exception e) {
- log.warn(
- "Error updating manga data from AniList for manga {}. Error: {}",
- manga.getTitle(),
- e.getMessage());
- }
- }
-
- log.warn(
- "Could not update manga data for {}. No provider data available/found.", manga.getTitle());
- }
-
- private void updateFromJikan(Manga manga) throws IOException, URISyntaxException {
- jikanRateLimiter.acquire();
- var mangaData = jikanClient.getMangaById(manga.getMalId());
-
- manga.setSynopsis(mangaData.data().synopsis());
- manga.setStatus(mangaData.data().status());
- manga.setScore(DoubleUtil.round((double) mangaData.data().score(), 2));
- manga.setPublishedFrom(mangaData.data().published().from());
- manga.setPublishedTo(mangaData.data().published().to());
- manga.setChapterCount(mangaData.data().chapters());
-
- var authors =
- mangaData.data().authors().stream()
- .map(
- authorData ->
- authorRepository
- .findByMalId(authorData.mal_id())
- .orElseGet(
- () ->
- authorRepository.save(
- Author.builder()
- .malId(authorData.mal_id())
- .name(authorData.name())
- .build())))
- .toList();
-
- updateMangaAuthors(manga, authors);
-
- var genres =
- mangaData.data().genres().stream()
- .map(
- genreData ->
- genreRepository
- .findByMalId(genreData.mal_id())
- .orElseGet(
- () ->
- genreRepository.save(
- Genre.builder()
- .malId(genreData.mal_id())
- .name(genreData.name())
- .build())))
- .toList();
-
- updateMangaGenres(manga, genres);
-
- if (isNull(manga.getCoverImage())) {
- downloadCoverImage(manga, mangaData.data().images().jpg().large_image_url());
- }
-
- var mangaEntity = mangaRepository.save(manga);
- var alternativeTitles =
- mangaData.data().title_synonyms().stream()
- .map(at -> MangaAlternativeTitle.builder().manga(mangaEntity).title(at).build())
- .toList();
- mangaAlternativeTitlesRepository.saveAll(alternativeTitles);
- }
-
- private void updateFromAniList(Manga manga) throws IOException, URISyntaxException {
- var query =
- """
- query ($id: Int) {
- Media (id: $id, type: MANGA) {
- startDate { year month day }
- endDate { year month day }
- description
- status
- averageScore
- chapters
- coverImage { large }
- genres
- staff {
- edges {
- role
- node {
- name {
- full
- }
- }
- }
- }
- }
- }
- """;
- var request =
- new AniListClient.GraphQLRequest(
- query, new AniListClient.GraphQLRequest.Variables(manga.getAniListId()));
- var media = aniListClient.getManga(request).data().Media();
-
- manga.setSynopsis(media.description());
- manga.setStatus(mapAniListStatus(media.status()));
- manga.setScore(DoubleUtil.round((double) media.averageScore() / 10, 2)); // 0-100 -> 0-10
- manga.setPublishedFrom(convertFuzzyDate(media.startDate()));
- manga.setPublishedTo(convertFuzzyDate(media.endDate()));
- manga.setChapterCount(media.chapters());
-
- var authors =
- media.staff().edges().stream()
- .filter(edge -> isAuthorRole(edge.role()))
- .map(edge -> edge.node().name().full())
- .distinct()
- .map(
- name ->
- authorRepository
- .findByName(name)
- .orElseGet(
- () -> authorRepository.save(Author.builder().name(name).build())))
- .toList();
-
- updateMangaAuthors(manga, authors);
-
- var genres =
- media.genres().stream()
- .map(
- name ->
- genreRepository
- .findByName(name)
- .orElseGet(() -> genreRepository.save(Genre.builder().name(name).build())))
- .toList();
-
- updateMangaGenres(manga, genres);
-
- if (isNull(manga.getCoverImage())) {
- downloadCoverImage(manga, media.coverImage().large());
- }
-
- mangaRepository.save(manga);
- }
-
- private boolean isAuthorRole(String role) {
- return role.equalsIgnoreCase("Story & Art")
- || role.equalsIgnoreCase("Story")
- || role.equalsIgnoreCase("Art");
- }
-
- private String mapAniListStatus(String status) {
- return switch (status) {
- case "RELEASING" -> "Publishing";
- case "FINISHED" -> "Finished";
- case "NOT_YET_RELEASED" -> "Not yet published";
- default -> "Unknown";
- };
- }
-
- private OffsetDateTime convertFuzzyDate(AniListClient.MangaResponse.Manga.FuzzyDate date) {
- if (isNull(date) || isNull(date.year())) {
- return null;
- }
- return OffsetDateTime.of(
- date.year(),
- isNull(date.month()) ? 1 : date.month(),
- isNull(date.day()) ? 1 : date.day(),
- 0,
- 0,
- 0,
- 0,
- ZoneOffset.UTC);
- }
-
- private void updateMangaAuthors(Manga manga, List authors) {
- var mangaAuthors =
- authors.stream()
- .map(
- author ->
- mangaAuthorRepository
- .findByMangaAndAuthor(manga, author)
- .orElseGet(
- () ->
- mangaAuthorRepository.save(
- MangaAuthor.builder().manga(manga).author(author).build())))
- .toList();
- manga.setMangaAuthors(mangaAuthors);
- }
-
- private void updateMangaGenres(Manga manga, List genres) {
- var mangaGenres =
- genres.stream()
- .map(
- genre ->
- mangaGenreRepository
- .findByMangaAndGenre(manga, genre)
- .orElseGet(
- () ->
- mangaGenreRepository.save(
- MangaGenre.builder().manga(manga).genre(genre).build())))
- .toList();
- manga.setMangaGenres(mangaGenres);
- }
-
- private void downloadCoverImage(Manga manga, String imageUrl)
- throws IOException, URISyntaxException {
- var inputStream =
- new BufferedInputStream(new URL(new URI(imageUrl).toASCIIString()).openStream());
-
- var bytes = inputStream.readAllBytes();
-
- inputStream.close();
- var image = imageService.uploadImage(bytes, "image/jpeg", "cover");
-
- manga.setCoverImage(image);
- }
-
- public MangaChapter persistMangaChapter(
- MangaProvider mangaProvider, ContentProviderMangaChapterResponseDTO chapter) {
- var mangaChapter =
- mangaChapterRepository
- .findByMangaProviderAndUrlIgnoreCase(mangaProvider, chapter.chapterUrl())
- .orElseGet(MangaChapter::new);
-
- mangaChapter.setMangaProvider(mangaProvider);
- mangaChapter.setTitle(chapter.chapterTitle());
- mangaChapter.setUrl(chapter.chapterUrl());
-
- var language = languageService.getOrThrow(chapter.languageCode());
- mangaChapter.setLanguage(language);
-
- if (nonNull(chapter.chapter())) {
- try {
- mangaChapter.setChapterNumber(Integer.parseInt(chapter.chapter()));
- } catch (NumberFormatException e) {
- log.warn(
- "Could not parse chapter number {} from manga {}",
- chapter.chapter(),
- mangaProvider.getManga().getTitle());
- }
- }
-
- return mangaChapterRepository.save(mangaChapter);
- }
-
- private MangaProvider getOrCreateMangaProvider(String title, Provider provider) {
- return mangaProviderRepository
- .findByMangaTitleIgnoreCaseAndProvider(title, provider)
- .orElseGet(
- () -> {
- jikanRateLimiter.acquire();
- var manga = mangaCreationService.getOrCreateManga(title, "manual", provider);
-
- return mangaProviderRepository.save(
- MangaProvider.builder()
- .manga(manga)
- .mangaTitle(manga.getTitle())
- .provider(provider)
- .url("manual")
- .build());
- });
- }
-
- private String removeFileExtension(String filename) {
- if (StringUtils.isBlank(filename)) {
- return filename;
- }
-
- int lastDotIndex = filename.lastIndexOf('.');
-
- // No dot, or dot is the first character (like .gitignore)
- if (lastDotIndex <= 0) {
- return filename;
- }
-
- return filename.substring(0, lastDotIndex);
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/service/MangaListService.java b/src/main/java/com/magamochi/mangamochi/service/MangaListService.java
deleted file mode 100644
index add1f16..0000000
--- a/src/main/java/com/magamochi/mangamochi/service/MangaListService.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.magamochi.mangamochi.service;
-
-import static java.util.Objects.isNull;
-
-import com.magamochi.mangamochi.model.entity.MangaProvider;
-import com.magamochi.mangamochi.model.repository.MangaProviderRepository;
-import com.magamochi.mangamochi.service.providers.PagedContentProviderFactory;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.log4j.Log4j2;
-import org.springframework.stereotype.Service;
-
-@Log4j2
-@Service
-@RequiredArgsConstructor
-public class MangaListService {
- private final ProviderService providerService;
- private final MangaCreationService mangaCreationService;
- private final PagedContentProviderFactory pagedContentProviderFactory;
-
- private final MangaProviderRepository mangaProviderRepository;
-
- public void updateMangaList(String contentProviderName, Integer page) {
- var contentProvider = pagedContentProviderFactory.getPagedContentProvider(contentProviderName);
- var provider = providerService.getOrCreateProvider(contentProviderName);
-
- var mangas = contentProvider.getMangasFromPage(page);
-
- mangas.forEach(
- mangaResponse -> {
- var mangaProvider =
- mangaProviderRepository.findByMangaTitleIgnoreCaseAndProvider(
- mangaResponse.title(), provider);
-
- if (mangaProvider.isPresent()) {
- return;
- }
-
- var manga =
- mangaCreationService.getOrCreateManga(
- mangaResponse.title(), mangaResponse.url(), provider);
-
- if (isNull(manga)) {
- return;
- }
-
- if (!mangaProviderRepository.existsByMangaAndProviderAndUrlIgnoreCase(
- manga, provider, mangaResponse.url())) {
- mangaProviderRepository.save(
- MangaProvider.builder()
- .manga(manga)
- .mangaTitle(mangaResponse.title())
- .provider(provider)
- .url(mangaResponse.url())
- .build());
- }
- });
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/service/MangaService.java b/src/main/java/com/magamochi/mangamochi/service/MangaService.java
deleted file mode 100644
index af57da5..0000000
--- a/src/main/java/com/magamochi/mangamochi/service/MangaService.java
+++ /dev/null
@@ -1,202 +0,0 @@
-package com.magamochi.mangamochi.service;
-
-import static java.util.Objects.nonNull;
-
-import com.magamochi.mangamochi.client.NtfyClient;
-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.MangaProvider;
-import com.magamochi.mangamochi.model.entity.UserMangaFollow;
-import com.magamochi.mangamochi.model.repository.*;
-import com.magamochi.mangamochi.model.specification.MangaSpecification;
-import com.magamochi.mangamochi.queue.MangaChapterDownloadProducer;
-import com.magamochi.mangamochi.service.providers.ContentProviderFactory;
-import java.net.*;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.log4j.Log4j2;
-import org.springframework.data.domain.Page;
-import org.springframework.data.domain.Pageable;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-@Log4j2
-@Service
-@RequiredArgsConstructor
-public class MangaService {
- private final MangaImportService mangaImportService;
- private final UserService userService;
- private final MangaRepository mangaRepository;
- private final MangaProviderRepository mangaProviderRepository;
-
- private final ContentProviderFactory contentProviderFactory;
- private final UserFavoriteMangaRepository userFavoriteMangaRepository;
-
- private final UserMangaFollowRepository userMangaFollowRepository;
-
- private final MangaChapterDownloadProducer mangaChapterDownloadProducer;
- private final MangaChapterRepository mangaChapterRepository;
-
- private final NtfyClient ntfyClient;
-
- public void fetchAllNotDownloadedChapters(Long mangaProviderId) {
- var mangaProvider =
- mangaProviderRepository
- .findById(mangaProviderId)
- .orElseThrow(
- () -> new NotFoundException("Manga Provider not found for ID: " + mangaProviderId));
-
- var chapterIds =
- mangaProvider.getMangaChapters().stream()
- .filter(mangaChapter -> !mangaChapter.getDownloaded())
- .map(MangaChapter::getId)
- .collect(Collectors.toSet());
-
- chapterIds.forEach(
- chapterId ->
- mangaChapterDownloadProducer.sendMangaChapterDownloadCommand(
- new MangaChapterDownloadCommand(chapterId)));
- }
-
- public Page getMangas(MangaListFilterDTO filterDTO, Pageable pageable) {
- var user = userService.getLoggedUser();
-
- var specification = MangaSpecification.getMangaListSpecification(filterDTO, user);
-
- var favoriteMangasIds =
- nonNull(user)
- ? userFavoriteMangaRepository.findByUser(user).stream()
- .map(ufm -> ufm.getManga().getId())
- .collect(Collectors.toSet())
- : Set.of();
-
- return mangaRepository
- .findAll(specification, pageable)
- .map(
- manga -> {
- var favorite = favoriteMangasIds.contains(manga.getId());
- return MangaListDTO.from(manga, favorite);
- });
- }
-
- public List getMangaChapters(Long mangaProviderId) {
- var mangaProvider = getMangaProviderThrowIfNotFound(mangaProviderId);
-
- return mangaProvider.getMangaChapters().stream()
- .sorted(Comparator.comparing(MangaChapter::getId))
- .map(MangaChapterDTO::from)
- .toList();
- }
-
- public MangaDTO getManga(Long mangaId) {
- var manga = findMangaByIdThrowIfNotFound(mangaId);
- var user = userService.getLoggedUser();
-
- var favoriteMangasIds =
- nonNull(user)
- ? userFavoriteMangaRepository.findByUser(user).stream()
- .map(ufm -> ufm.getManga().getId())
- .collect(Collectors.toSet())
- : Set.of();
-
- var followingMangaIds =
- nonNull(user)
- ? userMangaFollowRepository.findByUser(user).stream()
- .map(umf -> umf.getManga().getId())
- .collect(Collectors.toSet())
- : Set.of();
-
- return MangaDTO.from(
- manga,
- favoriteMangasIds.contains(manga.getId()),
- followingMangaIds.contains(manga.getId()));
- }
-
- public void fetchFollowedMangaChapters(Long mangaProviderId) {
- var mangaProvider =
- mangaProviderRepository
- .findById(mangaProviderId)
- .orElseThrow(
- () -> new NotFoundException("Manga Provider not found for ID: " + mangaProviderId));
-
- var currentAvailableChapterCount =
- mangaChapterRepository.findByMangaProviderId(mangaProviderId).size();
-
- fetchMangaChapters(mangaProviderId);
- mangaChapterRepository.flush();
-
- var availableChapterCount =
- mangaChapterRepository.findByMangaProviderId(mangaProviderId).size();
-
- if (availableChapterCount <= currentAvailableChapterCount) {
- return;
- }
-
- log.info("New chapters found for Manga Provider {}", mangaProviderId);
-
- var userMangaFollows = userMangaFollowRepository.findByManga(mangaProvider.getManga());
- userMangaFollows.forEach(
- umf ->
- ntfyClient.notify(
- new NtfyClient.Request(
- "mangamochi-" + umf.getUser().getId().toString(),
- umf.getManga().getTitle(),
- "New chapter available on " + mangaProvider.getProvider().getName())));
- }
-
- public void fetchMangaChapters(Long mangaProviderId) {
- var mangaProvider = getMangaProviderThrowIfNotFound(mangaProviderId);
-
- var contentProvider =
- contentProviderFactory.getContentProvider(mangaProvider.getProvider().getName());
- var availableChapters = contentProvider.getAvailableChapters(mangaProvider);
-
- availableChapters.forEach(
- chapter -> mangaImportService.persistMangaChapter(mangaProvider, chapter));
- }
-
- public Manga findMangaByIdThrowIfNotFound(Long mangaId) {
- return mangaRepository
- .findById(mangaId)
- .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));
- }
-
- @Transactional
- public void follow(Long mangaId) {
- var user = userService.getLoggedUserThrowIfNotFound();
-
- var manga = findMangaByIdThrowIfNotFound(mangaId);
- manga.setFollow(true);
-
- if (userMangaFollowRepository.existsByUserAndManga(user, manga)) {
- return;
- }
-
- userMangaFollowRepository.save(UserMangaFollow.builder().user(user).manga(manga).build());
- }
-
- @Transactional
- public void unfollow(Long mangaId) {
- var user = userService.getLoggedUserThrowIfNotFound();
- var manga = findMangaByIdThrowIfNotFound(mangaId);
-
- var userMangaFollow = userMangaFollowRepository.findByUserAndManga(user, manga);
- userMangaFollow.ifPresent(userMangaFollowRepository::delete);
-
- if (!userMangaFollowRepository.existsByManga(manga)) {
- manga.setFollow(false);
- }
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/service/ProviderManualMangaImportService.java b/src/main/java/com/magamochi/mangamochi/service/ProviderManualMangaImportService.java
deleted file mode 100644
index db9413f..0000000
--- a/src/main/java/com/magamochi/mangamochi/service/ProviderManualMangaImportService.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package com.magamochi.mangamochi.service;
-
-import static java.util.Objects.isNull;
-import static java.util.Objects.nonNull;
-
-import com.magamochi.mangamochi.exception.NotFoundException;
-import com.magamochi.mangamochi.model.dto.ImportMangaResponseDTO;
-import com.magamochi.mangamochi.model.dto.ImportRequestDTO;
-import com.magamochi.mangamochi.model.entity.*;
-import com.magamochi.mangamochi.model.enumeration.ProviderStatus;
-import com.magamochi.mangamochi.model.repository.*;
-import com.magamochi.mangamochi.service.providers.ManualImportContentProviderFactory;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.log4j.Log4j2;
-import org.springframework.stereotype.Service;
-
-@Log4j2
-@Service
-@RequiredArgsConstructor
-public class ProviderManualMangaImportService {
- private final MangaCreationService mangaCreationService;
-
- private final ManualImportContentProviderFactory contentProviderFactory;
-
- private final ProviderRepository providerRepository;
- private final MangaProviderRepository mangaProviderRepository;
-
- public ImportMangaResponseDTO importFromProvider(Long providerId, ImportRequestDTO requestDTO) {
- var provider = getProvider(providerId);
- var contentProvider = contentProviderFactory.getManualImportContentProvider(provider.getName());
-
- var title = contentProvider.getMangaTitle(requestDTO.id());
-
- var malId = nonNull(requestDTO.metadataId()) ? Long.parseLong(requestDTO.metadataId()) : null;
- var aniListId = nonNull(requestDTO.aniListId()) ? Long.parseLong(requestDTO.aniListId()) : null;
-
- var manga =
- nonNull(malId) || nonNull(aniListId)
- ? mangaCreationService.getOrCreateManga(malId, aniListId)
- : mangaCreationService.getOrCreateManga(title, requestDTO.id(), provider);
-
- if (isNull(manga)) {
- throw new NotFoundException("Manga could not be found or created for ID: " + requestDTO.id());
- }
-
- if (!mangaProviderRepository.existsByMangaAndProviderAndUrlIgnoreCase(
- manga, provider, requestDTO.id())) {
- mangaProviderRepository.save(
- MangaProvider.builder()
- .manga(manga)
- .mangaTitle(title)
- .provider(provider)
- .url(requestDTO.id())
- .build());
- }
-
- return new ImportMangaResponseDTO(manga.getId());
- }
-
- public Provider getProvider(Long providerId) {
- var provider =
- providerRepository
- .findById(providerId)
- .orElseThrow(() -> new NotFoundException("Provider not found"));
-
- if (!provider.getStatus().equals(ProviderStatus.ACTIVE)) {
- throw new IllegalStateException("Provider is not active");
- }
-
- if (!provider.getManualImport()) {
- throw new IllegalArgumentException("Manual import not supported");
- }
-
- return provider;
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/service/ProviderService.java b/src/main/java/com/magamochi/mangamochi/service/ProviderService.java
deleted file mode 100644
index 88581c2..0000000
--- a/src/main/java/com/magamochi/mangamochi/service/ProviderService.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.magamochi.mangamochi.service;
-
-import static java.util.Objects.nonNull;
-
-import com.magamochi.mangamochi.model.dto.ProviderListDTO;
-import com.magamochi.mangamochi.model.entity.Provider;
-import com.magamochi.mangamochi.model.enumeration.ProviderStatus;
-import com.magamochi.mangamochi.model.repository.ProviderRepository;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Service;
-
-@Service
-@RequiredArgsConstructor
-public class ProviderService {
- private final ProviderRepository providerRepository;
-
- public ProviderListDTO getProviders(Boolean manualImport) {
- var providers = providerRepository.findAll();
-
- if (nonNull(manualImport) && manualImport) {
- providers = providers.stream().filter(Provider::getManualImport).toList();
- }
-
- return ProviderListDTO.from(providers);
- }
-
- public Provider getOrCreateProvider(String providerName) {
- return getOrCreateProvider(providerName, true);
- }
-
- public Provider getOrCreateProvider(String providerName, Boolean supportsChapterFetch) {
- return providerRepository
- .findByNameIgnoreCase(providerName)
- .orElseGet(
- () ->
- providerRepository.save(
- Provider.builder()
- .name(providerName)
- .status(ProviderStatus.ACTIVE)
- .supportsChapterFetch(supportsChapterFetch)
- .build()));
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/service/UserService.java b/src/main/java/com/magamochi/mangamochi/service/UserService.java
deleted file mode 100644
index 8287619..0000000
--- a/src/main/java/com/magamochi/mangamochi/service/UserService.java
+++ /dev/null
@@ -1,116 +0,0 @@
-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.RefreshTokenRequestDTO;
-import com.magamochi.mangamochi.model.dto.RegistrationRequestDTO;
-import com.magamochi.mangamochi.model.entity.User;
-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.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 {
- private final AuthenticationManager authenticationManager;
- private final UserDetailsService userDetailsService;
- private final JwtUtil jwtUtil;
- private final UserRepository userRepository;
- private final PasswordEncoder passwordEncoder;
-
- public AuthenticationResponseDTO authenticate(AuthenticationRequestDTO request) {
- authenticationManager.authenticate(
- new UsernamePasswordAuthenticationToken(request.email(), request.password()));
-
- var userDetails = userDetailsService.loadUserByUsername(request.email());
-
- var accessToken = jwtUtil.generateAccessToken(userDetails);
- var refreshToken = jwtUtil.generateRefreshToken(userDetails);
-
- var user = findUserByEmailThrowIfNotFound(userDetails.getUsername());
-
- return new AuthenticationResponseDTO(
- user.getId(),
- accessToken,
- refreshToken,
- userDetails.getUsername(),
- user.getName(),
- user.getRole());
- }
-
- public AuthenticationResponseDTO refreshAuthToken(
- RefreshTokenRequestDTO authenticationRequestDTO) {
- var username = jwtUtil.extractUsernameFromRefreshToken(authenticationRequestDTO.refreshToken());
-
- var userDetails = userDetailsService.loadUserByUsername(username);
-
- if (!jwtUtil.validateRefreshToken(authenticationRequestDTO.refreshToken(), userDetails)) {
- throw new BadCredentialsException("Invalid refresh token");
- }
-
- var newAccessToken = jwtUtil.generateAccessToken(userDetails);
- var newRefreshToken = jwtUtil.generateRefreshToken(userDetails);
-
- var user = findUserByEmailThrowIfNotFound(userDetails.getUsername());
-
- return new AuthenticationResponseDTO(
- user.getId(),
- newAccessToken,
- newRefreshToken,
- userDetails.getUsername(),
- user.getName(),
- user.getRole());
- }
-
- public void register(RegistrationRequestDTO request) {
- if (userRepository.existsByEmail(request.email())) {
- throw new ConflictException("An user with this email already exists.");
- }
-
- userRepository.save(
- User.builder()
- .name(request.name())
- .email(request.email())
- .password(passwordEncoder.encode(request.password()))
- .role(UserRole.USER)
- .build());
- }
-
- public User getLoggedUserThrowIfNotFound() {
- var authentication = SecurityContextHolder.getContext().getAuthentication();
- if (isNull(authentication) || authentication.getName().equals("anonymousUser")) {
- throw new NotFoundException("User not found.");
- }
-
- return findUserByEmailThrowIfNotFound(authentication.getName());
- }
-
- public User getLoggedUser() {
- var authentication = SecurityContextHolder.getContext().getAuthentication();
- if (isNull(authentication) || authentication.getName().equals("anonymousUser")) {
- return null;
- }
-
- return userRepository.findByEmail(authentication.getName()).orElse(null);
- }
-
- private User findUserByEmailThrowIfNotFound(String email) {
- return userRepository
- .findByEmail(email)
- .orElseThrow(() -> new NotFoundException("User not found."));
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/ContentProvider.java b/src/main/java/com/magamochi/mangamochi/service/providers/ContentProvider.java
deleted file mode 100644
index 9cb16cb..0000000
--- a/src/main/java/com/magamochi/mangamochi/service/providers/ContentProvider.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.magamochi.mangamochi.service.providers;
-
-import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO;
-import com.magamochi.mangamochi.model.entity.MangaProvider;
-import java.util.List;
-import java.util.Map;
-
-public interface ContentProvider {
- List getAvailableChapters(MangaProvider provider);
-
- Map getChapterImagesUrls(String chapterUrl);
-}
diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/PagedContentProvider.java b/src/main/java/com/magamochi/mangamochi/service/providers/PagedContentProvider.java
deleted file mode 100644
index 7cc59fe..0000000
--- a/src/main/java/com/magamochi/mangamochi/service/providers/PagedContentProvider.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.magamochi.mangamochi.service.providers;
-
-import com.magamochi.mangamochi.model.dto.ContentProviderMangaInfoResponseDTO;
-import java.util.List;
-
-public interface PagedContentProvider {
- Integer getTotalPages();
-
- List getMangasFromPage(Integer page);
-}
diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/impl/BatoProvider.java b/src/main/java/com/magamochi/mangamochi/service/providers/impl/BatoProvider.java
deleted file mode 100644
index d3bd1e1..0000000
--- a/src/main/java/com/magamochi/mangamochi/service/providers/impl/BatoProvider.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package com.magamochi.mangamochi.service.providers.impl;
-
-import static java.util.Objects.isNull;
-
-import com.magamochi.mangamochi.exception.UnprocessableException;
-import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO;
-import com.magamochi.mangamochi.model.entity.MangaProvider;
-import com.magamochi.mangamochi.service.FlareService;
-import com.magamochi.mangamochi.service.providers.ContentProvider;
-import com.magamochi.mangamochi.service.providers.ContentProviders;
-import com.magamochi.mangamochi.service.providers.ManualImportContentProvider;
-import java.util.*;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.log4j.Log4j2;
-import org.springframework.stereotype.Service;
-
-@Log4j2
-@Service(ContentProviders.BATO)
-@RequiredArgsConstructor
-public class BatoProvider implements ContentProvider, ManualImportContentProvider {
- private static final String URL = "https://battwo.com";
-
- private final FlareService flareService;
-
- @Override
- public List getAvailableChapters(MangaProvider provider) {
- try {
- var document =
- flareService.getContentAsJsoupDocument(provider.getUrl(), ContentProviders.BATO);
-
- // Direct selector for chapter links
- var chapterLinks = document.select("div.scrollable-panel a[href*=/title/]");
-
- // TODO: fix chapter and language code
- return chapterLinks.stream()
- .map(
- chapterLink ->
- new ContentProviderMangaChapterResponseDTO(
- chapterLink.text(), chapterLink.attr("href"), null, null))
- .toList();
- } catch (Exception e) {
- log.warn(e.getMessage());
- return null;
- }
- }
-
- @Override
- public Map getChapterImagesUrls(String chapterUrl) {
- try {
- var document =
- flareService.getContentAsJsoupDocument(
- URL + chapterUrl + "?load=2", ContentProviders.BATO);
-
- // Select all chapter page images
- var imgElements = document.select("img.z-10.w-full.h-full");
-
- List imageUrls = new ArrayList<>();
- for (var img : imgElements) {
- String src = img.attr("src");
-
- // Normalize if needed
- if (!src.startsWith("http")) {
- src = "https://battwo.com" + src;
- }
-
- imageUrls.add(src);
- }
-
- return IntStream.range(0, imageUrls.size())
- .boxed()
- .collect(
- Collectors.toMap(
- i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new));
-
- } catch (Exception e) {
- log.warn(e.getMessage());
- return null;
- }
- }
-
- @Override
- public String getMangaTitle(String value) {
- var document = flareService.getContentAsJsoupDocument(value, ContentProviders.BATO);
-
- var titleElement = document.selectFirst("h3 a[href*=/title/]");
- if (isNull(titleElement)) {
- titleElement = document.selectFirst("h3.item-title > a");
-
- if (isNull(titleElement)) {
- throw new UnprocessableException("Manga title not found for url: " + value);
- }
- }
-
- return titleElement.text();
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/task/UpdateMangaDataTask.java b/src/main/java/com/magamochi/mangamochi/task/UpdateMangaDataTask.java
deleted file mode 100644
index f0988b2..0000000
--- a/src/main/java/com/magamochi/mangamochi/task/UpdateMangaDataTask.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.magamochi.mangamochi.task;
-
-import static java.util.Objects.isNull;
-
-import com.magamochi.mangamochi.model.dto.UpdateMangaDataCommand;
-import com.magamochi.mangamochi.model.repository.*;
-import com.magamochi.mangamochi.queue.UpdateMangaDataProducer;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.log4j.Log4j2;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-@Log4j2
-@Component
-@RequiredArgsConstructor
-public class UpdateMangaDataTask {
- private final MangaRepository mangaRepository;
- private final UpdateMangaDataProducer updateMangaDataProducer;
-
- @Scheduled(cron = "@daily")
- public void updateMangaData() {
- var mangas =
- mangaRepository.findAll().stream().filter(manga -> isNull(manga.getScore())).toList();
-
- mangas.forEach(
- manga ->
- updateMangaDataProducer.sendUpdateMangaDataCommand(
- new UpdateMangaDataCommand(manga.getId())));
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/task/UpdateMangaListTask.java b/src/main/java/com/magamochi/mangamochi/task/UpdateMangaListTask.java
deleted file mode 100644
index 99cb900..0000000
--- a/src/main/java/com/magamochi/mangamochi/task/UpdateMangaListTask.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.magamochi.mangamochi.task;
-
-import com.magamochi.mangamochi.exception.NotFoundException;
-import com.magamochi.mangamochi.model.dto.MangaListUpdateCommand;
-import com.magamochi.mangamochi.model.repository.ProviderRepository;
-import com.magamochi.mangamochi.queue.UpdateMangaListProducer;
-import com.magamochi.mangamochi.service.providers.PagedContentProvider;
-import com.magamochi.mangamochi.service.providers.PagedContentProviderFactory;
-import java.util.stream.IntStream;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.log4j.Log4j2;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-@Log4j2
-@Component
-@RequiredArgsConstructor
-public class UpdateMangaListTask {
- @Value("${content-providers.update-enabled}")
- private Boolean updateEnabled;
-
- private final PagedContentProviderFactory contentProviderFactory;
- private final UpdateMangaListProducer updateMangaListProducer;
- private final ProviderRepository providerRepository;
-
- @Scheduled(cron = "${content-providers.cron-expression}")
- public void updateMangaListScheduled() {
- if (!updateEnabled) {
- return;
- }
-
- updateMangaList();
- }
-
- public void updateMangaList() {
- log.info("Queuing manga list updates...");
-
- var contentProviders = contentProviderFactory.getContentProviders();
- contentProviders.forEach(this::updateProviderMangaList);
- }
-
- public void updateProviderMangaList(Long providerId) {
- var provider =
- providerRepository
- .findById(providerId)
- .orElseThrow(() -> new NotFoundException("Provider not found"));
- var contentProvider = contentProviderFactory.getPagedContentProvider(provider.getName());
-
- updateProviderMangaList(provider.getName(), contentProvider);
- }
-
- private void updateProviderMangaList(
- String contentProviderName, PagedContentProvider contentProvider) {
- log.info("Getting total pages for provider {}", contentProviderName);
-
- var pages = contentProvider.getTotalPages();
-
- IntStream.rangeClosed(1, pages)
- .forEach(
- page ->
- updateMangaListProducer.sendUpdateMangaListCommand(
- new MangaListUpdateCommand(contentProviderName, page)));
-
- log.info("Manga list update queued for content provider {}.", contentProviderName);
- }
-}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/ImportMangaResponseDTO.java b/src/main/java/com/magamochi/model/dto/ImportMangaResponseDTO.java
similarity index 70%
rename from src/main/java/com/magamochi/mangamochi/model/dto/ImportMangaResponseDTO.java
rename to src/main/java/com/magamochi/model/dto/ImportMangaResponseDTO.java
index 7a4327c..3810e7f 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/ImportMangaResponseDTO.java
+++ b/src/main/java/com/magamochi/model/dto/ImportMangaResponseDTO.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.model.dto;
import jakarta.validation.constraints.NotNull;
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/ImportRequestDTO.java b/src/main/java/com/magamochi/model/dto/ImportRequestDTO.java
similarity index 76%
rename from src/main/java/com/magamochi/mangamochi/model/dto/ImportRequestDTO.java
rename to src/main/java/com/magamochi/model/dto/ImportRequestDTO.java
index 2b0d9f9..e83fce5 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/ImportRequestDTO.java
+++ b/src/main/java/com/magamochi/model/dto/ImportRequestDTO.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.model.dto;
import jakarta.validation.constraints.NotNull;
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterArchiveDTO.java b/src/main/java/com/magamochi/model/dto/MangaChapterArchiveDTO.java
similarity index 81%
rename from src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterArchiveDTO.java
rename to src/main/java/com/magamochi/model/dto/MangaChapterArchiveDTO.java
index 503602a..30c7500 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterArchiveDTO.java
+++ b/src/main/java/com/magamochi/model/dto/MangaChapterArchiveDTO.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.model.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterDownloadCommand.java b/src/main/java/com/magamochi/model/dto/MangaChapterDownloadCommand.java
similarity index 58%
rename from src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterDownloadCommand.java
rename to src/main/java/com/magamochi/model/dto/MangaChapterDownloadCommand.java
index 3f72132..13d8b3a 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterDownloadCommand.java
+++ b/src/main/java/com/magamochi/model/dto/MangaChapterDownloadCommand.java
@@ -1,3 +1,3 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.model.dto;
public record MangaChapterDownloadCommand(Long chapterId) {}
diff --git a/src/main/java/com/magamochi/model/dto/MangaContentImagesDTO.java b/src/main/java/com/magamochi/model/dto/MangaContentImagesDTO.java
new file mode 100644
index 0000000..72dd26a
--- /dev/null
+++ b/src/main/java/com/magamochi/model/dto/MangaContentImagesDTO.java
@@ -0,0 +1,27 @@
+package com.magamochi.model.dto;
+
+import com.magamochi.content.model.entity.MangaContent;
+import com.magamochi.content.model.entity.MangaContentImage;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import java.util.Comparator;
+import java.util.List;
+
+public record MangaContentImagesDTO(
+ @NotNull Long id,
+ @NotBlank String mangaTitle,
+ Long previousContentId,
+ Long nextContentId,
+ @NotNull List<@NotBlank String> contentImageKeys) {
+ public static MangaContentImagesDTO from(MangaContent mangaContent, Long prevId, Long nextId) {
+ return new MangaContentImagesDTO(
+ mangaContent.getId(),
+ mangaContent.getTitle(),
+ prevId,
+ nextId,
+ mangaContent.getMangaContentImages().stream()
+ .sorted(Comparator.comparing(MangaContentImage::getPosition))
+ .map(mangaContentImage -> mangaContentImage.getImage().getObjectKey())
+ .toList());
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/MangaDexMangaDTO.java b/src/main/java/com/magamochi/model/dto/MangaDexMangaDTO.java
similarity index 86%
rename from src/main/java/com/magamochi/mangamochi/model/dto/MangaDexMangaDTO.java
rename to src/main/java/com/magamochi/model/dto/MangaDexMangaDTO.java
index 38c6753..127fcdf 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/MangaDexMangaDTO.java
+++ b/src/main/java/com/magamochi/model/dto/MangaDexMangaDTO.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.model.dto;
import java.util.List;
import java.util.Map;
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/UpdateMangaFollowChapterListCommand.java b/src/main/java/com/magamochi/model/dto/UpdateMangaFollowChapterListCommand.java
similarity index 63%
rename from src/main/java/com/magamochi/mangamochi/model/dto/UpdateMangaFollowChapterListCommand.java
rename to src/main/java/com/magamochi/model/dto/UpdateMangaFollowChapterListCommand.java
index 848720b..442ae9f 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/UpdateMangaFollowChapterListCommand.java
+++ b/src/main/java/com/magamochi/model/dto/UpdateMangaFollowChapterListCommand.java
@@ -1,3 +1,3 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.model.dto;
public record UpdateMangaFollowChapterListCommand(Long mangaProviderId) {}
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/UserFavoriteManga.java b/src/main/java/com/magamochi/model/entity/UserFavoriteManga.java
similarity index 80%
rename from src/main/java/com/magamochi/mangamochi/model/entity/UserFavoriteManga.java
rename to src/main/java/com/magamochi/model/entity/UserFavoriteManga.java
index a684f17..7788116 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/UserFavoriteManga.java
+++ b/src/main/java/com/magamochi/model/entity/UserFavoriteManga.java
@@ -1,5 +1,7 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.model.entity;
+import com.magamochi.catalog.model.entity.Manga;
+import com.magamochi.user.model.entity.User;
import jakarta.persistence.*;
import java.time.Instant;
import lombok.*;
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/UserMangaFollow.java b/src/main/java/com/magamochi/model/entity/UserMangaFollow.java
similarity index 75%
rename from src/main/java/com/magamochi/mangamochi/model/entity/UserMangaFollow.java
rename to src/main/java/com/magamochi/model/entity/UserMangaFollow.java
index 633c17d..36a7d63 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/UserMangaFollow.java
+++ b/src/main/java/com/magamochi/model/entity/UserMangaFollow.java
@@ -1,5 +1,7 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.model.entity;
+import com.magamochi.catalog.model.entity.Manga;
+import com.magamochi.user.model.entity.User;
import jakarta.persistence.*;
import lombok.*;
diff --git a/src/main/java/com/magamochi/model/enumeration/ArchiveFileType.java b/src/main/java/com/magamochi/model/enumeration/ArchiveFileType.java
new file mode 100644
index 0000000..4a26d3f
--- /dev/null
+++ b/src/main/java/com/magamochi/model/enumeration/ArchiveFileType.java
@@ -0,0 +1,6 @@
+package com.magamochi.model.enumeration;
+
+public enum ArchiveFileType {
+ CBZ,
+ CBR
+}
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/UserFavoriteMangaRepository.java b/src/main/java/com/magamochi/model/repository/UserFavoriteMangaRepository.java
similarity index 64%
rename from src/main/java/com/magamochi/mangamochi/model/repository/UserFavoriteMangaRepository.java
rename to src/main/java/com/magamochi/model/repository/UserFavoriteMangaRepository.java
index 5567b26..f5c478e 100644
--- a/src/main/java/com/magamochi/mangamochi/model/repository/UserFavoriteMangaRepository.java
+++ b/src/main/java/com/magamochi/model/repository/UserFavoriteMangaRepository.java
@@ -1,8 +1,8 @@
-package com.magamochi.mangamochi.model.repository;
+package com.magamochi.model.repository;
-import com.magamochi.mangamochi.model.entity.Manga;
-import com.magamochi.mangamochi.model.entity.User;
-import com.magamochi.mangamochi.model.entity.UserFavoriteManga;
+import com.magamochi.catalog.model.entity.Manga;
+import com.magamochi.model.entity.UserFavoriteManga;
+import com.magamochi.user.model.entity.User;
import java.util.Optional;
import java.util.Set;
import org.springframework.data.jpa.repository.JpaRepository;
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/UserMangaFollowRepository.java b/src/main/java/com/magamochi/model/repository/UserMangaFollowRepository.java
similarity index 68%
rename from src/main/java/com/magamochi/mangamochi/model/repository/UserMangaFollowRepository.java
rename to src/main/java/com/magamochi/model/repository/UserMangaFollowRepository.java
index 9bb374a..db84305 100644
--- a/src/main/java/com/magamochi/mangamochi/model/repository/UserMangaFollowRepository.java
+++ b/src/main/java/com/magamochi/model/repository/UserMangaFollowRepository.java
@@ -1,8 +1,8 @@
-package com.magamochi.mangamochi.model.repository;
+package com.magamochi.model.repository;
-import com.magamochi.mangamochi.model.entity.Manga;
-import com.magamochi.mangamochi.model.entity.User;
-import com.magamochi.mangamochi.model.entity.UserMangaFollow;
+import com.magamochi.catalog.model.entity.Manga;
+import com.magamochi.model.entity.UserMangaFollow;
+import com.magamochi.user.model.entity.User;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
diff --git a/src/main/java/com/magamochi/mangamochi/model/specification/MangaSpecification.java b/src/main/java/com/magamochi/model/specification/MangaSpecification.java
similarity index 92%
rename from src/main/java/com/magamochi/mangamochi/model/specification/MangaSpecification.java
rename to src/main/java/com/magamochi/model/specification/MangaSpecification.java
index 267938e..6ade1e1 100644
--- a/src/main/java/com/magamochi/mangamochi/model/specification/MangaSpecification.java
+++ b/src/main/java/com/magamochi/model/specification/MangaSpecification.java
@@ -1,11 +1,11 @@
-package com.magamochi.mangamochi.model.specification;
+package com.magamochi.model.specification;
import static java.util.Objects.nonNull;
-import com.magamochi.mangamochi.model.dto.MangaListFilterDTO;
-import com.magamochi.mangamochi.model.entity.Author;
-import com.magamochi.mangamochi.model.entity.Manga;
-import com.magamochi.mangamochi.model.entity.User;
+import com.magamochi.catalog.model.dto.MangaListFilterDTO;
+import com.magamochi.catalog.model.entity.Author;
+import com.magamochi.catalog.model.entity.Manga;
+import com.magamochi.user.model.entity.User;
import jakarta.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;
diff --git a/src/main/java/com/magamochi/mangamochi/queue/MangaChapterDownloadProducer.java b/src/main/java/com/magamochi/queue/MangaChapterDownloadProducer.java
similarity index 86%
rename from src/main/java/com/magamochi/mangamochi/queue/MangaChapterDownloadProducer.java
rename to src/main/java/com/magamochi/queue/MangaChapterDownloadProducer.java
index dfafb53..740eaf4 100644
--- a/src/main/java/com/magamochi/mangamochi/queue/MangaChapterDownloadProducer.java
+++ b/src/main/java/com/magamochi/queue/MangaChapterDownloadProducer.java
@@ -1,6 +1,6 @@
-package com.magamochi.mangamochi.queue;
+package com.magamochi.queue;
-import com.magamochi.mangamochi.model.dto.MangaChapterDownloadCommand;
+import com.magamochi.model.dto.MangaChapterDownloadCommand;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
diff --git a/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaFollowChapterListConsumer.java b/src/main/java/com/magamochi/queue/UpdateMangaFollowChapterListConsumer.java
similarity index 65%
rename from src/main/java/com/magamochi/mangamochi/queue/UpdateMangaFollowChapterListConsumer.java
rename to src/main/java/com/magamochi/queue/UpdateMangaFollowChapterListConsumer.java
index 4d1632f..75efa22 100644
--- a/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaFollowChapterListConsumer.java
+++ b/src/main/java/com/magamochi/queue/UpdateMangaFollowChapterListConsumer.java
@@ -1,7 +1,7 @@
-package com.magamochi.mangamochi.queue;
+package com.magamochi.queue;
-import com.magamochi.mangamochi.model.dto.UpdateMangaFollowChapterListCommand;
-import com.magamochi.mangamochi.service.MangaService;
+import com.magamochi.model.dto.UpdateMangaFollowChapterListCommand;
+import com.magamochi.service.OldMangaService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
@@ -11,11 +11,11 @@ import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UpdateMangaFollowChapterListConsumer {
- private final MangaService mangaService;
+ private final OldMangaService oldMangaService;
@RabbitListener(queues = "${rabbit-mq.queues.manga-follow-update-chapter}")
public void receiveMangaFollowUpdateChapterCommand(UpdateMangaFollowChapterListCommand command) {
log.info("Received update followed manga chapter list command: {}", command);
- mangaService.fetchFollowedMangaChapters(command.mangaProviderId());
+ // oldMangaService.fetchFollowedMangaChapters(command.mangaProviderId());
}
}
diff --git a/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaFollowChapterListProducer.java b/src/main/java/com/magamochi/queue/UpdateMangaFollowChapterListProducer.java
similarity index 86%
rename from src/main/java/com/magamochi/mangamochi/queue/UpdateMangaFollowChapterListProducer.java
rename to src/main/java/com/magamochi/queue/UpdateMangaFollowChapterListProducer.java
index 14ee2c9..124d3a5 100644
--- a/src/main/java/com/magamochi/mangamochi/queue/UpdateMangaFollowChapterListProducer.java
+++ b/src/main/java/com/magamochi/queue/UpdateMangaFollowChapterListProducer.java
@@ -1,6 +1,6 @@
-package com.magamochi.mangamochi.queue;
+package com.magamochi.queue;
-import com.magamochi.mangamochi.model.dto.UpdateMangaFollowChapterListCommand;
+import com.magamochi.model.dto.UpdateMangaFollowChapterListCommand;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
diff --git a/src/main/java/com/magamochi/service/MangaChapterService.java b/src/main/java/com/magamochi/service/MangaChapterService.java
new file mode 100644
index 0000000..01ac56e
--- /dev/null
+++ b/src/main/java/com/magamochi/service/MangaChapterService.java
@@ -0,0 +1,86 @@
+package com.magamochi.service;
+
+import com.magamochi.common.exception.UnprocessableException;
+import com.magamochi.content.model.entity.MangaContent;
+import com.magamochi.content.model.entity.MangaContentImage;
+import com.magamochi.content.model.repository.MangaContentImageRepository;
+import com.magamochi.content.model.repository.MangaContentRepository;
+import com.magamochi.model.dto.MangaChapterArchiveDTO;
+import com.magamochi.model.enumeration.ArchiveFileType;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+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;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class MangaChapterService {
+ private final MangaContentRepository mangaContentRepository;
+ private final MangaContentImageRepository mangaContentImageRepository;
+
+ private final OldImageService oldImageService;
+
+ public void markAsRead(Long chapterId) {
+ // TODO: implement this
+ // 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 = mangaContentImageRepository.findAllByMangaContent(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 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(oldImageService.getImageStream(imgSrc.getImage()), zipOutputStream);
+ zipOutputStream.closeEntry();
+ }
+
+ zipOutputStream.finish();
+ zipOutputStream.flush();
+ IOUtils.closeQuietly(zipOutputStream);
+ return byteArrayOutputStream;
+ }
+
+ private MangaContent getMangaChapterThrowIfNotFound(Long chapterId) {
+ return mangaContentRepository
+ .findById(chapterId)
+ .orElseThrow(() -> new RuntimeException("Manga Chapter not found for ID: " + chapterId));
+ }
+}
diff --git a/src/main/java/com/magamochi/service/MangaImportService.java b/src/main/java/com/magamochi/service/MangaImportService.java
new file mode 100644
index 0000000..510f9c2
--- /dev/null
+++ b/src/main/java/com/magamochi/service/MangaImportService.java
@@ -0,0 +1,438 @@
+// package com.magamochi.service;
+//
+// import static java.util.Objects.isNull;
+// import static java.util.Objects.nonNull;
+//
+// import com.google.common.util.concurrent.RateLimiter;
+// import com.magamochi.catalog.model.entity.Genre;
+// import com.magamochi.catalog.model.repository.GenreRepository;
+// import com.magamochi.catalog.client.AniListClient;
+// import com.magamochi.catalog.client.JikanClient;
+// import com.magamochi.common.exception.NotFoundException;
+// import com.magamochi.ingestion.model.entity.ContentProvider;
+// import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
+// import com.magamochi.model.entity.*;
+// import com.magamochi.model.repository.*;
+// import com.magamochi.catalog.util.DoubleUtil;
+// import java.io.*;
+// import java.net.URI;
+// import java.net.URISyntaxException;
+// import java.net.URL;
+// import java.time.OffsetDateTime;
+// import java.time.ZoneOffset;
+// import java.util.ArrayList;
+// import java.util.Comparator;
+// import java.util.List;
+// import java.util.stream.IntStream;
+// import java.util.zip.ZipEntry;
+// import java.util.zip.ZipInputStream;
+// import lombok.RequiredArgsConstructor;
+// import lombok.extern.log4j.Log4j2;
+// import org.apache.commons.lang3.StringUtils;
+// import org.springframework.stereotype.Service;
+// import org.springframework.web.multipart.MultipartFile;
+//
+// @Log4j2
+//// @Service
+// @RequiredArgsConstructor
+// public class MangaImportService {
+// private final ProviderService providerService;
+// private final MangaCreationService mangaCreationService;
+// private final ImageService imageService;
+// private final LanguageService languageService;
+//
+// private final GenreRepository genreRepository;
+// private final MangaGenreRepository mangaGenreRepository;
+// private final MangaContentProviderRepository mangaContentProviderRepository;
+// private final AuthorRepository authorRepository;
+// private final MangaAuthorRepository mangaAuthorRepository;
+// private final MangaChapterRepository mangaChapterRepository;
+// private final MangaRepository mangaRepository;
+//
+// private final JikanClient jikanClient;
+// private final AniListClient aniListClient;
+// private final MangaChapterImageRepository mangaChapterImageRepository;
+// private final MangaAlternativeTitlesRepository mangaAlternativeTitlesRepository;
+//
+// private final RateLimiter jikanRateLimiter;
+//
+// public void importMangaFiles(String malId, List files) {
+// log.info("Importing manga files for MAL ID {}", malId);
+// var provider = providerService.getOrCreateProvider("Manual Import", false);
+//
+// jikanRateLimiter.acquire();
+// var mangaData = jikanClient.getMangaById(Long.parseLong(malId));
+//
+// var mangaProvider = getOrCreateMangaProvider(mangaData.data().title(), provider);
+//
+// var sortedFiles =
+// files.stream().sorted(Comparator.comparing(MultipartFile::getName)).toList();
+//
+// IntStream.rangeClosed(1, sortedFiles.size())
+// .forEach(
+// fileIndex -> {
+// var file = sortedFiles.get(fileIndex - 1);
+// log.info(
+// "Importing file {}/{}: {}, for Mangá {}",
+// fileIndex,
+// sortedFiles.size(),
+// file.getOriginalFilename(),
+// mangaProvider.getManga().getTitle());
+//
+// var chapter =
+// persistMangaChapter(
+// mangaProvider,
+// new ContentProviderMangaChapterResponseDTO(
+// removeFileExtension(file.getOriginalFilename()),
+// "manual_" + file.getOriginalFilename(),
+// file.getOriginalFilename(),
+// "en-US"));
+//
+// List allChapterImages = new ArrayList<>();
+// try (InputStream is = file.getInputStream();
+// ZipInputStream zis = new ZipInputStream(is)) {
+// ZipEntry entry;
+// var position = 0;
+//
+// while ((entry = zis.getNextEntry()) != null) {
+// if (entry.isDirectory()) {
+// continue;
+// }
+//
+// var os = new ByteArrayOutputStream();
+// zis.transferTo(os);
+// var bytes = os.toByteArray();
+//
+// var image =
+// imageService.uploadImage(bytes, "image/jpeg", "chapter/" + chapter.getId());
+//
+// var chapterImage =
+// MangaChapterImage.builder()
+// .position(position++)
+// .image(image)
+// .mangaChapter(chapter)
+// .build();
+//
+// allChapterImages.add(chapterImage);
+// zis.closeEntry();
+// }
+//
+// log.info("Chapter images added for chapter {}", chapter.getTitle());
+// } catch (IOException e) {
+// throw new RuntimeException(e);
+// }
+//
+// mangaChapterImageRepository.saveAll(allChapterImages);
+// chapter.setDownloaded(true);
+// mangaChapterRepository.save(chapter);
+// });
+//
+// log.info("Import manga files for MAL ID {} completed.", malId);
+// }
+//
+// public void updateMangaData(Long mangaId) {
+// var manga =
+// mangaRepository
+// .findById(mangaId)
+// .orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
+//
+// updateMangaData(manga);
+// }
+//
+// public void updateMangaData(Manga manga) {
+// log.info("Updating manga {}", manga.getTitle());
+//
+// if (nonNull(manga.getMalId())) {
+// try {
+// updateFromJikan(manga);
+// return;
+// } catch (Exception e) {
+// log.warn(
+// "Error updating manga data from Jikan for manga {}. Trying AniList... Error: {}",
+// manga.getTitle(),
+// e.getMessage());
+// }
+// }
+//
+// if (nonNull(manga.getAniListId())) {
+// try {
+// updateFromAniList(manga);
+// return;
+// } catch (Exception e) {
+// log.warn(
+// "Error updating manga data from AniList for manga {}. Error: {}",
+// manga.getTitle(),
+// e.getMessage());
+// }
+// }
+//
+// log.warn(
+// "Could not update manga data for {}. No provider data available/found.",
+// manga.getTitle());
+// }
+//
+// private void updateFromJikan(Manga manga) throws IOException, URISyntaxException {
+// jikanRateLimiter.acquire();
+// var mangaData = jikanClient.getMangaById(manga.getMalId());
+//
+// manga.setSynopsis(mangaData.data().synopsis());
+// manga.setStatus(mangaData.data().status());
+// manga.setScore(DoubleUtil.round((double) mangaData.data().score(), 2));
+// manga.setPublishedFrom(mangaData.data().published().from());
+// manga.setPublishedTo(mangaData.data().published().to());
+// manga.setChapterCount(mangaData.data().chapters());
+//
+// var authors =
+// mangaData.data().authors().stream()
+// .map(
+// authorData ->
+// authorRepository
+// .findByMalId(authorData.mal_id())
+// .orElseGet(
+// () ->
+// authorRepository.save(
+// Author.builder()
+// .malId(authorData.mal_id())
+// .name(authorData.name())
+// .build())))
+// .toList();
+//
+// updateMangaAuthors(manga, authors);
+//
+// var genres =
+// mangaData.data().genres().stream()
+// .map(
+// genreData ->
+// genreRepository
+// .findByMalId(genreData.mal_id())
+// .orElseGet(
+// () ->
+// genreRepository.save(
+// Genre.builder()
+// .malId(genreData.mal_id())
+// .name(genreData.name())
+// .build())))
+// .toList();
+//
+// updateMangaGenres(manga, genres);
+//
+// if (isNull(manga.getCoverImage())) {
+// downloadCoverImage(manga, mangaData.data().images().jpg().large_image_url());
+// }
+//
+// var mangaEntity = mangaRepository.save(manga);
+// var alternativeTitles =
+// mangaData.data().title_synonyms().stream()
+// .map(at -> MangaAlternativeTitle.builder().manga(mangaEntity).title(at).build())
+// .toList();
+// mangaAlternativeTitlesRepository.saveAll(alternativeTitles);
+// }
+//
+// private void updateFromAniList(Manga manga) throws IOException, URISyntaxException {
+// var query =
+// """
+// query ($id: Int) {
+// Media (id: $id, type: MANGA) {
+// startDate { year month day }
+// endDate { year month day }
+// description
+// status
+// averageScore
+// chapters
+// coverImage { large }
+// genres
+// staff {
+// edges {
+// role
+// node {
+// name {
+// full
+// }
+// }
+// }
+// }
+// }
+// }
+// """;
+// var request =
+// new AniListClient.GraphQLRequest(
+// query, new AniListClient.GraphQLRequest.Variables(manga.getAniListId()));
+// var media = aniListClient.getManga(request).data().Media();
+//
+// manga.setSynopsis(media.description());
+// manga.setStatus(mapAniListStatus(media.status()));
+// manga.setScore(DoubleUtil.round((double) media.averageScore() / 10, 2)); // 0-100 -> 0-10
+// manga.setPublishedFrom(convertFuzzyDate(media.startDate()));
+// manga.setPublishedTo(convertFuzzyDate(media.endDate()));
+// manga.setChapterCount(media.chapters());
+//
+// var authors =
+// media.staff().edges().stream()
+// .filter(edge -> isAuthorRole(edge.role()))
+// .map(edge -> edge.node().name().full())
+// .distinct()
+// .map(
+// name ->
+// authorRepository
+// .findByName(name)
+// .orElseGet(
+// () -> authorRepository.save(Author.builder().name(name).build())))
+// .toList();
+//
+// updateMangaAuthors(manga, authors);
+//
+// var genres =
+// media.genres().stream()
+// .map(
+// name ->
+// genreRepository
+// .findByName(name)
+// .orElseGet(() ->
+// genreRepository.save(Genre.builder().name(name).build())))
+// .toList();
+//
+// updateMangaGenres(manga, genres);
+//
+// if (isNull(manga.getCoverImage())) {
+// downloadCoverImage(manga, media.coverImage().large());
+// }
+//
+// mangaRepository.save(manga);
+// }
+//
+// private boolean isAuthorRole(String role) {
+// return role.equalsIgnoreCase("Story & Art")
+// || role.equalsIgnoreCase("Story")
+// || role.equalsIgnoreCase("Art");
+// }
+//
+// private String mapAniListStatus(String status) {
+// return switch (status) {
+// case "RELEASING" -> "Publishing";
+// case "FINISHED" -> "Finished";
+// case "NOT_YET_RELEASED" -> "Not yet published";
+// default -> "Unknown";
+// };
+// }
+//
+// private OffsetDateTime convertFuzzyDate(AniListClient.MangaResponse.Manga.FuzzyDate date) {
+// if (isNull(date) || isNull(date.year())) {
+// return null;
+// }
+// return OffsetDateTime.of(
+// date.year(),
+// isNull(date.month()) ? 1 : date.month(),
+// isNull(date.day()) ? 1 : date.day(),
+// 0,
+// 0,
+// 0,
+// 0,
+// ZoneOffset.UTC);
+// }
+//
+// private void updateMangaAuthors(Manga manga, List authors) {
+// var mangaAuthors =
+// authors.stream()
+// .map(
+// author ->
+// mangaAuthorRepository
+// .findByMangaAndAuthor(manga, author)
+// .orElseGet(
+// () ->
+// mangaAuthorRepository.save(
+// MangaAuthor.builder().manga(manga).author(author).build())))
+// .toList();
+// manga.setMangaAuthors(mangaAuthors);
+// }
+//
+// private void updateMangaGenres(Manga manga, List genres) {
+// var mangaGenres =
+// genres.stream()
+// .map(
+// genre ->
+// mangaGenreRepository
+// .findByMangaAndGenre(manga, genre)
+// .orElseGet(
+// () ->
+// mangaGenreRepository.save(
+// MangaGenre.builder().manga(manga).genre(genre).build())))
+// .toList();
+// manga.setMangaGenres(mangaGenres);
+// }
+//
+// private void downloadCoverImage(Manga manga, String imageUrl)
+// throws IOException, URISyntaxException {
+// var inputStream =
+// new BufferedInputStream(new URL(new URI(imageUrl).toASCIIString()).openStream());
+//
+// var bytes = inputStream.readAllBytes();
+//
+// inputStream.close();
+// var image = imageService.uploadImage(bytes, "image/jpeg", "cover");
+//
+// manga.setCoverImage(image);
+// }
+//
+// public MangaChapter persistMangaChapter(
+// MangaContentProvider mangaContentProvider, ContentProviderMangaChapterResponseDTO chapter) {
+// var mangaChapter =
+// mangaChapterRepository
+// .findByMangaContentProviderAndUrlIgnoreCase(mangaContentProvider,
+// chapter.url())
+// .orElseGet(MangaChapter::new);
+//
+// mangaChapter.setMangaContentProvider(mangaContentProvider);
+// mangaChapter.setTitle(chapter.title());
+// mangaChapter.setUrl(chapter.url());
+//
+// var language = languageService.getOrThrow(chapter.languageCode());
+// mangaChapter.setLanguage(language);
+//
+// if (nonNull(chapter.chapter())) {
+// try {
+// mangaChapter.setChapterNumber(Integer.parseInt(chapter.chapter()));
+// } catch (NumberFormatException e) {
+// log.warn(
+// "Could not parse chapter number {} from manga {}",
+// chapter.chapter(),
+// mangaContentProvider.getManga().getTitle());
+// }
+// }
+//
+// return mangaChapterRepository.save(mangaChapter);
+// }
+//
+// private MangaContentProvider getOrCreateMangaProvider(
+// String title, ContentProvider contentProvider) {
+// return mangaContentProviderRepository
+// .findByMangaTitleIgnoreCaseAndContentProvider(title, contentProvider)
+// .orElseGet(
+// () -> {
+// jikanRateLimiter.acquire();
+// var manga = mangaCreationService.getOrCreateManga(title, "manual", contentProvider);
+//
+// return mangaContentProviderRepository.save(
+// MangaContentProvider.builder()
+// .manga(manga)
+// .mangaTitle(manga.getTitle())
+// .contentProvider(contentProvider)
+// .url("manual")
+// .build());
+// });
+// }
+//
+// private String removeFileExtension(String filename) {
+// if (StringUtils.isBlank(filename)) {
+// return filename;
+// }
+//
+// int lastDotIndex = filename.lastIndexOf('.');
+//
+// // No dot, or dot is the first character (like .gitignore)
+// if (lastDotIndex <= 0) {
+// return filename;
+// }
+//
+// return filename.substring(0, lastDotIndex);
+// }
+// }
diff --git a/src/main/java/com/magamochi/service/OldImageService.java b/src/main/java/com/magamochi/service/OldImageService.java
new file mode 100644
index 0000000..fe7ba46
--- /dev/null
+++ b/src/main/java/com/magamochi/service/OldImageService.java
@@ -0,0 +1,18 @@
+package com.magamochi.service;
+
+import com.magamochi.image.model.entity.Image;
+import java.io.InputStream;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class OldImageService {
+ private final OldS3Service oldS3Service;
+
+ public InputStream getImageStream(Image image) {
+ return oldS3Service.getFile(image.getObjectKey());
+ }
+}
diff --git a/src/main/java/com/magamochi/service/OldMangaService.java b/src/main/java/com/magamochi/service/OldMangaService.java
new file mode 100644
index 0000000..412cdb8
--- /dev/null
+++ b/src/main/java/com/magamochi/service/OldMangaService.java
@@ -0,0 +1,116 @@
+package com.magamochi.service;
+
+import com.magamochi.catalog.model.entity.Manga;
+import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
+import com.magamochi.catalog.model.repository.MangaRepository;
+import com.magamochi.common.exception.NotFoundException;
+import com.magamochi.content.model.entity.MangaContent;
+import com.magamochi.model.dto.*;
+import com.magamochi.model.entity.UserMangaFollow;
+import com.magamochi.model.repository.*;
+import com.magamochi.queue.MangaChapterDownloadProducer;
+import com.magamochi.user.service.UserService;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class OldMangaService {
+ private final UserService userService;
+ private final MangaRepository mangaRepository;
+ private final MangaContentProviderRepository mangaContentProviderRepository;
+
+ private final UserMangaFollowRepository userMangaFollowRepository;
+
+ private final MangaChapterDownloadProducer mangaChapterDownloadProducer;
+
+ public void fetchAllNotDownloadedChapters(Long mangaProviderId) {
+ var mangaProvider =
+ mangaContentProviderRepository
+ .findById(mangaProviderId)
+ .orElseThrow(
+ () -> new NotFoundException("Manga Provider not found for ID: " + mangaProviderId));
+
+ var chapterIds =
+ mangaProvider.getMangaContents().stream()
+ .filter(mangaChapter -> !mangaChapter.getDownloaded())
+ .map(MangaContent::getId)
+ .collect(Collectors.toSet());
+
+ chapterIds.forEach(
+ chapterId ->
+ mangaChapterDownloadProducer.sendMangaChapterDownloadCommand(
+ new MangaChapterDownloadCommand(chapterId)));
+ }
+
+ // public void fetchFollowedMangaChapters(Long mangaProviderId) {
+ // var mangaProvider =
+ // mangaContentProviderRepository
+ // .findById(mangaProviderId)
+ // .orElseThrow(
+ // () -> new NotFoundException("Manga Provider not found for ID: " +
+ // mangaProviderId));
+ //
+ // var currentAvailableChapterCount =
+ // mangaChapterRepository.findByMangaContentProviderId(mangaProviderId).size();
+ //
+ // fetchMangaChapters(mangaProviderId);
+ // mangaChapterRepository.flush();
+ //
+ // var availableChapterCount =
+ // mangaChapterRepository.findByMangaContentProviderId(mangaProviderId).size();
+ //
+ // if (availableChapterCount <= currentAvailableChapterCount) {
+ // return;
+ // }
+ //
+ // log.info("New chapters found for Manga Provider {}", mangaProviderId);
+ //
+ // var userMangaFollows = userMangaFollowRepository.findByManga(mangaProvider.getManga());
+ // userMangaFollows.forEach(
+ // umf ->
+ // ntfyClient.notify(
+ // new NtfyClient.Request(
+ // "mangamochi-" + umf.getUser().getId().toString(),
+ // umf.getManga().getTitle(),
+ // "New chapter available on " +
+ // mangaProvider.getContentProvider().getName())));
+ // }
+
+ public Manga findMangaByIdThrowIfNotFound(Long mangaId) {
+ return mangaRepository
+ .findById(mangaId)
+ .orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
+ }
+
+ @Transactional
+ public void follow(Long mangaId) {
+ var user = userService.getLoggedUserThrowIfNotFound();
+
+ var manga = findMangaByIdThrowIfNotFound(mangaId);
+ manga.setFollow(true);
+
+ if (userMangaFollowRepository.existsByUserAndManga(user, manga)) {
+ return;
+ }
+
+ userMangaFollowRepository.save(UserMangaFollow.builder().user(user).manga(manga).build());
+ }
+
+ @Transactional
+ public void unfollow(Long mangaId) {
+ var user = userService.getLoggedUserThrowIfNotFound();
+ var manga = findMangaByIdThrowIfNotFound(mangaId);
+
+ var userMangaFollow = userMangaFollowRepository.findByUserAndManga(user, manga);
+ userMangaFollow.ifPresent(userMangaFollowRepository::delete);
+
+ if (!userMangaFollowRepository.existsByManga(manga)) {
+ manga.setFollow(false);
+ }
+ }
+}
diff --git a/src/main/java/com/magamochi/service/OldS3Service.java b/src/main/java/com/magamochi/service/OldS3Service.java
new file mode 100644
index 0000000..d8cdc8d
--- /dev/null
+++ b/src/main/java/com/magamochi/service/OldS3Service.java
@@ -0,0 +1,23 @@
+package com.magamochi.service;
+
+import java.io.InputStream;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.*;
+
+@Service
+@RequiredArgsConstructor
+public class OldS3Service {
+ @Value("${minio.bucket}")
+ private String bucket;
+
+ private final S3Client s3Client;
+
+ public InputStream getFile(String key) {
+ var request = GetObjectRequest.builder().bucket(bucket).key(key).build();
+
+ return s3Client.getObject(request);
+ }
+}
diff --git a/src/main/java/com/magamochi/service/ProviderManualMangaImportService.java b/src/main/java/com/magamochi/service/ProviderManualMangaImportService.java
new file mode 100644
index 0000000..eff4cd9
--- /dev/null
+++ b/src/main/java/com/magamochi/service/ProviderManualMangaImportService.java
@@ -0,0 +1,78 @@
+package com.magamochi.service;
+
+import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
+import com.magamochi.common.exception.NotFoundException;
+import com.magamochi.ingestion.model.entity.ContentProvider;
+import com.magamochi.ingestion.model.repository.ContentProviderRepository;
+import com.magamochi.ingestion.providers.ManualImportContentProviderFactory;
+import com.magamochi.model.dto.ImportMangaResponseDTO;
+import com.magamochi.model.dto.ImportRequestDTO;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.apache.commons.lang3.NotImplementedException;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class ProviderManualMangaImportService {
+
+ private final ManualImportContentProviderFactory contentProviderFactory;
+
+ private final ContentProviderRepository contentProviderRepository;
+ private final MangaContentProviderRepository mangaContentProviderRepository;
+
+ public ImportMangaResponseDTO importFromProvider(Long providerId, ImportRequestDTO requestDTO) {
+ throw new NotImplementedException();
+ // var provider = getProvider(providerId);
+ // var contentProvider =
+ // contentProviderFactory.getManualImportContentProvider(provider.getName());
+ //
+ // var title = contentProvider.getMangaTitle(requestDTO.id());
+ //
+ // var malId = nonNull(requestDTO.metadataId()) ? Long.parseLong(requestDTO.metadataId()) :
+ // null;
+ // var aniListId = nonNull(requestDTO.aniListId()) ? Long.parseLong(requestDTO.aniListId()) :
+ // null;
+ //
+ // var manga =
+ // nonNull(malId) || nonNull(aniListId)
+ // ? mangaCreationService.getOrCreateManga(malId, aniListId)
+ // : mangaCreationService.getOrCreateManga(title, requestDTO.id(), provider);
+ //
+ // if (isNull(manga)) {
+ // throw new NotFoundException("Manga could not be found or created for ID: " +
+ // requestDTO.id());
+ // }
+ //
+ // if (!mangaContentProviderRepository.existsByMangaAndContentProviderAndUrlIgnoreCase(
+ // manga, provider, requestDTO.id())) {
+ // mangaContentProviderRepository.save(
+ // MangaContentProvider.builder()
+ // .manga(manga)
+ // .mangaTitle(title)
+ // .contentProvider(provider)
+ // .url(requestDTO.id())
+ // .build());
+ // }
+ //
+ // return new ImportMangaResponseDTO(manga.getId());
+ }
+
+ public ContentProvider getProvider(Long providerId) {
+ var provider =
+ contentProviderRepository
+ .findById(providerId)
+ .orElseThrow(() -> new NotFoundException("Provider not found"));
+
+ if (!provider.isActive()) {
+ throw new IllegalStateException("Provider is not active");
+ }
+
+ if (!provider.getManualImport()) {
+ throw new IllegalArgumentException("Manual import not supported");
+ }
+
+ return provider;
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/service/UserFavoriteMangaService.java b/src/main/java/com/magamochi/service/UserFavoriteMangaService.java
similarity index 78%
rename from src/main/java/com/magamochi/mangamochi/service/UserFavoriteMangaService.java
rename to src/main/java/com/magamochi/service/UserFavoriteMangaService.java
index f8571db..f4b3d8a 100644
--- a/src/main/java/com/magamochi/mangamochi/service/UserFavoriteMangaService.java
+++ b/src/main/java/com/magamochi/service/UserFavoriteMangaService.java
@@ -1,10 +1,11 @@
-package com.magamochi.mangamochi.service;
+package com.magamochi.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 com.magamochi.catalog.model.entity.Manga;
+import com.magamochi.catalog.model.repository.MangaRepository;
+import com.magamochi.common.exception.NotFoundException;
+import com.magamochi.model.entity.UserFavoriteManga;
+import com.magamochi.model.repository.UserFavoriteMangaRepository;
+import com.magamochi.user.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
diff --git a/src/main/java/com/magamochi/mangamochi/task/MangaFollowUpdateTask.java b/src/main/java/com/magamochi/task/MangaFollowUpdateTask.java
similarity index 72%
rename from src/main/java/com/magamochi/mangamochi/task/MangaFollowUpdateTask.java
rename to src/main/java/com/magamochi/task/MangaFollowUpdateTask.java
index 8e67714..9099c23 100644
--- a/src/main/java/com/magamochi/mangamochi/task/MangaFollowUpdateTask.java
+++ b/src/main/java/com/magamochi/task/MangaFollowUpdateTask.java
@@ -1,10 +1,9 @@
-package com.magamochi.mangamochi.task;
+package com.magamochi.task;
-import com.magamochi.mangamochi.model.dto.UpdateMangaFollowChapterListCommand;
-import com.magamochi.mangamochi.model.entity.Manga;
-import com.magamochi.mangamochi.model.enumeration.ProviderStatus;
-import com.magamochi.mangamochi.model.repository.MangaRepository;
-import com.magamochi.mangamochi.queue.UpdateMangaFollowChapterListProducer;
+import com.magamochi.catalog.model.entity.Manga;
+import com.magamochi.catalog.model.repository.MangaRepository;
+import com.magamochi.model.dto.UpdateMangaFollowChapterListCommand;
+import com.magamochi.queue.UpdateMangaFollowChapterListProducer;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
@@ -43,11 +42,10 @@ public class MangaFollowUpdateTask {
private void updateFollowedManga(Manga manga) {
log.info("Fetching available mangas for followed Manga {}", manga.getTitle());
- var mangaProviders = manga.getMangaProviders();
+ var mangaProviders = manga.getMangaContentProviders();
mangaProviders.stream()
- .filter(
- mangaProvider -> mangaProvider.getProvider().getStatus().equals(ProviderStatus.ACTIVE))
+ .filter(mangaProvider -> mangaProvider.getContentProvider().isActive())
.forEach(
mangaProvider ->
producer.sendUpdateMangaFollowChapterListCommand(
diff --git a/src/main/java/com/magamochi/user/controller/UserController.java b/src/main/java/com/magamochi/user/controller/UserController.java
new file mode 100644
index 0000000..30555a2
--- /dev/null
+++ b/src/main/java/com/magamochi/user/controller/UserController.java
@@ -0,0 +1,29 @@
+package com.magamochi.user.controller;
+
+import com.magamochi.common.model.dto.DefaultResponseDTO;
+import com.magamochi.user.model.dto.RegistrationRequestDTO;
+import com.magamochi.user.service.UserService;
+import io.swagger.v3.oas.annotations.Operation;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/user")
+@CrossOrigin(origins = "*")
+@RequiredArgsConstructor
+public class UserController {
+ private final UserService userService;
+
+ @Operation(
+ summary = "Register an user",
+ description = "Register a new user.",
+ tags = {"User"},
+ operationId = "registerUser")
+ @PostMapping("/register")
+ public DefaultResponseDTO registerUser(
+ @RequestBody RegistrationRequestDTO registrationRequestDTO) {
+ userService.register(registrationRequestDTO);
+
+ return DefaultResponseDTO.ok().build();
+ }
+}
diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/RegistrationRequestDTO.java b/src/main/java/com/magamochi/user/model/dto/RegistrationRequestDTO.java
similarity index 65%
rename from src/main/java/com/magamochi/mangamochi/model/dto/RegistrationRequestDTO.java
rename to src/main/java/com/magamochi/user/model/dto/RegistrationRequestDTO.java
index 55cb983..f73d8a7 100644
--- a/src/main/java/com/magamochi/mangamochi/model/dto/RegistrationRequestDTO.java
+++ b/src/main/java/com/magamochi/user/model/dto/RegistrationRequestDTO.java
@@ -1,3 +1,3 @@
-package com.magamochi.mangamochi.model.dto;
+package com.magamochi.user.model.dto;
public record RegistrationRequestDTO(String name, String email, String password) {}
diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/User.java b/src/main/java/com/magamochi/user/model/entity/User.java
similarity index 80%
rename from src/main/java/com/magamochi/mangamochi/model/entity/User.java
rename to src/main/java/com/magamochi/user/model/entity/User.java
index b40f6ff..a9b7f2c 100644
--- a/src/main/java/com/magamochi/mangamochi/model/entity/User.java
+++ b/src/main/java/com/magamochi/user/model/entity/User.java
@@ -1,6 +1,7 @@
-package com.magamochi.mangamochi.model.entity;
+package com.magamochi.user.model.entity;
-import com.magamochi.mangamochi.model.enumeration.UserRole;
+import com.magamochi.model.entity.UserFavoriteManga;
+import com.magamochi.user.model.enumeration.UserRole;
import jakarta.persistence.*;
import java.util.Set;
import lombok.*;
diff --git a/src/main/java/com/magamochi/user/model/enumeration/UserRole.java b/src/main/java/com/magamochi/user/model/enumeration/UserRole.java
new file mode 100644
index 0000000..21b7e54
--- /dev/null
+++ b/src/main/java/com/magamochi/user/model/enumeration/UserRole.java
@@ -0,0 +1,5 @@
+package com.magamochi.user.model.enumeration;
+
+public enum UserRole {
+ USER
+}
diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/UserRepository.java b/src/main/java/com/magamochi/user/repository/UserRepository.java
similarity index 70%
rename from src/main/java/com/magamochi/mangamochi/model/repository/UserRepository.java
rename to src/main/java/com/magamochi/user/repository/UserRepository.java
index 6b31929..b41596d 100644
--- a/src/main/java/com/magamochi/mangamochi/model/repository/UserRepository.java
+++ b/src/main/java/com/magamochi/user/repository/UserRepository.java
@@ -1,6 +1,6 @@
-package com.magamochi.mangamochi.model.repository;
+package com.magamochi.user.repository;
-import com.magamochi.mangamochi.model.entity.User;
+import com.magamochi.user.model.entity.User;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
diff --git a/src/main/java/com/magamochi/mangamochi/service/CustomUserDetailsService.java b/src/main/java/com/magamochi/user/service/CustomUserDetailsService.java
similarity index 63%
rename from src/main/java/com/magamochi/mangamochi/service/CustomUserDetailsService.java
rename to src/main/java/com/magamochi/user/service/CustomUserDetailsService.java
index 9ce19af..6351076 100644
--- a/src/main/java/com/magamochi/mangamochi/service/CustomUserDetailsService.java
+++ b/src/main/java/com/magamochi/user/service/CustomUserDetailsService.java
@@ -1,10 +1,11 @@
-package com.magamochi.mangamochi.service;
+package com.magamochi.user.service;
-import com.magamochi.mangamochi.model.repository.UserRepository;
+import com.magamochi.common.exception.NotFoundException;
+import com.magamochi.user.model.entity.User;
+import com.magamochi.user.repository.UserRepository;
import java.util.Collections;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
-import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -17,15 +18,17 @@ public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
- var user =
- userRepository
- .findByEmail(username)
- .orElseThrow(
- () -> new UsernameNotFoundException("User not found with email: " + username));
+ var user = find(username);
- return new User(
+ return new org.springframework.security.core.userdetails.User(
user.getEmail(),
user.getPassword(),
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole())));
}
+
+ private User find(String email) {
+ return userRepository
+ .findByEmail(email)
+ .orElseThrow(() -> new NotFoundException("User not found with email " + email + "."));
+ }
}
diff --git a/src/main/java/com/magamochi/user/service/UserService.java b/src/main/java/com/magamochi/user/service/UserService.java
new file mode 100644
index 0000000..019d5d2
--- /dev/null
+++ b/src/main/java/com/magamochi/user/service/UserService.java
@@ -0,0 +1,61 @@
+package com.magamochi.user.service;
+
+import static java.util.Objects.isNull;
+
+import com.magamochi.common.exception.ConflictException;
+import com.magamochi.common.exception.NotFoundException;
+import com.magamochi.user.model.dto.RegistrationRequestDTO;
+import com.magamochi.user.model.entity.User;
+import com.magamochi.user.model.enumeration.UserRole;
+import com.magamochi.user.repository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class UserService {
+ private final UserRepository userRepository;
+ private final PasswordEncoder passwordEncoder;
+
+ public void register(RegistrationRequestDTO request) {
+ if (userRepository.existsByEmail(request.email())) {
+ throw new ConflictException("An user with this email already exists.");
+ }
+
+ userRepository.save(
+ User.builder()
+ .name(request.name())
+ .email(request.email())
+ .password(passwordEncoder.encode(request.password()))
+ .role(UserRole.USER)
+ .build());
+ }
+
+ public User getLoggedUserThrowIfNotFound() {
+ var authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (isNull(authentication) || authentication.getName().equals("anonymousUser")) {
+ throw new NotFoundException("User not found.");
+ }
+
+ return find(authentication.getName());
+ }
+
+ public User getLoggedUser() {
+ var authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (isNull(authentication) || authentication.getName().equals("anonymousUser")) {
+ return null;
+ }
+
+ return userRepository.findByEmail(authentication.getName()).orElse(null);
+ }
+
+ public User find(String email) {
+ return userRepository
+ .findByEmail(email)
+ .orElseThrow(() -> new NotFoundException("User not found with email " + email + "."));
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 2ab855a..7613222 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -8,12 +8,12 @@ spring:
jpa:
properties:
hibernate:
- default_schema: mangamochi
+ default_schema: ${DB_SCHEMA:mangamochi}
flyway:
enabled: true
schemas:
- - mangamochi
- default-schema: mangamochi
+ - ${DB_SCHEMA:mangamochi}
+ default-schema: ${DB_SCHEMA:mangamochi}
servlet:
multipart:
max-file-size: 2GB
@@ -42,57 +42,36 @@ minio:
endpoint: ${MINIO_ENDPOINT}
accessKey: ${MINIO_USER}
secretKey: ${MINIO_PASS}
- bucket: mangamochi
+ bucket: ${MINIO_BUCKET}
+
+storage:
+ base-url: ${minio.endpoint}/${minio.bucket}
ntfy:
- endpoint: ${NTFY_ENDPOINT:https://ntfy.badger-pirarucu.ts.net}
+ endpoint: ${NTFY_ENDPOINT}
jwt:
- secret: /JcSdxjeyeuMGoK5GD9w7OfqK/j+nvHR7uVUY12pNis=
+ secret: ${JWT_SECRET}
expiration: 3600000
- refresh-secret: MIV9ctIwrImmrZBjh9QueNEcDOLLVv9Rephii+0DKbk=
+ refresh-secret: ${JWT_REFRESH_SECRET}
refresh-expiration: 2629746000
-resilience4j:
- retry:
- instances:
- FlareSolverrRetry:
- max-attempts: 2
- wait-duration:
- seconds: 5
- retry-exceptions:
- - feign.FeignException
- MangaDexRetry:
- max-attempts: 5
- wait-duration:
- seconds: 5
- retry-exceptions:
- - feign.FeignException
- JikanRetry:
- max-attempts: 5
- wait-duration:
- seconds: 5
- retry-exceptions:
- - feign.FeignException
- AniListRetry:
- max-attempts: 5
- wait-duration:
- seconds: 5
- retry-exceptions:
- - feign.FeignException
- ImageDownloadRetry:
- max-attempts: 3
- wait-duration:
- seconds: 5
- retry-exceptions:
- - java.io.IOException
- - java.net.SocketTimeoutException
+topics:
+ image-updates: ${IMAGE_UPDATES_TOPIC:mangamochi.image.updates}
+
+queues:
+ manga-ingest: ${MANGA_INGEST_QUEUE:mangamochi.manga.ingest}
+ manga-update: ${MANGA_UPDATE_QUEUE:mangamochi.manga.update}
+ manga-content-ingest: ${MANGA_CONTENT_INGEST_QUEUE:mangamochi.manga.content.ingest}
+ manga-content-image-ingest: ${MANGA_CONTENT_IMAGE_INGEST_QUEUE:mangamochi.manga.content.image.ingest}
+ manga-content-image-update: ${MANGA_CONTENT_IMAGE_UPDATE_QUEUE:mangamochi.manga.content.image.update}
+ provider-page-ingest: ${PROVIDER_PAGE_INGEST_QUEUE:mangamochi.provider.page.ingest}
+ image-fetch: ${IMAGE_FETCH_QUEUE:mangamochi.image.fetch}
+ manga-cover-update: ${MANGA_COVER_UDPATE_QUEUE:mangamochi.manga.cover.update}
rabbit-mq:
queues:
- manga-data-update: ${MANGA_DATA_UPDATE_QUEUE:mangaDataUpdateQueue}
manga-chapter-download: ${MANGA_CHAPTER_DOWNLOAD_QUEUE:mangaChapterDownloadQueue}
- manga-list-update: ${MANGA_LIST_UPDATE_QUEUE:mangaListUpdateQueue}
manga-follow-update-chapter: ${MANGA_FOLLOW_UPDATE_CHAPTER_QUEUE:mangaFollowUpdateChapterQueue}
image-service:
diff --git a/src/main/resources/db/migration/V0001__CREATE_TABLES.sql b/src/main/resources/db/migration/V0001__CREATE_TABLES.sql
new file mode 100644
index 0000000..ca96fe7
--- /dev/null
+++ b/src/main/resources/db/migration/V0001__CREATE_TABLES.sql
@@ -0,0 +1,163 @@
+CREATE TABLE IF NOT EXISTS languages
+(
+ id SERIAL PRIMARY KEY,
+ code VARCHAR(12) NOT NULL UNIQUE,
+ name VARCHAR(100) NOT NULL
+);
+
+INSERT INTO languages (code, name)
+VALUES ('en-US', 'English'),
+ ('es', 'Spanish'),
+ ('ja-JP', 'Japanese'),
+ ('pt-BR', 'Portuguese (Brazil)')
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE images
+(
+ id UUID NOT NULL PRIMARY KEY,
+ object_key VARCHAR NOT NULL,
+ file_hash VARCHAR(64) UNIQUE,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE mangas
+(
+ id BIGSERIAL NOT NULL PRIMARY KEY,
+ ani_list_id BIGINT UNIQUE,
+ mal_id BIGINT UNIQUE,
+ title VARCHAR,
+ status VARCHAR,
+ synopsis VARCHAR,
+ cover_image_id UUID REFERENCES images (id),
+ score DOUBLE PRECISION,
+ published_from TIMESTAMPTZ,
+ published_to TIMESTAMPTZ,
+ chapter_count INT DEFAULT 0,
+ follow BOOLEAN NOT NULL DEFAULT FALSE,
+ state VARCHAR NOT NULL DEFAULT 'PENDING',
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE content_providers
+(
+ id BIGSERIAL NOT NULL PRIMARY KEY,
+ name VARCHAR NOT NULL,
+ url VARCHAR NOT NULL,
+ active BOOLEAN NOT NULL DEFAULT TRUE,
+ supports_content_fetch BOOLEAN NOT NULL DEFAULT TRUE,
+ manual_import BOOLEAN NOT NULL DEFAULT FALSE,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE manga_content_provider
+(
+ id BIGSERIAL NOT NULL PRIMARY KEY,
+ manga_id BIGINT NOT NULL REFERENCES mangas (id) ON DELETE CASCADE,
+ content_provider_id BIGINT NOT NULL REFERENCES content_providers (id) ON DELETE CASCADE,
+ manga_title VARCHAR NOT NULL,
+ url VARCHAR NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE (manga_id, content_provider_id)
+);
+
+CREATE TABLE manga_contents
+(
+ id BIGSERIAL NOT NULL PRIMARY KEY,
+ manga_content_provider_id BIGINT NOT NULL REFERENCES manga_content_provider (id) ON DELETE CASCADE,
+ type VARCHAR(50) NOT NULL,
+ title VARCHAR NOT NULL,
+ url VARCHAR NOT NULL,
+ language_id BIGINT REFERENCES languages (id),
+ downloaded BOOLEAN DEFAULT FALSE,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE manga_content_images
+(
+ id BIGSERIAL NOT NULL PRIMARY KEY,
+ manga_content_id BIGINT NOT NULL REFERENCES manga_contents (id) ON DELETE CASCADE,
+ image_id UUID REFERENCES images (id) ON DELETE CASCADE,
+ position INT NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE manga_ingest_reviews
+(
+ id BIGSERIAL NOT NULL PRIMARY KEY,
+ content_provider_id BIGINT NOT NULL REFERENCES content_providers (id) ON DELETE CASCADE,
+ manga_title VARCHAR NOT NULL,
+ url VARCHAR NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE authors
+(
+ id BIGSERIAL NOT NULL PRIMARY KEY,
+ name VARCHAR,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE manga_author
+(
+ id BIGSERIAL NOT NULL PRIMARY KEY,
+ manga_id BIGINT REFERENCES mangas (id),
+ author_id BIGINT REFERENCES authors (id),
+ UNIQUE (manga_id, author_id)
+);
+
+CREATE TABLE genres
+(
+ id BIGSERIAL NOT NULL PRIMARY KEY,
+ name VARCHAR
+);
+
+CREATE TABLE manga_genre
+(
+ id BIGSERIAL NOT NULL PRIMARY KEY,
+ manga_id BIGINT REFERENCES mangas (id),
+ genre_id BIGINT REFERENCES genres (id),
+ UNIQUE (manga_id, genre_id)
+);
+
+CREATE TABLE users
+(
+ id SERIAL PRIMARY KEY,
+ email VARCHAR NOT NULL UNIQUE,
+ name VARCHAR NOT NULL,
+ password VARCHAR NOT NULL,
+ role VARCHAR
+);
+
+CREATE TABLE user_favorite_mangas
+(
+ id BIGSERIAL PRIMARY KEY,
+ user_id BIGINT NOT NULL,
+ manga_id BIGINT NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE (user_id, manga_id)
+);
+
+CREATE TABLE manga_alternative_titles
+(
+ id BIGSERIAL PRIMARY KEY,
+ manga_id BIGINT REFERENCES mangas (id),
+ title VARCHAR NOT NULL
+);
+
+CREATE INDEX idx_manga_alternative_titles_manga_id ON manga_alternative_titles (manga_id);
+
+CREATE INDEX idx_manga_alternative_titles_title ON manga_alternative_titles (title);
+
+CREATE TABLE user_manga_follow
+(
+ id BIGSERIAL NOT NULL PRIMARY KEY,
+ user_id BIGINT REFERENCES users (id),
+ manga_id BIGINT REFERENCES mangas (id)
+);
diff --git a/src/main/resources/db/migration/V0001__IMAGES_TABLE.sql b/src/main/resources/db/migration/V0001__IMAGES_TABLE.sql
deleted file mode 100644
index f501dbf..0000000
--- a/src/main/resources/db/migration/V0001__IMAGES_TABLE.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-CREATE TABLE images
-(
- id UUID NOT NULL PRIMARY KEY,
- file_key VARCHAR NOT NULL,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0002__CONTENT_PROVIDERS.sql b/src/main/resources/db/migration/V0002__CONTENT_PROVIDERS.sql
new file mode 100644
index 0000000..45cfb40
--- /dev/null
+++ b/src/main/resources/db/migration/V0002__CONTENT_PROVIDERS.sql
@@ -0,0 +1,4 @@
+INSERT INTO content_providers(name, url, active, supports_content_fetch, manual_import)
+VALUES ('Manga Livre Blog', 'https://mangalivre.blog', TRUE, TRUE, FALSE),
+ ('Manga Livre.to', 'https://mangalivre.to', TRUE, TRUE, FALSE),
+ ('Pink Rosa Scan', 'https://scanpinkrosa.blogspot.com', TRUE, TRUE, FALSE);
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0002__MANGA.sql b/src/main/resources/db/migration/V0002__MANGA.sql
deleted file mode 100644
index ba54fe3..0000000
--- a/src/main/resources/db/migration/V0002__MANGA.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-CREATE TABLE mangas
-(
- id BIGSERIAL NOT NULL PRIMARY KEY,
- mal_id BIGINT UNIQUE,
- title VARCHAR,
- alternative_titles TEXT[],
- status VARCHAR,
- synopsis VARCHAR,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
diff --git a/src/main/resources/db/migration/V0003__PROVIDER.sql b/src/main/resources/db/migration/V0003__PROVIDER.sql
deleted file mode 100644
index 95add80..0000000
--- a/src/main/resources/db/migration/V0003__PROVIDER.sql
+++ /dev/null
@@ -1,21 +0,0 @@
-CREATE TABLE providers
-(
- id BIGSERIAL NOT NULL PRIMARY KEY,
- name VARCHAR NOT NULL,
- status VARCHAR NOT NULL,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE TABLE manga_provider
-(
- id BIGSERIAL NOT NULL PRIMARY KEY,
- manga_id BIGINT NOT NULL REFERENCES mangas (id) ON DELETE CASCADE,
- provider_id BIGINT NOT NULL REFERENCES providers (id) ON DELETE CASCADE,
- manga_title VARCHAR NOT NULL,
- url VARCHAR NOT NULL,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
- UNIQUE (manga_id, provider_id)
-)
-
diff --git a/src/main/resources/db/migration/V0004__CHAPTER.sql b/src/main/resources/db/migration/V0004__CHAPTER.sql
deleted file mode 100644
index b809161..0000000
--- a/src/main/resources/db/migration/V0004__CHAPTER.sql
+++ /dev/null
@@ -1,20 +0,0 @@
-CREATE TABLE manga_chapters
-(
- id BIGSERIAL NOT NULL PRIMARY KEY,
- manga_provider_id BIGINT NOT NULL REFERENCES manga_provider (id) ON DELETE CASCADE,
- title VARCHAR NOT NULL,
- url VARCHAR NOT NULL,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE TABLE manga_chapter_images
-(
- id BIGSERIAL NOT NULL PRIMARY KEY,
- manga_chapter_id BIGINT NOT NULL REFERENCES manga_chapters (id) ON DELETE CASCADE,
- image_id UUID REFERENCES images (id) ON DELETE CASCADE,
- position INT NOT NULL,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-)
-
diff --git a/src/main/resources/db/migration/V0005__MANGA_IMPORT_REVIEW.sql b/src/main/resources/db/migration/V0005__MANGA_IMPORT_REVIEW.sql
deleted file mode 100644
index a470787..0000000
--- a/src/main/resources/db/migration/V0005__MANGA_IMPORT_REVIEW.sql
+++ /dev/null
@@ -1,8 +0,0 @@
-CREATE TABLE manga_import_reviews
-(
- id BIGSERIAL NOT NULL PRIMARY KEY,
- provider_id BIGINT NOT NULL REFERENCES providers (id) ON DELETE CASCADE,
- title VARCHAR NOT NULL,
- url VARCHAR NOT NULL,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
diff --git a/src/main/resources/db/migration/V0006__MANGA_DATA.sql b/src/main/resources/db/migration/V0006__MANGA_DATA.sql
deleted file mode 100644
index 07855df..0000000
--- a/src/main/resources/db/migration/V0006__MANGA_DATA.sql
+++ /dev/null
@@ -1,5 +0,0 @@
-ALTER TABLE mangas
- ADD COLUMN cover_image_id UUID REFERENCES images (id),
- ADD COLUMN score DOUBLE PRECISION,
- ADD COLUMN published_from TIMESTAMPTZ,
- ADD COLUMN published_to TIMESTAMPTZ;
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0007__MANGA_DATA.sql b/src/main/resources/db/migration/V0007__MANGA_DATA.sql
deleted file mode 100644
index 423884e..0000000
--- a/src/main/resources/db/migration/V0007__MANGA_DATA.sql
+++ /dev/null
@@ -1,31 +0,0 @@
-CREATE TABLE authors
-(
- id BIGSERIAL NOT NULL PRIMARY KEY,
- mal_id BIGINT UNIQUE,
- name VARCHAR,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE TABLE manga_author
-(
- id BIGSERIAL NOT NULL PRIMARY KEY,
- manga_id BIGINT REFERENCES mangas (id),
- author_id BIGINT REFERENCES authors (id),
- UNIQUE (manga_id, author_id)
-);
-
-CREATE TABLE genres
-(
- id BIGSERIAL NOT NULL PRIMARY KEY,
- mal_id BIGINT UNIQUE,
- name VARCHAR
-);
-
-CREATE TABLE manga_genre
-(
- id BIGSERIAL NOT NULL PRIMARY KEY,
- manga_id BIGINT REFERENCES mangas (id),
- genre_id BIGINT REFERENCES genres (id),
- UNIQUE (manga_id, genre_id)
-);
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0008__MANGA_CHAPTERS.sql b/src/main/resources/db/migration/V0008__MANGA_CHAPTERS.sql
deleted file mode 100644
index 38a5d65..0000000
--- a/src/main/resources/db/migration/V0008__MANGA_CHAPTERS.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE manga_chapters ADD COLUMN downloaded BOOLEAN DEFAULT FALSE;
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0009__MANGA_CHAPTERS.sql b/src/main/resources/db/migration/V0009__MANGA_CHAPTERS.sql
deleted file mode 100644
index 919388c..0000000
--- a/src/main/resources/db/migration/V0009__MANGA_CHAPTERS.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE manga_chapters ADD COLUMN read BOOLEAN DEFAULT FALSE;
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0010__USERS.sql b/src/main/resources/db/migration/V0010__USERS.sql
deleted file mode 100644
index ccb4ab3..0000000
--- a/src/main/resources/db/migration/V0010__USERS.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-CREATE TABLE users (
- id SERIAL PRIMARY KEY,
- email VARCHAR NOT NULL UNIQUE,
- name VARCHAR NOT NULL,
- password VARCHAR NOT NULL,
- role VARCHAR
-);
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0011__USER_FAVORITE_MANGA.sql b/src/main/resources/db/migration/V0011__USER_FAVORITE_MANGA.sql
deleted file mode 100644
index 8309f7c..0000000
--- a/src/main/resources/db/migration/V0011__USER_FAVORITE_MANGA.sql
+++ /dev/null
@@ -1,8 +0,0 @@
-CREATE TABLE user_favorite_mangas
-(
- id BIGSERIAL PRIMARY KEY,
- user_id BIGINT NOT NULL,
- manga_id BIGINT NOT NULL,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
- UNIQUE (user_id, manga_id)
-);
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0012__MANGA_CHAPTERS.sql b/src/main/resources/db/migration/V0012__MANGA_CHAPTERS.sql
deleted file mode 100644
index 43685c2..0000000
--- a/src/main/resources/db/migration/V0012__MANGA_CHAPTERS.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE mangas
- ADD COLUMN chapter_count INT DEFAULT 0;
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0013__MANGA_CHAPTER_LANGUAGE.sql b/src/main/resources/db/migration/V0013__MANGA_CHAPTER_LANGUAGE.sql
deleted file mode 100644
index f7ac5df..0000000
--- a/src/main/resources/db/migration/V0013__MANGA_CHAPTER_LANGUAGE.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-ALTER TABLE manga_chapters
- ADD COLUMN language VARCHAR(10) DEFAULT 'pt-br',
- ADD COLUMN chapter_number INTEGER;
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0014__MANGA_ALTERNATIVE_TITLES_TABLE.sql b/src/main/resources/db/migration/V0014__MANGA_ALTERNATIVE_TITLES_TABLE.sql
deleted file mode 100644
index 0efe1b5..0000000
--- a/src/main/resources/db/migration/V0014__MANGA_ALTERNATIVE_TITLES_TABLE.sql
+++ /dev/null
@@ -1,18 +0,0 @@
-CREATE TABLE manga_alternative_titles
-(
- id BIGSERIAL PRIMARY KEY,
- manga_id BIGINT REFERENCES mangas (id),
- title VARCHAR NOT NULL
-);
-
-INSERT INTO manga_alternative_titles (manga_id, title)
-SELECT id as manga_id,
- unnest(alternative_titles) as title
-FROM mangas
-WHERE alternative_titles IS NOT NULL;
-
-CREATE INDEX idx_manga_alternative_titles_manga_id ON manga_alternative_titles (manga_id);
-
-CREATE INDEX idx_manga_alternative_titles_title ON manga_alternative_titles (title);
-
-ALTER TABLE mangas DROP COLUMN alternative_titles;
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0015__PROVIDER_FETCH_FLAG.sql b/src/main/resources/db/migration/V0015__PROVIDER_FETCH_FLAG.sql
deleted file mode 100644
index 778fbac..0000000
--- a/src/main/resources/db/migration/V0015__PROVIDER_FETCH_FLAG.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE providers
- ADD COLUMN supports_chapter_fetch BOOLEAN NOT NULL DEFAULT true;
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0016__MANGA_FOLLOW.sql b/src/main/resources/db/migration/V0016__MANGA_FOLLOW.sql
deleted file mode 100644
index efbe73d..0000000
--- a/src/main/resources/db/migration/V0016__MANGA_FOLLOW.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-ALTER TABLE mangas
- ADD COLUMN follow BOOLEAN NOT NULL DEFAULT FALSE;
-
-CREATE TABLE user_manga_follow
-(
- id BIGSERIAL NOT NULL PRIMARY KEY,
- user_id BIGINT REFERENCES users (id),
- manga_id BIGINT REFERENCES mangas (id)
-);
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0017__MANGA_LIVRE_CLEANUP.sql b/src/main/resources/db/migration/V0017__MANGA_LIVRE_CLEANUP.sql
deleted file mode 100644
index 3530988..0000000
--- a/src/main/resources/db/migration/V0017__MANGA_LIVRE_CLEANUP.sql
+++ /dev/null
@@ -1,43 +0,0 @@
-DO
-$$
- DECLARE
- _target_provider_name TEXT := 'Manga Livre';
- _target_provider_id BIGINT;
- _deleted_chapters_count INTEGER;
- _deleted_manga_providers_count INTEGER;
-
- BEGIN
- SELECT id
- INTO _target_provider_id
- FROM providers
- WHERE name = _target_provider_name;
-
- IF _target_provider_id IS NULL THEN
- RAISE EXCEPTION 'Provider with name "%" not found.', _target_provider_name;
- END IF;
-
- UPDATE providers SET status = 'INACTIVE' WHERE id = _target_provider_id;
-
- -- Delete non-downloaded manga chapters associated with the target provider
- DELETE
- FROM manga_chapters mc
- USING manga_provider mp
- WHERE mc.manga_provider_id = mp.id
- AND mp.provider_id = _target_provider_id
- AND mc.downloaded = FALSE;
-
- GET DIAGNOSTICS _deleted_chapters_count = ROW_COUNT;
- RAISE NOTICE 'Deleted % non-downloaded chapters for provider ID %.', _deleted_chapters_count, _target_provider_id;
-
- -- Delete MangaProvider records ONLY if NO chapters
- DELETE
- FROM manga_provider mp
- WHERE mp.provider_id = _target_provider_id
- AND NOT EXISTS (SELECT 1
- FROM manga_chapters mc
- WHERE mc.manga_provider_id = mp.id);
-
- GET DIAGNOSTICS _deleted_manga_providers_count = ROW_COUNT;
- RAISE NOTICE 'Deleted % manga_provider entries', _deleted_manga_providers_count;
- END
-$$;
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0018__MANGA_PROVIDER_FLAG.sql b/src/main/resources/db/migration/V0018__MANGA_PROVIDER_FLAG.sql
deleted file mode 100644
index a450c21..0000000
--- a/src/main/resources/db/migration/V0018__MANGA_PROVIDER_FLAG.sql
+++ /dev/null
@@ -1,6 +0,0 @@
-ALTER TABLE providers
- ADD COLUMN manual_import BOOLEAN DEFAULT FALSE;
-
-UPDATE providers
-SET manual_import = TRUE
-WHERE name IN ('MangaDex', 'Bato');
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0019__CHAPTER_LANGUAGE.sql b/src/main/resources/db/migration/V0019__CHAPTER_LANGUAGE.sql
deleted file mode 100644
index 225e023..0000000
--- a/src/main/resources/db/migration/V0019__CHAPTER_LANGUAGE.sql
+++ /dev/null
@@ -1,40 +0,0 @@
-CREATE TABLE IF NOT EXISTS languages
-(
- id SERIAL PRIMARY KEY,
- code VARCHAR(12) NOT NULL UNIQUE,
- name VARCHAR(100) NOT NULL
-);
-
-INSERT INTO languages (code, name)
-VALUES ('en-US', 'English'),
- ('es', 'Spanish'),
- ('ja-JP', 'Japanese'),
- ('pt-BR', 'Portuguese (Brazil)')
-ON CONFLICT DO NOTHING;
-
-ALTER TABLE manga_chapters
- ADD COLUMN IF NOT EXISTS language_id BIGINT REFERENCES languages (id);
-
-UPDATE manga_chapters
-SET language = NULL;
-
-UPDATE manga_chapters mc
-SET language_id = (SELECT id FROM languages WHERE code = 'pt-BR')
-FROM manga_provider mp
- JOIN providers p ON mp.provider_id = p.id
-WHERE mc.manga_provider_id = mp.id
- AND mc.language IS NULL
- AND p.name ILIKE ANY
- (ARRAY ['Manga Livre Blog', 'Pink Rosa Scan', 'Manga Livre.to', 'Manga Livre', 'MangaDex', 'Bato', 'Taimu']);
-
-UPDATE manga_chapters mc
-SET language_id = (SELECT id FROM languages WHERE code = 'en-US')
-FROM manga_provider mp
- JOIN providers p ON mp.provider_id = p.id
-WHERE mc.manga_provider_id = mp.id
- AND mc.language IS NULL
- AND p.name ILIKE ANY
- (ARRAY ['Manual Import']);
-
-ALTER TABLE manga_chapters
- DROP COLUMN IF EXISTS language;
\ No newline at end of file
diff --git a/src/main/resources/db/migration/V0020__ADD_ANILIST_ID.sql b/src/main/resources/db/migration/V0020__ADD_ANILIST_ID.sql
deleted file mode 100644
index 0b9a4f7..0000000
--- a/src/main/resources/db/migration/V0020__ADD_ANILIST_ID.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE mangas ADD COLUMN ani_list_id BIGINT;
diff --git a/src/test/java/com/magamochi/mangamochi/MangamochiApplicationTests.java b/src/test/java/com/magamochi/MangamochiApplicationTests.java
similarity index 86%
rename from src/test/java/com/magamochi/mangamochi/MangamochiApplicationTests.java
rename to src/test/java/com/magamochi/MangamochiApplicationTests.java
index 3846a8d..4ac1bfd 100644
--- a/src/test/java/com/magamochi/mangamochi/MangamochiApplicationTests.java
+++ b/src/test/java/com/magamochi/MangamochiApplicationTests.java
@@ -1,4 +1,4 @@
-package com.magamochi.mangamochi;
+package com.magamochi;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;