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;