feat: content service
This commit is contained in:
parent
7cb571939e
commit
55ca251f62
@ -4,9 +4,9 @@ import static java.util.Objects.isNull;
|
|||||||
|
|
||||||
import com.magamochi.catalog.model.entity.Manga;
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import com.magamochi.catalog.model.entity.MangaAlternativeTitle;
|
import com.magamochi.catalog.model.entity.MangaAlternativeTitle;
|
||||||
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
import com.magamochi.catalog.model.enumeration.MangaStatus;
|
import com.magamochi.catalog.model.enumeration.MangaStatus;
|
||||||
import com.magamochi.model.entity.MangaChapter;
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
import com.magamochi.model.entity.MangaContentProvider;
|
|
||||||
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;
|
||||||
@ -59,9 +59,9 @@ public record MangaDTO(
|
|||||||
@NotNull Integer chaptersDownloaded,
|
@NotNull Integer chaptersDownloaded,
|
||||||
@NotNull Boolean supportsChapterFetch) {
|
@NotNull Boolean supportsChapterFetch) {
|
||||||
public static MangaProviderDTO from(MangaContentProvider mangaContentProvider) {
|
public static MangaProviderDTO from(MangaContentProvider mangaContentProvider) {
|
||||||
var chapters = mangaContentProvider.getMangaChapters();
|
var chapters = mangaContentProvider.getMangaContents();
|
||||||
var chaptersAvailable = chapters.size();
|
var chaptersAvailable = chapters.size();
|
||||||
var chaptersDownloaded = (int) chapters.stream().filter(MangaChapter::getDownloaded).count();
|
var chaptersDownloaded = (int) chapters.stream().filter(MangaContent::getDownloaded).count();
|
||||||
|
|
||||||
return new MangaProviderDTO(
|
return new MangaProviderDTO(
|
||||||
mangaContentProvider.getId(),
|
mangaContentProvider.getId(),
|
||||||
@ -69,7 +69,7 @@ public record MangaDTO(
|
|||||||
mangaContentProvider.getContentProvider().isActive(),
|
mangaContentProvider.getContentProvider().isActive(),
|
||||||
chaptersAvailable,
|
chaptersAvailable,
|
||||||
chaptersDownloaded,
|
chaptersDownloaded,
|
||||||
mangaContentProvider.getContentProvider().getSupportsChapterFetch());
|
mangaContentProvider.getContentProvider().getSupportsContentFetch());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package com.magamochi.catalog.model.entity;
|
|||||||
import com.magamochi.catalog.model.enumeration.MangaState;
|
import com.magamochi.catalog.model.enumeration.MangaState;
|
||||||
import com.magamochi.catalog.model.enumeration.MangaStatus;
|
import com.magamochi.catalog.model.enumeration.MangaStatus;
|
||||||
import com.magamochi.image.model.entity.Image;
|
import com.magamochi.image.model.entity.Image;
|
||||||
import com.magamochi.model.entity.MangaContentProvider;
|
|
||||||
import com.magamochi.model.entity.UserFavoriteManga;
|
import com.magamochi.model.entity.UserFavoriteManga;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package com.magamochi.model.entity;
|
package com.magamochi.catalog.model.entity;
|
||||||
|
|
||||||
import com.magamochi.catalog.model.entity.Manga;
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
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;
|
||||||
@ -34,7 +34,7 @@ public class MangaContentProvider {
|
|||||||
private String url;
|
private String url;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "mangaContentProvider")
|
@OneToMany(mappedBy = "mangaContentProvider")
|
||||||
List<MangaChapter> mangaChapters;
|
List<MangaContent> mangaContents;
|
||||||
|
|
||||||
@CreationTimestamp private Instant createdAt;
|
@CreationTimestamp private Instant createdAt;
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
package com.magamochi.catalog.model.repository;
|
package com.magamochi.catalog.model.repository;
|
||||||
|
|
||||||
import com.magamochi.model.entity.MangaContentProvider;
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import org.springframework.stereotype.Service;
|
|||||||
public class LanguageService {
|
public class LanguageService {
|
||||||
public final LanguageRepository languageRepository;
|
public final LanguageRepository languageRepository;
|
||||||
|
|
||||||
public Language getOrThrow(String code) {
|
public Language find(String code) {
|
||||||
return languageRepository
|
return languageRepository
|
||||||
.findByCodeIgnoreCase(code)
|
.findByCodeIgnoreCase(code)
|
||||||
.orElseThrow(() -> new NotFoundException("Language with code " + code + " not found"));
|
.orElseThrow(() -> new NotFoundException("Language with code " + code + " not found"));
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.magamochi.catalog.service;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
|
import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
|
||||||
|
import com.magamochi.common.exception.NotFoundException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MangaContentProviderService {
|
||||||
|
private final MangaContentProviderRepository mangaContentProviderRepository;
|
||||||
|
|
||||||
|
public MangaContentProvider find(long mangaContentProviderId) {
|
||||||
|
return mangaContentProviderRepository
|
||||||
|
.findById(mangaContentProviderId)
|
||||||
|
.orElseThrow(
|
||||||
|
() ->
|
||||||
|
new NotFoundException(
|
||||||
|
"MangaContentProvider not found - ID: " + mangaContentProviderId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,11 +2,11 @@ package com.magamochi.catalog.service;
|
|||||||
|
|
||||||
import static java.util.Objects.isNull;
|
import static java.util.Objects.isNull;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
import com.magamochi.catalog.model.entity.MangaIngestReview;
|
import com.magamochi.catalog.model.entity.MangaIngestReview;
|
||||||
import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
|
import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
|
||||||
import com.magamochi.catalog.model.repository.MangaIngestReviewRepository;
|
import com.magamochi.catalog.model.repository.MangaIngestReviewRepository;
|
||||||
import com.magamochi.ingestion.service.ContentProviderService;
|
import com.magamochi.ingestion.service.ContentProviderService;
|
||||||
import com.magamochi.model.entity.MangaContentProvider;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
|||||||
@ -16,6 +16,12 @@ public class RabbitConfig {
|
|||||||
@Value("${queues.manga-ingest}")
|
@Value("${queues.manga-ingest}")
|
||||||
private String mangaIngestQueue;
|
private String mangaIngestQueue;
|
||||||
|
|
||||||
|
@Value("${queues.manga-content-ingest}")
|
||||||
|
private String mangaContentIngestQueue;
|
||||||
|
|
||||||
|
@Value("${queues.manga-content-image-ingest}")
|
||||||
|
private String mangaContentImageIngestQueue;
|
||||||
|
|
||||||
@Value("${queues.provider-page-ingest}")
|
@Value("${queues.provider-page-ingest}")
|
||||||
private String providerPageIngestQueue;
|
private String providerPageIngestQueue;
|
||||||
|
|
||||||
@ -25,6 +31,9 @@ public class RabbitConfig {
|
|||||||
@Value("${queues.manga-cover-update}")
|
@Value("${queues.manga-cover-update}")
|
||||||
private String mangaCoverUpdateQueue;
|
private String mangaCoverUpdateQueue;
|
||||||
|
|
||||||
|
@Value("${queues.manga-content-image-update}")
|
||||||
|
private String mangaContentImageUpdateQueue;
|
||||||
|
|
||||||
@Value("${queues.image-fetch}")
|
@Value("${queues.image-fetch}")
|
||||||
private String imageFetchQueue;
|
private String imageFetchQueue;
|
||||||
|
|
||||||
@ -46,6 +55,11 @@ public class RabbitConfig {
|
|||||||
return new Queue(mangaUpdateQueue, false);
|
return new Queue(mangaUpdateQueue, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Queue mangaContentImageUpdateQueue() {
|
||||||
|
return new Queue(mangaContentImageUpdateQueue, false);
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Queue mangaCoverUpdateQueue() {
|
public Queue mangaCoverUpdateQueue() {
|
||||||
return new Queue(mangaCoverUpdateQueue, false);
|
return new Queue(mangaCoverUpdateQueue, false);
|
||||||
@ -62,6 +76,27 @@ public class RabbitConfig {
|
|||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Binding bindingMangaContentImageUpdateQueue(
|
||||||
|
Queue mangaContentImageUpdateQueue, TopicExchange imageUpdatesExchange) {
|
||||||
|
return new Binding(
|
||||||
|
mangaContentImageUpdateQueue.getName(),
|
||||||
|
Binding.DestinationType.QUEUE,
|
||||||
|
imageUpdatesExchange.getName(),
|
||||||
|
String.format("image.update.%s", ContentType.CONTENT_IMAGE.name().toLowerCase()),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Queue mangaContentIngestQueue() {
|
||||||
|
return new Queue(mangaContentIngestQueue, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Queue mangaContentImageIngestQueue() {
|
||||||
|
return new Queue(mangaContentImageIngestQueue, false);
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Queue mangaIngestQueue() {
|
public Queue mangaIngestQueue() {
|
||||||
return new Queue(mangaIngestQueue, false);
|
return new Queue(mangaIngestQueue, false);
|
||||||
@ -74,33 +109,17 @@ public class RabbitConfig {
|
|||||||
|
|
||||||
// TODO: remove unused queues
|
// TODO: remove unused queues
|
||||||
|
|
||||||
@Value("${rabbit-mq.queues.manga-data-update}")
|
|
||||||
private String mangaDataUpdateQueue;
|
|
||||||
|
|
||||||
@Value("${rabbit-mq.queues.manga-chapter-download}")
|
@Value("${rabbit-mq.queues.manga-chapter-download}")
|
||||||
private String mangaChapterDownloadQueue;
|
private String mangaChapterDownloadQueue;
|
||||||
|
|
||||||
@Value("${rabbit-mq.queues.manga-list-update}")
|
|
||||||
private String mangaListUpdateQueue;
|
|
||||||
|
|
||||||
@Value("${rabbit-mq.queues.manga-follow-update-chapter}")
|
@Value("${rabbit-mq.queues.manga-follow-update-chapter}")
|
||||||
private String mangaFollowUpdateChapterQueue;
|
private String mangaFollowUpdateChapterQueue;
|
||||||
|
|
||||||
@Bean
|
|
||||||
public Queue mangaDataUpdateQueue() {
|
|
||||||
return new Queue(mangaDataUpdateQueue, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Queue mangaChapterDownloadQueue() {
|
public Queue mangaChapterDownloadQueue() {
|
||||||
return new Queue(mangaChapterDownloadQueue, false);
|
return new Queue(mangaChapterDownloadQueue, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public Queue mangaListUpdateQueue() {
|
|
||||||
return new Queue(mangaListUpdateQueue, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Queue mangaFollowUpdateChapterQueue() {
|
public Queue mangaFollowUpdateChapterQueue() {
|
||||||
return new Queue(mangaFollowUpdateChapterQueue, false);
|
return new Queue(mangaFollowUpdateChapterQueue, false);
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
package com.magamochi.common.model.enumeration;
|
package com.magamochi.common.model.enumeration;
|
||||||
|
|
||||||
public enum ContentType {
|
public enum ContentType {
|
||||||
MANGA_COVER
|
MANGA_COVER,
|
||||||
|
CHAPTER,
|
||||||
|
VOLUME,
|
||||||
|
CONTENT_IMAGE,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.magamochi.common.queue.command;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public record MangaContentImageIngestCommand(
|
||||||
|
long mangaContentId, @NotBlank String url, int position, boolean isLast) {}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package com.magamochi.common.queue.command;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public record MangaContentIngestCommand(
|
||||||
|
long mangaContentProviderId,
|
||||||
|
@NotBlank String title,
|
||||||
|
@NotBlank String url,
|
||||||
|
@NotBlank String languageCode) {}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package com.magamochi.content.controller;
|
||||||
|
|
||||||
|
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||||
|
import com.magamochi.content.model.dto.MangaContentDTO;
|
||||||
|
import com.magamochi.content.service.ContentService;
|
||||||
|
import com.magamochi.model.dto.MangaContentImagesDTO;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/content")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ContentController {
|
||||||
|
private final ContentService contentService;
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Get the content for a specific manga/content provider combination",
|
||||||
|
description = "Retrieve the content for a specific manga/content provider combination.",
|
||||||
|
tags = {"Content"},
|
||||||
|
operationId = "getMangaProviderContent")
|
||||||
|
@GetMapping("/{mangaContentProviderId}")
|
||||||
|
public DefaultResponseDTO<List<MangaContentDTO>> getMangaProviderContent(
|
||||||
|
@PathVariable @NotNull Long mangaContentProviderId) {
|
||||||
|
return DefaultResponseDTO.ok(contentService.getContent(mangaContentProviderId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Get the content images for a specific manga/provider combination",
|
||||||
|
description =
|
||||||
|
"Retrieve a list of manga content images for a specific manga/provider combination.",
|
||||||
|
tags = {"Content"},
|
||||||
|
operationId = "getMangaContentImages")
|
||||||
|
@GetMapping("/{mangaContentId}/images")
|
||||||
|
public DefaultResponseDTO<MangaContentImagesDTO> getMangaContentImages(
|
||||||
|
@PathVariable Long mangaContentId) {
|
||||||
|
return DefaultResponseDTO.ok(contentService.getContentImages(mangaContentId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.magamochi.model.dto;
|
package com.magamochi.content.model.dto;
|
||||||
|
|
||||||
import static java.util.Objects.isNull;
|
import static java.util.Objects.isNull;
|
||||||
|
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.magamochi.content.model.dto;
|
||||||
|
|
||||||
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public record MangaContentDTO(
|
||||||
|
@NotNull Long id,
|
||||||
|
@NotBlank String title,
|
||||||
|
@NotNull Boolean downloaded,
|
||||||
|
@NotNull Boolean isRead,
|
||||||
|
LanguageDTO language) {
|
||||||
|
public static MangaContentDTO from(MangaContent mangaContent) {
|
||||||
|
return new MangaContentDTO(
|
||||||
|
mangaContent.getId(),
|
||||||
|
mangaContent.getTitle(),
|
||||||
|
mangaContent.getDownloaded(),
|
||||||
|
false,
|
||||||
|
LanguageDTO.from(mangaContent.getLanguage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
package com.magamochi.model.entity;
|
package com.magamochi.content.model.entity;
|
||||||
|
|
||||||
import com.magamochi.catalog.model.entity.Language;
|
import com.magamochi.catalog.model.entity.Language;
|
||||||
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
|
import com.magamochi.common.model.enumeration.ContentType;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -9,13 +11,13 @@ import org.hibernate.annotations.CreationTimestamp;
|
|||||||
import org.hibernate.annotations.UpdateTimestamp;
|
import org.hibernate.annotations.UpdateTimestamp;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "manga_chapters")
|
@Table(name = "manga_contents")
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class MangaChapter {
|
public class MangaContent {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
@ -24,22 +26,20 @@ public class MangaChapter {
|
|||||||
@JoinColumn(name = "manga_content_provider_id")
|
@JoinColumn(name = "manga_content_provider_id")
|
||||||
private MangaContentProvider mangaContentProvider;
|
private MangaContentProvider mangaContentProvider;
|
||||||
|
|
||||||
|
@Builder.Default private ContentType type = ContentType.CHAPTER;
|
||||||
|
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
private String url;
|
private String url;
|
||||||
|
|
||||||
@Builder.Default private Boolean downloaded = false;
|
@Builder.Default private Boolean downloaded = false;
|
||||||
|
|
||||||
@Builder.Default private Boolean read = false;
|
|
||||||
|
|
||||||
@CreationTimestamp private Instant createdAt;
|
@CreationTimestamp private Instant createdAt;
|
||||||
|
|
||||||
@UpdateTimestamp private Instant updatedAt;
|
@UpdateTimestamp private Instant updatedAt;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "mangaChapter")
|
@OneToMany(mappedBy = "mangaContent")
|
||||||
private List<MangaChapterImage> mangaChapterImages;
|
private List<MangaContentImage> mangaContentImages;
|
||||||
|
|
||||||
private Integer chapterNumber;
|
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "language_id")
|
@JoinColumn(name = "language_id")
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.magamochi.model.entity;
|
package com.magamochi.content.model.entity;
|
||||||
|
|
||||||
import com.magamochi.image.model.entity.Image;
|
import com.magamochi.image.model.entity.Image;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
@ -8,20 +8,20 @@ import org.hibernate.annotations.CreationTimestamp;
|
|||||||
import org.hibernate.annotations.UpdateTimestamp;
|
import org.hibernate.annotations.UpdateTimestamp;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "manga_chapter_images")
|
@Table(name = "manga_content_images")
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class MangaChapterImage {
|
public class MangaContentImage {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "manga_chapter_id")
|
@JoinColumn(name = "manga_content_id")
|
||||||
private MangaChapter mangaChapter;
|
private MangaContent mangaContent;
|
||||||
|
|
||||||
@OneToOne
|
@OneToOne
|
||||||
@JoinColumn(name = "image_id")
|
@JoinColumn(name = "image_id")
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.magamochi.content.model.repository;
|
||||||
|
|
||||||
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
|
import com.magamochi.content.model.entity.MangaContentImage;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface MangaContentImageRepository extends JpaRepository<MangaContentImage, Long> {
|
||||||
|
List<MangaContentImage> findAllByMangaContent(MangaContent mangaContent);
|
||||||
|
|
||||||
|
boolean existsByMangaContent_IdAndPosition(Long mangaContentId, int position);
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package com.magamochi.content.model.repository;
|
||||||
|
|
||||||
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface MangaContentRepository extends JpaRepository<MangaContent, Long> {
|
||||||
|
boolean existsByMangaContentProvider_IdAndUrlIgnoreCase(Long mangaContentProviderId, String url);
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.magamochi.content.queue.consumer;
|
||||||
|
|
||||||
|
import com.magamochi.common.queue.command.MangaContentImageIngestCommand;
|
||||||
|
import com.magamochi.content.service.ContentIngestService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Log4j2
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MangaContentImageIngestConsumer {
|
||||||
|
private final ContentIngestService contentIngestService;
|
||||||
|
|
||||||
|
@RabbitListener(queues = "${queues.manga-content-image-ingest}")
|
||||||
|
public void receiveMangaContentImageIngestCommand(MangaContentImageIngestCommand command) {
|
||||||
|
log.info("Received manga content ingest command: {}", command);
|
||||||
|
contentIngestService.ingestImages(
|
||||||
|
command.mangaContentId(), command.url(), command.position(), command.isLast());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.magamochi.content.queue.consumer;
|
||||||
|
|
||||||
|
import com.magamochi.common.queue.command.ImageUpdateCommand;
|
||||||
|
import com.magamochi.content.service.ContentIngestService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Log4j2
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MangaContentImageUpdateConsumer {
|
||||||
|
private final ContentIngestService contentIngestService;
|
||||||
|
|
||||||
|
@RabbitListener(queues = "${queues.manga-content-image-update}")
|
||||||
|
public void receiveMangaContentImageUpdateCommand(ImageUpdateCommand command) {
|
||||||
|
log.info("Received manga content image update command: {}", command);
|
||||||
|
contentIngestService.updateMangaContentImage(command.entityId(), command.imageId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.magamochi.content.queue.consumer;
|
||||||
|
|
||||||
|
import com.magamochi.common.queue.command.MangaContentIngestCommand;
|
||||||
|
import com.magamochi.content.service.ContentIngestService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Log4j2
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MangaContentIngestConsumer {
|
||||||
|
private final ContentIngestService contentIngestService;
|
||||||
|
|
||||||
|
@RabbitListener(queues = "${queues.manga-content-ingest}")
|
||||||
|
public void receiveMangaContentIngestCommand(MangaContentIngestCommand command) {
|
||||||
|
log.info("Received manga content ingest command: {}", command);
|
||||||
|
contentIngestService.ingest(
|
||||||
|
command.mangaContentProviderId(), command.title(), command.url(), command.languageCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,106 @@
|
|||||||
|
package com.magamochi.content.service;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.service.LanguageService;
|
||||||
|
import com.magamochi.catalog.service.MangaContentProviderService;
|
||||||
|
import com.magamochi.common.exception.NotFoundException;
|
||||||
|
import com.magamochi.common.model.enumeration.ContentType;
|
||||||
|
import com.magamochi.common.queue.command.ImageFetchCommand;
|
||||||
|
import com.magamochi.common.queue.producer.ImageFetchProducer;
|
||||||
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
|
import com.magamochi.content.model.entity.MangaContentImage;
|
||||||
|
import com.magamochi.content.model.repository.MangaContentImageRepository;
|
||||||
|
import com.magamochi.content.model.repository.MangaContentRepository;
|
||||||
|
import com.magamochi.image.service.ImageService;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import java.util.UUID;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Log4j2
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ContentIngestService {
|
||||||
|
private final ContentService contentService;
|
||||||
|
private final MangaContentProviderService mangaContentProviderService;
|
||||||
|
private final LanguageService languageService;
|
||||||
|
|
||||||
|
private final MangaContentRepository mangaContentRepository;
|
||||||
|
private final MangaContentImageRepository mangaContentImageRepository;
|
||||||
|
|
||||||
|
private final ImageFetchProducer imageFetchProducer;
|
||||||
|
private final ImageService imageService;
|
||||||
|
|
||||||
|
public void ingest(
|
||||||
|
long mangaContentProviderId,
|
||||||
|
@NotBlank String title,
|
||||||
|
@NotBlank String url,
|
||||||
|
@NotBlank String languageCode) {
|
||||||
|
log.info("Ingesting Manga Content ({}) for provider {}", title, mangaContentProviderId);
|
||||||
|
|
||||||
|
var mangaContentProvider = mangaContentProviderService.find(mangaContentProviderId);
|
||||||
|
|
||||||
|
if (mangaContentRepository.existsByMangaContentProvider_IdAndUrlIgnoreCase(
|
||||||
|
mangaContentProvider.getId(), url)) {
|
||||||
|
log.info(
|
||||||
|
"Manga Content ({}) for provider {} already exists. Skipped.",
|
||||||
|
title,
|
||||||
|
mangaContentProviderId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var language = languageService.find(languageCode);
|
||||||
|
|
||||||
|
var mangaContent =
|
||||||
|
mangaContentRepository.save(
|
||||||
|
MangaContent.builder()
|
||||||
|
.mangaContentProvider(mangaContentProvider)
|
||||||
|
.title(title)
|
||||||
|
.url(url)
|
||||||
|
.language(language)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Ingested Manga Content ({}) for provider {}: {}",
|
||||||
|
title,
|
||||||
|
mangaContentProviderId,
|
||||||
|
mangaContent.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void ingestImages(
|
||||||
|
long mangaContentId, @NotBlank String url, int position, boolean isLast) {
|
||||||
|
log.info(
|
||||||
|
"Ingesting Manga Content Image for MangaContent {}, position {}", mangaContentId, position);
|
||||||
|
|
||||||
|
var mangaContent = contentService.find(mangaContentId);
|
||||||
|
|
||||||
|
if (mangaContentImageRepository.existsByMangaContent_IdAndPosition(mangaContentId, position)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mangaContentImage =
|
||||||
|
mangaContentImageRepository.save(
|
||||||
|
MangaContentImage.builder().mangaContent(mangaContent).position(position).build());
|
||||||
|
|
||||||
|
imageFetchProducer.sendImageFetchCommand(
|
||||||
|
new ImageFetchCommand(mangaContentImage.getId(), ContentType.CONTENT_IMAGE, url));
|
||||||
|
|
||||||
|
if (isLast) {
|
||||||
|
mangaContent.setDownloaded(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updateMangaContentImage(long mangaContentImageId, UUID imageId) {
|
||||||
|
var mangaContentImage =
|
||||||
|
mangaContentImageRepository
|
||||||
|
.findById(mangaContentImageId)
|
||||||
|
.orElseThrow(
|
||||||
|
() -> new NotFoundException("Image not found for ID: " + mangaContentImageId));
|
||||||
|
|
||||||
|
var image = imageService.find(imageId);
|
||||||
|
mangaContentImage.setImage(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
package com.magamochi.content.service;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.service.MangaContentProviderService;
|
||||||
|
import com.magamochi.common.exception.NotFoundException;
|
||||||
|
import com.magamochi.content.model.dto.MangaContentDTO;
|
||||||
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
|
import com.magamochi.content.model.repository.MangaContentRepository;
|
||||||
|
import com.magamochi.model.dto.MangaContentImagesDTO;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ContentService {
|
||||||
|
private final MangaContentProviderService mangaContentProviderService;
|
||||||
|
|
||||||
|
private final MangaContentRepository mangaContentRepository;
|
||||||
|
|
||||||
|
public List<MangaContentDTO> getContent(@NotNull Long mangaContentProviderId) {
|
||||||
|
var mangaContentProvider = mangaContentProviderService.find(mangaContentProviderId);
|
||||||
|
|
||||||
|
return mangaContentProvider.getMangaContents().stream()
|
||||||
|
.sorted(Comparator.comparing(MangaContent::getId))
|
||||||
|
.map(MangaContentDTO::from)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MangaContent find(Long id) {
|
||||||
|
return mangaContentRepository
|
||||||
|
.findById(id)
|
||||||
|
.orElseThrow(() -> new NotFoundException("MangaContent not found for ID: " + id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public MangaContentImagesDTO getContentImages(Long mangaContentId) {
|
||||||
|
var mangaContent = find(mangaContentId);
|
||||||
|
|
||||||
|
var chapters =
|
||||||
|
mangaContent.getMangaContentProvider().getMangaContents().stream()
|
||||||
|
.sorted(Comparator.comparing(MangaContent::getId))
|
||||||
|
.toList();
|
||||||
|
Long prevId = null;
|
||||||
|
Long nextId = null;
|
||||||
|
|
||||||
|
// TODO: this doesn't perform well for large datasets
|
||||||
|
for (var i = 0; i < chapters.size(); i++) {
|
||||||
|
if (chapters.get(i).getId().equals(mangaContent.getId())) {
|
||||||
|
if (i > 0) {
|
||||||
|
prevId = chapters.get(i - 1).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < chapters.size() - 1) {
|
||||||
|
nextId = chapters.get(i + 1).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangaContentImagesDTO.from(mangaContent, prevId, nextId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package com.magamochi.controller;
|
package com.magamochi.controller;
|
||||||
|
|
||||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||||
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;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@ -21,30 +20,6 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
public class MangaChapterController {
|
public class MangaChapterController {
|
||||||
private final MangaChapterService mangaChapterService;
|
private final MangaChapterService mangaChapterService;
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Fetch chapter",
|
|
||||||
description = "Fetch the chapter from the provider",
|
|
||||||
tags = {"Manga Chapter"},
|
|
||||||
operationId = "fetchChapter")
|
|
||||||
@PostMapping(value = "/{chapterId}/fetch")
|
|
||||||
public DefaultResponseDTO<Void> fetchChapter(@PathVariable Long chapterId) {
|
|
||||||
mangaChapterService.fetchChapter(chapterId);
|
|
||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Get the images for a specific manga/provider combination",
|
|
||||||
description =
|
|
||||||
"Retrieve a list of manga chapter images for a specific manga/provider combination.",
|
|
||||||
tags = {"Manga Chapter"},
|
|
||||||
operationId = "getMangaChapterImages")
|
|
||||||
@GetMapping("/{chapterId}/images")
|
|
||||||
public DefaultResponseDTO<MangaChapterImagesDTO> getMangaChapterImages(
|
|
||||||
@PathVariable Long chapterId) {
|
|
||||||
return DefaultResponseDTO.ok(mangaChapterService.getMangaChapterImages(chapterId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Mark a chapter as read",
|
summary = "Mark a chapter as read",
|
||||||
description = "Mark a chapter as read by its ID.",
|
description = "Mark a chapter as read by its ID.",
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
package com.magamochi.controller;
|
package com.magamochi.controller;
|
||||||
|
|
||||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||||
import com.magamochi.model.dto.MangaChapterDTO;
|
|
||||||
import com.magamochi.service.OldMangaService;
|
import com.magamochi.service.OldMangaService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import java.util.List;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@ -14,17 +12,6 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
public class MangaController {
|
public class MangaController {
|
||||||
private final OldMangaService oldMangaService;
|
private final OldMangaService oldMangaService;
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Get the available chapters for a specific manga/provider combination",
|
|
||||||
description = "Retrieve a list of manga chapters for a specific manga/provider combination.",
|
|
||||||
tags = {"Manga"},
|
|
||||||
operationId = "getMangaChapters")
|
|
||||||
@GetMapping("/{mangaProviderId}/chapters")
|
|
||||||
public DefaultResponseDTO<List<MangaChapterDTO>> getMangaChapters(
|
|
||||||
@PathVariable Long mangaProviderId) {
|
|
||||||
return DefaultResponseDTO.ok(oldMangaService.getMangaChapters(mangaProviderId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Fetch all chapters",
|
summary = "Fetch all chapters",
|
||||||
description = "Fetch all not yet downloaded chapters from the provider",
|
description = "Fetch all not yet downloaded chapters from the provider",
|
||||||
@ -37,18 +24,6 @@ public class MangaController {
|
|||||||
return DefaultResponseDTO.ok().build();
|
return DefaultResponseDTO.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Fetch the available chapters for a specific manga/provider combination",
|
|
||||||
description = "Fetch a list of manga chapters for a specific manga/provider combination.",
|
|
||||||
tags = {"Manga"},
|
|
||||||
operationId = "fetchMangaChapters")
|
|
||||||
@PostMapping("/{mangaProviderId}/fetch-chapters")
|
|
||||||
public DefaultResponseDTO<Void> fetchMangaChapters(@PathVariable Long mangaProviderId) {
|
|
||||||
oldMangaService.fetchMangaChapters(mangaProviderId);
|
|
||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Follow the manga specified by its ID",
|
summary = "Follow the manga specified by its ID",
|
||||||
description = "Follow the manga specified by its ID.",
|
description = "Follow the manga specified by its ID.",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.magamochi.image.service;
|
|||||||
|
|
||||||
import static java.util.Objects.nonNull;
|
import static java.util.Objects.nonNull;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.RateLimiter;
|
||||||
import com.magamochi.common.model.enumeration.ContentType;
|
import com.magamochi.common.model.enumeration.ContentType;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
@ -21,6 +22,8 @@ import org.springframework.stereotype.Service;
|
|||||||
public class ImageFetchService {
|
public class ImageFetchService {
|
||||||
private final ImageService imageManagerService;
|
private final ImageService imageManagerService;
|
||||||
|
|
||||||
|
private final RateLimiter imageDownloadRateLimiter;
|
||||||
|
|
||||||
private final HttpClient httpClient =
|
private final HttpClient httpClient =
|
||||||
HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build();
|
HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build();
|
||||||
private final Tika tika = new Tika();
|
private final Tika tika = new Tika();
|
||||||
@ -28,6 +31,8 @@ public class ImageFetchService {
|
|||||||
public UUID fetchImage(String imageUrl, ContentType contentType) {
|
public UUID fetchImage(String imageUrl, ContentType contentType) {
|
||||||
try {
|
try {
|
||||||
var request = HttpRequest.newBuilder(URI.create(imageUrl)).GET().build();
|
var request = HttpRequest.newBuilder(URI.create(imageUrl)).GET().build();
|
||||||
|
|
||||||
|
imageDownloadRateLimiter.acquire();
|
||||||
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
||||||
|
|
||||||
var imageBytes = response.body();
|
var imageBytes = response.body();
|
||||||
|
|||||||
@ -51,4 +51,30 @@ public class IngestionController {
|
|||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
return DefaultResponseDTO.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Fetch content list from a content provider",
|
||||||
|
description =
|
||||||
|
"Triggers the ingestion process for a specific content provider, fetching content list and queuing it for processing.",
|
||||||
|
tags = {"Ingestion"},
|
||||||
|
operationId = "fetchContentProviderContentList")
|
||||||
|
@PostMapping("/manga-content-providers/{mangaContentProviderId}/fetch")
|
||||||
|
public DefaultResponseDTO<Void> fetchContentProviderContentList(
|
||||||
|
@PathVariable Long mangaContentProviderId) {
|
||||||
|
ingestionService.fetchMangaContentProviderContentList(mangaContentProviderId);
|
||||||
|
|
||||||
|
return DefaultResponseDTO.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Fetch content from a content provider",
|
||||||
|
description = "Fetch the content (images) from the content provider",
|
||||||
|
tags = {"Ingestion"},
|
||||||
|
operationId = "fetchContentProviderContent")
|
||||||
|
@PostMapping(value = "/manga-content/{mangaContentId}/fetch")
|
||||||
|
public DefaultResponseDTO<Void> fetchContentProviderContent(@PathVariable Long mangaContentId) {
|
||||||
|
ingestionService.fetchContent(mangaContentId);
|
||||||
|
|
||||||
|
return DefaultResponseDTO.ok().build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.magamochi.ingestion.model.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public record ContentImageInfoDTO(int position, @NotBlank String url) {}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.magamochi.ingestion.model.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public record ContentInfoDTO(
|
||||||
|
@NotBlank String title, @NotBlank String url, @NotBlank String languageCode) {}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
package com.magamochi.ingestion.model.entity;
|
package com.magamochi.ingestion.model.entity;
|
||||||
|
|
||||||
import com.magamochi.model.entity.MangaContentProvider;
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -24,7 +24,7 @@ public class ContentProvider {
|
|||||||
|
|
||||||
private boolean active;
|
private boolean active;
|
||||||
|
|
||||||
private Boolean supportsChapterFetch;
|
private Boolean supportsContentFetch;
|
||||||
|
|
||||||
private Boolean manualImport;
|
private Boolean manualImport;
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
package com.magamochi.ingestion.model.repository;
|
package com.magamochi.ingestion.model.repository;
|
||||||
|
|
||||||
import com.magamochi.ingestion.model.entity.ContentProvider;
|
import com.magamochi.ingestion.model.entity.ContentProvider;
|
||||||
import java.util.Optional;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
public interface ContentProviderRepository extends JpaRepository<ContentProvider, Long> {
|
public interface ContentProviderRepository extends JpaRepository<ContentProvider, Long> {}
|
||||||
Optional<ContentProvider> findByNameIgnoreCase(String name);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
package com.magamochi.ingestion.providers;
|
package com.magamochi.ingestion.providers;
|
||||||
|
|
||||||
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
import com.magamochi.model.entity.MangaContentProvider;
|
import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
|
||||||
|
import com.magamochi.ingestion.model.dto.ContentInfoDTO;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public interface ContentProvider {
|
public interface ContentProvider {
|
||||||
List<ContentProviderMangaChapterResponseDTO> getAvailableChapters(MangaContentProvider provider);
|
List<ContentInfoDTO> getAvailableChapters(MangaContentProvider provider);
|
||||||
|
|
||||||
Map<Integer, String> getChapterImagesUrls(String chapterUrl);
|
List<ContentImageInfoDTO> getContentImages(String chapterUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,15 +3,15 @@ package com.magamochi.ingestion.providers.impl;
|
|||||||
import static java.util.Objects.isNull;
|
import static java.util.Objects.isNull;
|
||||||
|
|
||||||
import com.google.common.util.concurrent.RateLimiter;
|
import com.google.common.util.concurrent.RateLimiter;
|
||||||
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
import com.magamochi.client.MangaDexClient;
|
import com.magamochi.client.MangaDexClient;
|
||||||
import com.magamochi.common.exception.UnprocessableException;
|
import com.magamochi.common.exception.UnprocessableException;
|
||||||
|
import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
|
||||||
|
import com.magamochi.ingestion.model.dto.ContentInfoDTO;
|
||||||
import com.magamochi.ingestion.providers.ContentProvider;
|
import com.magamochi.ingestion.providers.ContentProvider;
|
||||||
import com.magamochi.ingestion.providers.ContentProviders;
|
import com.magamochi.ingestion.providers.ContentProviders;
|
||||||
import com.magamochi.ingestion.providers.ManualImportContentProvider;
|
import com.magamochi.ingestion.providers.ManualImportContentProvider;
|
||||||
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
|
||||||
import com.magamochi.model.entity.MangaContentProvider;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
@ -26,8 +26,7 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro
|
|||||||
private final RateLimiter mangaDexRateLimiter;
|
private final RateLimiter mangaDexRateLimiter;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ContentProviderMangaChapterResponseDTO> getAvailableChapters(
|
public List<ContentInfoDTO> getAvailableChapters(MangaContentProvider provider) {
|
||||||
MangaContentProvider provider) {
|
|
||||||
try {
|
try {
|
||||||
mangaDexRateLimiter.acquire();
|
mangaDexRateLimiter.acquire();
|
||||||
var response =
|
var response =
|
||||||
@ -85,10 +84,9 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro
|
|||||||
})
|
})
|
||||||
.map(
|
.map(
|
||||||
c ->
|
c ->
|
||||||
new ContentProviderMangaChapterResponseDTO(
|
new ContentInfoDTO(
|
||||||
c.attributes().chapter() + " - " + c.attributes().title(),
|
c.attributes().chapter() + " - " + c.attributes().title(),
|
||||||
c.id().toString(),
|
c.id().toString(),
|
||||||
c.attributes().chapter(),
|
|
||||||
languagesToImport.get(c.attributes().translatedLanguage())))
|
languagesToImport.get(c.attributes().translatedLanguage())))
|
||||||
.toList();
|
.toList();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -98,7 +96,7 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<Integer, String> getChapterImagesUrls(String chapterUrl) {
|
public List<ContentImageInfoDTO> getContentImages(String chapterUrl) {
|
||||||
mangaDexRateLimiter.acquire();
|
mangaDexRateLimiter.acquire();
|
||||||
var chapter = mangaDexClient.getMangaChapter(UUID.fromString(chapterUrl));
|
var chapter = mangaDexClient.getMangaChapter(UUID.fromString(chapterUrl));
|
||||||
|
|
||||||
@ -109,12 +107,8 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro
|
|||||||
|
|
||||||
return IntStream.range(0, chapterImageHashes.size())
|
return IntStream.range(0, chapterImageHashes.size())
|
||||||
.boxed()
|
.boxed()
|
||||||
.collect(
|
.map(position -> new ContentImageInfoDTO(position, chapterImageHashes.get(position)))
|
||||||
Collectors.toMap(
|
.toList();
|
||||||
i -> i,
|
|
||||||
chapterImageHashes::get,
|
|
||||||
(existing, replacement) -> existing,
|
|
||||||
LinkedHashMap::new));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
package com.magamochi.ingestion.providers.impl;
|
package com.magamochi.ingestion.providers.impl;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
|
import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
|
||||||
|
import com.magamochi.ingestion.model.dto.ContentInfoDTO;
|
||||||
import com.magamochi.ingestion.model.dto.MangaInfoDTO;
|
import com.magamochi.ingestion.model.dto.MangaInfoDTO;
|
||||||
import com.magamochi.ingestion.providers.ContentProvider;
|
import com.magamochi.ingestion.providers.ContentProvider;
|
||||||
import com.magamochi.ingestion.providers.ContentProviders;
|
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.entity.MangaContentProvider;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
@ -27,8 +27,7 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
|
|||||||
private final String url = "https://mangalivre.blog/manga/";
|
private final String url = "https://mangalivre.blog/manga/";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ContentProviderMangaChapterResponseDTO> getAvailableChapters(
|
public List<ContentInfoDTO> getAvailableChapters(MangaContentProvider mangaContentProvider) {
|
||||||
MangaContentProvider mangaContentProvider) {
|
|
||||||
log.info(
|
log.info(
|
||||||
"Getting available chapters from {}, manga {}",
|
"Getting available chapters from {}, manga {}",
|
||||||
ContentProviders.MANGA_LIVRE_BLOG,
|
ContentProviders.MANGA_LIVRE_BLOG,
|
||||||
@ -49,8 +48,8 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
|
|||||||
var chapterNumberElement =
|
var chapterNumberElement =
|
||||||
linkElement.getElementsByClass("chapter-number").getFirst();
|
linkElement.getElementsByClass("chapter-number").getFirst();
|
||||||
|
|
||||||
return new ContentProviderMangaChapterResponseDTO(
|
return new ContentInfoDTO(
|
||||||
chapterNumberElement.text(), linkElement.attr("href"), null, "pt-BR");
|
chapterNumberElement.text(), linkElement.attr("href"), "pt-BR");
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
} catch (IOException | NoSuchElementException e) {
|
} catch (IOException | NoSuchElementException e) {
|
||||||
@ -60,7 +59,7 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<Integer, String> getChapterImagesUrls(String chapterUrl) {
|
public List<ContentImageInfoDTO> getContentImages(String chapterUrl) {
|
||||||
log.info("Getting images from {}, url {}", ContentProviders.MANGA_LIVRE_BLOG, chapterUrl);
|
log.info("Getting images from {}, url {}", ContentProviders.MANGA_LIVRE_BLOG, chapterUrl);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -90,12 +89,11 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
|
|||||||
|
|
||||||
return IntStream.range(0, imageUrls.size())
|
return IntStream.range(0, imageUrls.size())
|
||||||
.boxed()
|
.boxed()
|
||||||
.collect(
|
.map(position -> new ContentImageInfoDTO(position, imageUrls.get(position)))
|
||||||
Collectors.toMap(
|
.toList();
|
||||||
i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new));
|
|
||||||
} catch (IOException | NoSuchElementException e) {
|
} catch (IOException | NoSuchElementException e) {
|
||||||
log.error("Error fetching mangas from MangaLivre", e);
|
log.error("Error fetching mangas from MangaLivre", e);
|
||||||
return Map.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,15 +2,15 @@ package com.magamochi.ingestion.providers.impl;
|
|||||||
|
|
||||||
import static java.util.Objects.nonNull;
|
import static java.util.Objects.nonNull;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
|
import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
|
||||||
|
import com.magamochi.ingestion.model.dto.ContentInfoDTO;
|
||||||
import com.magamochi.ingestion.model.dto.MangaInfoDTO;
|
import com.magamochi.ingestion.model.dto.MangaInfoDTO;
|
||||||
import com.magamochi.ingestion.providers.ContentProvider;
|
import com.magamochi.ingestion.providers.ContentProvider;
|
||||||
import com.magamochi.ingestion.providers.ContentProviders;
|
import com.magamochi.ingestion.providers.ContentProviders;
|
||||||
import com.magamochi.ingestion.providers.PagedContentProvider;
|
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.entity.MangaContentProvider;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
@ -26,8 +26,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
|
|||||||
private final FlareService flareService;
|
private final FlareService flareService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ContentProviderMangaChapterResponseDTO> getAvailableChapters(
|
public List<ContentInfoDTO> getAvailableChapters(MangaContentProvider provider) {
|
||||||
MangaContentProvider provider) {
|
|
||||||
log.info(
|
log.info(
|
||||||
"Getting available chapters from {}, manga {}",
|
"Getting available chapters from {}, manga {}",
|
||||||
ContentProviders.MANGA_LIVRE_TO,
|
ContentProviders.MANGA_LIVRE_TO,
|
||||||
@ -49,8 +48,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
|
|||||||
var url = linkElement.attr("href");
|
var url = linkElement.attr("href");
|
||||||
var title = linkElement.text();
|
var title = linkElement.text();
|
||||||
|
|
||||||
return new ContentProviderMangaChapterResponseDTO(
|
return new ContentInfoDTO(title.trim(), url.trim(), "pt-BR");
|
||||||
title.trim(), url.trim(), null, "pt-BR");
|
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
} catch (NoSuchElementException e) {
|
} catch (NoSuchElementException e) {
|
||||||
@ -60,7 +58,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<Integer, String> getChapterImagesUrls(String chapterUrl) {
|
public List<ContentImageInfoDTO> getContentImages(String chapterUrl) {
|
||||||
log.info("Getting images from {}, url {}", ContentProviders.MANGA_LIVRE_TO, chapterUrl);
|
log.info("Getting images from {}, url {}", ContentProviders.MANGA_LIVRE_TO, chapterUrl);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -79,12 +77,11 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
|
|||||||
|
|
||||||
return IntStream.range(0, imageUrls.size())
|
return IntStream.range(0, imageUrls.size())
|
||||||
.boxed()
|
.boxed()
|
||||||
.collect(
|
.map(position -> new ContentImageInfoDTO(position, imageUrls.get(position)))
|
||||||
Collectors.toMap(
|
.toList();
|
||||||
i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new));
|
|
||||||
} catch (NoSuchElementException e) {
|
} catch (NoSuchElementException e) {
|
||||||
log.error("Error parsing manga images from MangaLivre", e);
|
log.error("Error parsing manga images from MangaLivre", e);
|
||||||
return Map.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,15 +3,15 @@ package com.magamochi.ingestion.providers.impl;
|
|||||||
import static java.util.Objects.isNull;
|
import static java.util.Objects.isNull;
|
||||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
|
import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
|
||||||
|
import com.magamochi.ingestion.model.dto.ContentInfoDTO;
|
||||||
import com.magamochi.ingestion.model.dto.MangaInfoDTO;
|
import com.magamochi.ingestion.model.dto.MangaInfoDTO;
|
||||||
import com.magamochi.ingestion.providers.ContentProvider;
|
import com.magamochi.ingestion.providers.ContentProvider;
|
||||||
import com.magamochi.ingestion.providers.ContentProviders;
|
import com.magamochi.ingestion.providers.ContentProviders;
|
||||||
import com.magamochi.ingestion.providers.PagedContentProvider;
|
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.entity.MangaContentProvider;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
@ -27,8 +27,7 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
|
|||||||
private final FlareService flareService;
|
private final FlareService flareService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ContentProviderMangaChapterResponseDTO> getAvailableChapters(
|
public List<ContentInfoDTO> getAvailableChapters(MangaContentProvider provider) {
|
||||||
MangaContentProvider provider) {
|
|
||||||
log.info(
|
log.info(
|
||||||
"Getting available chapters from {}, manga {}",
|
"Getting available chapters from {}, manga {}",
|
||||||
ContentProviders.PINK_ROSA_SCAN,
|
ContentProviders.PINK_ROSA_SCAN,
|
||||||
@ -57,11 +56,8 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
|
|||||||
.getFirst()
|
.getFirst()
|
||||||
.getElementsByClass("text-sm truncate")
|
.getElementsByClass("text-sm truncate")
|
||||||
.getFirst();
|
.getFirst();
|
||||||
return new ContentProviderMangaChapterResponseDTO(
|
return new ContentInfoDTO(
|
||||||
chapterTitleElement.text().trim(),
|
chapterTitleElement.text().trim(), chapterItemElement.attr("href"), "pt-BR");
|
||||||
chapterItemElement.attr("href"),
|
|
||||||
null,
|
|
||||||
"pt-BR");
|
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
} catch (NoSuchElementException e) {
|
} catch (NoSuchElementException e) {
|
||||||
@ -71,7 +67,7 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<Integer, String> getChapterImagesUrls(String chapterUrl) {
|
public List<ContentImageInfoDTO> getContentImages(String chapterUrl) {
|
||||||
log.info("Getting images from {}, url {}", ContentProviders.PINK_ROSA_SCAN, chapterUrl);
|
log.info("Getting images from {}, url {}", ContentProviders.PINK_ROSA_SCAN, chapterUrl);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -99,12 +95,11 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
|
|||||||
|
|
||||||
return IntStream.range(0, imageUrls.size())
|
return IntStream.range(0, imageUrls.size())
|
||||||
.boxed()
|
.boxed()
|
||||||
.collect(
|
.map(position -> new ContentImageInfoDTO(position, imageUrls.get(position)))
|
||||||
Collectors.toMap(
|
.toList();
|
||||||
i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new));
|
|
||||||
} catch (NoSuchElementException e) {
|
} catch (NoSuchElementException e) {
|
||||||
log.error("Error parsing mangas from Pink Rosa Scan", e);
|
log.error("Error parsing mangas from Pink Rosa Scan", e);
|
||||||
return Map.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.magamochi.ingestion.queue.producer;
|
||||||
|
|
||||||
|
import com.magamochi.common.queue.command.MangaContentImageIngestCommand;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Log4j2
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MangaContentImageIngestProducer {
|
||||||
|
private final RabbitTemplate rabbitTemplate;
|
||||||
|
|
||||||
|
@Value("${queues.manga-content-image-ingest}")
|
||||||
|
private String mangaContentImageIngestQueue;
|
||||||
|
|
||||||
|
public void sendMangaContentImageIngestCommand(MangaContentImageIngestCommand command) {
|
||||||
|
rabbitTemplate.convertAndSend(mangaContentImageIngestQueue, command);
|
||||||
|
log.info("Sent manga content image ingest command: {}", command);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.magamochi.ingestion.queue.producer;
|
||||||
|
|
||||||
|
import com.magamochi.common.queue.command.MangaContentIngestCommand;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Log4j2
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MangaContentIngestProducer {
|
||||||
|
private final RabbitTemplate rabbitTemplate;
|
||||||
|
|
||||||
|
@Value("${queues.manga-content-ingest}")
|
||||||
|
private String mangaContentIngestQueue;
|
||||||
|
|
||||||
|
public void sendMangaContentIngestCommand(MangaContentIngestCommand command) {
|
||||||
|
rabbitTemplate.convertAndSend(mangaContentIngestQueue, command);
|
||||||
|
log.info("Sent manga content ingest command: {}", command);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,15 @@
|
|||||||
package com.magamochi.ingestion.service;
|
package com.magamochi.ingestion.service;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.service.MangaContentProviderService;
|
||||||
|
import com.magamochi.common.queue.command.MangaContentImageIngestCommand;
|
||||||
|
import com.magamochi.common.queue.command.MangaContentIngestCommand;
|
||||||
import com.magamochi.common.queue.command.MangaIngestCommand;
|
import com.magamochi.common.queue.command.MangaIngestCommand;
|
||||||
|
import com.magamochi.content.service.ContentService;
|
||||||
|
import com.magamochi.ingestion.providers.ContentProviderFactory;
|
||||||
import com.magamochi.ingestion.providers.PagedContentProviderFactory;
|
import com.magamochi.ingestion.providers.PagedContentProviderFactory;
|
||||||
import com.magamochi.ingestion.queue.command.ProviderPageIngestCommand;
|
import com.magamochi.ingestion.queue.command.ProviderPageIngestCommand;
|
||||||
|
import com.magamochi.ingestion.queue.producer.MangaContentImageIngestProducer;
|
||||||
|
import com.magamochi.ingestion.queue.producer.MangaContentIngestProducer;
|
||||||
import com.magamochi.ingestion.queue.producer.MangaIngestProducer;
|
import com.magamochi.ingestion.queue.producer.MangaIngestProducer;
|
||||||
import com.magamochi.ingestion.queue.producer.ProviderPageIngestProducer;
|
import com.magamochi.ingestion.queue.producer.ProviderPageIngestProducer;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
@ -13,10 +20,16 @@ import org.springframework.stereotype.Service;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class IngestionService {
|
public class IngestionService {
|
||||||
private final ContentProviderService contentProviderService;
|
private final ContentProviderService contentProviderService;
|
||||||
|
private final ContentService contentService;
|
||||||
|
private final MangaContentProviderService mangaContentProviderService;
|
||||||
|
|
||||||
|
private final ContentProviderFactory contentProviderFactory;
|
||||||
private final PagedContentProviderFactory pagedContentProviderFactory;
|
private final PagedContentProviderFactory pagedContentProviderFactory;
|
||||||
|
|
||||||
private final ProviderPageIngestProducer providerPageIngestProducer;
|
private final ProviderPageIngestProducer providerPageIngestProducer;
|
||||||
private final MangaIngestProducer mangaIngestProducer;
|
private final MangaIngestProducer mangaIngestProducer;
|
||||||
|
private final MangaContentIngestProducer mangaContentIngestProducer;
|
||||||
|
private final MangaContentImageIngestProducer mangaContentImageIngestProducer;
|
||||||
|
|
||||||
public void fetchContentProviderMangas(long contentProviderId) {
|
public void fetchContentProviderMangas(long contentProviderId) {
|
||||||
var contentProvider = contentProviderService.find(contentProviderId);
|
var contentProvider = contentProviderService.find(contentProviderId);
|
||||||
@ -48,4 +61,46 @@ public class IngestionService {
|
|||||||
var contentProviders = contentProviderService.getProviders(null);
|
var contentProviders = contentProviderService.getProviders(null);
|
||||||
contentProviders.providers().forEach(dto -> fetchContentProviderMangas(dto.id()));
|
contentProviders.providers().forEach(dto -> fetchContentProviderMangas(dto.id()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void fetchMangaContentProviderContentList(Long mangaContentProviderId) {
|
||||||
|
var mangaContentProvider = mangaContentProviderService.find(mangaContentProviderId);
|
||||||
|
|
||||||
|
var contentProvider =
|
||||||
|
contentProviderFactory.getContentProvider(
|
||||||
|
mangaContentProvider.getContentProvider().getName());
|
||||||
|
|
||||||
|
var availableChapters = contentProvider.getAvailableChapters(mangaContentProvider);
|
||||||
|
|
||||||
|
availableChapters.forEach(
|
||||||
|
content ->
|
||||||
|
mangaContentIngestProducer.sendMangaContentIngestCommand(
|
||||||
|
new MangaContentIngestCommand(
|
||||||
|
mangaContentProvider.getId(),
|
||||||
|
content.title(),
|
||||||
|
content.url(),
|
||||||
|
content.languageCode())));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fetchContent(Long mangaContentId) {
|
||||||
|
var mangaContent = contentService.find(mangaContentId);
|
||||||
|
var mangaContentProvider = mangaContent.getMangaContentProvider();
|
||||||
|
|
||||||
|
var contentProvider =
|
||||||
|
contentProviderFactory.getContentProvider(
|
||||||
|
mangaContentProvider.getContentProvider().getName());
|
||||||
|
|
||||||
|
var chapterImagesUrl = contentProvider.getContentImages(mangaContent.getUrl());
|
||||||
|
|
||||||
|
IntStream.range(0, chapterImagesUrl.size())
|
||||||
|
.forEach(
|
||||||
|
i -> {
|
||||||
|
var item = chapterImagesUrl.get(i);
|
||||||
|
|
||||||
|
var isLast = i == chapterImagesUrl.size() - 1;
|
||||||
|
|
||||||
|
mangaContentImageIngestProducer.sendMangaContentImageIngestCommand(
|
||||||
|
new MangaContentImageIngestCommand(
|
||||||
|
mangaContent.getId(), item.url(), item.position(), isLast));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
package com.magamochi.model.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
|
|
||||||
public record ContentProviderMangaChapterResponseDTO(
|
|
||||||
@NotBlank String chapterTitle,
|
|
||||||
@NotBlank String chapterUrl,
|
|
||||||
String chapter,
|
|
||||||
String languageCode) {}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
package com.magamochi.model.dto;
|
|
||||||
|
|
||||||
import com.magamochi.model.entity.MangaChapter;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
|
|
||||||
public record MangaChapterDTO(
|
|
||||||
@NotNull Long id,
|
|
||||||
@NotBlank String title,
|
|
||||||
@NotNull Boolean downloaded,
|
|
||||||
@NotNull Boolean isRead,
|
|
||||||
LanguageDTO language) {
|
|
||||||
public static MangaChapterDTO from(MangaChapter mangaChapter) {
|
|
||||||
return new MangaChapterDTO(
|
|
||||||
mangaChapter.getId(),
|
|
||||||
mangaChapter.getTitle(),
|
|
||||||
mangaChapter.getDownloaded(),
|
|
||||||
mangaChapter.getRead(),
|
|
||||||
LanguageDTO.from(mangaChapter.getLanguage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
package com.magamochi.model.dto;
|
|
||||||
|
|
||||||
import com.magamochi.model.entity.MangaChapter;
|
|
||||||
import com.magamochi.model.entity.MangaChapterImage;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public record MangaChapterImagesDTO(
|
|
||||||
@NotNull Long id,
|
|
||||||
@NotBlank String mangaTitle,
|
|
||||||
Long previousChapterId,
|
|
||||||
Long nextChapterId,
|
|
||||||
@NotNull List<@NotBlank String> chapterImageKeys) {
|
|
||||||
public static MangaChapterImagesDTO from(MangaChapter mangaChapter, Long prevId, Long nextId) {
|
|
||||||
return new MangaChapterImagesDTO(
|
|
||||||
mangaChapter.getId(),
|
|
||||||
mangaChapter.getTitle(),
|
|
||||||
prevId,
|
|
||||||
nextId,
|
|
||||||
mangaChapter.getMangaChapterImages().stream()
|
|
||||||
.sorted(Comparator.comparing(MangaChapterImage::getPosition))
|
|
||||||
.map(mangaChapterImage -> mangaChapterImage.getImage().getObjectKey())
|
|
||||||
.toList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
package com.magamochi.model.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public record MangaChapterResponseDTO(
|
|
||||||
@NotNull UUID id,
|
|
||||||
// @NotNull Long mangaProviderId,
|
|
||||||
@NotNull String chapterTitle,
|
|
||||||
@NotNull String chapterUrl) {}
|
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.magamochi.model.dto;
|
||||||
|
|
||||||
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
|
import com.magamochi.content.model.entity.MangaContentImage;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record MangaContentImagesDTO(
|
||||||
|
@NotNull Long id,
|
||||||
|
@NotBlank String mangaTitle,
|
||||||
|
Long previousContentId,
|
||||||
|
Long nextContentId,
|
||||||
|
@NotNull List<@NotBlank String> contentImageKeys) {
|
||||||
|
public static MangaContentImagesDTO from(MangaContent mangaContent, Long prevId, Long nextId) {
|
||||||
|
return new MangaContentImagesDTO(
|
||||||
|
mangaContent.getId(),
|
||||||
|
mangaContent.getTitle(),
|
||||||
|
prevId,
|
||||||
|
nextId,
|
||||||
|
mangaContent.getMangaContentImages().stream()
|
||||||
|
.sorted(Comparator.comparing(MangaContentImage::getPosition))
|
||||||
|
.map(mangaContentImage -> mangaContentImage.getImage().getObjectKey())
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +0,0 @@
|
|||||||
package com.magamochi.model.repository;
|
|
||||||
|
|
||||||
import com.magamochi.model.entity.MangaChapter;
|
|
||||||
import com.magamochi.model.entity.MangaChapterImage;
|
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface MangaChapterImageRepository extends JpaRepository<MangaChapterImage, Long> {
|
|
||||||
List<MangaChapterImage> findAllByMangaChapter(MangaChapter mangaChapter);
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
package com.magamochi.model.repository;
|
|
||||||
|
|
||||||
import com.magamochi.model.entity.MangaChapter;
|
|
||||||
import com.magamochi.model.entity.MangaContentProvider;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface MangaChapterRepository extends JpaRepository<MangaChapter, Long> {
|
|
||||||
Optional<MangaChapter> findByMangaContentProviderAndUrlIgnoreCase(
|
|
||||||
MangaContentProvider mangaContentProvider, @NotBlank String url);
|
|
||||||
|
|
||||||
List<MangaChapter> findByMangaContentProviderId(Long mangaProvider_id);
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
package com.magamochi.queue;
|
|
||||||
|
|
||||||
import com.magamochi.model.dto.MangaChapterDownloadCommand;
|
|
||||||
import com.magamochi.service.MangaChapterService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.log4j.Log4j2;
|
|
||||||
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Log4j2
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class MangaChapterDownloadConsumer {
|
|
||||||
private final MangaChapterService mangaChapterService;
|
|
||||||
|
|
||||||
@RabbitListener(queues = "${rabbit-mq.queues.manga-chapter-download}")
|
|
||||||
public void receiveMangaChapterDownloadCommand(MangaChapterDownloadCommand command) {
|
|
||||||
log.info("Received manga chapter download command: {}", command);
|
|
||||||
|
|
||||||
try {
|
|
||||||
mangaChapterService.fetchChapter(command.chapterId());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Couldn't download chapter {}. {}", command.chapterId(), e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -16,6 +16,6 @@ public class UpdateMangaFollowChapterListConsumer {
|
|||||||
@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);
|
||||||
oldMangaService.fetchFollowedMangaChapters(command.mangaProviderId());
|
// oldMangaService.fetchFollowedMangaChapters(command.mangaProviderId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,15 @@
|
|||||||
package com.magamochi.service;
|
package com.magamochi.service;
|
||||||
|
|
||||||
import com.google.common.util.concurrent.RateLimiter;
|
|
||||||
import com.magamochi.common.exception.UnprocessableException;
|
import com.magamochi.common.exception.UnprocessableException;
|
||||||
import com.magamochi.ingestion.providers.ContentProviderFactory;
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
|
import com.magamochi.content.model.entity.MangaContentImage;
|
||||||
|
import com.magamochi.content.model.repository.MangaContentImageRepository;
|
||||||
|
import com.magamochi.content.model.repository.MangaContentRepository;
|
||||||
import com.magamochi.model.dto.MangaChapterArchiveDTO;
|
import com.magamochi.model.dto.MangaChapterArchiveDTO;
|
||||||
import com.magamochi.model.dto.MangaChapterImagesDTO;
|
|
||||||
import com.magamochi.model.entity.MangaChapter;
|
|
||||||
import com.magamochi.model.entity.MangaChapterImage;
|
|
||||||
import com.magamochi.model.enumeration.ArchiveFileType;
|
import com.magamochi.model.enumeration.ArchiveFileType;
|
||||||
import com.magamochi.model.repository.MangaChapterImageRepository;
|
|
||||||
import com.magamochi.model.repository.MangaChapterRepository;
|
|
||||||
import io.github.resilience4j.retry.Retry;
|
|
||||||
import io.github.resilience4j.retry.RetryRegistry;
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
@ -26,144 +17,29 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
import org.apache.tomcat.util.http.fileupload.IOUtils;
|
import org.apache.tomcat.util.http.fileupload.IOUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
@Log4j2
|
@Log4j2
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class MangaChapterService {
|
public class MangaChapterService {
|
||||||
private final MangaChapterRepository mangaChapterRepository;
|
private final MangaContentRepository mangaContentRepository;
|
||||||
private final MangaChapterImageRepository mangaChapterImageRepository;
|
private final MangaContentImageRepository mangaContentImageRepository;
|
||||||
|
|
||||||
private final OldImageService oldImageService;
|
private final OldImageService oldImageService;
|
||||||
|
|
||||||
private final ContentProviderFactory contentProviderFactory;
|
|
||||||
|
|
||||||
private final RateLimiter imageDownloadRateLimiter;
|
|
||||||
private final RetryRegistry retryRegistry;
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void fetchChapter(Long chapterId) {
|
|
||||||
var chapter = getMangaChapterThrowIfNotFound(chapterId);
|
|
||||||
|
|
||||||
var mangaProvider = chapter.getMangaContentProvider();
|
|
||||||
var provider =
|
|
||||||
contentProviderFactory.getContentProvider(mangaProvider.getContentProvider().getName());
|
|
||||||
|
|
||||||
var chapterImagesUrls = provider.getChapterImagesUrls(chapter.getUrl());
|
|
||||||
if (chapterImagesUrls.isEmpty()) {
|
|
||||||
throw new UnprocessableException(
|
|
||||||
"No images found on provider for Manga Chapter ID: " + chapterId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var retryConfig = retryRegistry.retry("ImageDownloadRetry").getRetryConfig();
|
|
||||||
|
|
||||||
var chapterImages =
|
|
||||||
chapterImagesUrls.entrySet().parallelStream()
|
|
||||||
.map(
|
|
||||||
entry -> {
|
|
||||||
imageDownloadRateLimiter.acquire();
|
|
||||||
|
|
||||||
try {
|
|
||||||
var finalUrl = new URI(entry.getValue().trim()).toASCIIString().trim();
|
|
||||||
var retry =
|
|
||||||
Retry.of("image-download-" + chapterId + "-" + entry.getKey(), retryConfig);
|
|
||||||
|
|
||||||
retry
|
|
||||||
.getEventPublisher()
|
|
||||||
.onRetry(
|
|
||||||
event ->
|
|
||||||
log.warn(
|
|
||||||
"Retrying image download {}/{} for chapter {}. Attempt #{}. Error: {}",
|
|
||||||
entry.getKey() + 1,
|
|
||||||
chapterImagesUrls.size(),
|
|
||||||
chapterId,
|
|
||||||
event.getNumberOfRetryAttempts(),
|
|
||||||
event.getLastThrowable().getMessage()));
|
|
||||||
|
|
||||||
return retry.executeCheckedSupplier(
|
|
||||||
() -> {
|
|
||||||
var url = new URL(finalUrl);
|
|
||||||
var connection = url.openConnection();
|
|
||||||
connection.setConnectTimeout(5000);
|
|
||||||
connection.setReadTimeout(5000);
|
|
||||||
|
|
||||||
try (var inputStream =
|
|
||||||
new BufferedInputStream(connection.getInputStream())) {
|
|
||||||
var bytes = inputStream.readAllBytes();
|
|
||||||
|
|
||||||
var image =
|
|
||||||
oldImageService.uploadImage(
|
|
||||||
bytes, "image/jpeg", "chapter/" + chapterId);
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"Downloaded image {}/{} for manga {} chapter {}: {}",
|
|
||||||
entry.getKey() + 1,
|
|
||||||
chapterImagesUrls.size(),
|
|
||||||
chapter.getMangaContentProvider().getManga().getTitle(),
|
|
||||||
chapterId,
|
|
||||||
entry.getValue());
|
|
||||||
|
|
||||||
return MangaChapterImage.builder()
|
|
||||||
.mangaChapter(chapter)
|
|
||||||
.position(entry.getKey())
|
|
||||||
.image(image)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw new UnprocessableException(
|
|
||||||
"Could not download image for chapter ID: " + chapterId, e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
mangaChapterImageRepository.saveAll(chapterImages);
|
|
||||||
|
|
||||||
chapter.setDownloaded(true);
|
|
||||||
mangaChapterRepository.save(chapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MangaChapterImagesDTO getMangaChapterImages(Long chapterId) {
|
|
||||||
var chapter = getMangaChapterThrowIfNotFound(chapterId);
|
|
||||||
|
|
||||||
var chapters =
|
|
||||||
chapter.getMangaContentProvider().getMangaChapters().stream()
|
|
||||||
.sorted(Comparator.comparing(MangaChapter::getId))
|
|
||||||
.toList();
|
|
||||||
Long prevId = null;
|
|
||||||
Long nextId = null;
|
|
||||||
|
|
||||||
// TODO: this doesn't perform well for large datasets
|
|
||||||
for (var i = 0; i < chapters.size(); i++) {
|
|
||||||
if (chapters.get(i).getId().equals(chapterId)) {
|
|
||||||
if (i > 0) {
|
|
||||||
prevId = chapters.get(i - 1).getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i < chapters.size() - 1) {
|
|
||||||
nextId = chapters.get(i + 1).getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MangaChapterImagesDTO.from(chapter, prevId, nextId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void markAsRead(Long chapterId) {
|
public void markAsRead(Long chapterId) {
|
||||||
var chapter = getMangaChapterThrowIfNotFound(chapterId);
|
// TODO: implement this
|
||||||
chapter.setRead(true);
|
// var chapter = getMangaChapterThrowIfNotFound(chapterId);
|
||||||
|
// chapter.setRead(true);
|
||||||
mangaChapterRepository.save(chapter);
|
//
|
||||||
|
// mangaChapterRepository.save(chapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MangaChapterArchiveDTO downloadChapter(Long chapterId, ArchiveFileType archiveFileType)
|
public MangaChapterArchiveDTO downloadChapter(Long chapterId, ArchiveFileType archiveFileType)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
var chapter = getMangaChapterThrowIfNotFound(chapterId);
|
var chapter = getMangaChapterThrowIfNotFound(chapterId);
|
||||||
|
|
||||||
var chapterImages = mangaChapterImageRepository.findAllByMangaChapter(chapter);
|
var chapterImages = mangaContentImageRepository.findAllByMangaContent(chapter);
|
||||||
|
|
||||||
var byteArrayOutputStream =
|
var byteArrayOutputStream =
|
||||||
switch (archiveFileType) {
|
switch (archiveFileType) {
|
||||||
@ -177,7 +53,7 @@ public class MangaChapterService {
|
|||||||
chapter.getTitle() + ".cbz", byteArrayOutputStream.toByteArray());
|
chapter.getTitle() + ".cbz", byteArrayOutputStream.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
private ByteArrayOutputStream getChapterCbzArchive(List<MangaChapterImage> chapterImages)
|
private ByteArrayOutputStream getChapterCbzArchive(List<MangaContentImage> chapterImages)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
var byteArrayOutputStream = new ByteArrayOutputStream();
|
var byteArrayOutputStream = new ByteArrayOutputStream();
|
||||||
var bufferedOutputStream = new BufferedOutputStream(byteArrayOutputStream);
|
var bufferedOutputStream = new BufferedOutputStream(byteArrayOutputStream);
|
||||||
@ -202,8 +78,8 @@ public class MangaChapterService {
|
|||||||
return byteArrayOutputStream;
|
return byteArrayOutputStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MangaChapter getMangaChapterThrowIfNotFound(Long chapterId) {
|
private MangaContent getMangaChapterThrowIfNotFound(Long chapterId) {
|
||||||
return mangaChapterRepository
|
return mangaContentRepository
|
||||||
.findById(chapterId)
|
.findById(chapterId)
|
||||||
.orElseThrow(() -> new RuntimeException("Manga Chapter not found for ID: " + chapterId));
|
.orElseThrow(() -> new RuntimeException("Manga Chapter not found for ID: " + chapterId));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -378,12 +378,12 @@
|
|||||||
// var mangaChapter =
|
// var mangaChapter =
|
||||||
// mangaChapterRepository
|
// mangaChapterRepository
|
||||||
// .findByMangaContentProviderAndUrlIgnoreCase(mangaContentProvider,
|
// .findByMangaContentProviderAndUrlIgnoreCase(mangaContentProvider,
|
||||||
// chapter.chapterUrl())
|
// chapter.url())
|
||||||
// .orElseGet(MangaChapter::new);
|
// .orElseGet(MangaChapter::new);
|
||||||
//
|
//
|
||||||
// mangaChapter.setMangaContentProvider(mangaContentProvider);
|
// mangaChapter.setMangaContentProvider(mangaContentProvider);
|
||||||
// mangaChapter.setTitle(chapter.chapterTitle());
|
// mangaChapter.setTitle(chapter.title());
|
||||||
// mangaChapter.setUrl(chapter.chapterUrl());
|
// mangaChapter.setUrl(chapter.url());
|
||||||
//
|
//
|
||||||
// var language = languageService.getOrThrow(chapter.languageCode());
|
// var language = languageService.getOrThrow(chapter.languageCode());
|
||||||
// mangaChapter.setLanguage(language);
|
// mangaChapter.setLanguage(language);
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package com.magamochi.service;
|
package com.magamochi.service;
|
||||||
|
|
||||||
import com.magamochi.image.model.entity.Image;
|
import com.magamochi.image.model.entity.Image;
|
||||||
import com.magamochi.image.model.repository.ImageRepository;
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
@ -12,14 +11,6 @@ import org.springframework.stereotype.Service;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class OldImageService {
|
public class OldImageService {
|
||||||
private final OldS3Service oldS3Service;
|
private final OldS3Service oldS3Service;
|
||||||
private final ImageRepository imageRepository;
|
|
||||||
|
|
||||||
public Image uploadImage(byte[] data, String contentType, String path) {
|
|
||||||
log.info("Uploading image {} to S3", path);
|
|
||||||
var fileKey = oldS3Service.uploadFile(data, contentType, path);
|
|
||||||
|
|
||||||
return imageRepository.save(Image.builder().objectKey(fileKey).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream getImageStream(Image image) {
|
public InputStream getImageStream(Image image) {
|
||||||
return oldS3Service.getFile(image.getObjectKey());
|
return oldS3Service.getFile(image.getObjectKey());
|
||||||
|
|||||||
@ -3,18 +3,13 @@ package com.magamochi.service;
|
|||||||
import com.magamochi.catalog.model.entity.Manga;
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
|
import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
|
||||||
import com.magamochi.catalog.model.repository.MangaRepository;
|
import com.magamochi.catalog.model.repository.MangaRepository;
|
||||||
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.content.model.entity.MangaContent;
|
||||||
import com.magamochi.model.dto.*;
|
import com.magamochi.model.dto.*;
|
||||||
import com.magamochi.model.entity.MangaChapter;
|
|
||||||
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.queue.MangaChapterDownloadProducer;
|
import com.magamochi.queue.MangaChapterDownloadProducer;
|
||||||
import com.magamochi.user.service.UserService;
|
import com.magamochi.user.service.UserService;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
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;
|
||||||
@ -25,19 +20,13 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class OldMangaService {
|
public class OldMangaService {
|
||||||
// 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 UserMangaFollowRepository userMangaFollowRepository;
|
private final UserMangaFollowRepository userMangaFollowRepository;
|
||||||
|
|
||||||
private final MangaChapterDownloadProducer mangaChapterDownloadProducer;
|
private final MangaChapterDownloadProducer mangaChapterDownloadProducer;
|
||||||
private final MangaChapterRepository mangaChapterRepository;
|
|
||||||
|
|
||||||
private final NtfyClient ntfyClient;
|
|
||||||
|
|
||||||
public void fetchAllNotDownloadedChapters(Long mangaProviderId) {
|
public void fetchAllNotDownloadedChapters(Long mangaProviderId) {
|
||||||
var mangaProvider =
|
var mangaProvider =
|
||||||
@ -47,9 +36,9 @@ public class OldMangaService {
|
|||||||
() -> new NotFoundException("Manga Provider not found for ID: " + mangaProviderId));
|
() -> new NotFoundException("Manga Provider not found for ID: " + mangaProviderId));
|
||||||
|
|
||||||
var chapterIds =
|
var chapterIds =
|
||||||
mangaProvider.getMangaChapters().stream()
|
mangaProvider.getMangaContents().stream()
|
||||||
.filter(mangaChapter -> !mangaChapter.getDownloaded())
|
.filter(mangaChapter -> !mangaChapter.getDownloaded())
|
||||||
.map(MangaChapter::getId)
|
.map(MangaContent::getId)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
chapterIds.forEach(
|
chapterIds.forEach(
|
||||||
@ -58,57 +47,39 @@ public class OldMangaService {
|
|||||||
new MangaChapterDownloadCommand(chapterId)));
|
new MangaChapterDownloadCommand(chapterId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<MangaChapterDTO> getMangaChapters(Long mangaProviderId) {
|
// public void fetchFollowedMangaChapters(Long mangaProviderId) {
|
||||||
var mangaProvider = getMangaProviderThrowIfNotFound(mangaProviderId);
|
// var mangaProvider =
|
||||||
|
// mangaContentProviderRepository
|
||||||
return mangaProvider.getMangaChapters().stream()
|
// .findById(mangaProviderId)
|
||||||
.sorted(Comparator.comparing(MangaChapter::getId))
|
// .orElseThrow(
|
||||||
.map(MangaChapterDTO::from)
|
// () -> new NotFoundException("Manga Provider not found for ID: " +
|
||||||
.toList();
|
// mangaProviderId));
|
||||||
}
|
//
|
||||||
|
// var currentAvailableChapterCount =
|
||||||
public void fetchFollowedMangaChapters(Long mangaProviderId) {
|
// mangaChapterRepository.findByMangaContentProviderId(mangaProviderId).size();
|
||||||
var mangaProvider =
|
//
|
||||||
mangaContentProviderRepository
|
// fetchMangaChapters(mangaProviderId);
|
||||||
.findById(mangaProviderId)
|
// mangaChapterRepository.flush();
|
||||||
.orElseThrow(
|
//
|
||||||
() -> new NotFoundException("Manga Provider not found for ID: " + mangaProviderId));
|
// var availableChapterCount =
|
||||||
|
// mangaChapterRepository.findByMangaContentProviderId(mangaProviderId).size();
|
||||||
var currentAvailableChapterCount =
|
//
|
||||||
mangaChapterRepository.findByMangaContentProviderId(mangaProviderId).size();
|
// if (availableChapterCount <= currentAvailableChapterCount) {
|
||||||
|
// return;
|
||||||
fetchMangaChapters(mangaProviderId);
|
// }
|
||||||
mangaChapterRepository.flush();
|
//
|
||||||
|
// log.info("New chapters found for Manga Provider {}", mangaProviderId);
|
||||||
var availableChapterCount =
|
//
|
||||||
mangaChapterRepository.findByMangaContentProviderId(mangaProviderId).size();
|
// var userMangaFollows = userMangaFollowRepository.findByManga(mangaProvider.getManga());
|
||||||
|
// userMangaFollows.forEach(
|
||||||
if (availableChapterCount <= currentAvailableChapterCount) {
|
// umf ->
|
||||||
return;
|
// ntfyClient.notify(
|
||||||
}
|
// new NtfyClient.Request(
|
||||||
|
// "mangamochi-" + umf.getUser().getId().toString(),
|
||||||
log.info("New chapters found for Manga Provider {}", mangaProviderId);
|
// umf.getManga().getTitle(),
|
||||||
|
// "New chapter available on " +
|
||||||
var userMangaFollows = userMangaFollowRepository.findByManga(mangaProvider.getManga());
|
// mangaProvider.getContentProvider().getName())));
|
||||||
userMangaFollows.forEach(
|
// }
|
||||||
umf ->
|
|
||||||
ntfyClient.notify(
|
|
||||||
new NtfyClient.Request(
|
|
||||||
"mangamochi-" + umf.getUser().getId().toString(),
|
|
||||||
umf.getManga().getTitle(),
|
|
||||||
"New chapter available on " + mangaProvider.getContentProvider().getName())));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void fetchMangaChapters(Long mangaProviderId) {
|
|
||||||
var mangaProvider = getMangaProviderThrowIfNotFound(mangaProviderId);
|
|
||||||
|
|
||||||
var contentProvider =
|
|
||||||
contentProviderFactory.getContentProvider(mangaProvider.getContentProvider().getName());
|
|
||||||
var availableChapters = contentProvider.getAvailableChapters(mangaProvider);
|
|
||||||
|
|
||||||
// availableChapters.forEach(
|
|
||||||
// chapter -> mangaImportService.persistMangaChapter(mangaProvider, chapter));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Manga findMangaByIdThrowIfNotFound(Long mangaId) {
|
public Manga findMangaByIdThrowIfNotFound(Long mangaId) {
|
||||||
return mangaRepository
|
return mangaRepository
|
||||||
@ -116,13 +87,6 @@ public class OldMangaService {
|
|||||||
.orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
|
.orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
|
||||||
}
|
}
|
||||||
|
|
||||||
private MangaContentProvider getMangaProviderThrowIfNotFound(Long mangaProviderId) {
|
|
||||||
return mangaContentProviderRepository
|
|
||||||
.findById(mangaProviderId)
|
|
||||||
.orElseThrow(
|
|
||||||
() -> new NotFoundException("Manga Provider not found for ID: " + mangaProviderId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void follow(Long mangaId) {
|
public void follow(Long mangaId) {
|
||||||
var user = userService.getLoggedUserThrowIfNotFound();
|
var user = userService.getLoggedUserThrowIfNotFound();
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
package com.magamochi.service;
|
package com.magamochi.service;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.UUID;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import software.amazon.awssdk.core.sync.RequestBody;
|
|
||||||
import software.amazon.awssdk.services.s3.S3Client;
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
import software.amazon.awssdk.services.s3.model.*;
|
import software.amazon.awssdk.services.s3.model.*;
|
||||||
|
|
||||||
@ -17,17 +15,6 @@ public class OldS3Service {
|
|||||||
|
|
||||||
private final S3Client s3Client;
|
private final S3Client s3Client;
|
||||||
|
|
||||||
public String uploadFile(byte[] data, String contentType, String path) {
|
|
||||||
var filename = "manga/" + path + "/" + UUID.randomUUID();
|
|
||||||
|
|
||||||
var request =
|
|
||||||
PutObjectRequest.builder().bucket(bucket).key(filename).contentType(contentType).build();
|
|
||||||
|
|
||||||
s3Client.putObject(request, RequestBody.fromBytes(data));
|
|
||||||
|
|
||||||
return filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream getFile(String key) {
|
public InputStream getFile(String key) {
|
||||||
var request = GetObjectRequest.builder().bucket(bucket).key(key).build();
|
var request = GetObjectRequest.builder().bucket(bucket).key(key).build();
|
||||||
|
|
||||||
|
|||||||
@ -96,8 +96,11 @@ topics:
|
|||||||
|
|
||||||
queues:
|
queues:
|
||||||
manga-ingest: ${MANGA_INGEST_QUEUE:mangaIngest}
|
manga-ingest: ${MANGA_INGEST_QUEUE:mangaIngest}
|
||||||
|
manga-content-ingest: ${MANGA_CONTENT_INGEST_QUEUE:mangaContentIngest}
|
||||||
|
manga-content-image-ingest: ${MANGA_CONTENT_IMAGE_INGEST_QUEUE:mangaContentImageIngest}
|
||||||
provider-page-ingest: ${PROVIDER_PAGE_INGEST_QUEUE:providerPageIngest}
|
provider-page-ingest: ${PROVIDER_PAGE_INGEST_QUEUE:providerPageIngest}
|
||||||
manga-update: ${MANGA_UPDATE_QUEUE:mangaUpdate}
|
manga-update: ${MANGA_UPDATE_QUEUE:mangaUpdate}
|
||||||
|
manga-content-image-update: ${MANGA_CONTENT_IMAGE_UPDATE_QUEUE:mangaContentImageUpdate}
|
||||||
image-fetch: ${IMAGE_FETCH_QUEUE:imageFetch}
|
image-fetch: ${IMAGE_FETCH_QUEUE:imageFetch}
|
||||||
manga-cover-update: ${MANGA_COVER_UDPATE_QUEUE:mangaCoverUpdate}
|
manga-cover-update: ${MANGA_COVER_UDPATE_QUEUE:mangaCoverUpdate}
|
||||||
|
|
||||||
|
|||||||
@ -15,8 +15,8 @@ ON CONFLICT DO NOTHING;
|
|||||||
CREATE TABLE images
|
CREATE TABLE images
|
||||||
(
|
(
|
||||||
id UUID NOT NULL PRIMARY KEY,
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
object_key VARCHAR NOT NULL,
|
object_key VARCHAR NOT NULL,
|
||||||
file_hash VARCHAR(64) UNIQUE,
|
file_hash VARCHAR(64) UNIQUE,
|
||||||
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
|
||||||
);
|
);
|
||||||
@ -46,7 +46,7 @@ CREATE TABLE content_providers
|
|||||||
name VARCHAR NOT NULL,
|
name VARCHAR NOT NULL,
|
||||||
url VARCHAR NOT NULL,
|
url VARCHAR NOT NULL,
|
||||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
supports_chapter_fetch BOOLEAN NOT NULL DEFAULT TRUE,
|
supports_content_fetch BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
manual_import BOOLEAN NOT NULL DEFAULT FALSE,
|
manual_import BOOLEAN NOT NULL 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
|
||||||
@ -64,24 +64,23 @@ CREATE TABLE manga_content_provider
|
|||||||
UNIQUE (manga_id, content_provider_id)
|
UNIQUE (manga_id, content_provider_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE manga_chapters
|
CREATE TABLE manga_contents
|
||||||
(
|
(
|
||||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
manga_content_provider_id BIGINT NOT NULL REFERENCES manga_content_provider (id) ON DELETE CASCADE,
|
manga_content_provider_id BIGINT NOT NULL REFERENCES manga_content_provider (id) ON DELETE CASCADE,
|
||||||
title VARCHAR NOT NULL,
|
type VARCHAR(50) NOT NULL,
|
||||||
url VARCHAR NOT NULL,
|
title VARCHAR NOT NULL,
|
||||||
chapter_number INTEGER,
|
url VARCHAR NOT NULL,
|
||||||
language_id BIGINT REFERENCES languages (id),
|
language_id BIGINT REFERENCES languages (id),
|
||||||
downloaded BOOLEAN DEFAULT FALSE,
|
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_chapter_images
|
CREATE TABLE manga_content_images
|
||||||
(
|
(
|
||||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
manga_chapter_id BIGINT NOT NULL REFERENCES manga_chapters (id) ON DELETE CASCADE,
|
manga_content_id BIGINT NOT NULL REFERENCES manga_contents (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,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
INSERT INTO content_providers(name, url, active, supports_chapter_fetch, manual_import)
|
INSERT INTO content_providers(name, url, active, supports_content_fetch, manual_import)
|
||||||
VALUES ('Manga Livre Blog', 'https://mangalivre.blog', TRUE, TRUE, FALSE),
|
VALUES ('Manga Livre Blog', 'https://mangalivre.blog', TRUE, TRUE, FALSE),
|
||||||
('Manga Livre.to', 'https://mangalivre.to', TRUE, TRUE, FALSE),
|
('Manga Livre.to', 'https://mangalivre.to', TRUE, TRUE, FALSE),
|
||||||
('Pink Rosa Scan', 'https://scanpinkrosa.blogspot.com', TRUE, TRUE, FALSE);
|
('Pink Rosa Scan', 'https://scanpinkrosa.blogspot.com', TRUE, TRUE, FALSE);
|
||||||
Loading…
x
Reference in New Issue
Block a user