refactor-architecture #27
2
pom.xml
2
pom.xml
@ -145,6 +145,8 @@
|
||||
<artifactId>lombok</artifactId>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
<source>22</source>
|
||||
<target>22</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
|
||||
@ -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<Manga> media) {}
|
||||
}
|
||||
|
||||
record Manga(
|
||||
Long id,
|
||||
Long idMal,
|
||||
Title title,
|
||||
String status,
|
||||
String description,
|
||||
Integer chapters,
|
||||
Integer averageScore,
|
||||
CoverImage coverImage,
|
||||
List<String> 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<StaffEdge> edges) {
|
||||
public record StaffEdge(String role, Staff node) {
|
||||
public record Staff(Name name) {
|
||||
public record Name(String full) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
package com.magamochi.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<String> title_synonyms,
|
||||
String status,
|
||||
boolean publishing,
|
||||
Boolean publishing,
|
||||
String synopsis,
|
||||
float score,
|
||||
int chapters,
|
||||
Float score,
|
||||
Integer chapters,
|
||||
PublishData published,
|
||||
List<AuthorData> authors,
|
||||
List<GenreData> genres) {
|
||||
@ -1,11 +1,20 @@
|
||||
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.common.dto.DefaultResponseDTO;
|
||||
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
|
||||
@ -13,6 +22,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
@RequiredArgsConstructor
|
||||
public class CatalogController {
|
||||
private final GenreService genreService;
|
||||
private final MangaService mangaService;
|
||||
|
||||
@Operation(
|
||||
summary = "Get a list of manga genres",
|
||||
@ -23,4 +33,26 @@ public class CatalogController {
|
||||
public DefaultResponseDTO<List<GenreDTO>> 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<Page<MangaListDTO>> 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<MangaDTO> getManga(@PathVariable Long mangaId) {
|
||||
return DefaultResponseDTO.ok(mangaService.get(mangaId));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<List<MangaIngestReviewDTO>> 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<Void> 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<Void> resolveMangaIngestReview(
|
||||
@RequestParam Long id, @RequestParam String malId) {
|
||||
mangaIngestReviewService.resolveImportReview(id, malId);
|
||||
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
package com.magamochi.model.dto;
|
||||
package com.magamochi.catalog.model.dto;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
|
||||
import com.magamochi.model.entity.Manga;
|
||||
import com.magamochi.model.entity.MangaAlternativeTitle;
|
||||
import com.magamochi.catalog.model.entity.Manga;
|
||||
import com.magamochi.catalog.model.entity.MangaAlternativeTitle;
|
||||
import com.magamochi.catalog.model.enumeration.MangaStatus;
|
||||
import com.magamochi.model.entity.MangaChapter;
|
||||
import com.magamochi.model.entity.MangaContentProvider;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
@ -15,7 +16,7 @@ public record MangaDTO(
|
||||
@NotNull Long id,
|
||||
@NotBlank String title,
|
||||
String coverImageKey,
|
||||
String status,
|
||||
MangaStatus status,
|
||||
OffsetDateTime publishedFrom,
|
||||
OffsetDateTime publishedTo,
|
||||
String synopsis,
|
||||
@ -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<String> authors,
|
||||
List<String> genres,
|
||||
List<String> alternativeTitles,
|
||||
String coverImageUrl) {}
|
||||
@ -1,21 +1,21 @@
|
||||
package com.magamochi.model.dto;
|
||||
package com.magamochi.catalog.model.dto;
|
||||
|
||||
import com.magamochi.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.getMangaTitle(),
|
||||
review.getContentProvider().getName(),
|
||||
review.getUrl(),
|
||||
"Title match not found",
|
||||
@ -1,8 +1,9 @@
|
||||
package com.magamochi.model.dto;
|
||||
package com.magamochi.catalog.model.dto;
|
||||
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import com.magamochi.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,
|
||||
@ -1,4 +1,4 @@
|
||||
package com.magamochi.model.dto;
|
||||
package com.magamochi.catalog.model.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.magamochi.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;
|
||||
@ -1,6 +1,5 @@
|
||||
package com.magamochi.catalog.model.entity;
|
||||
|
||||
import com.magamochi.model.entity.MangaGenre;
|
||||
import jakarta.persistence.*;
|
||||
import java.util.List;
|
||||
import lombok.*;
|
||||
@ -17,8 +16,6 @@ public class Genre {
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private Long malId;
|
||||
|
||||
private String name;
|
||||
|
||||
@OneToMany(mappedBy = "genre")
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.magamochi.model.entity;
|
||||
package com.magamochi.catalog.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
@ -1,5 +1,10 @@
|
||||
package com.magamochi.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.model.entity.Image;
|
||||
import com.magamochi.model.entity.MangaContentProvider;
|
||||
import com.magamochi.model.entity.UserFavoriteManga;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
@ -26,14 +31,11 @@ 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<MangaContentProvider> mangaContentProviders;
|
||||
|
||||
@ -47,6 +49,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<MangaAuthor> mangaAuthors;
|
||||
|
||||
@ -58,8 +72,4 @@ public class Manga {
|
||||
|
||||
@OneToMany(mappedBy = "manga")
|
||||
private List<MangaAlternativeTitle> alternativeTitles;
|
||||
|
||||
@Builder.Default private Integer chapterCount = 0;
|
||||
|
||||
@Builder.Default private Boolean follow = false;
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package com.magamochi.model.entity;
|
||||
package com.magamochi.catalog.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
@ -1,4 +1,4 @@
|
||||
package com.magamochi.model.entity;
|
||||
package com.magamochi.catalog.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
@ -1,6 +1,5 @@
|
||||
package com.magamochi.model.entity;
|
||||
package com.magamochi.catalog.model.entity;
|
||||
|
||||
import com.magamochi.catalog.model.entity.Genre;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.magamochi.model.entity;
|
||||
package com.magamochi.catalog.model.entity;
|
||||
|
||||
import com.magamochi.ingestion.model.entity.ContentProvider;
|
||||
import jakarta.persistence.*;
|
||||
@ -7,18 +7,18 @@ 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;
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
package com.magamochi.catalog.model.enumeration;
|
||||
|
||||
public enum MangaState {
|
||||
PENDING,
|
||||
AVAILABLE,
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package com.magamochi.model.enumeration;
|
||||
package com.magamochi.catalog.model.enumeration;
|
||||
|
||||
public enum MangaStatus {
|
||||
ONGOING,
|
||||
@ -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<Author, Long> {}
|
||||
@ -1,11 +1,6 @@
|
||||
package com.magamochi.catalog.model.repository;
|
||||
|
||||
import com.magamochi.catalog.model.entity.Genre;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface GenreRepository extends JpaRepository<Genre, Long> {
|
||||
Optional<Genre> findByMalId(Long malId);
|
||||
|
||||
Optional<Genre> findByName(String name);
|
||||
}
|
||||
public interface GenreRepository extends JpaRepository<Genre, Long> {}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package com.magamochi.model.repository;
|
||||
package com.magamochi.catalog.model.repository;
|
||||
|
||||
import com.magamochi.model.entity.Language;
|
||||
import com.magamochi.catalog.model.entity.Language;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package com.magamochi.model.repository;
|
||||
package com.magamochi.catalog.model.repository;
|
||||
|
||||
import com.magamochi.model.entity.MangaAlternativeTitle;
|
||||
import com.magamochi.catalog.model.entity.MangaAlternativeTitle;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface MangaAlternativeTitlesRepository
|
||||
@ -1,8 +1,8 @@
|
||||
package com.magamochi.model.repository;
|
||||
package com.magamochi.catalog.model.repository;
|
||||
|
||||
import com.magamochi.model.entity.Author;
|
||||
import com.magamochi.model.entity.Manga;
|
||||
import com.magamochi.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;
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
package com.magamochi.catalog.model.repository;
|
||||
|
||||
import com.magamochi.model.entity.MangaContentProvider;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface MangaContentProviderRepository extends JpaRepository<MangaContentProvider, Long> {
|
||||
boolean existsByMangaTitleIgnoreCaseAndContentProvider_Id(
|
||||
@NotBlank String mangaTitle, long contentProviderId);
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
package com.magamochi.model.repository;
|
||||
package com.magamochi.catalog.model.repository;
|
||||
|
||||
import com.magamochi.catalog.model.entity.Genre;
|
||||
import com.magamochi.model.entity.Manga;
|
||||
import com.magamochi.model.entity.MangaGenre;
|
||||
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;
|
||||
|
||||
@ -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<MangaIngestReview, Long> {
|
||||
boolean existsByMangaTitleIgnoreCaseAndContentProvider_Id(String mangaTitle, long providerId);
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
package com.magamochi.model.repository;
|
||||
package com.magamochi.catalog.model.repository;
|
||||
|
||||
import com.magamochi.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;
|
||||
@ -0,0 +1,3 @@
|
||||
package com.magamochi.catalog.queue.command;
|
||||
|
||||
public record MangaUpdateCommand(long mangaId) {}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
179
src/main/java/com/magamochi/catalog/service/AniListService.java
Normal file
179
src/main/java/com/magamochi/catalog/service/AniListService.java
Normal file
@ -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<String, AniListClient.Manga> 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");
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
package com.magamochi.service;
|
||||
package com.magamochi.catalog.service;
|
||||
|
||||
import com.magamochi.catalog.model.entity.Language;
|
||||
import com.magamochi.catalog.model.repository.LanguageRepository;
|
||||
import com.magamochi.common.exception.NotFoundException;
|
||||
import com.magamochi.model.entity.Language;
|
||||
import com.magamochi.model.repository.LanguageRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@ -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<MangaIngestReviewDTO> 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));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
package com.magamochi.catalog.service;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
|
||||
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 com.magamochi.model.entity.MangaContentProvider;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<ProviderResult> 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<ProviderResult> 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) {}
|
||||
}
|
||||
@ -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.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<MangaListDTO> 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()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
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 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 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);
|
||||
// manga.setCoverImageId(imageId);
|
||||
//
|
||||
// log.info("Manga with ID {} cover image updated successfully", mangaId);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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<String, Long> 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,9 @@
|
||||
package com.magamochi.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.model.dto.TitleMatchRequestDTO;
|
||||
import com.magamochi.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<String> options, Integer threshold) {}
|
||||
|
||||
@Builder
|
||||
public record TitleMatchResponse(boolean matchFound, String bestMatch, Double similarity) {}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package com.magamochi.util;
|
||||
package com.magamochi.catalog.util;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
@ -1,51 +0,0 @@
|
||||
package com.magamochi.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<String> 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<StaffEdge> edges) {
|
||||
public record StaffEdge(String role, Staff node) {
|
||||
public record Staff(Name name) {
|
||||
public record Name(String full) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,6 +16,22 @@ public class RabbitConfig {
|
||||
@Value("${queues.provider-page-ingest}")
|
||||
private String providerPageIngestQueue;
|
||||
|
||||
@Value("${queues.manga-update}")
|
||||
private String mangaUpdateQueue;
|
||||
|
||||
@Value("${queues.image-fetch}")
|
||||
private String imageFetchQueue;
|
||||
|
||||
@Bean
|
||||
public Queue imageFetchQueue() {
|
||||
return new Queue(imageFetchQueue, false);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Queue mangaUpdateQueue() {
|
||||
return new Queue(mangaUpdateQueue, false);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Queue mangaIngestQueue() {
|
||||
return new Queue(mangaIngestQueue, false);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package com.magamochi.common.exception;
|
||||
|
||||
import com.magamochi.common.dto.ErrorResponseDTO;
|
||||
import com.magamochi.common.model.dto.ErrorResponseDTO;
|
||||
import java.time.Instant;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package com.magamochi.common.dto;
|
||||
package com.magamochi.common.model.dto;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import java.time.Instant;
|
||||
@ -1,4 +1,4 @@
|
||||
package com.magamochi.common.dto;
|
||||
package com.magamochi.common.model.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
@ -0,0 +1,5 @@
|
||||
package com.magamochi.common.model.enumeration;
|
||||
|
||||
public enum ContentType {
|
||||
MANGA_COVER
|
||||
}
|
||||
@ -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) {}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package com.magamochi.controller;
|
||||
|
||||
import com.magamochi.common.dto.DefaultResponseDTO;
|
||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||
import com.magamochi.model.dto.AuthenticationRequestDTO;
|
||||
import com.magamochi.model.dto.AuthenticationResponseDTO;
|
||||
import com.magamochi.model.dto.RefreshTokenRequestDTO;
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
package com.magamochi.controller;
|
||||
|
||||
import com.magamochi.client.NtfyClient;
|
||||
import com.magamochi.common.dto.DefaultResponseDTO;
|
||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||
import com.magamochi.ingestion.task.IngestFromContentProvidersTask;
|
||||
import com.magamochi.model.dto.UpdateMangaDataCommand;
|
||||
import com.magamochi.model.repository.UserRepository;
|
||||
import com.magamochi.queue.UpdateMangaDataProducer;
|
||||
import com.magamochi.task.ImageCleanupTask;
|
||||
import com.magamochi.task.MangaFollowUpdateTask;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@ -21,19 +19,6 @@ public class ManagementController {
|
||||
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<Void> triggerUpdateMangaData(@PathVariable Long mangaId) {
|
||||
updateMangaDataProducer.sendUpdateMangaDataCommand(new UpdateMangaDataCommand(mangaId));
|
||||
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Cleanup unused S3 images",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package com.magamochi.controller;
|
||||
|
||||
import com.magamochi.common.dto.DefaultResponseDTO;
|
||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||
import com.magamochi.model.dto.MangaChapterImagesDTO;
|
||||
import com.magamochi.model.enumeration.ArchiveFileType;
|
||||
import com.magamochi.service.MangaChapterService;
|
||||
|
||||
@ -1,48 +1,18 @@
|
||||
package com.magamochi.controller;
|
||||
|
||||
import com.magamochi.common.dto.DefaultResponseDTO;
|
||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||
import com.magamochi.model.dto.MangaChapterDTO;
|
||||
import com.magamochi.model.dto.MangaDTO;
|
||||
import com.magamochi.model.dto.MangaListDTO;
|
||||
import com.magamochi.model.dto.MangaListFilterDTO;
|
||||
import com.magamochi.service.MangaService;
|
||||
import com.magamochi.service.OldMangaService;
|
||||
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<Page<MangaListDTO>> 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<MangaDTO> getManga(@PathVariable Long mangaId) {
|
||||
return DefaultResponseDTO.ok(mangaService.getManga(mangaId));
|
||||
}
|
||||
private final OldMangaService oldMangaService;
|
||||
|
||||
@Operation(
|
||||
summary = "Get the available chapters for a specific manga/provider combination",
|
||||
@ -52,7 +22,7 @@ public class MangaController {
|
||||
@GetMapping("/{mangaProviderId}/chapters")
|
||||
public DefaultResponseDTO<List<MangaChapterDTO>> getMangaChapters(
|
||||
@PathVariable Long mangaProviderId) {
|
||||
return DefaultResponseDTO.ok(mangaService.getMangaChapters(mangaProviderId));
|
||||
return DefaultResponseDTO.ok(oldMangaService.getMangaChapters(mangaProviderId));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@ -62,7 +32,7 @@ public class MangaController {
|
||||
operationId = "fetchAllChapters")
|
||||
@PostMapping(value = "/{mangaProviderId}/fetch-all-chapters")
|
||||
public DefaultResponseDTO<Void> fetchAllChapters(@PathVariable Long mangaProviderId) {
|
||||
mangaService.fetchAllNotDownloadedChapters(mangaProviderId);
|
||||
oldMangaService.fetchAllNotDownloadedChapters(mangaProviderId);
|
||||
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
@ -74,7 +44,7 @@ public class MangaController {
|
||||
operationId = "fetchMangaChapters")
|
||||
@PostMapping("/{mangaProviderId}/fetch-chapters")
|
||||
public DefaultResponseDTO<Void> fetchMangaChapters(@PathVariable Long mangaProviderId) {
|
||||
mangaService.fetchMangaChapters(mangaProviderId);
|
||||
oldMangaService.fetchMangaChapters(mangaProviderId);
|
||||
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
@ -86,7 +56,7 @@ public class MangaController {
|
||||
operationId = "followManga")
|
||||
@PostMapping("/{mangaId}/followManga")
|
||||
public DefaultResponseDTO<Void> followManga(@PathVariable Long mangaId) {
|
||||
mangaService.follow(mangaId);
|
||||
oldMangaService.follow(mangaId);
|
||||
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
@ -98,7 +68,7 @@ public class MangaController {
|
||||
operationId = "unfollowManga")
|
||||
@PostMapping("/{mangaId}/unfollowManga")
|
||||
public DefaultResponseDTO<Void> unfollowManga(@PathVariable Long mangaId) {
|
||||
mangaService.unfollow(mangaId);
|
||||
oldMangaService.unfollow(mangaId);
|
||||
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
package com.magamochi.controller;
|
||||
|
||||
import com.magamochi.common.dto.DefaultResponseDTO;
|
||||
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.MangaImportService;
|
||||
import com.magamochi.service.ProviderManualMangaImportService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@ -20,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(
|
||||
@ -55,7 +55,7 @@ public class MangaImportController {
|
||||
@RequestPart("files")
|
||||
@NotNull
|
||||
List<MultipartFile> files) {
|
||||
mangaImportService.importMangaFiles(malId, files);
|
||||
// mangaImportService.importMangaFiles(malId, files);
|
||||
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
package com.magamochi.controller;
|
||||
|
||||
import com.magamochi.common.dto.DefaultResponseDTO;
|
||||
import com.magamochi.model.dto.ImportReviewDTO;
|
||||
import com.magamochi.service.MangaImportReviewService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/manga/import/review")
|
||||
@RequiredArgsConstructor
|
||||
public class MangaImportReviewController {
|
||||
private final MangaImportReviewService mangaImportReviewService;
|
||||
|
||||
@Operation(
|
||||
summary = "Get list of pending import reviews",
|
||||
description = "Get list of pending import reviews.",
|
||||
tags = {"Manga Import Review"},
|
||||
operationId = "getImportReviews")
|
||||
@GetMapping
|
||||
public DefaultResponseDTO<List<ImportReviewDTO>> getImportReviews() {
|
||||
return DefaultResponseDTO.ok(mangaImportReviewService.getImportReviews());
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Delete pending import review",
|
||||
description = "Delete pending import review by ID.",
|
||||
tags = {"Manga Import Review"},
|
||||
operationId = "deleteImportReview")
|
||||
@DeleteMapping("/{id}")
|
||||
public DefaultResponseDTO<Void> deleteImportReview(@PathVariable Long id) {
|
||||
mangaImportReviewService.deleteImportReview(id);
|
||||
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Resolve import review",
|
||||
description = "Resolve import review by ID.",
|
||||
tags = {"Manga Import Review"},
|
||||
operationId = "resolveImportReview")
|
||||
@PostMapping
|
||||
public DefaultResponseDTO<Void> resolveImportReview(
|
||||
@RequestParam Long importReviewId, @RequestParam String malId) {
|
||||
mangaImportReviewService.resolveImportReview(importReviewId, malId);
|
||||
|
||||
return DefaultResponseDTO.ok().build();
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
package com.magamochi.controller;
|
||||
|
||||
import com.magamochi.common.dto.DefaultResponseDTO;
|
||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||
import com.magamochi.service.UserFavoriteMangaService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package com.magamochi.ingestion.controller;
|
||||
|
||||
import com.magamochi.common.dto.DefaultResponseDTO;
|
||||
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;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.magamochi.ingestion.model.dto;
|
||||
|
||||
import com.magamochi.model.enumeration.MangaStatus;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record MangaInfoDTO(@NotBlank String title, @NotBlank String url, MangaStatus status) {}
|
||||
public record MangaInfoDTO(@NotBlank String title, @NotBlank String url) {}
|
||||
|
||||
@ -6,7 +6,6 @@ import com.magamochi.ingestion.providers.ContentProviders;
|
||||
import com.magamochi.ingestion.providers.PagedContentProvider;
|
||||
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
||||
import com.magamochi.model.entity.MangaContentProvider;
|
||||
import com.magamochi.model.enumeration.MangaStatus;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
@ -116,25 +115,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 MangaInfoDTO(title, url, status);
|
||||
return new MangaInfoDTO(title, url);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@ import com.magamochi.ingestion.providers.PagedContentProvider;
|
||||
import com.magamochi.ingestion.service.FlareService;
|
||||
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
||||
import com.magamochi.model.entity.MangaContentProvider;
|
||||
import com.magamochi.model.enumeration.MangaStatus;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
@ -116,7 +115,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
|
||||
}
|
||||
}
|
||||
|
||||
return new MangaInfoDTO(title.trim(), url.trim(), MangaStatus.UNKNOWN);
|
||||
return new MangaInfoDTO(title.trim(), url.trim());
|
||||
})
|
||||
.toList();
|
||||
} catch (NoSuchElementException e) {
|
||||
|
||||
@ -10,7 +10,6 @@ import com.magamochi.ingestion.providers.PagedContentProvider;
|
||||
import com.magamochi.ingestion.service.FlareService;
|
||||
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
||||
import com.magamochi.model.entity.MangaContentProvider;
|
||||
import com.magamochi.model.enumeration.MangaStatus;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
@ -142,7 +141,7 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
|
||||
var textElement = linkElement.getElementsByTag("h3");
|
||||
var title = textElement.text().trim();
|
||||
|
||||
return new MangaInfoDTO(title, url, MangaStatus.UNKNOWN);
|
||||
return new MangaInfoDTO(title, url);
|
||||
})
|
||||
.toList();
|
||||
} catch (NoSuchElementException e) {
|
||||
|
||||
@ -2,7 +2,7 @@ package com.magamochi.model.dto;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
|
||||
import com.magamochi.model.entity.Language;
|
||||
import com.magamochi.catalog.model.entity.Language;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
package com.magamochi.model.dto;
|
||||
|
||||
public record MangaListUpdateCommand(String contentProviderName, Integer page) {}
|
||||
@ -1,7 +0,0 @@
|
||||
package com.magamochi.model.dto;
|
||||
|
||||
import com.magamochi.ingestion.model.dto.MangaInfoDTO;
|
||||
import java.util.List;
|
||||
|
||||
public record MangaMessageDTO(
|
||||
String contentProviderName, List<MangaInfoDTO> mangaInfoResponseDTOs) {}
|
||||
@ -1,15 +0,0 @@
|
||||
package com.magamochi.model.dto;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class TitleMatchRequestDTO {
|
||||
private String title;
|
||||
|
||||
private List<String> options;
|
||||
|
||||
@Builder.Default private int threshold = 85;
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
package com.magamochi.model.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class TitleMatchResponseDTO {
|
||||
boolean matchFound;
|
||||
String bestMatch;
|
||||
Double similarity;
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
package com.magamochi.model.dto;
|
||||
|
||||
public record UpdateMangaDataCommand(Long mangaId) {}
|
||||
@ -1,5 +1,6 @@
|
||||
package com.magamochi.model.entity;
|
||||
|
||||
import com.magamochi.catalog.model.entity.Language;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.magamochi.model.entity;
|
||||
|
||||
import com.magamochi.catalog.model.entity.Manga;
|
||||
import com.magamochi.ingestion.model.entity.ContentProvider;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.magamochi.model.entity;
|
||||
|
||||
import com.magamochi.catalog.model.entity.Manga;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import lombok.*;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.magamochi.model.entity;
|
||||
|
||||
import com.magamochi.catalog.model.entity.Manga;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
package com.magamochi.model.repository;
|
||||
|
||||
import com.magamochi.model.entity.Author;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface AuthorRepository extends JpaRepository<Author, Long> {
|
||||
Optional<Author> findByMalId(Long aLong);
|
||||
|
||||
Optional<Author> findByName(String name);
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
package com.magamochi.model.repository;
|
||||
|
||||
import com.magamochi.ingestion.model.entity.ContentProvider;
|
||||
import com.magamochi.model.entity.Manga;
|
||||
import com.magamochi.model.entity.MangaContentProvider;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface MangaContentProviderRepository extends JpaRepository<MangaContentProvider, Long> {
|
||||
boolean existsByMangaAndContentProviderAndUrlIgnoreCase(
|
||||
Manga manga, ContentProvider contentProvider, String url);
|
||||
|
||||
Optional<MangaContentProvider> findByMangaTitleIgnoreCaseAndContentProvider(
|
||||
String mangaTitle, ContentProvider contentProvider);
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
package com.magamochi.model.repository;
|
||||
|
||||
import com.magamochi.model.entity.MangaImportReview;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface MangaImportReviewRepository extends JpaRepository<MangaImportReview, Long> {
|
||||
boolean existsByTitleIgnoreCaseAndUrlIgnoreCase(String title, String url);
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
package com.magamochi.model.repository;
|
||||
|
||||
import com.magamochi.model.entity.Manga;
|
||||
import com.magamochi.catalog.model.entity.Manga;
|
||||
import com.magamochi.model.entity.User;
|
||||
import com.magamochi.model.entity.UserFavoriteManga;
|
||||
import java.util.Optional;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package com.magamochi.model.repository;
|
||||
|
||||
import com.magamochi.model.entity.Manga;
|
||||
import com.magamochi.catalog.model.entity.Manga;
|
||||
import com.magamochi.model.entity.User;
|
||||
import com.magamochi.model.entity.UserMangaFollow;
|
||||
import java.util.List;
|
||||
|
||||
@ -2,9 +2,9 @@ package com.magamochi.model.specification;
|
||||
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import com.magamochi.model.dto.MangaListFilterDTO;
|
||||
import com.magamochi.model.entity.Author;
|
||||
import com.magamochi.model.entity.Manga;
|
||||
import com.magamochi.catalog.model.dto.MangaListFilterDTO;
|
||||
import com.magamochi.catalog.model.entity.Author;
|
||||
import com.magamochi.catalog.model.entity.Manga;
|
||||
import com.magamochi.model.entity.User;
|
||||
import jakarta.persistence.criteria.*;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
package com.magamochi.queue;
|
||||
|
||||
import com.magamochi.model.dto.UpdateMangaDataCommand;
|
||||
import com.magamochi.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());
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
package com.magamochi.queue;
|
||||
|
||||
import com.magamochi.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);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
package com.magamochi.queue;
|
||||
|
||||
import com.magamochi.model.dto.UpdateMangaFollowChapterListCommand;
|
||||
import com.magamochi.service.MangaService;
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
package com.magamochi.queue;
|
||||
|
||||
import com.magamochi.model.dto.MangaListUpdateCommand;
|
||||
import com.magamochi.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());
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
package com.magamochi.queue;
|
||||
|
||||
import com.magamochi.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);
|
||||
}
|
||||
}
|
||||
@ -1,163 +0,0 @@
|
||||
package com.magamochi.service;
|
||||
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import com.magamochi.client.AniListClient;
|
||||
import com.magamochi.client.JikanClient;
|
||||
import com.magamochi.ingestion.model.entity.ContentProvider;
|
||||
import com.magamochi.model.dto.TitleMatchRequestDTO;
|
||||
import com.magamochi.model.dto.UpdateMangaDataCommand;
|
||||
import com.magamochi.model.entity.Manga;
|
||||
import com.magamochi.model.entity.MangaImportReview;
|
||||
import com.magamochi.model.repository.MangaImportReviewRepository;
|
||||
import com.magamochi.model.repository.MangaRepository;
|
||||
import com.magamochi.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, ContentProvider contentProvider) {
|
||||
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, contentProvider);
|
||||
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, contentProvider);
|
||||
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, contentProvider);
|
||||
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.<Manga>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, ContentProvider contentProvider) {
|
||||
if (mangaImportReviewRepository.existsByTitleIgnoreCaseAndUrlIgnoreCase(title, url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mangaImportReviewRepository.save(
|
||||
MangaImportReview.builder().title(title).url(url).contentProvider(contentProvider).build());
|
||||
}
|
||||
}
|
||||
@ -1,86 +0,0 @@
|
||||
package com.magamochi.service;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import com.magamochi.client.JikanClient;
|
||||
import com.magamochi.common.exception.NotFoundException;
|
||||
import com.magamochi.model.dto.ImportReviewDTO;
|
||||
import com.magamochi.model.dto.UpdateMangaDataCommand;
|
||||
import com.magamochi.model.entity.Manga;
|
||||
import com.magamochi.model.entity.MangaContentProvider;
|
||||
import com.magamochi.model.entity.MangaImportReview;
|
||||
import com.magamochi.model.repository.MangaContentProviderRepository;
|
||||
import com.magamochi.model.repository.MangaImportReviewRepository;
|
||||
import com.magamochi.model.repository.MangaRepository;
|
||||
import com.magamochi.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 MangaContentProviderRepository mangaContentProviderRepository;
|
||||
|
||||
private final JikanClient jikanClient;
|
||||
|
||||
private final RateLimiter jikanRateLimiter;
|
||||
|
||||
private final UpdateMangaDataProducer updateMangaDataProducer;
|
||||
|
||||
public List<ImportReviewDTO> getImportReviews() {
|
||||
return mangaImportReviewRepository.findAll().stream().map(ImportReviewDTO::from).toList();
|
||||
}
|
||||
|
||||
public void deleteImportReview(Long id) {
|
||||
var importReview = getImportReviewThrowIfNotFound(id);
|
||||
|
||||
mangaImportReviewRepository.delete(importReview);
|
||||
}
|
||||
|
||||
public void resolveImportReview(Long id, String malId) {
|
||||
var importReview = getImportReviewThrowIfNotFound(id);
|
||||
|
||||
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.getTitle())
|
||||
.contentProvider(importReview.getContentProvider())
|
||||
.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));
|
||||
}
|
||||
}
|
||||
@ -1,434 +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.client.AniListClient;
|
||||
import com.magamochi.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.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<MultipartFile> 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<MangaChapterImage> 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<Author> 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<Genre> 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.chapterUrl())
|
||||
.orElseGet(MangaChapter::new);
|
||||
|
||||
mangaChapter.setMangaContentProvider(mangaContentProvider);
|
||||
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(),
|
||||
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);
|
||||
}
|
||||
}
|
||||
// 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<MultipartFile> 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<MangaChapterImage> 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<Author> 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<Genre> 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.chapterUrl())
|
||||
// .orElseGet(MangaChapter::new);
|
||||
//
|
||||
// mangaChapter.setMangaContentProvider(mangaContentProvider);
|
||||
// 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(),
|
||||
// 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);
|
||||
// }
|
||||
// }
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
package com.magamochi.service;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
|
||||
import com.magamochi.ingestion.providers.PagedContentProviderFactory;
|
||||
import com.magamochi.model.entity.MangaContentProvider;
|
||||
import com.magamochi.model.repository.MangaContentProviderRepository;
|
||||
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 MangaContentProviderRepository mangaContentProviderRepository;
|
||||
|
||||
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 =
|
||||
mangaContentProviderRepository.findByMangaTitleIgnoreCaseAndContentProvider(
|
||||
mangaResponse.title(), provider);
|
||||
|
||||
if (mangaProvider.isPresent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var manga =
|
||||
mangaCreationService.getOrCreateManga(
|
||||
mangaResponse.title(), mangaResponse.url(), provider);
|
||||
|
||||
if (isNull(manga)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mangaContentProviderRepository.existsByMangaAndContentProviderAndUrlIgnoreCase(
|
||||
manga, provider, mangaResponse.url())) {
|
||||
mangaContentProviderRepository.save(
|
||||
MangaContentProvider.builder()
|
||||
.manga(manga)
|
||||
.mangaTitle(mangaResponse.title())
|
||||
.contentProvider(provider)
|
||||
.url(mangaResponse.url())
|
||||
.build());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,40 +1,35 @@
|
||||
package com.magamochi.service;
|
||||
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import com.magamochi.catalog.model.entity.Manga;
|
||||
import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
|
||||
import com.magamochi.catalog.model.repository.MangaRepository;
|
||||
import com.magamochi.client.NtfyClient;
|
||||
import com.magamochi.common.exception.NotFoundException;
|
||||
import com.magamochi.ingestion.providers.ContentProviderFactory;
|
||||
import com.magamochi.model.dto.*;
|
||||
import com.magamochi.model.entity.Manga;
|
||||
import com.magamochi.model.entity.MangaChapter;
|
||||
import com.magamochi.model.entity.MangaContentProvider;
|
||||
import com.magamochi.model.entity.UserMangaFollow;
|
||||
import com.magamochi.model.repository.*;
|
||||
import com.magamochi.model.specification.MangaSpecification;
|
||||
import com.magamochi.queue.MangaChapterDownloadProducer;
|
||||
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;
|
||||
public class OldMangaService {
|
||||
// private final MangaImportService mangaImportService;
|
||||
private final UserService userService;
|
||||
private final MangaRepository mangaRepository;
|
||||
private final MangaContentProviderRepository mangaContentProviderRepository;
|
||||
|
||||
private final ContentProviderFactory contentProviderFactory;
|
||||
private final UserFavoriteMangaRepository userFavoriteMangaRepository;
|
||||
|
||||
private final UserMangaFollowRepository userMangaFollowRepository;
|
||||
|
||||
@ -62,27 +57,6 @@ public class MangaService {
|
||||
new MangaChapterDownloadCommand(chapterId)));
|
||||
}
|
||||
|
||||
public Page<MangaListDTO> 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<MangaChapterDTO> getMangaChapters(Long mangaProviderId) {
|
||||
var mangaProvider = getMangaProviderThrowIfNotFound(mangaProviderId);
|
||||
|
||||
@ -92,30 +66,6 @@ public class MangaService {
|
||||
.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 =
|
||||
mangaContentProviderRepository
|
||||
@ -155,8 +105,8 @@ public class MangaService {
|
||||
contentProviderFactory.getContentProvider(mangaProvider.getContentProvider().getName());
|
||||
var availableChapters = contentProvider.getAvailableChapters(mangaProvider);
|
||||
|
||||
availableChapters.forEach(
|
||||
chapter -> mangaImportService.persistMangaChapter(mangaProvider, chapter));
|
||||
// availableChapters.forEach(
|
||||
// chapter -> mangaImportService.persistMangaChapter(mangaProvider, chapter));
|
||||
}
|
||||
|
||||
public Manga findMangaByIdThrowIfNotFound(Long mangaId) {
|
||||
@ -1,25 +1,21 @@
|
||||
package com.magamochi.service;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
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 com.magamochi.model.entity.MangaContentProvider;
|
||||
import com.magamochi.model.repository.MangaContentProviderRepository;
|
||||
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 MangaCreationService mangaCreationService;
|
||||
|
||||
private final ManualImportContentProviderFactory contentProviderFactory;
|
||||
|
||||
@ -27,35 +23,40 @@ public class ProviderManualMangaImportService {
|
||||
private final MangaContentProviderRepository mangaContentProviderRepository;
|
||||
|
||||
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 (!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());
|
||||
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) {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
package com.magamochi.service;
|
||||
|
||||
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.Manga;
|
||||
import com.magamochi.model.entity.UserFavoriteManga;
|
||||
import com.magamochi.model.repository.MangaRepository;
|
||||
import com.magamochi.model.repository.UserFavoriteMangaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package com.magamochi.task;
|
||||
|
||||
import com.magamochi.catalog.model.entity.Manga;
|
||||
import com.magamochi.catalog.model.repository.MangaRepository;
|
||||
import com.magamochi.model.dto.UpdateMangaFollowChapterListCommand;
|
||||
import com.magamochi.model.entity.Manga;
|
||||
import com.magamochi.model.repository.MangaRepository;
|
||||
import com.magamochi.queue.UpdateMangaFollowChapterListProducer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
|
||||
@ -91,6 +91,8 @@ resilience4j:
|
||||
queues:
|
||||
manga-ingest: ${MANGA_INGEST_QUEUE:mangaIngest}
|
||||
provider-page-ingest: ${PROVIDER_PAGE_INGEST_QUEUE:providerPageIngest}
|
||||
manga-update: ${MANGA_UPDATE_QUEUE:mangaUpdate}
|
||||
image-fetch: ${IMAGE_FETCH_QUEUE:imageFetch}
|
||||
|
||||
rabbit-mq:
|
||||
queues:
|
||||
|
||||
@ -34,6 +34,7 @@ CREATE TABLE mangas
|
||||
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
|
||||
);
|
||||
@ -70,6 +71,8 @@ CREATE TABLE manga_chapters
|
||||
url VARCHAR NOT NULL,
|
||||
chapter_number INTEGER,
|
||||
language_id BIGINT REFERENCES languages (id),
|
||||
downloaded BOOLEAN DEFAULT FALSE,
|
||||
read BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@ -80,17 +83,15 @@ CREATE TABLE manga_chapter_images
|
||||
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,
|
||||
downloaded BOOLEAN DEFAULT FALSE,
|
||||
read BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE manga_import_reviews
|
||||
CREATE TABLE manga_ingest_reviews
|
||||
(
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
content_provider_id BIGINT NOT NULL REFERENCES content_providers (id) ON DELETE CASCADE,
|
||||
title VARCHAR NOT NULL,
|
||||
manga_title VARCHAR NOT NULL,
|
||||
url VARCHAR NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@ -98,7 +99,6 @@ CREATE TABLE manga_import_reviews
|
||||
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
|
||||
@ -114,9 +114,8 @@ CREATE TABLE manga_author
|
||||
|
||||
CREATE TABLE genres
|
||||
(
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
mal_id BIGINT UNIQUE,
|
||||
name VARCHAR
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
name VARCHAR
|
||||
);
|
||||
|
||||
CREATE TABLE manga_genre
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user