refactor-architecture #27
2
pom.xml
2
pom.xml
@ -145,6 +145,8 @@
|
|||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
</path>
|
</path>
|
||||||
</annotationProcessorPaths>
|
</annotationProcessorPaths>
|
||||||
|
<source>22</source>
|
||||||
|
<target>22</target>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
<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.time.OffsetDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springframework.cloud.openfeign.FeignClient;
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
@ -9,7 +8,6 @@ import org.springframework.web.bind.annotation.PathVariable;
|
|||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
@FeignClient(name = "jikan", url = "https://api.jikan.moe/v4/manga")
|
@FeignClient(name = "jikan", url = "https://api.jikan.moe/v4/manga")
|
||||||
@Retry(name = "JikanRetry")
|
|
||||||
public interface JikanClient {
|
public interface JikanClient {
|
||||||
@GetMapping
|
@GetMapping
|
||||||
SearchResponse mangaSearch(@RequestParam String q);
|
SearchResponse mangaSearch(@RequestParam String q);
|
||||||
@ -30,10 +28,10 @@ public interface JikanClient {
|
|||||||
String title,
|
String title,
|
||||||
List<String> title_synonyms,
|
List<String> title_synonyms,
|
||||||
String status,
|
String status,
|
||||||
boolean publishing,
|
Boolean publishing,
|
||||||
String synopsis,
|
String synopsis,
|
||||||
float score,
|
Float score,
|
||||||
int chapters,
|
Integer chapters,
|
||||||
PublishData published,
|
PublishData published,
|
||||||
List<AuthorData> authors,
|
List<AuthorData> authors,
|
||||||
List<GenreData> genres) {
|
List<GenreData> genres) {
|
||||||
@ -1,11 +1,20 @@
|
|||||||
package com.magamochi.catalog.controller;
|
package com.magamochi.catalog.controller;
|
||||||
|
|
||||||
import com.magamochi.catalog.model.dto.GenreDTO;
|
import com.magamochi.catalog.model.dto.GenreDTO;
|
||||||
|
import com.magamochi.catalog.model.dto.MangaDTO;
|
||||||
|
import com.magamochi.catalog.model.dto.MangaListDTO;
|
||||||
|
import com.magamochi.catalog.model.dto.MangaListFilterDTO;
|
||||||
import com.magamochi.catalog.service.GenreService;
|
import com.magamochi.catalog.service.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.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@ -13,6 +22,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class CatalogController {
|
public class CatalogController {
|
||||||
private final GenreService genreService;
|
private final GenreService genreService;
|
||||||
|
private final MangaService mangaService;
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get a list of manga genres",
|
summary = "Get a list of manga genres",
|
||||||
@ -23,4 +33,26 @@ public class CatalogController {
|
|||||||
public DefaultResponseDTO<List<GenreDTO>> getGenres() {
|
public DefaultResponseDTO<List<GenreDTO>> getGenres() {
|
||||||
return DefaultResponseDTO.ok(genreService.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 static java.util.Objects.isNull;
|
||||||
|
|
||||||
import com.magamochi.model.entity.Manga;
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import com.magamochi.model.entity.MangaAlternativeTitle;
|
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.MangaChapter;
|
||||||
import com.magamochi.model.entity.MangaContentProvider;
|
import com.magamochi.model.entity.MangaContentProvider;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
@ -15,7 +16,7 @@ public record MangaDTO(
|
|||||||
@NotNull Long id,
|
@NotNull Long id,
|
||||||
@NotBlank String title,
|
@NotBlank String title,
|
||||||
String coverImageKey,
|
String coverImageKey,
|
||||||
String status,
|
MangaStatus status,
|
||||||
OffsetDateTime publishedFrom,
|
OffsetDateTime publishedFrom,
|
||||||
OffsetDateTime publishedTo,
|
OffsetDateTime publishedTo,
|
||||||
String synopsis,
|
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.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
public record ImportReviewDTO(
|
public record MangaIngestReviewDTO(
|
||||||
@NotNull Long id,
|
@NotNull Long id,
|
||||||
@NotBlank String title,
|
@NotBlank String title,
|
||||||
@NotBlank String providerName,
|
@NotBlank String contentProviderName,
|
||||||
String externalUrl,
|
String externalUrl,
|
||||||
@NotBlank String reason,
|
@NotBlank String reason,
|
||||||
@NotNull Instant createdAt) {
|
@NotNull Instant createdAt) {
|
||||||
public static ImportReviewDTO from(MangaImportReview review) {
|
public static MangaIngestReviewDTO from(MangaIngestReview review) {
|
||||||
return new ImportReviewDTO(
|
return new MangaIngestReviewDTO(
|
||||||
review.getId(),
|
review.getId(),
|
||||||
review.getTitle(),
|
review.getMangaTitle(),
|
||||||
review.getContentProvider().getName(),
|
review.getContentProvider().getName(),
|
||||||
review.getUrl(),
|
review.getUrl(),
|
||||||
"Title match not found",
|
"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 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.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
@ -12,7 +13,7 @@ public record MangaListDTO(
|
|||||||
@NotNull Long id,
|
@NotNull Long id,
|
||||||
@NotBlank String title,
|
@NotBlank String title,
|
||||||
String coverImageKey,
|
String coverImageKey,
|
||||||
String status,
|
MangaStatus status,
|
||||||
OffsetDateTime publishedFrom,
|
OffsetDateTime publishedFrom,
|
||||||
OffsetDateTime publishedTo,
|
OffsetDateTime publishedTo,
|
||||||
Integer providerCount,
|
Integer providerCount,
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.magamochi.model.dto;
|
package com.magamochi.catalog.model.dto;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.magamochi.model.entity;
|
package com.magamochi.catalog.model.entity;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@ -19,8 +19,6 @@ public class Author {
|
|||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
private Long malId;
|
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@CreationTimestamp private Instant createdAt;
|
@CreationTimestamp private Instant createdAt;
|
||||||
@ -1,6 +1,5 @@
|
|||||||
package com.magamochi.catalog.model.entity;
|
package com.magamochi.catalog.model.entity;
|
||||||
|
|
||||||
import com.magamochi.model.entity.MangaGenre;
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
@ -17,8 +16,6 @@ public class Genre {
|
|||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
private Long malId;
|
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "genre")
|
@OneToMany(mappedBy = "genre")
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package com.magamochi.model.entity;
|
package com.magamochi.catalog.model.entity;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
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 jakarta.persistence.*;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
@ -26,14 +31,11 @@ public class Manga {
|
|||||||
|
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
private String status;
|
@Enumerated(EnumType.STRING)
|
||||||
|
private MangaStatus status;
|
||||||
|
|
||||||
private String synopsis;
|
private String synopsis;
|
||||||
|
|
||||||
@CreationTimestamp private Instant createdAt;
|
|
||||||
|
|
||||||
@UpdateTimestamp private Instant updatedAt;
|
|
||||||
|
|
||||||
@OneToMany(mappedBy = "manga")
|
@OneToMany(mappedBy = "manga")
|
||||||
private List<MangaContentProvider> mangaContentProviders;
|
private List<MangaContentProvider> mangaContentProviders;
|
||||||
|
|
||||||
@ -47,6 +49,18 @@ public class Manga {
|
|||||||
|
|
||||||
private OffsetDateTime publishedTo;
|
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")
|
@OneToMany(mappedBy = "manga")
|
||||||
private List<MangaAuthor> mangaAuthors;
|
private List<MangaAuthor> mangaAuthors;
|
||||||
|
|
||||||
@ -58,8 +72,4 @@ public class Manga {
|
|||||||
|
|
||||||
@OneToMany(mappedBy = "manga")
|
@OneToMany(mappedBy = "manga")
|
||||||
private List<MangaAlternativeTitle> alternativeTitles;
|
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 jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.magamochi.model.entity;
|
package com.magamochi.catalog.model.entity;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
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 jakarta.persistence.*;
|
||||||
import lombok.*;
|
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 com.magamochi.ingestion.model.entity.ContentProvider;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
@ -7,18 +7,18 @@ import lombok.*;
|
|||||||
import org.hibernate.annotations.CreationTimestamp;
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "manga_import_reviews")
|
@Table(name = "manga_ingest_reviews")
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class MangaImportReview {
|
public class MangaIngestReview {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
private String title;
|
private String mangaTitle;
|
||||||
|
|
||||||
private String url;
|
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 {
|
public enum MangaStatus {
|
||||||
ONGOING,
|
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;
|
package com.magamochi.catalog.model.repository;
|
||||||
|
|
||||||
import com.magamochi.catalog.model.entity.Genre;
|
import com.magamochi.catalog.model.entity.Genre;
|
||||||
import java.util.Optional;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
public interface GenreRepository extends JpaRepository<Genre, Long> {
|
public interface GenreRepository extends JpaRepository<Genre, Long> {}
|
||||||
Optional<Genre> findByMalId(Long malId);
|
|
||||||
|
|
||||||
Optional<Genre> findByName(String name);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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 java.util.Optional;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
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;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
public interface MangaAlternativeTitlesRepository
|
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.catalog.model.entity.Author;
|
||||||
import com.magamochi.model.entity.Manga;
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import com.magamochi.model.entity.MangaAuthor;
|
import com.magamochi.catalog.model.entity.MangaAuthor;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
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.catalog.model.entity.Genre;
|
||||||
import com.magamochi.model.entity.Manga;
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import com.magamochi.model.entity.MangaGenre;
|
import com.magamochi.catalog.model.entity.MangaGenre;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
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.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
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.common.exception.NotFoundException;
|
||||||
import com.magamochi.model.entity.Language;
|
|
||||||
import com.magamochi.model.repository.LanguageRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
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.apache.commons.lang3.StringUtils.isBlank;
|
||||||
import static org.springframework.util.CollectionUtils.isEmpty;
|
|
||||||
|
|
||||||
import com.magamochi.model.dto.TitleMatchRequestDTO;
|
import lombok.Builder;
|
||||||
import com.magamochi.model.dto.TitleMatchResponseDTO;
|
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
import org.apache.commons.text.similarity.LevenshteinDistance;
|
import org.apache.commons.text.similarity.LevenshteinDistance;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -14,18 +13,24 @@ import org.springframework.stereotype.Service;
|
|||||||
public class TitleMatcherService {
|
public class TitleMatcherService {
|
||||||
private final LevenshteinDistance levenshteinDistance = LevenshteinDistance.getDefaultInstance();
|
private final LevenshteinDistance levenshteinDistance = LevenshteinDistance.getDefaultInstance();
|
||||||
|
|
||||||
public TitleMatchResponseDTO findBestMatch(TitleMatchRequestDTO request) {
|
public TitleMatchResponse findBestMatch(TitleMatchRequest request) {
|
||||||
if (isBlank(request.getTitle()) || isEmpty(request.getOptions())) {
|
if (isBlank(request.title()) || !request.options().iterator().hasNext()) {
|
||||||
throw new IllegalArgumentException("Title and options are required");
|
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;
|
String bestMatch = null;
|
||||||
double bestScore = 0.0;
|
double bestScore = 0.0;
|
||||||
|
|
||||||
for (var option : request.getOptions()) {
|
for (var option : request.options()) {
|
||||||
var score = calculateSimilarityScore(request.getTitle(), option);
|
var score = calculateSimilarityScore(request.title(), option);
|
||||||
|
|
||||||
if (score > bestScore) {
|
if (score > bestScore) {
|
||||||
bestScore = score;
|
bestScore = score;
|
||||||
@ -33,20 +38,20 @@ public class TitleMatcherService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bestScore >= request.getThreshold()) {
|
if (bestScore >= threshold) {
|
||||||
log.info(
|
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)
|
.matchFound(true)
|
||||||
.bestMatch(bestMatch)
|
.bestMatch(bestMatch)
|
||||||
.similarity(bestScore)
|
.similarity(bestScore)
|
||||||
.build();
|
.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) {
|
private double calculateSimilarityScore(String title, String option) {
|
||||||
@ -64,4 +69,10 @@ public class TitleMatcherService {
|
|||||||
// Format to two decimal places for a cleaner result
|
// Format to two decimal places for a cleaner result
|
||||||
return Math.round(similarity * 100.0) / 100.0;
|
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.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
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}")
|
@Value("${queues.provider-page-ingest}")
|
||||||
private String providerPageIngestQueue;
|
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
|
@Bean
|
||||||
public Queue mangaIngestQueue() {
|
public Queue mangaIngestQueue() {
|
||||||
return new Queue(mangaIngestQueue, false);
|
return new Queue(mangaIngestQueue, false);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package com.magamochi.common.exception;
|
package com.magamochi.common.exception;
|
||||||
|
|
||||||
import com.magamochi.common.dto.ErrorResponseDTO;
|
import com.magamochi.common.model.dto.ErrorResponseDTO;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
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 jakarta.annotation.Nullable;
|
||||||
import java.time.Instant;
|
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.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
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);
|
return RateLimiter.create(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RateLimiter aniListRateLimiter() {
|
||||||
|
return RateLimiter.create(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public RateLimiter imageDownloadRateLimiter() {
|
public RateLimiter imageDownloadRateLimiter() {
|
||||||
return RateLimiter.create(10);
|
return RateLimiter.create(10);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package com.magamochi.controller;
|
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.AuthenticationRequestDTO;
|
||||||
import com.magamochi.model.dto.AuthenticationResponseDTO;
|
import com.magamochi.model.dto.AuthenticationResponseDTO;
|
||||||
import com.magamochi.model.dto.RefreshTokenRequestDTO;
|
import com.magamochi.model.dto.RefreshTokenRequestDTO;
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
package com.magamochi.controller;
|
package com.magamochi.controller;
|
||||||
|
|
||||||
import com.magamochi.client.NtfyClient;
|
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.ingestion.task.IngestFromContentProvidersTask;
|
||||||
import com.magamochi.model.dto.UpdateMangaDataCommand;
|
|
||||||
import com.magamochi.model.repository.UserRepository;
|
import com.magamochi.model.repository.UserRepository;
|
||||||
import com.magamochi.queue.UpdateMangaDataProducer;
|
|
||||||
import com.magamochi.task.ImageCleanupTask;
|
import com.magamochi.task.ImageCleanupTask;
|
||||||
import com.magamochi.task.MangaFollowUpdateTask;
|
import com.magamochi.task.MangaFollowUpdateTask;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@ -21,19 +19,6 @@ public class ManagementController {
|
|||||||
private final MangaFollowUpdateTask mangaFollowUpdateTask;
|
private final MangaFollowUpdateTask mangaFollowUpdateTask;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final NtfyClient ntfyClient;
|
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(
|
@Operation(
|
||||||
summary = "Cleanup unused S3 images",
|
summary = "Cleanup unused S3 images",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package com.magamochi.controller;
|
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.dto.MangaChapterImagesDTO;
|
||||||
import com.magamochi.model.enumeration.ArchiveFileType;
|
import com.magamochi.model.enumeration.ArchiveFileType;
|
||||||
import com.magamochi.service.MangaChapterService;
|
import com.magamochi.service.MangaChapterService;
|
||||||
|
|||||||
@ -1,48 +1,18 @@
|
|||||||
package com.magamochi.controller;
|
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.MangaChapterDTO;
|
||||||
import com.magamochi.model.dto.MangaDTO;
|
import com.magamochi.service.OldMangaService;
|
||||||
import com.magamochi.model.dto.MangaListDTO;
|
|
||||||
import com.magamochi.model.dto.MangaListFilterDTO;
|
|
||||||
import com.magamochi.service.MangaService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/mangas")
|
@RequestMapping("/mangas")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class MangaController {
|
public class MangaController {
|
||||||
private final MangaService mangaService;
|
private final OldMangaService oldMangaService;
|
||||||
|
|
||||||
@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));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get the available chapters for a specific manga/provider combination",
|
summary = "Get the available chapters for a specific manga/provider combination",
|
||||||
@ -52,7 +22,7 @@ public class MangaController {
|
|||||||
@GetMapping("/{mangaProviderId}/chapters")
|
@GetMapping("/{mangaProviderId}/chapters")
|
||||||
public DefaultResponseDTO<List<MangaChapterDTO>> getMangaChapters(
|
public DefaultResponseDTO<List<MangaChapterDTO>> getMangaChapters(
|
||||||
@PathVariable Long mangaProviderId) {
|
@PathVariable Long mangaProviderId) {
|
||||||
return DefaultResponseDTO.ok(mangaService.getMangaChapters(mangaProviderId));
|
return DefaultResponseDTO.ok(oldMangaService.getMangaChapters(mangaProviderId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@ -62,7 +32,7 @@ public class MangaController {
|
|||||||
operationId = "fetchAllChapters")
|
operationId = "fetchAllChapters")
|
||||||
@PostMapping(value = "/{mangaProviderId}/fetch-all-chapters")
|
@PostMapping(value = "/{mangaProviderId}/fetch-all-chapters")
|
||||||
public DefaultResponseDTO<Void> fetchAllChapters(@PathVariable Long mangaProviderId) {
|
public DefaultResponseDTO<Void> fetchAllChapters(@PathVariable Long mangaProviderId) {
|
||||||
mangaService.fetchAllNotDownloadedChapters(mangaProviderId);
|
oldMangaService.fetchAllNotDownloadedChapters(mangaProviderId);
|
||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
return DefaultResponseDTO.ok().build();
|
||||||
}
|
}
|
||||||
@ -74,7 +44,7 @@ public class MangaController {
|
|||||||
operationId = "fetchMangaChapters")
|
operationId = "fetchMangaChapters")
|
||||||
@PostMapping("/{mangaProviderId}/fetch-chapters")
|
@PostMapping("/{mangaProviderId}/fetch-chapters")
|
||||||
public DefaultResponseDTO<Void> fetchMangaChapters(@PathVariable Long mangaProviderId) {
|
public DefaultResponseDTO<Void> fetchMangaChapters(@PathVariable Long mangaProviderId) {
|
||||||
mangaService.fetchMangaChapters(mangaProviderId);
|
oldMangaService.fetchMangaChapters(mangaProviderId);
|
||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
return DefaultResponseDTO.ok().build();
|
||||||
}
|
}
|
||||||
@ -86,7 +56,7 @@ public class MangaController {
|
|||||||
operationId = "followManga")
|
operationId = "followManga")
|
||||||
@PostMapping("/{mangaId}/followManga")
|
@PostMapping("/{mangaId}/followManga")
|
||||||
public DefaultResponseDTO<Void> followManga(@PathVariable Long mangaId) {
|
public DefaultResponseDTO<Void> followManga(@PathVariable Long mangaId) {
|
||||||
mangaService.follow(mangaId);
|
oldMangaService.follow(mangaId);
|
||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
return DefaultResponseDTO.ok().build();
|
||||||
}
|
}
|
||||||
@ -98,7 +68,7 @@ public class MangaController {
|
|||||||
operationId = "unfollowManga")
|
operationId = "unfollowManga")
|
||||||
@PostMapping("/{mangaId}/unfollowManga")
|
@PostMapping("/{mangaId}/unfollowManga")
|
||||||
public DefaultResponseDTO<Void> unfollowManga(@PathVariable Long mangaId) {
|
public DefaultResponseDTO<Void> unfollowManga(@PathVariable Long mangaId) {
|
||||||
mangaService.unfollow(mangaId);
|
oldMangaService.unfollow(mangaId);
|
||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
return DefaultResponseDTO.ok().build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
package com.magamochi.controller;
|
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.ImportMangaResponseDTO;
|
||||||
import com.magamochi.model.dto.ImportRequestDTO;
|
import com.magamochi.model.dto.ImportRequestDTO;
|
||||||
import com.magamochi.service.MangaImportService;
|
// import com.magamochi.service.MangaImportService;
|
||||||
import com.magamochi.service.ProviderManualMangaImportService;
|
import com.magamochi.service.ProviderManualMangaImportService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
@ -20,7 +20,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
@RequestMapping("/manga/import")
|
@RequestMapping("/manga/import")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class MangaImportController {
|
public class MangaImportController {
|
||||||
private final MangaImportService mangaImportService;
|
// private final MangaImportService mangaImportService;
|
||||||
private final ProviderManualMangaImportService providerManualMangaImportService;
|
private final ProviderManualMangaImportService providerManualMangaImportService;
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@ -55,7 +55,7 @@ public class MangaImportController {
|
|||||||
@RequestPart("files")
|
@RequestPart("files")
|
||||||
@NotNull
|
@NotNull
|
||||||
List<MultipartFile> files) {
|
List<MultipartFile> files) {
|
||||||
mangaImportService.importMangaFiles(malId, files);
|
// mangaImportService.importMangaFiles(malId, files);
|
||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
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;
|
package com.magamochi.controller;
|
||||||
|
|
||||||
import com.magamochi.common.dto.DefaultResponseDTO;
|
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||||
import com.magamochi.service.UserFavoriteMangaService;
|
import com.magamochi.service.UserFavoriteMangaService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package com.magamochi.ingestion.controller;
|
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.model.dto.ContentProviderListDTO;
|
||||||
import com.magamochi.ingestion.service.ContentProviderService;
|
import com.magamochi.ingestion.service.ContentProviderService;
|
||||||
import com.magamochi.ingestion.service.IngestionService;
|
import com.magamochi.ingestion.service.IngestionService;
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package com.magamochi.ingestion.model.dto;
|
package com.magamochi.ingestion.model.dto;
|
||||||
|
|
||||||
import com.magamochi.model.enumeration.MangaStatus;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
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.ingestion.providers.PagedContentProvider;
|
||||||
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
||||||
import com.magamochi.model.entity.MangaContentProvider;
|
import com.magamochi.model.entity.MangaContentProvider;
|
||||||
import com.magamochi.model.enumeration.MangaStatus;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@ -116,25 +115,13 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
|
|||||||
try {
|
try {
|
||||||
var linkElement = element.getElementsByTag("a").getFirst();
|
var linkElement = element.getElementsByTag("a").getFirst();
|
||||||
|
|
||||||
var imageContainer =
|
|
||||||
linkElement.getElementsByClass("manga-card-image").getFirst();
|
|
||||||
var contentContainer =
|
var contentContainer =
|
||||||
linkElement.getElementsByClass("manga-card-content").getFirst();
|
linkElement.getElementsByClass("manga-card-content").getFirst();
|
||||||
|
|
||||||
var title = contentContainer.getElementsByTag("h3").text();
|
var title = contentContainer.getElementsByTag("h3").text();
|
||||||
var url = linkElement.attr("href");
|
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) {
|
} catch (Exception e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import com.magamochi.ingestion.providers.PagedContentProvider;
|
|||||||
import com.magamochi.ingestion.service.FlareService;
|
import com.magamochi.ingestion.service.FlareService;
|
||||||
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
||||||
import com.magamochi.model.entity.MangaContentProvider;
|
import com.magamochi.model.entity.MangaContentProvider;
|
||||||
import com.magamochi.model.enumeration.MangaStatus;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.IntStream;
|
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();
|
.toList();
|
||||||
} catch (NoSuchElementException e) {
|
} catch (NoSuchElementException e) {
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import com.magamochi.ingestion.providers.PagedContentProvider;
|
|||||||
import com.magamochi.ingestion.service.FlareService;
|
import com.magamochi.ingestion.service.FlareService;
|
||||||
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
||||||
import com.magamochi.model.entity.MangaContentProvider;
|
import com.magamochi.model.entity.MangaContentProvider;
|
||||||
import com.magamochi.model.enumeration.MangaStatus;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
@ -142,7 +141,7 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
|
|||||||
var textElement = linkElement.getElementsByTag("h3");
|
var textElement = linkElement.getElementsByTag("h3");
|
||||||
var title = textElement.text().trim();
|
var title = textElement.text().trim();
|
||||||
|
|
||||||
return new MangaInfoDTO(title, url, MangaStatus.UNKNOWN);
|
return new MangaInfoDTO(title, url);
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
} catch (NoSuchElementException e) {
|
} catch (NoSuchElementException e) {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package com.magamochi.model.dto;
|
|||||||
|
|
||||||
import static java.util.Objects.isNull;
|
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.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
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;
|
package com.magamochi.model.entity;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.model.entity.Language;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.magamochi.model.entity;
|
package com.magamochi.model.entity;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import com.magamochi.ingestion.model.entity.ContentProvider;
|
import com.magamochi.ingestion.model.entity.ContentProvider;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.magamochi.model.entity;
|
package com.magamochi.model.entity;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.magamochi.model.entity;
|
package com.magamochi.model.entity;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
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;
|
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.User;
|
||||||
import com.magamochi.model.entity.UserFavoriteManga;
|
import com.magamochi.model.entity.UserFavoriteManga;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package com.magamochi.model.repository;
|
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.User;
|
||||||
import com.magamochi.model.entity.UserMangaFollow;
|
import com.magamochi.model.entity.UserMangaFollow;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|||||||
@ -2,9 +2,9 @@ package com.magamochi.model.specification;
|
|||||||
|
|
||||||
import static java.util.Objects.nonNull;
|
import static java.util.Objects.nonNull;
|
||||||
|
|
||||||
import com.magamochi.model.dto.MangaListFilterDTO;
|
import com.magamochi.catalog.model.dto.MangaListFilterDTO;
|
||||||
import com.magamochi.model.entity.Author;
|
import com.magamochi.catalog.model.entity.Author;
|
||||||
import com.magamochi.model.entity.Manga;
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import com.magamochi.model.entity.User;
|
import com.magamochi.model.entity.User;
|
||||||
import jakarta.persistence.criteria.*;
|
import jakarta.persistence.criteria.*;
|
||||||
import java.util.ArrayList;
|
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;
|
package com.magamochi.queue;
|
||||||
|
|
||||||
import com.magamochi.model.dto.UpdateMangaFollowChapterListCommand;
|
import com.magamochi.model.dto.UpdateMangaFollowChapterListCommand;
|
||||||
import com.magamochi.service.MangaService;
|
import com.magamochi.service.OldMangaService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||||
@ -11,11 +11,11 @@ import org.springframework.stereotype.Service;
|
|||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UpdateMangaFollowChapterListConsumer {
|
public class UpdateMangaFollowChapterListConsumer {
|
||||||
private final MangaService mangaService;
|
private final OldMangaService oldMangaService;
|
||||||
|
|
||||||
@RabbitListener(queues = "${rabbit-mq.queues.manga-follow-update-chapter}")
|
@RabbitListener(queues = "${rabbit-mq.queues.manga-follow-update-chapter}")
|
||||||
public void receiveMangaFollowUpdateChapterCommand(UpdateMangaFollowChapterListCommand command) {
|
public void receiveMangaFollowUpdateChapterCommand(UpdateMangaFollowChapterListCommand command) {
|
||||||
log.info("Received update followed manga chapter list command: {}", 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;
|
// package com.magamochi.service;
|
||||||
|
//
|
||||||
import static java.util.Objects.isNull;
|
// import static java.util.Objects.isNull;
|
||||||
import static java.util.Objects.nonNull;
|
// import static java.util.Objects.nonNull;
|
||||||
|
//
|
||||||
import com.google.common.util.concurrent.RateLimiter;
|
// import com.google.common.util.concurrent.RateLimiter;
|
||||||
import com.magamochi.catalog.model.entity.Genre;
|
// import com.magamochi.catalog.model.entity.Genre;
|
||||||
import com.magamochi.catalog.model.repository.GenreRepository;
|
// import com.magamochi.catalog.model.repository.GenreRepository;
|
||||||
import com.magamochi.client.AniListClient;
|
// import com.magamochi.catalog.client.AniListClient;
|
||||||
import com.magamochi.client.JikanClient;
|
// import com.magamochi.catalog.client.JikanClient;
|
||||||
import com.magamochi.common.exception.NotFoundException;
|
// import com.magamochi.common.exception.NotFoundException;
|
||||||
import com.magamochi.ingestion.model.entity.ContentProvider;
|
// import com.magamochi.ingestion.model.entity.ContentProvider;
|
||||||
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
// import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
||||||
import com.magamochi.model.entity.*;
|
// import com.magamochi.model.entity.*;
|
||||||
import com.magamochi.model.repository.*;
|
// import com.magamochi.model.repository.*;
|
||||||
import com.magamochi.util.DoubleUtil;
|
// import com.magamochi.catalog.util.DoubleUtil;
|
||||||
import java.io.*;
|
// import java.io.*;
|
||||||
import java.net.URI;
|
// import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
// import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
// import java.net.URL;
|
||||||
import java.time.OffsetDateTime;
|
// import java.time.OffsetDateTime;
|
||||||
import java.time.ZoneOffset;
|
// import java.time.ZoneOffset;
|
||||||
import java.util.ArrayList;
|
// import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
// import java.util.Comparator;
|
||||||
import java.util.List;
|
// import java.util.List;
|
||||||
import java.util.stream.IntStream;
|
// import java.util.stream.IntStream;
|
||||||
import java.util.zip.ZipEntry;
|
// import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
// import java.util.zip.ZipInputStream;
|
||||||
import lombok.RequiredArgsConstructor;
|
// import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.log4j.Log4j2;
|
// import lombok.extern.log4j.Log4j2;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
// import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
// import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
// import org.springframework.web.multipart.MultipartFile;
|
||||||
|
//
|
||||||
@Log4j2
|
// @Log4j2
|
||||||
@Service
|
//// @Service
|
||||||
@RequiredArgsConstructor
|
// @RequiredArgsConstructor
|
||||||
public class MangaImportService {
|
// public class MangaImportService {
|
||||||
private final ProviderService providerService;
|
// private final ProviderService providerService;
|
||||||
private final MangaCreationService mangaCreationService;
|
// private final MangaCreationService mangaCreationService;
|
||||||
private final ImageService imageService;
|
// private final ImageService imageService;
|
||||||
private final LanguageService languageService;
|
// private final LanguageService languageService;
|
||||||
|
//
|
||||||
private final GenreRepository genreRepository;
|
// private final GenreRepository genreRepository;
|
||||||
private final MangaGenreRepository mangaGenreRepository;
|
// private final MangaGenreRepository mangaGenreRepository;
|
||||||
private final MangaContentProviderRepository mangaContentProviderRepository;
|
// private final MangaContentProviderRepository mangaContentProviderRepository;
|
||||||
private final AuthorRepository authorRepository;
|
// private final AuthorRepository authorRepository;
|
||||||
private final MangaAuthorRepository mangaAuthorRepository;
|
// private final MangaAuthorRepository mangaAuthorRepository;
|
||||||
private final MangaChapterRepository mangaChapterRepository;
|
// private final MangaChapterRepository mangaChapterRepository;
|
||||||
private final MangaRepository mangaRepository;
|
// private final MangaRepository mangaRepository;
|
||||||
|
//
|
||||||
private final JikanClient jikanClient;
|
// private final JikanClient jikanClient;
|
||||||
private final AniListClient aniListClient;
|
// private final AniListClient aniListClient;
|
||||||
private final MangaChapterImageRepository mangaChapterImageRepository;
|
// private final MangaChapterImageRepository mangaChapterImageRepository;
|
||||||
private final MangaAlternativeTitlesRepository mangaAlternativeTitlesRepository;
|
// private final MangaAlternativeTitlesRepository mangaAlternativeTitlesRepository;
|
||||||
|
//
|
||||||
private final RateLimiter jikanRateLimiter;
|
// private final RateLimiter jikanRateLimiter;
|
||||||
|
//
|
||||||
public void importMangaFiles(String malId, List<MultipartFile> files) {
|
// public void importMangaFiles(String malId, List<MultipartFile> files) {
|
||||||
log.info("Importing manga files for MAL ID {}", malId);
|
// log.info("Importing manga files for MAL ID {}", malId);
|
||||||
var provider = providerService.getOrCreateProvider("Manual Import", false);
|
// var provider = providerService.getOrCreateProvider("Manual Import", false);
|
||||||
|
//
|
||||||
jikanRateLimiter.acquire();
|
// jikanRateLimiter.acquire();
|
||||||
var mangaData = jikanClient.getMangaById(Long.parseLong(malId));
|
// var mangaData = jikanClient.getMangaById(Long.parseLong(malId));
|
||||||
|
//
|
||||||
var mangaProvider = getOrCreateMangaProvider(mangaData.data().title(), provider);
|
// var mangaProvider = getOrCreateMangaProvider(mangaData.data().title(), provider);
|
||||||
|
//
|
||||||
var sortedFiles = files.stream().sorted(Comparator.comparing(MultipartFile::getName)).toList();
|
// var sortedFiles =
|
||||||
|
// files.stream().sorted(Comparator.comparing(MultipartFile::getName)).toList();
|
||||||
IntStream.rangeClosed(1, sortedFiles.size())
|
//
|
||||||
.forEach(
|
// IntStream.rangeClosed(1, sortedFiles.size())
|
||||||
fileIndex -> {
|
// .forEach(
|
||||||
var file = sortedFiles.get(fileIndex - 1);
|
// fileIndex -> {
|
||||||
log.info(
|
// var file = sortedFiles.get(fileIndex - 1);
|
||||||
"Importing file {}/{}: {}, for Mangá {}",
|
// log.info(
|
||||||
fileIndex,
|
// "Importing file {}/{}: {}, for Mangá {}",
|
||||||
sortedFiles.size(),
|
// fileIndex,
|
||||||
file.getOriginalFilename(),
|
// sortedFiles.size(),
|
||||||
mangaProvider.getManga().getTitle());
|
// file.getOriginalFilename(),
|
||||||
|
// mangaProvider.getManga().getTitle());
|
||||||
var chapter =
|
//
|
||||||
persistMangaChapter(
|
// var chapter =
|
||||||
mangaProvider,
|
// persistMangaChapter(
|
||||||
new ContentProviderMangaChapterResponseDTO(
|
// mangaProvider,
|
||||||
removeFileExtension(file.getOriginalFilename()),
|
// new ContentProviderMangaChapterResponseDTO(
|
||||||
"manual_" + file.getOriginalFilename(),
|
// removeFileExtension(file.getOriginalFilename()),
|
||||||
file.getOriginalFilename(),
|
// "manual_" + file.getOriginalFilename(),
|
||||||
"en-US"));
|
// file.getOriginalFilename(),
|
||||||
|
// "en-US"));
|
||||||
List<MangaChapterImage> allChapterImages = new ArrayList<>();
|
//
|
||||||
try (InputStream is = file.getInputStream();
|
// List<MangaChapterImage> allChapterImages = new ArrayList<>();
|
||||||
ZipInputStream zis = new ZipInputStream(is)) {
|
// try (InputStream is = file.getInputStream();
|
||||||
ZipEntry entry;
|
// ZipInputStream zis = new ZipInputStream(is)) {
|
||||||
var position = 0;
|
// ZipEntry entry;
|
||||||
|
// var position = 0;
|
||||||
while ((entry = zis.getNextEntry()) != null) {
|
//
|
||||||
if (entry.isDirectory()) {
|
// while ((entry = zis.getNextEntry()) != null) {
|
||||||
continue;
|
// if (entry.isDirectory()) {
|
||||||
}
|
// continue;
|
||||||
|
// }
|
||||||
var os = new ByteArrayOutputStream();
|
//
|
||||||
zis.transferTo(os);
|
// var os = new ByteArrayOutputStream();
|
||||||
var bytes = os.toByteArray();
|
// zis.transferTo(os);
|
||||||
|
// var bytes = os.toByteArray();
|
||||||
var image =
|
//
|
||||||
imageService.uploadImage(bytes, "image/jpeg", "chapter/" + chapter.getId());
|
// var image =
|
||||||
|
// imageService.uploadImage(bytes, "image/jpeg", "chapter/" + chapter.getId());
|
||||||
var chapterImage =
|
//
|
||||||
MangaChapterImage.builder()
|
// var chapterImage =
|
||||||
.position(position++)
|
// MangaChapterImage.builder()
|
||||||
.image(image)
|
// .position(position++)
|
||||||
.mangaChapter(chapter)
|
// .image(image)
|
||||||
.build();
|
// .mangaChapter(chapter)
|
||||||
|
// .build();
|
||||||
allChapterImages.add(chapterImage);
|
//
|
||||||
zis.closeEntry();
|
// allChapterImages.add(chapterImage);
|
||||||
}
|
// zis.closeEntry();
|
||||||
|
// }
|
||||||
log.info("Chapter images added for chapter {}", chapter.getTitle());
|
//
|
||||||
} catch (IOException e) {
|
// log.info("Chapter images added for chapter {}", chapter.getTitle());
|
||||||
throw new RuntimeException(e);
|
// } catch (IOException e) {
|
||||||
}
|
// throw new RuntimeException(e);
|
||||||
|
// }
|
||||||
mangaChapterImageRepository.saveAll(allChapterImages);
|
//
|
||||||
chapter.setDownloaded(true);
|
// mangaChapterImageRepository.saveAll(allChapterImages);
|
||||||
mangaChapterRepository.save(chapter);
|
// chapter.setDownloaded(true);
|
||||||
});
|
// mangaChapterRepository.save(chapter);
|
||||||
|
// });
|
||||||
log.info("Import manga files for MAL ID {} completed.", malId);
|
//
|
||||||
}
|
// log.info("Import manga files for MAL ID {} completed.", malId);
|
||||||
|
// }
|
||||||
public void updateMangaData(Long mangaId) {
|
//
|
||||||
var manga =
|
// public void updateMangaData(Long mangaId) {
|
||||||
mangaRepository
|
// var manga =
|
||||||
.findById(mangaId)
|
// mangaRepository
|
||||||
.orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
|
// .findById(mangaId)
|
||||||
|
// .orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
|
||||||
updateMangaData(manga);
|
//
|
||||||
}
|
// updateMangaData(manga);
|
||||||
|
// }
|
||||||
public void updateMangaData(Manga manga) {
|
//
|
||||||
log.info("Updating manga {}", manga.getTitle());
|
// public void updateMangaData(Manga manga) {
|
||||||
|
// log.info("Updating manga {}", manga.getTitle());
|
||||||
if (nonNull(manga.getMalId())) {
|
//
|
||||||
try {
|
// if (nonNull(manga.getMalId())) {
|
||||||
updateFromJikan(manga);
|
// try {
|
||||||
return;
|
// updateFromJikan(manga);
|
||||||
} catch (Exception e) {
|
// return;
|
||||||
log.warn(
|
// } catch (Exception e) {
|
||||||
"Error updating manga data from Jikan for manga {}. Trying AniList... Error: {}",
|
// log.warn(
|
||||||
manga.getTitle(),
|
// "Error updating manga data from Jikan for manga {}. Trying AniList... Error: {}",
|
||||||
e.getMessage());
|
// manga.getTitle(),
|
||||||
}
|
// e.getMessage());
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
if (nonNull(manga.getAniListId())) {
|
//
|
||||||
try {
|
// if (nonNull(manga.getAniListId())) {
|
||||||
updateFromAniList(manga);
|
// try {
|
||||||
return;
|
// updateFromAniList(manga);
|
||||||
} catch (Exception e) {
|
// return;
|
||||||
log.warn(
|
// } catch (Exception e) {
|
||||||
"Error updating manga data from AniList for manga {}. Error: {}",
|
// log.warn(
|
||||||
manga.getTitle(),
|
// "Error updating manga data from AniList for manga {}. Error: {}",
|
||||||
e.getMessage());
|
// manga.getTitle(),
|
||||||
}
|
// e.getMessage());
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
log.warn(
|
//
|
||||||
"Could not update manga data for {}. No provider data available/found.", manga.getTitle());
|
// 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());
|
// private void updateFromJikan(Manga manga) throws IOException, URISyntaxException {
|
||||||
|
// jikanRateLimiter.acquire();
|
||||||
manga.setSynopsis(mangaData.data().synopsis());
|
// var mangaData = jikanClient.getMangaById(manga.getMalId());
|
||||||
manga.setStatus(mangaData.data().status());
|
//
|
||||||
manga.setScore(DoubleUtil.round((double) mangaData.data().score(), 2));
|
// manga.setSynopsis(mangaData.data().synopsis());
|
||||||
manga.setPublishedFrom(mangaData.data().published().from());
|
// manga.setStatus(mangaData.data().status());
|
||||||
manga.setPublishedTo(mangaData.data().published().to());
|
// manga.setScore(DoubleUtil.round((double) mangaData.data().score(), 2));
|
||||||
manga.setChapterCount(mangaData.data().chapters());
|
// manga.setPublishedFrom(mangaData.data().published().from());
|
||||||
|
// manga.setPublishedTo(mangaData.data().published().to());
|
||||||
var authors =
|
// manga.setChapterCount(mangaData.data().chapters());
|
||||||
mangaData.data().authors().stream()
|
//
|
||||||
.map(
|
// var authors =
|
||||||
authorData ->
|
// mangaData.data().authors().stream()
|
||||||
authorRepository
|
// .map(
|
||||||
.findByMalId(authorData.mal_id())
|
// authorData ->
|
||||||
.orElseGet(
|
// authorRepository
|
||||||
() ->
|
// .findByMalId(authorData.mal_id())
|
||||||
authorRepository.save(
|
// .orElseGet(
|
||||||
Author.builder()
|
// () ->
|
||||||
.malId(authorData.mal_id())
|
// authorRepository.save(
|
||||||
.name(authorData.name())
|
// Author.builder()
|
||||||
.build())))
|
// .malId(authorData.mal_id())
|
||||||
.toList();
|
// .name(authorData.name())
|
||||||
|
// .build())))
|
||||||
updateMangaAuthors(manga, authors);
|
// .toList();
|
||||||
|
//
|
||||||
var genres =
|
// updateMangaAuthors(manga, authors);
|
||||||
mangaData.data().genres().stream()
|
//
|
||||||
.map(
|
// var genres =
|
||||||
genreData ->
|
// mangaData.data().genres().stream()
|
||||||
genreRepository
|
// .map(
|
||||||
.findByMalId(genreData.mal_id())
|
// genreData ->
|
||||||
.orElseGet(
|
// genreRepository
|
||||||
() ->
|
// .findByMalId(genreData.mal_id())
|
||||||
genreRepository.save(
|
// .orElseGet(
|
||||||
Genre.builder()
|
// () ->
|
||||||
.malId(genreData.mal_id())
|
// genreRepository.save(
|
||||||
.name(genreData.name())
|
// Genre.builder()
|
||||||
.build())))
|
// .malId(genreData.mal_id())
|
||||||
.toList();
|
// .name(genreData.name())
|
||||||
|
// .build())))
|
||||||
updateMangaGenres(manga, genres);
|
// .toList();
|
||||||
|
//
|
||||||
if (isNull(manga.getCoverImage())) {
|
// updateMangaGenres(manga, genres);
|
||||||
downloadCoverImage(manga, mangaData.data().images().jpg().large_image_url());
|
//
|
||||||
}
|
// 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()
|
// var mangaEntity = mangaRepository.save(manga);
|
||||||
.map(at -> MangaAlternativeTitle.builder().manga(mangaEntity).title(at).build())
|
// var alternativeTitles =
|
||||||
.toList();
|
// mangaData.data().title_synonyms().stream()
|
||||||
mangaAlternativeTitlesRepository.saveAll(alternativeTitles);
|
// .map(at -> MangaAlternativeTitle.builder().manga(mangaEntity).title(at).build())
|
||||||
}
|
// .toList();
|
||||||
|
// mangaAlternativeTitlesRepository.saveAll(alternativeTitles);
|
||||||
private void updateFromAniList(Manga manga) throws IOException, URISyntaxException {
|
// }
|
||||||
var query =
|
//
|
||||||
"""
|
// private void updateFromAniList(Manga manga) throws IOException, URISyntaxException {
|
||||||
query ($id: Int) {
|
// var query =
|
||||||
Media (id: $id, type: MANGA) {
|
// """
|
||||||
startDate { year month day }
|
// query ($id: Int) {
|
||||||
endDate { year month day }
|
// Media (id: $id, type: MANGA) {
|
||||||
description
|
// startDate { year month day }
|
||||||
status
|
// endDate { year month day }
|
||||||
averageScore
|
// description
|
||||||
chapters
|
// status
|
||||||
coverImage { large }
|
// averageScore
|
||||||
genres
|
// chapters
|
||||||
staff {
|
// coverImage { large }
|
||||||
edges {
|
// genres
|
||||||
role
|
// staff {
|
||||||
node {
|
// edges {
|
||||||
name {
|
// role
|
||||||
full
|
// node {
|
||||||
}
|
// name {
|
||||||
}
|
// full
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
""";
|
// }
|
||||||
var request =
|
// }
|
||||||
new AniListClient.GraphQLRequest(
|
// """;
|
||||||
query, new AniListClient.GraphQLRequest.Variables(manga.getAniListId()));
|
// var request =
|
||||||
var media = aniListClient.getManga(request).data().Media();
|
// new AniListClient.GraphQLRequest(
|
||||||
|
// query, new AniListClient.GraphQLRequest.Variables(manga.getAniListId()));
|
||||||
manga.setSynopsis(media.description());
|
// var media = aniListClient.getManga(request).data().Media();
|
||||||
manga.setStatus(mapAniListStatus(media.status()));
|
//
|
||||||
manga.setScore(DoubleUtil.round((double) media.averageScore() / 10, 2)); // 0-100 -> 0-10
|
// manga.setSynopsis(media.description());
|
||||||
manga.setPublishedFrom(convertFuzzyDate(media.startDate()));
|
// manga.setStatus(mapAniListStatus(media.status()));
|
||||||
manga.setPublishedTo(convertFuzzyDate(media.endDate()));
|
// manga.setScore(DoubleUtil.round((double) media.averageScore() / 10, 2)); // 0-100 -> 0-10
|
||||||
manga.setChapterCount(media.chapters());
|
// manga.setPublishedFrom(convertFuzzyDate(media.startDate()));
|
||||||
|
// manga.setPublishedTo(convertFuzzyDate(media.endDate()));
|
||||||
var authors =
|
// manga.setChapterCount(media.chapters());
|
||||||
media.staff().edges().stream()
|
//
|
||||||
.filter(edge -> isAuthorRole(edge.role()))
|
// var authors =
|
||||||
.map(edge -> edge.node().name().full())
|
// media.staff().edges().stream()
|
||||||
.distinct()
|
// .filter(edge -> isAuthorRole(edge.role()))
|
||||||
.map(
|
// .map(edge -> edge.node().name().full())
|
||||||
name ->
|
// .distinct()
|
||||||
authorRepository
|
// .map(
|
||||||
.findByName(name)
|
// name ->
|
||||||
.orElseGet(
|
// authorRepository
|
||||||
() -> authorRepository.save(Author.builder().name(name).build())))
|
// .findByName(name)
|
||||||
.toList();
|
// .orElseGet(
|
||||||
|
// () -> authorRepository.save(Author.builder().name(name).build())))
|
||||||
updateMangaAuthors(manga, authors);
|
// .toList();
|
||||||
|
//
|
||||||
var genres =
|
// updateMangaAuthors(manga, authors);
|
||||||
media.genres().stream()
|
//
|
||||||
.map(
|
// var genres =
|
||||||
name ->
|
// media.genres().stream()
|
||||||
genreRepository
|
// .map(
|
||||||
.findByName(name)
|
// name ->
|
||||||
.orElseGet(() -> genreRepository.save(Genre.builder().name(name).build())))
|
// genreRepository
|
||||||
.toList();
|
// .findByName(name)
|
||||||
|
// .orElseGet(() ->
|
||||||
updateMangaGenres(manga, genres);
|
// genreRepository.save(Genre.builder().name(name).build())))
|
||||||
|
// .toList();
|
||||||
if (isNull(manga.getCoverImage())) {
|
//
|
||||||
downloadCoverImage(manga, media.coverImage().large());
|
// updateMangaGenres(manga, genres);
|
||||||
}
|
//
|
||||||
|
// if (isNull(manga.getCoverImage())) {
|
||||||
mangaRepository.save(manga);
|
// downloadCoverImage(manga, media.coverImage().large());
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private boolean isAuthorRole(String role) {
|
// mangaRepository.save(manga);
|
||||||
return role.equalsIgnoreCase("Story & Art")
|
// }
|
||||||
|| role.equalsIgnoreCase("Story")
|
//
|
||||||
|| role.equalsIgnoreCase("Art");
|
// private boolean isAuthorRole(String role) {
|
||||||
}
|
// return role.equalsIgnoreCase("Story & Art")
|
||||||
|
// || role.equalsIgnoreCase("Story")
|
||||||
private String mapAniListStatus(String status) {
|
// || role.equalsIgnoreCase("Art");
|
||||||
return switch (status) {
|
// }
|
||||||
case "RELEASING" -> "Publishing";
|
//
|
||||||
case "FINISHED" -> "Finished";
|
// private String mapAniListStatus(String status) {
|
||||||
case "NOT_YET_RELEASED" -> "Not yet published";
|
// return switch (status) {
|
||||||
default -> "Unknown";
|
// 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;
|
//
|
||||||
}
|
// private OffsetDateTime convertFuzzyDate(AniListClient.MangaResponse.Manga.FuzzyDate date) {
|
||||||
return OffsetDateTime.of(
|
// if (isNull(date) || isNull(date.year())) {
|
||||||
date.year(),
|
// return null;
|
||||||
isNull(date.month()) ? 1 : date.month(),
|
// }
|
||||||
isNull(date.day()) ? 1 : date.day(),
|
// return OffsetDateTime.of(
|
||||||
0,
|
// date.year(),
|
||||||
0,
|
// isNull(date.month()) ? 1 : date.month(),
|
||||||
0,
|
// isNull(date.day()) ? 1 : date.day(),
|
||||||
0,
|
// 0,
|
||||||
ZoneOffset.UTC);
|
// 0,
|
||||||
}
|
// 0,
|
||||||
|
// 0,
|
||||||
private void updateMangaAuthors(Manga manga, List<Author> authors) {
|
// ZoneOffset.UTC);
|
||||||
var mangaAuthors =
|
// }
|
||||||
authors.stream()
|
//
|
||||||
.map(
|
// private void updateMangaAuthors(Manga manga, List<Author> authors) {
|
||||||
author ->
|
// var mangaAuthors =
|
||||||
mangaAuthorRepository
|
// authors.stream()
|
||||||
.findByMangaAndAuthor(manga, author)
|
// .map(
|
||||||
.orElseGet(
|
// author ->
|
||||||
() ->
|
// mangaAuthorRepository
|
||||||
mangaAuthorRepository.save(
|
// .findByMangaAndAuthor(manga, author)
|
||||||
MangaAuthor.builder().manga(manga).author(author).build())))
|
// .orElseGet(
|
||||||
.toList();
|
// () ->
|
||||||
manga.setMangaAuthors(mangaAuthors);
|
// mangaAuthorRepository.save(
|
||||||
}
|
// MangaAuthor.builder().manga(manga).author(author).build())))
|
||||||
|
// .toList();
|
||||||
private void updateMangaGenres(Manga manga, List<Genre> genres) {
|
// manga.setMangaAuthors(mangaAuthors);
|
||||||
var mangaGenres =
|
// }
|
||||||
genres.stream()
|
//
|
||||||
.map(
|
// private void updateMangaGenres(Manga manga, List<Genre> genres) {
|
||||||
genre ->
|
// var mangaGenres =
|
||||||
mangaGenreRepository
|
// genres.stream()
|
||||||
.findByMangaAndGenre(manga, genre)
|
// .map(
|
||||||
.orElseGet(
|
// genre ->
|
||||||
() ->
|
// mangaGenreRepository
|
||||||
mangaGenreRepository.save(
|
// .findByMangaAndGenre(manga, genre)
|
||||||
MangaGenre.builder().manga(manga).genre(genre).build())))
|
// .orElseGet(
|
||||||
.toList();
|
// () ->
|
||||||
manga.setMangaGenres(mangaGenres);
|
// mangaGenreRepository.save(
|
||||||
}
|
// MangaGenre.builder().manga(manga).genre(genre).build())))
|
||||||
|
// .toList();
|
||||||
private void downloadCoverImage(Manga manga, String imageUrl)
|
// manga.setMangaGenres(mangaGenres);
|
||||||
throws IOException, URISyntaxException {
|
// }
|
||||||
var inputStream =
|
//
|
||||||
new BufferedInputStream(new URL(new URI(imageUrl).toASCIIString()).openStream());
|
// private void downloadCoverImage(Manga manga, String imageUrl)
|
||||||
|
// throws IOException, URISyntaxException {
|
||||||
var bytes = inputStream.readAllBytes();
|
// var inputStream =
|
||||||
|
// new BufferedInputStream(new URL(new URI(imageUrl).toASCIIString()).openStream());
|
||||||
inputStream.close();
|
//
|
||||||
var image = imageService.uploadImage(bytes, "image/jpeg", "cover");
|
// var bytes = inputStream.readAllBytes();
|
||||||
|
//
|
||||||
manga.setCoverImage(image);
|
// inputStream.close();
|
||||||
}
|
// var image = imageService.uploadImage(bytes, "image/jpeg", "cover");
|
||||||
|
//
|
||||||
public MangaChapter persistMangaChapter(
|
// manga.setCoverImage(image);
|
||||||
MangaContentProvider mangaContentProvider, ContentProviderMangaChapterResponseDTO chapter) {
|
// }
|
||||||
var mangaChapter =
|
//
|
||||||
mangaChapterRepository
|
// public MangaChapter persistMangaChapter(
|
||||||
.findByMangaContentProviderAndUrlIgnoreCase(mangaContentProvider, chapter.chapterUrl())
|
// MangaContentProvider mangaContentProvider, ContentProviderMangaChapterResponseDTO chapter) {
|
||||||
.orElseGet(MangaChapter::new);
|
// var mangaChapter =
|
||||||
|
// mangaChapterRepository
|
||||||
mangaChapter.setMangaContentProvider(mangaContentProvider);
|
// .findByMangaContentProviderAndUrlIgnoreCase(mangaContentProvider,
|
||||||
mangaChapter.setTitle(chapter.chapterTitle());
|
// chapter.chapterUrl())
|
||||||
mangaChapter.setUrl(chapter.chapterUrl());
|
// .orElseGet(MangaChapter::new);
|
||||||
|
//
|
||||||
var language = languageService.getOrThrow(chapter.languageCode());
|
// mangaChapter.setMangaContentProvider(mangaContentProvider);
|
||||||
mangaChapter.setLanguage(language);
|
// mangaChapter.setTitle(chapter.chapterTitle());
|
||||||
|
// mangaChapter.setUrl(chapter.chapterUrl());
|
||||||
if (nonNull(chapter.chapter())) {
|
//
|
||||||
try {
|
// var language = languageService.getOrThrow(chapter.languageCode());
|
||||||
mangaChapter.setChapterNumber(Integer.parseInt(chapter.chapter()));
|
// mangaChapter.setLanguage(language);
|
||||||
} catch (NumberFormatException e) {
|
//
|
||||||
log.warn(
|
// if (nonNull(chapter.chapter())) {
|
||||||
"Could not parse chapter number {} from manga {}",
|
// try {
|
||||||
chapter.chapter(),
|
// mangaChapter.setChapterNumber(Integer.parseInt(chapter.chapter()));
|
||||||
mangaContentProvider.getManga().getTitle());
|
// } catch (NumberFormatException e) {
|
||||||
}
|
// log.warn(
|
||||||
}
|
// "Could not parse chapter number {} from manga {}",
|
||||||
|
// chapter.chapter(),
|
||||||
return mangaChapterRepository.save(mangaChapter);
|
// mangaContentProvider.getManga().getTitle());
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
private MangaContentProvider getOrCreateMangaProvider(
|
//
|
||||||
String title, ContentProvider contentProvider) {
|
// return mangaChapterRepository.save(mangaChapter);
|
||||||
return mangaContentProviderRepository
|
// }
|
||||||
.findByMangaTitleIgnoreCaseAndContentProvider(title, contentProvider)
|
//
|
||||||
.orElseGet(
|
// private MangaContentProvider getOrCreateMangaProvider(
|
||||||
() -> {
|
// String title, ContentProvider contentProvider) {
|
||||||
jikanRateLimiter.acquire();
|
// return mangaContentProviderRepository
|
||||||
var manga = mangaCreationService.getOrCreateManga(title, "manual", contentProvider);
|
// .findByMangaTitleIgnoreCaseAndContentProvider(title, contentProvider)
|
||||||
|
// .orElseGet(
|
||||||
return mangaContentProviderRepository.save(
|
// () -> {
|
||||||
MangaContentProvider.builder()
|
// jikanRateLimiter.acquire();
|
||||||
.manga(manga)
|
// var manga = mangaCreationService.getOrCreateManga(title, "manual", contentProvider);
|
||||||
.mangaTitle(manga.getTitle())
|
//
|
||||||
.contentProvider(contentProvider)
|
// return mangaContentProviderRepository.save(
|
||||||
.url("manual")
|
// MangaContentProvider.builder()
|
||||||
.build());
|
// .manga(manga)
|
||||||
});
|
// .mangaTitle(manga.getTitle())
|
||||||
}
|
// .contentProvider(contentProvider)
|
||||||
|
// .url("manual")
|
||||||
private String removeFileExtension(String filename) {
|
// .build());
|
||||||
if (StringUtils.isBlank(filename)) {
|
// });
|
||||||
return filename;
|
// }
|
||||||
}
|
//
|
||||||
|
// private String removeFileExtension(String filename) {
|
||||||
int lastDotIndex = filename.lastIndexOf('.');
|
// if (StringUtils.isBlank(filename)) {
|
||||||
|
// return filename;
|
||||||
// No dot, or dot is the first character (like .gitignore)
|
// }
|
||||||
if (lastDotIndex <= 0) {
|
//
|
||||||
return filename;
|
// int lastDotIndex = filename.lastIndexOf('.');
|
||||||
}
|
//
|
||||||
|
// // No dot, or dot is the first character (like .gitignore)
|
||||||
return filename.substring(0, lastDotIndex);
|
// 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;
|
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.client.NtfyClient;
|
||||||
import com.magamochi.common.exception.NotFoundException;
|
import com.magamochi.common.exception.NotFoundException;
|
||||||
import com.magamochi.ingestion.providers.ContentProviderFactory;
|
import com.magamochi.ingestion.providers.ContentProviderFactory;
|
||||||
import com.magamochi.model.dto.*;
|
import com.magamochi.model.dto.*;
|
||||||
import com.magamochi.model.entity.Manga;
|
|
||||||
import com.magamochi.model.entity.MangaChapter;
|
import com.magamochi.model.entity.MangaChapter;
|
||||||
import com.magamochi.model.entity.MangaContentProvider;
|
import com.magamochi.model.entity.MangaContentProvider;
|
||||||
import com.magamochi.model.entity.UserMangaFollow;
|
import com.magamochi.model.entity.UserMangaFollow;
|
||||||
import com.magamochi.model.repository.*;
|
import com.magamochi.model.repository.*;
|
||||||
import com.magamochi.model.specification.MangaSpecification;
|
|
||||||
import com.magamochi.queue.MangaChapterDownloadProducer;
|
import com.magamochi.queue.MangaChapterDownloadProducer;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.log4j.Log4j2;
|
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.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Log4j2
|
@Log4j2
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class MangaService {
|
public class OldMangaService {
|
||||||
private final MangaImportService mangaImportService;
|
// private final MangaImportService mangaImportService;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final MangaRepository mangaRepository;
|
private final MangaRepository mangaRepository;
|
||||||
private final MangaContentProviderRepository mangaContentProviderRepository;
|
private final MangaContentProviderRepository mangaContentProviderRepository;
|
||||||
|
|
||||||
private final ContentProviderFactory contentProviderFactory;
|
private final ContentProviderFactory contentProviderFactory;
|
||||||
private final UserFavoriteMangaRepository userFavoriteMangaRepository;
|
|
||||||
|
|
||||||
private final UserMangaFollowRepository userMangaFollowRepository;
|
private final UserMangaFollowRepository userMangaFollowRepository;
|
||||||
|
|
||||||
@ -62,27 +57,6 @@ public class MangaService {
|
|||||||
new MangaChapterDownloadCommand(chapterId)));
|
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) {
|
public List<MangaChapterDTO> getMangaChapters(Long mangaProviderId) {
|
||||||
var mangaProvider = getMangaProviderThrowIfNotFound(mangaProviderId);
|
var mangaProvider = getMangaProviderThrowIfNotFound(mangaProviderId);
|
||||||
|
|
||||||
@ -92,30 +66,6 @@ public class MangaService {
|
|||||||
.toList();
|
.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) {
|
public void fetchFollowedMangaChapters(Long mangaProviderId) {
|
||||||
var mangaProvider =
|
var mangaProvider =
|
||||||
mangaContentProviderRepository
|
mangaContentProviderRepository
|
||||||
@ -155,8 +105,8 @@ public class MangaService {
|
|||||||
contentProviderFactory.getContentProvider(mangaProvider.getContentProvider().getName());
|
contentProviderFactory.getContentProvider(mangaProvider.getContentProvider().getName());
|
||||||
var availableChapters = contentProvider.getAvailableChapters(mangaProvider);
|
var availableChapters = contentProvider.getAvailableChapters(mangaProvider);
|
||||||
|
|
||||||
availableChapters.forEach(
|
// availableChapters.forEach(
|
||||||
chapter -> mangaImportService.persistMangaChapter(mangaProvider, chapter));
|
// chapter -> mangaImportService.persistMangaChapter(mangaProvider, chapter));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Manga findMangaByIdThrowIfNotFound(Long mangaId) {
|
public Manga findMangaByIdThrowIfNotFound(Long mangaId) {
|
||||||
@ -1,25 +1,21 @@
|
|||||||
package com.magamochi.service;
|
package com.magamochi.service;
|
||||||
|
|
||||||
import static java.util.Objects.isNull;
|
import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
|
||||||
import static java.util.Objects.nonNull;
|
|
||||||
|
|
||||||
import com.magamochi.common.exception.NotFoundException;
|
import com.magamochi.common.exception.NotFoundException;
|
||||||
import com.magamochi.ingestion.model.entity.ContentProvider;
|
import com.magamochi.ingestion.model.entity.ContentProvider;
|
||||||
import com.magamochi.ingestion.model.repository.ContentProviderRepository;
|
import com.magamochi.ingestion.model.repository.ContentProviderRepository;
|
||||||
import com.magamochi.ingestion.providers.ManualImportContentProviderFactory;
|
import com.magamochi.ingestion.providers.ManualImportContentProviderFactory;
|
||||||
import com.magamochi.model.dto.ImportMangaResponseDTO;
|
import com.magamochi.model.dto.ImportMangaResponseDTO;
|
||||||
import com.magamochi.model.dto.ImportRequestDTO;
|
import com.magamochi.model.dto.ImportRequestDTO;
|
||||||
import com.magamochi.model.entity.MangaContentProvider;
|
|
||||||
import com.magamochi.model.repository.MangaContentProviderRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.apache.commons.lang3.NotImplementedException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Log4j2
|
@Log4j2
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ProviderManualMangaImportService {
|
public class ProviderManualMangaImportService {
|
||||||
private final MangaCreationService mangaCreationService;
|
|
||||||
|
|
||||||
private final ManualImportContentProviderFactory contentProviderFactory;
|
private final ManualImportContentProviderFactory contentProviderFactory;
|
||||||
|
|
||||||
@ -27,35 +23,40 @@ public class ProviderManualMangaImportService {
|
|||||||
private final MangaContentProviderRepository mangaContentProviderRepository;
|
private final MangaContentProviderRepository mangaContentProviderRepository;
|
||||||
|
|
||||||
public ImportMangaResponseDTO importFromProvider(Long providerId, ImportRequestDTO requestDTO) {
|
public ImportMangaResponseDTO importFromProvider(Long providerId, ImportRequestDTO requestDTO) {
|
||||||
var provider = getProvider(providerId);
|
throw new NotImplementedException();
|
||||||
var contentProvider = contentProviderFactory.getManualImportContentProvider(provider.getName());
|
// var provider = getProvider(providerId);
|
||||||
|
// var contentProvider =
|
||||||
var title = contentProvider.getMangaTitle(requestDTO.id());
|
// contentProviderFactory.getManualImportContentProvider(provider.getName());
|
||||||
|
//
|
||||||
var malId = nonNull(requestDTO.metadataId()) ? Long.parseLong(requestDTO.metadataId()) : null;
|
// var title = contentProvider.getMangaTitle(requestDTO.id());
|
||||||
var aniListId = nonNull(requestDTO.aniListId()) ? Long.parseLong(requestDTO.aniListId()) : null;
|
//
|
||||||
|
// var malId = nonNull(requestDTO.metadataId()) ? Long.parseLong(requestDTO.metadataId()) :
|
||||||
var manga =
|
// null;
|
||||||
nonNull(malId) || nonNull(aniListId)
|
// var aniListId = nonNull(requestDTO.aniListId()) ? Long.parseLong(requestDTO.aniListId()) :
|
||||||
? mangaCreationService.getOrCreateManga(malId, aniListId)
|
// null;
|
||||||
: mangaCreationService.getOrCreateManga(title, requestDTO.id(), provider);
|
//
|
||||||
|
// var manga =
|
||||||
if (isNull(manga)) {
|
// nonNull(malId) || nonNull(aniListId)
|
||||||
throw new NotFoundException("Manga could not be found or created for ID: " + requestDTO.id());
|
// ? mangaCreationService.getOrCreateManga(malId, aniListId)
|
||||||
}
|
// : mangaCreationService.getOrCreateManga(title, requestDTO.id(), provider);
|
||||||
|
//
|
||||||
if (!mangaContentProviderRepository.existsByMangaAndContentProviderAndUrlIgnoreCase(
|
// if (isNull(manga)) {
|
||||||
manga, provider, requestDTO.id())) {
|
// throw new NotFoundException("Manga could not be found or created for ID: " +
|
||||||
mangaContentProviderRepository.save(
|
// requestDTO.id());
|
||||||
MangaContentProvider.builder()
|
// }
|
||||||
.manga(manga)
|
//
|
||||||
.mangaTitle(title)
|
// if (!mangaContentProviderRepository.existsByMangaAndContentProviderAndUrlIgnoreCase(
|
||||||
.contentProvider(provider)
|
// manga, provider, requestDTO.id())) {
|
||||||
.url(requestDTO.id())
|
// mangaContentProviderRepository.save(
|
||||||
.build());
|
// MangaContentProvider.builder()
|
||||||
}
|
// .manga(manga)
|
||||||
|
// .mangaTitle(title)
|
||||||
return new ImportMangaResponseDTO(manga.getId());
|
// .contentProvider(provider)
|
||||||
|
// .url(requestDTO.id())
|
||||||
|
// .build());
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return new ImportMangaResponseDTO(manga.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContentProvider getProvider(Long providerId) {
|
public ContentProvider getProvider(Long providerId) {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
package com.magamochi.service;
|
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.common.exception.NotFoundException;
|
||||||
import com.magamochi.model.entity.Manga;
|
|
||||||
import com.magamochi.model.entity.UserFavoriteManga;
|
import com.magamochi.model.entity.UserFavoriteManga;
|
||||||
import com.magamochi.model.repository.MangaRepository;
|
|
||||||
import com.magamochi.model.repository.UserFavoriteMangaRepository;
|
import com.magamochi.model.repository.UserFavoriteMangaRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
package com.magamochi.task;
|
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.dto.UpdateMangaFollowChapterListCommand;
|
||||||
import com.magamochi.model.entity.Manga;
|
|
||||||
import com.magamochi.model.repository.MangaRepository;
|
|
||||||
import com.magamochi.queue.UpdateMangaFollowChapterListProducer;
|
import com.magamochi.queue.UpdateMangaFollowChapterListProducer;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
|||||||
@ -91,6 +91,8 @@ resilience4j:
|
|||||||
queues:
|
queues:
|
||||||
manga-ingest: ${MANGA_INGEST_QUEUE:mangaIngest}
|
manga-ingest: ${MANGA_INGEST_QUEUE:mangaIngest}
|
||||||
provider-page-ingest: ${PROVIDER_PAGE_INGEST_QUEUE:providerPageIngest}
|
provider-page-ingest: ${PROVIDER_PAGE_INGEST_QUEUE:providerPageIngest}
|
||||||
|
manga-update: ${MANGA_UPDATE_QUEUE:mangaUpdate}
|
||||||
|
image-fetch: ${IMAGE_FETCH_QUEUE:imageFetch}
|
||||||
|
|
||||||
rabbit-mq:
|
rabbit-mq:
|
||||||
queues:
|
queues:
|
||||||
|
|||||||
@ -34,6 +34,7 @@ CREATE TABLE mangas
|
|||||||
published_to TIMESTAMPTZ,
|
published_to TIMESTAMPTZ,
|
||||||
chapter_count INT DEFAULT 0,
|
chapter_count INT DEFAULT 0,
|
||||||
follow BOOLEAN NOT NULL DEFAULT FALSE,
|
follow BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
state VARCHAR NOT NULL DEFAULT 'PENDING',
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_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,
|
url VARCHAR NOT NULL,
|
||||||
chapter_number INTEGER,
|
chapter_number INTEGER,
|
||||||
language_id BIGINT REFERENCES languages (id),
|
language_id BIGINT REFERENCES languages (id),
|
||||||
|
downloaded BOOLEAN DEFAULT FALSE,
|
||||||
|
read BOOLEAN DEFAULT FALSE,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_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,
|
manga_chapter_id BIGINT NOT NULL REFERENCES manga_chapters (id) ON DELETE CASCADE,
|
||||||
image_id UUID REFERENCES images (id) ON DELETE CASCADE,
|
image_id UUID REFERENCES images (id) ON DELETE CASCADE,
|
||||||
position INT NOT NULL,
|
position INT NOT NULL,
|
||||||
downloaded BOOLEAN DEFAULT FALSE,
|
|
||||||
read BOOLEAN DEFAULT FALSE,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_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,
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
content_provider_id BIGINT NOT NULL REFERENCES content_providers (id) ON DELETE CASCADE,
|
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,
|
url VARCHAR NOT NULL,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
@ -98,7 +99,6 @@ CREATE TABLE manga_import_reviews
|
|||||||
CREATE TABLE authors
|
CREATE TABLE authors
|
||||||
(
|
(
|
||||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
mal_id BIGINT UNIQUE,
|
|
||||||
name VARCHAR,
|
name VARCHAR,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_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
|
CREATE TABLE genres
|
||||||
(
|
(
|
||||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
mal_id BIGINT UNIQUE,
|
name VARCHAR
|
||||||
name VARCHAR
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE manga_genre
|
CREATE TABLE manga_genre
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user