diff --git a/src/main/java/com/magamochi/catalog/model/dto/MangaDTO.java b/src/main/java/com/magamochi/catalog/model/dto/MangaDTO.java index 4cdfe51..091186a 100644 --- a/src/main/java/com/magamochi/catalog/model/dto/MangaDTO.java +++ b/src/main/java/com/magamochi/catalog/model/dto/MangaDTO.java @@ -4,9 +4,9 @@ import static java.util.Objects.isNull; import com.magamochi.catalog.model.entity.Manga; import com.magamochi.catalog.model.entity.MangaAlternativeTitle; +import com.magamochi.catalog.model.entity.MangaContentProvider; import com.magamochi.catalog.model.enumeration.MangaStatus; -import com.magamochi.model.entity.MangaChapter; -import com.magamochi.model.entity.MangaContentProvider; +import com.magamochi.content.model.entity.MangaContent; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.OffsetDateTime; @@ -59,9 +59,9 @@ public record MangaDTO( @NotNull Integer chaptersDownloaded, @NotNull Boolean supportsChapterFetch) { public static MangaProviderDTO from(MangaContentProvider mangaContentProvider) { - var chapters = mangaContentProvider.getMangaChapters(); + var chapters = mangaContentProvider.getMangaContents(); var chaptersAvailable = chapters.size(); - var chaptersDownloaded = (int) chapters.stream().filter(MangaChapter::getDownloaded).count(); + var chaptersDownloaded = (int) chapters.stream().filter(MangaContent::getDownloaded).count(); return new MangaProviderDTO( mangaContentProvider.getId(), @@ -69,7 +69,7 @@ public record MangaDTO( mangaContentProvider.getContentProvider().isActive(), chaptersAvailable, chaptersDownloaded, - mangaContentProvider.getContentProvider().getSupportsChapterFetch()); + mangaContentProvider.getContentProvider().getSupportsContentFetch()); } } } diff --git a/src/main/java/com/magamochi/catalog/model/entity/Manga.java b/src/main/java/com/magamochi/catalog/model/entity/Manga.java index 8a307ed..8662417 100644 --- a/src/main/java/com/magamochi/catalog/model/entity/Manga.java +++ b/src/main/java/com/magamochi/catalog/model/entity/Manga.java @@ -3,7 +3,6 @@ package com.magamochi.catalog.model.entity; import com.magamochi.catalog.model.enumeration.MangaState; import com.magamochi.catalog.model.enumeration.MangaStatus; import com.magamochi.image.model.entity.Image; -import com.magamochi.model.entity.MangaContentProvider; import com.magamochi.model.entity.UserFavoriteManga; import jakarta.persistence.*; import java.time.Instant; diff --git a/src/main/java/com/magamochi/model/entity/MangaContentProvider.java b/src/main/java/com/magamochi/catalog/model/entity/MangaContentProvider.java similarity index 86% rename from src/main/java/com/magamochi/model/entity/MangaContentProvider.java rename to src/main/java/com/magamochi/catalog/model/entity/MangaContentProvider.java index 9bfcc2c..cd4e150 100644 --- a/src/main/java/com/magamochi/model/entity/MangaContentProvider.java +++ b/src/main/java/com/magamochi/catalog/model/entity/MangaContentProvider.java @@ -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 jakarta.persistence.*; import java.time.Instant; @@ -34,7 +34,7 @@ public class MangaContentProvider { private String url; @OneToMany(mappedBy = "mangaContentProvider") - List mangaChapters; + List mangaContents; @CreationTimestamp private Instant createdAt; diff --git a/src/main/java/com/magamochi/catalog/model/repository/MangaContentProviderRepository.java b/src/main/java/com/magamochi/catalog/model/repository/MangaContentProviderRepository.java index de3f1bd..7632849 100644 --- a/src/main/java/com/magamochi/catalog/model/repository/MangaContentProviderRepository.java +++ b/src/main/java/com/magamochi/catalog/model/repository/MangaContentProviderRepository.java @@ -1,6 +1,6 @@ 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 org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/magamochi/catalog/service/LanguageService.java b/src/main/java/com/magamochi/catalog/service/LanguageService.java index 83833eb..fe8af57 100644 --- a/src/main/java/com/magamochi/catalog/service/LanguageService.java +++ b/src/main/java/com/magamochi/catalog/service/LanguageService.java @@ -11,7 +11,7 @@ import org.springframework.stereotype.Service; public class LanguageService { public final LanguageRepository languageRepository; - public Language getOrThrow(String code) { + public Language find(String code) { return languageRepository .findByCodeIgnoreCase(code) .orElseThrow(() -> new NotFoundException("Language with code " + code + " not found")); diff --git a/src/main/java/com/magamochi/catalog/service/MangaContentProviderService.java b/src/main/java/com/magamochi/catalog/service/MangaContentProviderService.java new file mode 100644 index 0000000..85aa949 --- /dev/null +++ b/src/main/java/com/magamochi/catalog/service/MangaContentProviderService.java @@ -0,0 +1,22 @@ +package com.magamochi.catalog.service; + +import com.magamochi.catalog.model.entity.MangaContentProvider; +import com.magamochi.catalog.model.repository.MangaContentProviderRepository; +import com.magamochi.common.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MangaContentProviderService { + private final MangaContentProviderRepository mangaContentProviderRepository; + + public MangaContentProvider find(long mangaContentProviderId) { + return mangaContentProviderRepository + .findById(mangaContentProviderId) + .orElseThrow( + () -> + new NotFoundException( + "MangaContentProvider not found - ID: " + mangaContentProviderId)); + } +} diff --git a/src/main/java/com/magamochi/catalog/service/MangaIngestService.java b/src/main/java/com/magamochi/catalog/service/MangaIngestService.java index 3d26e35..5f3d919 100644 --- a/src/main/java/com/magamochi/catalog/service/MangaIngestService.java +++ b/src/main/java/com/magamochi/catalog/service/MangaIngestService.java @@ -2,11 +2,11 @@ package com.magamochi.catalog.service; import static java.util.Objects.isNull; +import com.magamochi.catalog.model.entity.MangaContentProvider; import com.magamochi.catalog.model.entity.MangaIngestReview; import com.magamochi.catalog.model.repository.MangaContentProviderRepository; import com.magamochi.catalog.model.repository.MangaIngestReviewRepository; import com.magamochi.ingestion.service.ContentProviderService; -import com.magamochi.model.entity.MangaContentProvider; import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; diff --git a/src/main/java/com/magamochi/common/config/RabbitConfig.java b/src/main/java/com/magamochi/common/config/RabbitConfig.java index 9a3ba42..a76091d 100644 --- a/src/main/java/com/magamochi/common/config/RabbitConfig.java +++ b/src/main/java/com/magamochi/common/config/RabbitConfig.java @@ -16,6 +16,12 @@ public class RabbitConfig { @Value("${queues.manga-ingest}") private String mangaIngestQueue; + @Value("${queues.manga-content-ingest}") + private String mangaContentIngestQueue; + + @Value("${queues.manga-content-image-ingest}") + private String mangaContentImageIngestQueue; + @Value("${queues.provider-page-ingest}") private String providerPageIngestQueue; @@ -62,6 +68,16 @@ public class RabbitConfig { null); } + @Bean + public Queue mangaContentIngestQueue() { + return new Queue(mangaContentIngestQueue, false); + } + + @Bean + public Queue mangaContentImageIngestQueue() { + return new Queue(mangaContentImageIngestQueue, false); + } + @Bean public Queue mangaIngestQueue() { return new Queue(mangaIngestQueue, false); diff --git a/src/main/java/com/magamochi/common/model/enumeration/ContentType.java b/src/main/java/com/magamochi/common/model/enumeration/ContentType.java index 79726ce..f236f37 100644 --- a/src/main/java/com/magamochi/common/model/enumeration/ContentType.java +++ b/src/main/java/com/magamochi/common/model/enumeration/ContentType.java @@ -1,5 +1,7 @@ package com.magamochi.common.model.enumeration; public enum ContentType { - MANGA_COVER + MANGA_COVER, + CHAPTER, + VOLUME } diff --git a/src/main/java/com/magamochi/common/queue/command/MangaContentImageIngestCommand.java b/src/main/java/com/magamochi/common/queue/command/MangaContentImageIngestCommand.java new file mode 100644 index 0000000..42ffb0b --- /dev/null +++ b/src/main/java/com/magamochi/common/queue/command/MangaContentImageIngestCommand.java @@ -0,0 +1,6 @@ +package com.magamochi.common.queue.command; + +import jakarta.validation.constraints.NotBlank; + +public record MangaContentImageIngestCommand( + long mangaContentId, @NotBlank String url, int position, boolean isLast) {} diff --git a/src/main/java/com/magamochi/common/queue/command/MangaContentIngestCommand.java b/src/main/java/com/magamochi/common/queue/command/MangaContentIngestCommand.java new file mode 100644 index 0000000..b04bd82 --- /dev/null +++ b/src/main/java/com/magamochi/common/queue/command/MangaContentIngestCommand.java @@ -0,0 +1,9 @@ +package com.magamochi.common.queue.command; + +import jakarta.validation.constraints.NotBlank; + +public record MangaContentIngestCommand( + long mangaContentProviderId, + @NotBlank String title, + @NotBlank String url, + @NotBlank String languageCode) {} diff --git a/src/main/java/com/magamochi/content/controller/ContentController.java b/src/main/java/com/magamochi/content/controller/ContentController.java new file mode 100644 index 0000000..e107044 --- /dev/null +++ b/src/main/java/com/magamochi/content/controller/ContentController.java @@ -0,0 +1,31 @@ +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 io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/content") +@RequiredArgsConstructor +public class ContentController { + private final ContentService contentService; + + @Operation( + summary = "Get the content for a specific manga/content provider combination", + description = "Retrieve the content for a specific manga/content provider combination.", + tags = {"Content"}, + operationId = "getMangaProviderContent") + @GetMapping("/{mangaContentProviderId}") + public DefaultResponseDTO> getMangaProviderContent( + @PathVariable @NotNull Long mangaContentProviderId) { + return DefaultResponseDTO.ok(contentService.getContent(mangaContentProviderId)); + } +} diff --git a/src/main/java/com/magamochi/model/dto/LanguageDTO.java b/src/main/java/com/magamochi/content/model/dto/LanguageDTO.java similarity index 92% rename from src/main/java/com/magamochi/model/dto/LanguageDTO.java rename to src/main/java/com/magamochi/content/model/dto/LanguageDTO.java index 32c409d..3f70990 100644 --- a/src/main/java/com/magamochi/model/dto/LanguageDTO.java +++ b/src/main/java/com/magamochi/content/model/dto/LanguageDTO.java @@ -1,4 +1,4 @@ -package com.magamochi.model.dto; +package com.magamochi.content.model.dto; import static java.util.Objects.isNull; diff --git a/src/main/java/com/magamochi/content/model/dto/MangaContentDTO.java b/src/main/java/com/magamochi/content/model/dto/MangaContentDTO.java new file mode 100644 index 0000000..86a52ca --- /dev/null +++ b/src/main/java/com/magamochi/content/model/dto/MangaContentDTO.java @@ -0,0 +1,21 @@ +package com.magamochi.content.model.dto; + +import com.magamochi.content.model.entity.MangaContent; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record MangaContentDTO( + @NotNull Long id, + @NotBlank String title, + @NotNull Boolean downloaded, + @NotNull Boolean isRead, + LanguageDTO language) { + public static MangaContentDTO from(MangaContent mangaContent) { + return new MangaContentDTO( + mangaContent.getId(), + mangaContent.getTitle(), + mangaContent.getDownloaded(), + false, + LanguageDTO.from(mangaContent.getLanguage())); + } +} diff --git a/src/main/java/com/magamochi/model/entity/MangaChapter.java b/src/main/java/com/magamochi/content/model/entity/MangaContent.java similarity index 67% rename from src/main/java/com/magamochi/model/entity/MangaChapter.java rename to src/main/java/com/magamochi/content/model/entity/MangaContent.java index 1e6ed63..000683e 100644 --- a/src/main/java/com/magamochi/model/entity/MangaChapter.java +++ b/src/main/java/com/magamochi/content/model/entity/MangaContent.java @@ -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.MangaContentProvider; +import com.magamochi.common.model.enumeration.ContentType; import jakarta.persistence.*; import java.time.Instant; import java.util.List; @@ -9,13 +11,13 @@ import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; @Entity -@Table(name = "manga_chapters") +@Table(name = "manga_contents") @Builder @NoArgsConstructor @AllArgsConstructor @Getter @Setter -public class MangaChapter { +public class MangaContent { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -24,22 +26,20 @@ public class MangaChapter { @JoinColumn(name = "manga_content_provider_id") private MangaContentProvider mangaContentProvider; + @Builder.Default private ContentType type = ContentType.CHAPTER; + private String title; private String url; @Builder.Default private Boolean downloaded = false; - @Builder.Default private Boolean read = false; - @CreationTimestamp private Instant createdAt; @UpdateTimestamp private Instant updatedAt; - @OneToMany(mappedBy = "mangaChapter") - private List mangaChapterImages; - - private Integer chapterNumber; + @OneToMany(mappedBy = "mangaContent") + private List mangaContentImages; @ManyToOne @JoinColumn(name = "language_id") diff --git a/src/main/java/com/magamochi/model/entity/MangaChapterImage.java b/src/main/java/com/magamochi/content/model/entity/MangaContentImage.java similarity index 75% rename from src/main/java/com/magamochi/model/entity/MangaChapterImage.java rename to src/main/java/com/magamochi/content/model/entity/MangaContentImage.java index 33cf78b..f0fc463 100644 --- a/src/main/java/com/magamochi/model/entity/MangaChapterImage.java +++ b/src/main/java/com/magamochi/content/model/entity/MangaContentImage.java @@ -1,4 +1,4 @@ -package com.magamochi.model.entity; +package com.magamochi.content.model.entity; import com.magamochi.image.model.entity.Image; import jakarta.persistence.*; @@ -8,20 +8,20 @@ import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; @Entity -@Table(name = "manga_chapter_images") +@Table(name = "manga_content_images") @Builder @NoArgsConstructor @AllArgsConstructor @Getter @Setter -public class MangaChapterImage { +public class MangaContentImage { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne - @JoinColumn(name = "manga_chapter_id") - private MangaChapter mangaChapter; + @JoinColumn(name = "manga_content_id") + private MangaContent mangaContent; @OneToOne @JoinColumn(name = "image_id") diff --git a/src/main/java/com/magamochi/content/model/repository/MangaChapterImageRepository.java b/src/main/java/com/magamochi/content/model/repository/MangaChapterImageRepository.java new file mode 100644 index 0000000..a5b4659 --- /dev/null +++ b/src/main/java/com/magamochi/content/model/repository/MangaChapterImageRepository.java @@ -0,0 +1,10 @@ +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 MangaChapterImageRepository extends JpaRepository { + List findAllByMangaContent(MangaContent mangaContent); +} diff --git a/src/main/java/com/magamochi/content/model/repository/MangaContentRepository.java b/src/main/java/com/magamochi/content/model/repository/MangaContentRepository.java new file mode 100644 index 0000000..2aadd67 --- /dev/null +++ b/src/main/java/com/magamochi/content/model/repository/MangaContentRepository.java @@ -0,0 +1,8 @@ +package com.magamochi.content.model.repository; + +import com.magamochi.content.model.entity.MangaContent; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MangaContentRepository extends JpaRepository { + boolean existsByMangaContentProvider_IdAndUrlIgnoreCase(Long mangaContentProviderId, String url); +} diff --git a/src/main/java/com/magamochi/content/queue/consumer/MangaContentIngestConsumer.java b/src/main/java/com/magamochi/content/queue/consumer/MangaContentIngestConsumer.java new file mode 100644 index 0000000..17df5bd --- /dev/null +++ b/src/main/java/com/magamochi/content/queue/consumer/MangaContentIngestConsumer.java @@ -0,0 +1,22 @@ +package com.magamochi.content.queue.consumer; + +import com.magamochi.common.queue.command.MangaContentIngestCommand; +import com.magamochi.content.service.ContentIngestService; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Service; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class MangaContentIngestConsumer { + private final ContentIngestService contentIngestService; + + @RabbitListener(queues = "${queues.manga-content-ingest}") + public void receiveMangaContentIngestCommand(MangaContentIngestCommand command) { + log.info("Received manga content ingest command: {}", command); + contentIngestService.ingest( + command.mangaContentProviderId(), command.title(), command.url(), command.languageCode()); + } +} diff --git a/src/main/java/com/magamochi/content/service/ContentIngestService.java b/src/main/java/com/magamochi/content/service/ContentIngestService.java new file mode 100644 index 0000000..06c6da1 --- /dev/null +++ b/src/main/java/com/magamochi/content/service/ContentIngestService.java @@ -0,0 +1,56 @@ +package com.magamochi.content.service; + +import com.magamochi.catalog.service.LanguageService; +import com.magamochi.catalog.service.MangaContentProviderService; +import com.magamochi.content.model.entity.MangaContent; +import com.magamochi.content.model.repository.MangaContentRepository; +import jakarta.validation.constraints.NotBlank; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class ContentIngestService { + private final MangaContentProviderService mangaContentProviderService; + private final LanguageService languageService; + + private final MangaContentRepository mangaContentRepository; + + 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()); + } +} diff --git a/src/main/java/com/magamochi/content/service/ContentService.java b/src/main/java/com/magamochi/content/service/ContentService.java new file mode 100644 index 0000000..38c2266 --- /dev/null +++ b/src/main/java/com/magamochi/content/service/ContentService.java @@ -0,0 +1,35 @@ +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 jakarta.validation.constraints.NotNull; +import java.util.Comparator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ContentService { + private final MangaContentProviderService mangaContentProviderService; + + private final MangaContentRepository mangaContentRepository; + + public List getContent(@NotNull Long mangaContentProviderId) { + var mangaContentProvider = mangaContentProviderService.find(mangaContentProviderId); + + return mangaContentProvider.getMangaContents().stream() + .sorted(Comparator.comparing(MangaContent::getId)) + .map(MangaContentDTO::from) + .toList(); + } + + public MangaContent find(Long id) { + return mangaContentRepository + .findById(id) + .orElseThrow(() -> new NotFoundException("MangaContent not found for ID: " + id)); + } +} diff --git a/src/main/java/com/magamochi/controller/MangaChapterController.java b/src/main/java/com/magamochi/controller/MangaChapterController.java index 1635b2d..cdcb7cc 100644 --- a/src/main/java/com/magamochi/controller/MangaChapterController.java +++ b/src/main/java/com/magamochi/controller/MangaChapterController.java @@ -21,18 +21,6 @@ import org.springframework.web.bind.annotation.*; public class MangaChapterController { private final MangaChapterService mangaChapterService; - @Operation( - summary = "Fetch chapter", - description = "Fetch the chapter from the provider", - tags = {"Manga Chapter"}, - operationId = "fetchChapter") - @PostMapping(value = "/{chapterId}/fetch") - public DefaultResponseDTO fetchChapter(@PathVariable Long chapterId) { - mangaChapterService.fetchChapter(chapterId); - - return DefaultResponseDTO.ok().build(); - } - @Operation( summary = "Get the images for a specific manga/provider combination", description = diff --git a/src/main/java/com/magamochi/controller/MangaController.java b/src/main/java/com/magamochi/controller/MangaController.java index 3ffdd3d..5c17b85 100644 --- a/src/main/java/com/magamochi/controller/MangaController.java +++ b/src/main/java/com/magamochi/controller/MangaController.java @@ -1,10 +1,8 @@ package com.magamochi.controller; import com.magamochi.common.model.dto.DefaultResponseDTO; -import com.magamochi.model.dto.MangaChapterDTO; import com.magamochi.service.OldMangaService; import io.swagger.v3.oas.annotations.Operation; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -14,17 +12,6 @@ import org.springframework.web.bind.annotation.*; public class MangaController { 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> getMangaChapters( - @PathVariable Long mangaProviderId) { - return DefaultResponseDTO.ok(oldMangaService.getMangaChapters(mangaProviderId)); - } - @Operation( summary = "Fetch all chapters", description = "Fetch all not yet downloaded chapters from the provider", @@ -37,18 +24,6 @@ public class MangaController { return DefaultResponseDTO.ok().build(); } - @Operation( - summary = "Fetch the available chapters for a specific manga/provider combination", - description = "Fetch a list of manga chapters for a specific manga/provider combination.", - tags = {"Manga"}, - operationId = "fetchMangaChapters") - @PostMapping("/{mangaProviderId}/fetch-chapters") - public DefaultResponseDTO fetchMangaChapters(@PathVariable Long mangaProviderId) { - oldMangaService.fetchMangaChapters(mangaProviderId); - - return DefaultResponseDTO.ok().build(); - } - @Operation( summary = "Follow the manga specified by its ID", description = "Follow the manga specified by its ID.", diff --git a/src/main/java/com/magamochi/ingestion/controller/IngestionController.java b/src/main/java/com/magamochi/ingestion/controller/IngestionController.java index 7b46f07..f8304bf 100644 --- a/src/main/java/com/magamochi/ingestion/controller/IngestionController.java +++ b/src/main/java/com/magamochi/ingestion/controller/IngestionController.java @@ -51,4 +51,30 @@ public class IngestionController { return DefaultResponseDTO.ok().build(); } + + @Operation( + summary = "Fetch content list from a content provider", + description = + "Triggers the ingestion process for a specific content provider, fetching content list and queuing it for processing.", + tags = {"Ingestion"}, + operationId = "fetchContentProviderContentList") + @PostMapping("/manga-content-providers/{mangaContentProviderId}/fetch") + public DefaultResponseDTO fetchContentProviderContentList( + @PathVariable Long mangaContentProviderId) { + ingestionService.fetchMangaContentProviderContentList(mangaContentProviderId); + + return DefaultResponseDTO.ok().build(); + } + + @Operation( + summary = "Fetch content from a content provider", + description = "Fetch the content (images) from the content provider", + tags = {"Ingestion"}, + operationId = "fetchContentProviderContent") + @PostMapping(value = "/manga-content/{mangaContentId}/fetch") + public DefaultResponseDTO fetchContentProviderContent(@PathVariable Long mangaContentId) { + ingestionService.fetchContent(mangaContentId); + + return DefaultResponseDTO.ok().build(); + } } diff --git a/src/main/java/com/magamochi/ingestion/model/dto/ContentImageInfoDTO.java b/src/main/java/com/magamochi/ingestion/model/dto/ContentImageInfoDTO.java new file mode 100644 index 0000000..33ea729 --- /dev/null +++ b/src/main/java/com/magamochi/ingestion/model/dto/ContentImageInfoDTO.java @@ -0,0 +1,5 @@ +package com.magamochi.ingestion.model.dto; + +import jakarta.validation.constraints.NotBlank; + +public record ContentImageInfoDTO(int position, @NotBlank String url) {} diff --git a/src/main/java/com/magamochi/ingestion/model/dto/ContentInfoDTO.java b/src/main/java/com/magamochi/ingestion/model/dto/ContentInfoDTO.java new file mode 100644 index 0000000..d967cbf --- /dev/null +++ b/src/main/java/com/magamochi/ingestion/model/dto/ContentInfoDTO.java @@ -0,0 +1,6 @@ +package com.magamochi.ingestion.model.dto; + +import jakarta.validation.constraints.NotBlank; + +public record ContentInfoDTO( + @NotBlank String title, @NotBlank String url, @NotBlank String languageCode) {} diff --git a/src/main/java/com/magamochi/ingestion/model/entity/ContentProvider.java b/src/main/java/com/magamochi/ingestion/model/entity/ContentProvider.java index 85baedf..26ab447 100644 --- a/src/main/java/com/magamochi/ingestion/model/entity/ContentProvider.java +++ b/src/main/java/com/magamochi/ingestion/model/entity/ContentProvider.java @@ -1,6 +1,6 @@ package com.magamochi.ingestion.model.entity; -import com.magamochi.model.entity.MangaContentProvider; +import com.magamochi.catalog.model.entity.MangaContentProvider; import jakarta.persistence.*; import java.time.Instant; import java.util.List; @@ -24,7 +24,7 @@ public class ContentProvider { private boolean active; - private Boolean supportsChapterFetch; + private Boolean supportsContentFetch; private Boolean manualImport; diff --git a/src/main/java/com/magamochi/ingestion/model/repository/ContentProviderRepository.java b/src/main/java/com/magamochi/ingestion/model/repository/ContentProviderRepository.java index 8232dec..8f1e612 100644 --- a/src/main/java/com/magamochi/ingestion/model/repository/ContentProviderRepository.java +++ b/src/main/java/com/magamochi/ingestion/model/repository/ContentProviderRepository.java @@ -1,9 +1,6 @@ package com.magamochi.ingestion.model.repository; import com.magamochi.ingestion.model.entity.ContentProvider; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -public interface ContentProviderRepository extends JpaRepository { - Optional findByNameIgnoreCase(String name); -} +public interface ContentProviderRepository extends JpaRepository {} diff --git a/src/main/java/com/magamochi/ingestion/providers/ContentProvider.java b/src/main/java/com/magamochi/ingestion/providers/ContentProvider.java index 17eb162..aec1b8d 100644 --- a/src/main/java/com/magamochi/ingestion/providers/ContentProvider.java +++ b/src/main/java/com/magamochi/ingestion/providers/ContentProvider.java @@ -1,12 +1,12 @@ package com.magamochi.ingestion.providers; -import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO; -import com.magamochi.model.entity.MangaContentProvider; +import com.magamochi.catalog.model.entity.MangaContentProvider; +import com.magamochi.ingestion.model.dto.ContentImageInfoDTO; +import com.magamochi.ingestion.model.dto.ContentInfoDTO; import java.util.List; -import java.util.Map; public interface ContentProvider { - List getAvailableChapters(MangaContentProvider provider); + List getAvailableChapters(MangaContentProvider provider); - Map getChapterImagesUrls(String chapterUrl); + List getContentImages(String chapterUrl); } diff --git a/src/main/java/com/magamochi/ingestion/providers/impl/MangaDexProvider.java b/src/main/java/com/magamochi/ingestion/providers/impl/MangaDexProvider.java index 5a46e81..bdc7765 100644 --- a/src/main/java/com/magamochi/ingestion/providers/impl/MangaDexProvider.java +++ b/src/main/java/com/magamochi/ingestion/providers/impl/MangaDexProvider.java @@ -3,15 +3,15 @@ package com.magamochi.ingestion.providers.impl; import static java.util.Objects.isNull; import com.google.common.util.concurrent.RateLimiter; +import com.magamochi.catalog.model.entity.MangaContentProvider; import com.magamochi.client.MangaDexClient; import com.magamochi.common.exception.UnprocessableException; +import com.magamochi.ingestion.model.dto.ContentImageInfoDTO; +import com.magamochi.ingestion.model.dto.ContentInfoDTO; import com.magamochi.ingestion.providers.ContentProvider; import com.magamochi.ingestion.providers.ContentProviders; import com.magamochi.ingestion.providers.ManualImportContentProvider; -import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO; -import com.magamochi.model.entity.MangaContentProvider; import java.util.*; -import java.util.stream.Collectors; import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -26,8 +26,7 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro private final RateLimiter mangaDexRateLimiter; @Override - public List getAvailableChapters( - MangaContentProvider provider) { + public List getAvailableChapters(MangaContentProvider provider) { try { mangaDexRateLimiter.acquire(); var response = @@ -85,10 +84,9 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro }) .map( c -> - new ContentProviderMangaChapterResponseDTO( + new ContentInfoDTO( c.attributes().chapter() + " - " + c.attributes().title(), c.id().toString(), - c.attributes().chapter(), languagesToImport.get(c.attributes().translatedLanguage()))) .toList(); } catch (Exception e) { @@ -98,7 +96,7 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro } @Override - public Map getChapterImagesUrls(String chapterUrl) { + public List getContentImages(String chapterUrl) { mangaDexRateLimiter.acquire(); var chapter = mangaDexClient.getMangaChapter(UUID.fromString(chapterUrl)); @@ -109,12 +107,8 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro return IntStream.range(0, chapterImageHashes.size()) .boxed() - .collect( - Collectors.toMap( - i -> i, - chapterImageHashes::get, - (existing, replacement) -> existing, - LinkedHashMap::new)); + .map(position -> new ContentImageInfoDTO(position, chapterImageHashes.get(position))) + .toList(); } @Override diff --git a/src/main/java/com/magamochi/ingestion/providers/impl/MangaLivreBlogProvider.java b/src/main/java/com/magamochi/ingestion/providers/impl/MangaLivreBlogProvider.java index 431b480..3c566de 100644 --- a/src/main/java/com/magamochi/ingestion/providers/impl/MangaLivreBlogProvider.java +++ b/src/main/java/com/magamochi/ingestion/providers/impl/MangaLivreBlogProvider.java @@ -1,15 +1,15 @@ 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.providers.ContentProvider; import com.magamochi.ingestion.providers.ContentProviders; import com.magamochi.ingestion.providers.PagedContentProvider; -import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO; -import com.magamochi.model.entity.MangaContentProvider; import java.io.IOException; import java.util.*; import java.util.regex.Pattern; -import java.util.stream.Collectors; import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -27,8 +27,7 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv private final String url = "https://mangalivre.blog/manga/"; @Override - public List getAvailableChapters( - MangaContentProvider mangaContentProvider) { + public List getAvailableChapters(MangaContentProvider mangaContentProvider) { log.info( "Getting available chapters from {}, manga {}", ContentProviders.MANGA_LIVRE_BLOG, @@ -49,8 +48,8 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv var chapterNumberElement = linkElement.getElementsByClass("chapter-number").getFirst(); - return new ContentProviderMangaChapterResponseDTO( - chapterNumberElement.text(), linkElement.attr("href"), null, "pt-BR"); + return new ContentInfoDTO( + chapterNumberElement.text(), linkElement.attr("href"), "pt-BR"); }) .toList(); } catch (IOException | NoSuchElementException e) { @@ -60,7 +59,7 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv } @Override - public Map getChapterImagesUrls(String chapterUrl) { + public List getContentImages(String chapterUrl) { log.info("Getting images from {}, url {}", ContentProviders.MANGA_LIVRE_BLOG, chapterUrl); try { @@ -90,12 +89,11 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv return IntStream.range(0, imageUrls.size()) .boxed() - .collect( - Collectors.toMap( - i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new)); + .map(position -> new ContentImageInfoDTO(position, imageUrls.get(position))) + .toList(); } catch (IOException | NoSuchElementException e) { log.error("Error fetching mangas from MangaLivre", e); - return Map.of(); + return List.of(); } } diff --git a/src/main/java/com/magamochi/ingestion/providers/impl/MangaLivreProvider.java b/src/main/java/com/magamochi/ingestion/providers/impl/MangaLivreProvider.java index 5f0be16..3ec9263 100644 --- a/src/main/java/com/magamochi/ingestion/providers/impl/MangaLivreProvider.java +++ b/src/main/java/com/magamochi/ingestion/providers/impl/MangaLivreProvider.java @@ -2,15 +2,15 @@ package com.magamochi.ingestion.providers.impl; 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.providers.ContentProvider; import com.magamochi.ingestion.providers.ContentProviders; import com.magamochi.ingestion.providers.PagedContentProvider; import com.magamochi.ingestion.service.FlareService; -import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO; -import com.magamochi.model.entity.MangaContentProvider; import java.util.*; -import java.util.stream.Collectors; import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -26,8 +26,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider private final FlareService flareService; @Override - public List getAvailableChapters( - MangaContentProvider provider) { + public List getAvailableChapters(MangaContentProvider provider) { log.info( "Getting available chapters from {}, manga {}", ContentProviders.MANGA_LIVRE_TO, @@ -49,8 +48,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider var url = linkElement.attr("href"); var title = linkElement.text(); - return new ContentProviderMangaChapterResponseDTO( - title.trim(), url.trim(), null, "pt-BR"); + return new ContentInfoDTO(title.trim(), url.trim(), "pt-BR"); }) .toList(); } catch (NoSuchElementException e) { @@ -60,7 +58,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider } @Override - public Map getChapterImagesUrls(String chapterUrl) { + public List getContentImages(String chapterUrl) { log.info("Getting images from {}, url {}", ContentProviders.MANGA_LIVRE_TO, chapterUrl); try { @@ -79,12 +77,11 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider return IntStream.range(0, imageUrls.size()) .boxed() - .collect( - Collectors.toMap( - i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new)); + .map(position -> new ContentImageInfoDTO(position, imageUrls.get(position))) + .toList(); } catch (NoSuchElementException e) { log.error("Error parsing manga images from MangaLivre", e); - return Map.of(); + return List.of(); } } diff --git a/src/main/java/com/magamochi/ingestion/providers/impl/PinkRosaScanProvider.java b/src/main/java/com/magamochi/ingestion/providers/impl/PinkRosaScanProvider.java index d631095..2e4ef56 100644 --- a/src/main/java/com/magamochi/ingestion/providers/impl/PinkRosaScanProvider.java +++ b/src/main/java/com/magamochi/ingestion/providers/impl/PinkRosaScanProvider.java @@ -3,15 +3,15 @@ package com.magamochi.ingestion.providers.impl; import static java.util.Objects.isNull; 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.providers.ContentProvider; import com.magamochi.ingestion.providers.ContentProviders; import com.magamochi.ingestion.providers.PagedContentProvider; import com.magamochi.ingestion.service.FlareService; -import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO; -import com.magamochi.model.entity.MangaContentProvider; import java.util.*; -import java.util.stream.Collectors; import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -27,8 +27,7 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid private final FlareService flareService; @Override - public List getAvailableChapters( - MangaContentProvider provider) { + public List getAvailableChapters(MangaContentProvider provider) { log.info( "Getting available chapters from {}, manga {}", ContentProviders.PINK_ROSA_SCAN, @@ -57,11 +56,8 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid .getFirst() .getElementsByClass("text-sm truncate") .getFirst(); - return new ContentProviderMangaChapterResponseDTO( - chapterTitleElement.text().trim(), - chapterItemElement.attr("href"), - null, - "pt-BR"); + return new ContentInfoDTO( + chapterTitleElement.text().trim(), chapterItemElement.attr("href"), "pt-BR"); }) .toList(); } catch (NoSuchElementException e) { @@ -71,7 +67,7 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid } @Override - public Map getChapterImagesUrls(String chapterUrl) { + public List getContentImages(String chapterUrl) { log.info("Getting images from {}, url {}", ContentProviders.PINK_ROSA_SCAN, chapterUrl); try { @@ -99,12 +95,11 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid return IntStream.range(0, imageUrls.size()) .boxed() - .collect( - Collectors.toMap( - i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new)); + .map(position -> new ContentImageInfoDTO(position, imageUrls.get(position))) + .toList(); } catch (NoSuchElementException e) { log.error("Error parsing mangas from Pink Rosa Scan", e); - return Map.of(); + return List.of(); } } diff --git a/src/main/java/com/magamochi/ingestion/queue/producer/MangaContentImageIngestProducer.java b/src/main/java/com/magamochi/ingestion/queue/producer/MangaContentImageIngestProducer.java new file mode 100644 index 0000000..38d16df --- /dev/null +++ b/src/main/java/com/magamochi/ingestion/queue/producer/MangaContentImageIngestProducer.java @@ -0,0 +1,23 @@ +package com.magamochi.ingestion.queue.producer; + +import com.magamochi.common.queue.command.MangaContentImageIngestCommand; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class MangaContentImageIngestProducer { + private final RabbitTemplate rabbitTemplate; + + @Value("${queues.manga-content-image-ingest}") + private String mangaContentImageIngestQueue; + + public void sendMangaContentImageIngestCommand(MangaContentImageIngestCommand command) { + rabbitTemplate.convertAndSend(mangaContentImageIngestQueue, command); + log.info("Sent manga content image ingest command: {}", command); + } +} diff --git a/src/main/java/com/magamochi/ingestion/queue/producer/MangaContentIngestProducer.java b/src/main/java/com/magamochi/ingestion/queue/producer/MangaContentIngestProducer.java new file mode 100644 index 0000000..68702c8 --- /dev/null +++ b/src/main/java/com/magamochi/ingestion/queue/producer/MangaContentIngestProducer.java @@ -0,0 +1,23 @@ +package com.magamochi.ingestion.queue.producer; + +import com.magamochi.common.queue.command.MangaContentIngestCommand; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class MangaContentIngestProducer { + private final RabbitTemplate rabbitTemplate; + + @Value("${queues.manga-content-ingest}") + private String mangaContentIngestQueue; + + public void sendMangaContentIngestCommand(MangaContentIngestCommand command) { + rabbitTemplate.convertAndSend(mangaContentIngestQueue, command); + log.info("Sent manga content ingest command: {}", command); + } +} diff --git a/src/main/java/com/magamochi/ingestion/service/IngestionService.java b/src/main/java/com/magamochi/ingestion/service/IngestionService.java index fed47cf..0467358 100644 --- a/src/main/java/com/magamochi/ingestion/service/IngestionService.java +++ b/src/main/java/com/magamochi/ingestion/service/IngestionService.java @@ -1,8 +1,15 @@ package com.magamochi.ingestion.service; +import com.magamochi.catalog.service.MangaContentProviderService; +import com.magamochi.common.queue.command.MangaContentImageIngestCommand; +import com.magamochi.common.queue.command.MangaContentIngestCommand; import com.magamochi.common.queue.command.MangaIngestCommand; +import com.magamochi.content.service.ContentService; +import com.magamochi.ingestion.providers.ContentProviderFactory; import com.magamochi.ingestion.providers.PagedContentProviderFactory; import com.magamochi.ingestion.queue.command.ProviderPageIngestCommand; +import com.magamochi.ingestion.queue.producer.MangaContentImageIngestProducer; +import com.magamochi.ingestion.queue.producer.MangaContentIngestProducer; import com.magamochi.ingestion.queue.producer.MangaIngestProducer; import com.magamochi.ingestion.queue.producer.ProviderPageIngestProducer; import java.util.stream.IntStream; @@ -13,10 +20,16 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor public class IngestionService { private final ContentProviderService contentProviderService; + private final ContentService contentService; + private final MangaContentProviderService mangaContentProviderService; + + private final ContentProviderFactory contentProviderFactory; private final PagedContentProviderFactory pagedContentProviderFactory; private final ProviderPageIngestProducer providerPageIngestProducer; private final MangaIngestProducer mangaIngestProducer; + private final MangaContentIngestProducer mangaContentIngestProducer; + private final MangaContentImageIngestProducer mangaContentImageIngestProducer; public void fetchContentProviderMangas(long contentProviderId) { var contentProvider = contentProviderService.find(contentProviderId); @@ -48,4 +61,127 @@ public class IngestionService { var contentProviders = contentProviderService.getProviders(null); contentProviders.providers().forEach(dto -> fetchContentProviderMangas(dto.id())); } + + public void fetchMangaContentProviderContentList(Long mangaContentProviderId) { + var mangaContentProvider = mangaContentProviderService.find(mangaContentProviderId); + + var contentProvider = + contentProviderFactory.getContentProvider( + mangaContentProvider.getContentProvider().getName()); + + var availableChapters = contentProvider.getAvailableChapters(mangaContentProvider); + + availableChapters.forEach( + content -> + mangaContentIngestProducer.sendMangaContentIngestCommand( + new MangaContentIngestCommand( + mangaContentProvider.getId(), + content.title(), + content.url(), + content.languageCode()))); + } + + public void fetchContent(Long mangaContentId) { + var mangaContent = contentService.find(mangaContentId); + var mangaContentProvider = mangaContent.getMangaContentProvider(); + + var contentProvider = + contentProviderFactory.getContentProvider( + mangaContentProvider.getContentProvider().getName()); + + var chapterImagesUrl = contentProvider.getContentImages(mangaContent.getUrl()); + + IntStream.range(0, chapterImagesUrl.size()) + .forEach( + i -> { + var item = chapterImagesUrl.get(i); + + var isLast = i == chapterImagesUrl.size() - 1; + + mangaContentImageIngestProducer.sendMangaContentImageIngestCommand( + new MangaContentImageIngestCommand( + mangaContent.getId(), item.url(), item.position(), isLast)); + }); + } + + // @Transactional + // public void fetchChapter(Long 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 MangaContentImage.builder() + // .mangaContent(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); + // mangaContentRepository.save(chapter); + // } } diff --git a/src/main/java/com/magamochi/model/dto/ContentProviderMangaChapterResponseDTO.java b/src/main/java/com/magamochi/model/dto/ContentProviderMangaChapterResponseDTO.java deleted file mode 100644 index 0ea052c..0000000 --- a/src/main/java/com/magamochi/model/dto/ContentProviderMangaChapterResponseDTO.java +++ /dev/null @@ -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) {} diff --git a/src/main/java/com/magamochi/model/dto/MangaChapterDTO.java b/src/main/java/com/magamochi/model/dto/MangaChapterDTO.java deleted file mode 100644 index 9e008b1..0000000 --- a/src/main/java/com/magamochi/model/dto/MangaChapterDTO.java +++ /dev/null @@ -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())); - } -} diff --git a/src/main/java/com/magamochi/model/dto/MangaChapterImagesDTO.java b/src/main/java/com/magamochi/model/dto/MangaChapterImagesDTO.java index ea6bcce..cbb9cfa 100644 --- a/src/main/java/com/magamochi/model/dto/MangaChapterImagesDTO.java +++ b/src/main/java/com/magamochi/model/dto/MangaChapterImagesDTO.java @@ -1,7 +1,7 @@ package com.magamochi.model.dto; -import com.magamochi.model.entity.MangaChapter; -import com.magamochi.model.entity.MangaChapterImage; +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; @@ -13,14 +13,14 @@ public record MangaChapterImagesDTO( Long previousChapterId, Long nextChapterId, @NotNull List<@NotBlank String> chapterImageKeys) { - public static MangaChapterImagesDTO from(MangaChapter mangaChapter, Long prevId, Long nextId) { + public static MangaChapterImagesDTO from(MangaContent mangaContent, Long prevId, Long nextId) { return new MangaChapterImagesDTO( - mangaChapter.getId(), - mangaChapter.getTitle(), + mangaContent.getId(), + mangaContent.getTitle(), prevId, nextId, - mangaChapter.getMangaChapterImages().stream() - .sorted(Comparator.comparing(MangaChapterImage::getPosition)) + mangaContent.getMangaContentImages().stream() + .sorted(Comparator.comparing(MangaContentImage::getPosition)) .map(mangaChapterImage -> mangaChapterImage.getImage().getObjectKey()) .toList()); } diff --git a/src/main/java/com/magamochi/model/repository/MangaChapterImageRepository.java b/src/main/java/com/magamochi/model/repository/MangaChapterImageRepository.java deleted file mode 100644 index 0dd8574..0000000 --- a/src/main/java/com/magamochi/model/repository/MangaChapterImageRepository.java +++ /dev/null @@ -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 { - List findAllByMangaChapter(MangaChapter mangaChapter); -} diff --git a/src/main/java/com/magamochi/model/repository/MangaChapterRepository.java b/src/main/java/com/magamochi/model/repository/MangaChapterRepository.java deleted file mode 100644 index 3b1b9c2..0000000 --- a/src/main/java/com/magamochi/model/repository/MangaChapterRepository.java +++ /dev/null @@ -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 { - Optional findByMangaContentProviderAndUrlIgnoreCase( - MangaContentProvider mangaContentProvider, @NotBlank String url); - - List findByMangaContentProviderId(Long mangaProvider_id); -} diff --git a/src/main/java/com/magamochi/queue/MangaChapterDownloadConsumer.java b/src/main/java/com/magamochi/queue/MangaChapterDownloadConsumer.java deleted file mode 100644 index 43da465..0000000 --- a/src/main/java/com/magamochi/queue/MangaChapterDownloadConsumer.java +++ /dev/null @@ -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()); - } - } -} diff --git a/src/main/java/com/magamochi/queue/UpdateMangaFollowChapterListConsumer.java b/src/main/java/com/magamochi/queue/UpdateMangaFollowChapterListConsumer.java index d6d9168..75efa22 100644 --- a/src/main/java/com/magamochi/queue/UpdateMangaFollowChapterListConsumer.java +++ b/src/main/java/com/magamochi/queue/UpdateMangaFollowChapterListConsumer.java @@ -16,6 +16,6 @@ public class UpdateMangaFollowChapterListConsumer { @RabbitListener(queues = "${rabbit-mq.queues.manga-follow-update-chapter}") public void receiveMangaFollowUpdateChapterCommand(UpdateMangaFollowChapterListCommand command) { log.info("Received update followed manga chapter list command: {}", command); - oldMangaService.fetchFollowedMangaChapters(command.mangaProviderId()); + // oldMangaService.fetchFollowedMangaChapters(command.mangaProviderId()); } } diff --git a/src/main/java/com/magamochi/service/MangaChapterService.java b/src/main/java/com/magamochi/service/MangaChapterService.java index ccd1209..2baba74 100644 --- a/src/main/java/com/magamochi/service/MangaChapterService.java +++ b/src/main/java/com/magamochi/service/MangaChapterService.java @@ -2,22 +2,18 @@ package com.magamochi.service; import com.google.common.util.concurrent.RateLimiter; import com.magamochi.common.exception.UnprocessableException; +import com.magamochi.content.model.entity.MangaContent; +import com.magamochi.content.model.entity.MangaContentImage; +import com.magamochi.content.model.repository.MangaChapterImageRepository; +import com.magamochi.content.model.repository.MangaContentRepository; import com.magamochi.ingestion.providers.ContentProviderFactory; 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.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.ByteArrayOutputStream; import java.io.IOException; -import java.net.URI; -import java.net.URL; import java.util.Comparator; import java.util.List; import java.util.zip.ZipEntry; @@ -26,13 +22,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.apache.tomcat.util.http.fileupload.IOUtils; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; @Log4j2 @Service @RequiredArgsConstructor public class MangaChapterService { - private final MangaChapterRepository mangaChapterRepository; + private final MangaContentRepository mangaContentRepository; private final MangaChapterImageRepository mangaChapterImageRepository; private final OldImageService oldImageService; @@ -42,94 +37,12 @@ public class MangaChapterService { 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)) + chapter.getMangaContentProvider().getMangaContents().stream() + .sorted(Comparator.comparing(MangaContent::getId)) .toList(); Long prevId = null; Long nextId = null; @@ -153,17 +66,18 @@ public class MangaChapterService { } public void markAsRead(Long chapterId) { - var chapter = getMangaChapterThrowIfNotFound(chapterId); - chapter.setRead(true); - - mangaChapterRepository.save(chapter); + // TODO: implement this + // var chapter = getMangaChapterThrowIfNotFound(chapterId); + // chapter.setRead(true); + // + // mangaChapterRepository.save(chapter); } public MangaChapterArchiveDTO downloadChapter(Long chapterId, ArchiveFileType archiveFileType) throws IOException { var chapter = getMangaChapterThrowIfNotFound(chapterId); - var chapterImages = mangaChapterImageRepository.findAllByMangaChapter(chapter); + var chapterImages = mangaChapterImageRepository.findAllByMangaContent(chapter); var byteArrayOutputStream = switch (archiveFileType) { @@ -177,7 +91,7 @@ public class MangaChapterService { chapter.getTitle() + ".cbz", byteArrayOutputStream.toByteArray()); } - private ByteArrayOutputStream getChapterCbzArchive(List chapterImages) + private ByteArrayOutputStream getChapterCbzArchive(List chapterImages) throws IOException { var byteArrayOutputStream = new ByteArrayOutputStream(); var bufferedOutputStream = new BufferedOutputStream(byteArrayOutputStream); @@ -202,8 +116,8 @@ public class MangaChapterService { return byteArrayOutputStream; } - private MangaChapter getMangaChapterThrowIfNotFound(Long chapterId) { - return mangaChapterRepository + private MangaContent getMangaChapterThrowIfNotFound(Long chapterId) { + return mangaContentRepository .findById(chapterId) .orElseThrow(() -> new RuntimeException("Manga Chapter not found for ID: " + chapterId)); } diff --git a/src/main/java/com/magamochi/service/MangaImportService.java b/src/main/java/com/magamochi/service/MangaImportService.java index ea64e30..510f9c2 100644 --- a/src/main/java/com/magamochi/service/MangaImportService.java +++ b/src/main/java/com/magamochi/service/MangaImportService.java @@ -378,12 +378,12 @@ // var mangaChapter = // mangaChapterRepository // .findByMangaContentProviderAndUrlIgnoreCase(mangaContentProvider, -// chapter.chapterUrl()) +// chapter.url()) // .orElseGet(MangaChapter::new); // // mangaChapter.setMangaContentProvider(mangaContentProvider); -// mangaChapter.setTitle(chapter.chapterTitle()); -// mangaChapter.setUrl(chapter.chapterUrl()); +// mangaChapter.setTitle(chapter.title()); +// mangaChapter.setUrl(chapter.url()); // // var language = languageService.getOrThrow(chapter.languageCode()); // mangaChapter.setLanguage(language); diff --git a/src/main/java/com/magamochi/service/OldMangaService.java b/src/main/java/com/magamochi/service/OldMangaService.java index b0de84c..0ddd4cf 100644 --- a/src/main/java/com/magamochi/service/OldMangaService.java +++ b/src/main/java/com/magamochi/service/OldMangaService.java @@ -1,20 +1,19 @@ package com.magamochi.service; import com.magamochi.catalog.model.entity.Manga; +import com.magamochi.catalog.model.entity.MangaContentProvider; import com.magamochi.catalog.model.repository.MangaContentProviderRepository; import com.magamochi.catalog.model.repository.MangaRepository; import com.magamochi.client.NtfyClient; import com.magamochi.common.exception.NotFoundException; +import com.magamochi.content.model.entity.MangaContent; +import com.magamochi.content.model.repository.MangaContentRepository; import com.magamochi.ingestion.providers.ContentProviderFactory; 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.repository.*; import com.magamochi.queue.MangaChapterDownloadProducer; import com.magamochi.user.service.UserService; -import java.util.Comparator; -import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -25,7 +24,6 @@ import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class OldMangaService { - // private final MangaImportService mangaImportService; private final UserService userService; private final MangaRepository mangaRepository; private final MangaContentProviderRepository mangaContentProviderRepository; @@ -35,7 +33,7 @@ public class OldMangaService { private final UserMangaFollowRepository userMangaFollowRepository; private final MangaChapterDownloadProducer mangaChapterDownloadProducer; - private final MangaChapterRepository mangaChapterRepository; + private final MangaContentRepository mangaContentRepository; private final NtfyClient ntfyClient; @@ -47,9 +45,9 @@ public class OldMangaService { () -> new NotFoundException("Manga Provider not found for ID: " + mangaProviderId)); var chapterIds = - mangaProvider.getMangaChapters().stream() + mangaProvider.getMangaContents().stream() .filter(mangaChapter -> !mangaChapter.getDownloaded()) - .map(MangaChapter::getId) + .map(MangaContent::getId) .collect(Collectors.toSet()); chapterIds.forEach( @@ -58,57 +56,39 @@ public class OldMangaService { new MangaChapterDownloadCommand(chapterId))); } - public List getMangaChapters(Long mangaProviderId) { - var mangaProvider = getMangaProviderThrowIfNotFound(mangaProviderId); - - return mangaProvider.getMangaChapters().stream() - .sorted(Comparator.comparing(MangaChapter::getId)) - .map(MangaChapterDTO::from) - .toList(); - } - - public void fetchFollowedMangaChapters(Long mangaProviderId) { - var mangaProvider = - mangaContentProviderRepository - .findById(mangaProviderId) - .orElseThrow( - () -> new NotFoundException("Manga Provider not found for ID: " + mangaProviderId)); - - var currentAvailableChapterCount = - mangaChapterRepository.findByMangaContentProviderId(mangaProviderId).size(); - - fetchMangaChapters(mangaProviderId); - mangaChapterRepository.flush(); - - var availableChapterCount = - mangaChapterRepository.findByMangaContentProviderId(mangaProviderId).size(); - - if (availableChapterCount <= currentAvailableChapterCount) { - return; - } - - log.info("New chapters found for Manga Provider {}", mangaProviderId); - - var userMangaFollows = userMangaFollowRepository.findByManga(mangaProvider.getManga()); - userMangaFollows.forEach( - umf -> - ntfyClient.notify( - new NtfyClient.Request( - "mangamochi-" + umf.getUser().getId().toString(), - umf.getManga().getTitle(), - "New chapter available on " + mangaProvider.getContentProvider().getName()))); - } - - public 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 void fetchFollowedMangaChapters(Long mangaProviderId) { + // var mangaProvider = + // mangaContentProviderRepository + // .findById(mangaProviderId) + // .orElseThrow( + // () -> new NotFoundException("Manga Provider not found for ID: " + + // mangaProviderId)); + // + // var currentAvailableChapterCount = + // mangaChapterRepository.findByMangaContentProviderId(mangaProviderId).size(); + // + // fetchMangaChapters(mangaProviderId); + // mangaChapterRepository.flush(); + // + // var availableChapterCount = + // mangaChapterRepository.findByMangaContentProviderId(mangaProviderId).size(); + // + // if (availableChapterCount <= currentAvailableChapterCount) { + // return; + // } + // + // log.info("New chapters found for Manga Provider {}", mangaProviderId); + // + // var userMangaFollows = userMangaFollowRepository.findByManga(mangaProvider.getManga()); + // userMangaFollows.forEach( + // umf -> + // ntfyClient.notify( + // new NtfyClient.Request( + // "mangamochi-" + umf.getUser().getId().toString(), + // umf.getManga().getTitle(), + // "New chapter available on " + + // mangaProvider.getContentProvider().getName()))); + // } public Manga findMangaByIdThrowIfNotFound(Long mangaId) { return mangaRepository diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 99de633..5d31dd7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -96,6 +96,8 @@ topics: queues: 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} manga-update: ${MANGA_UPDATE_QUEUE:mangaUpdate} image-fetch: ${IMAGE_FETCH_QUEUE:imageFetch} diff --git a/src/main/resources/db/migration/V0001__CREATE_TABLES.sql b/src/main/resources/db/migration/V0001__CREATE_TABLES.sql index 05db389..ca96fe7 100644 --- a/src/main/resources/db/migration/V0001__CREATE_TABLES.sql +++ b/src/main/resources/db/migration/V0001__CREATE_TABLES.sql @@ -15,8 +15,8 @@ ON CONFLICT DO NOTHING; CREATE TABLE images ( id UUID NOT NULL PRIMARY KEY, - object_key VARCHAR NOT NULL, - file_hash VARCHAR(64) UNIQUE, + object_key VARCHAR NOT NULL, + file_hash VARCHAR(64) UNIQUE, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); @@ -46,7 +46,7 @@ CREATE TABLE content_providers name VARCHAR NOT NULL, url VARCHAR NOT NULL, 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, created_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) ); -CREATE TABLE manga_chapters +CREATE TABLE manga_contents ( - id BIGSERIAL NOT NULL PRIMARY KEY, - manga_content_provider_id BIGINT NOT NULL REFERENCES manga_content_provider (id) ON DELETE CASCADE, - title VARCHAR NOT NULL, - url VARCHAR NOT NULL, - chapter_number INTEGER, + id BIGSERIAL NOT NULL PRIMARY KEY, + manga_content_provider_id BIGINT NOT NULL REFERENCES manga_content_provider (id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + title VARCHAR NOT NULL, + url VARCHAR NOT NULL, language_id BIGINT REFERENCES languages (id), - downloaded BOOLEAN DEFAULT FALSE, - read BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + downloaded BOOLEAN DEFAULT FALSE, + created_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, - 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, position INT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/src/main/resources/db/migration/V0002__CONTENT_PROVIDERS.sql b/src/main/resources/db/migration/V0002__CONTENT_PROVIDERS.sql index dce63ff..45cfb40 100644 --- a/src/main/resources/db/migration/V0002__CONTENT_PROVIDERS.sql +++ b/src/main/resources/db/migration/V0002__CONTENT_PROVIDERS.sql @@ -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), ('Manga Livre.to', 'https://mangalivre.to', TRUE, TRUE, FALSE), ('Pink Rosa Scan', 'https://scanpinkrosa.blogspot.com', TRUE, TRUE, FALSE); \ No newline at end of file