feat: content service

This commit is contained in:
Rodrigo Verdiani 2026-03-18 11:11:24 -03:00
parent 7cb571939e
commit 05d66d7cf4
49 changed files with 612 additions and 398 deletions

View File

@ -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());
} }
} }
} }

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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"));

View File

@ -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));
}
}

View File

@ -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;

View File

@ -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;
@ -62,6 +68,16 @@ public class RabbitConfig {
null); 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);

View File

@ -1,5 +1,7 @@
package com.magamochi.common.model.enumeration; package com.magamochi.common.model.enumeration;
public enum ContentType { public enum ContentType {
MANGA_COVER MANGA_COVER,
CHAPTER,
VOLUME
} }

View File

@ -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) {}

View File

@ -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) {}

View File

@ -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<List<MangaContentDTO>> getMangaProviderContent(
@PathVariable @NotNull Long mangaContentProviderId) {
return DefaultResponseDTO.ok(contentService.getContent(mangaContentProviderId));
}
}

View File

@ -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;

View File

@ -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()));
}
}

View File

@ -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")

View File

@ -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")

View File

@ -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<MangaContentImage, Long> {
List<MangaContentImage> findAllByMangaContent(MangaContent mangaContent);
}

View File

@ -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);
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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<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));
}
}

View File

@ -21,18 +21,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( @Operation(
summary = "Get the images for a specific manga/provider combination", summary = "Get the images for a specific manga/provider combination",
description = description =

View File

@ -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.",

View File

@ -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();
}
} }

View File

@ -0,0 +1,5 @@
package com.magamochi.ingestion.model.dto;
import jakarta.validation.constraints.NotBlank;
public record ContentImageInfoDTO(int position, @NotBlank String url) {}

View File

@ -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) {}

View File

@ -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;

View File

@ -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);
}

View File

@ -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);
} }

View File

@ -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

View File

@ -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();
} }
} }

View File

@ -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();
} }
} }

View File

@ -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();
} }
} }

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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,127 @@ 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));
});
}
// @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);
// }
} }

View File

@ -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) {}

View File

@ -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()));
}
}

View File

@ -1,7 +1,7 @@
package com.magamochi.model.dto; package com.magamochi.model.dto;
import com.magamochi.model.entity.MangaChapter; import com.magamochi.content.model.entity.MangaContent;
import com.magamochi.model.entity.MangaChapterImage; import com.magamochi.content.model.entity.MangaContentImage;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.util.Comparator; import java.util.Comparator;
@ -13,14 +13,14 @@ public record MangaChapterImagesDTO(
Long previousChapterId, Long previousChapterId,
Long nextChapterId, Long nextChapterId,
@NotNull List<@NotBlank String> chapterImageKeys) { @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( return new MangaChapterImagesDTO(
mangaChapter.getId(), mangaContent.getId(),
mangaChapter.getTitle(), mangaContent.getTitle(),
prevId, prevId,
nextId, nextId,
mangaChapter.getMangaChapterImages().stream() mangaContent.getMangaContentImages().stream()
.sorted(Comparator.comparing(MangaChapterImage::getPosition)) .sorted(Comparator.comparing(MangaContentImage::getPosition))
.map(mangaChapterImage -> mangaChapterImage.getImage().getObjectKey()) .map(mangaChapterImage -> mangaChapterImage.getImage().getObjectKey())
.toList()); .toList());
} }

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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());
}
}
}

View File

@ -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());
} }
} }

View File

@ -2,22 +2,18 @@ package com.magamochi.service;
import com.google.common.util.concurrent.RateLimiter; import com.google.common.util.concurrent.RateLimiter;
import com.magamochi.common.exception.UnprocessableException; 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.ingestion.providers.ContentProviderFactory;
import com.magamochi.model.dto.MangaChapterArchiveDTO; import com.magamochi.model.dto.MangaChapterArchiveDTO;
import com.magamochi.model.dto.MangaChapterImagesDTO; 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 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.Comparator;
import java.util.List; import java.util.List;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
@ -26,13 +22,12 @@ 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 MangaChapterImageRepository mangaChapterImageRepository;
private final OldImageService oldImageService; private final OldImageService oldImageService;
@ -42,94 +37,12 @@ public class MangaChapterService {
private final RateLimiter imageDownloadRateLimiter; private final RateLimiter imageDownloadRateLimiter;
private final RetryRegistry retryRegistry; 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) { public MangaChapterImagesDTO getMangaChapterImages(Long chapterId) {
var chapter = getMangaChapterThrowIfNotFound(chapterId); var chapter = getMangaChapterThrowIfNotFound(chapterId);
var chapters = var chapters =
chapter.getMangaContentProvider().getMangaChapters().stream() chapter.getMangaContentProvider().getMangaContents().stream()
.sorted(Comparator.comparing(MangaChapter::getId)) .sorted(Comparator.comparing(MangaContent::getId))
.toList(); .toList();
Long prevId = null; Long prevId = null;
Long nextId = null; Long nextId = null;
@ -153,17 +66,18 @@ public class MangaChapterService {
} }
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 = mangaChapterImageRepository.findAllByMangaContent(chapter);
var byteArrayOutputStream = var byteArrayOutputStream =
switch (archiveFileType) { switch (archiveFileType) {
@ -177,7 +91,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 +116,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));
} }

View File

@ -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);

View File

@ -1,20 +1,19 @@
package com.magamochi.service; package com.magamochi.service;
import com.magamochi.catalog.model.entity.Manga; 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.MangaContentProviderRepository;
import com.magamochi.catalog.model.repository.MangaRepository; import com.magamochi.catalog.model.repository.MangaRepository;
import com.magamochi.client.NtfyClient; import com.magamochi.client.NtfyClient;
import com.magamochi.common.exception.NotFoundException; import com.magamochi.common.exception.NotFoundException;
import com.magamochi.content.model.entity.MangaContent;
import com.magamochi.content.model.repository.MangaContentRepository;
import com.magamochi.ingestion.providers.ContentProviderFactory; import com.magamochi.ingestion.providers.ContentProviderFactory;
import com.magamochi.model.dto.*; import com.magamochi.model.dto.*;
import com.magamochi.model.entity.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,7 +24,6 @@ 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;
@ -35,7 +33,7 @@ public class OldMangaService {
private final UserMangaFollowRepository userMangaFollowRepository; private final UserMangaFollowRepository userMangaFollowRepository;
private final MangaChapterDownloadProducer mangaChapterDownloadProducer; private final MangaChapterDownloadProducer mangaChapterDownloadProducer;
private final MangaChapterRepository mangaChapterRepository; private final MangaContentRepository mangaContentRepository;
private final NtfyClient ntfyClient; private final NtfyClient ntfyClient;
@ -47,9 +45,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 +56,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

View File

@ -96,6 +96,8 @@ 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}
image-fetch: ${IMAGE_FETCH_QUEUE:imageFetch} image-fetch: ${IMAGE_FETCH_QUEUE:imageFetch}

View File

@ -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,

View File

@ -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);