refactor-architecture #27

Merged
rov merged 11 commits from refactor-architecture into main 2026-03-18 16:55:37 -03:00
43 changed files with 295 additions and 298 deletions
Showing only changes of commit 5f66490dae - Show all commits

View File

@ -1,4 +1,4 @@
package com.magamochi.config; package com.magamochi.common.config;
import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory;
@ -10,6 +10,24 @@ import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
public class RabbitConfig { public class RabbitConfig {
@Value("${queues.manga-ingest}")
private String mangaIngestQueue;
@Value("${queues.provider-page-ingest}")
private String providerPageIngestQueue;
@Bean
public Queue mangaIngestQueue() {
return new Queue(mangaIngestQueue, false);
}
@Bean
public Queue providerPageIngestQueue() {
return new Queue(providerPageIngestQueue, false);
}
// TODO: remove unused queues
@Value("${rabbit-mq.queues.manga-data-update}") @Value("${rabbit-mq.queues.manga-data-update}")
private String mangaDataUpdateQueue; private String mangaDataUpdateQueue;

View File

@ -0,0 +1,6 @@
package com.magamochi.common.queue.command;
import jakarta.validation.constraints.NotBlank;
public record MangaIngestCommand(
long providerId, @NotBlank String mangaTitle, @NotBlank String url) {}

View File

@ -2,12 +2,12 @@ package com.magamochi.controller;
import com.magamochi.client.NtfyClient; import com.magamochi.client.NtfyClient;
import com.magamochi.common.dto.DefaultResponseDTO; import com.magamochi.common.dto.DefaultResponseDTO;
import com.magamochi.ingestion.task.IngestFromContentProvidersTask;
import com.magamochi.model.dto.UpdateMangaDataCommand; import com.magamochi.model.dto.UpdateMangaDataCommand;
import com.magamochi.model.repository.UserRepository; import com.magamochi.model.repository.UserRepository;
import com.magamochi.queue.UpdateMangaDataProducer; import com.magamochi.queue.UpdateMangaDataProducer;
import com.magamochi.task.ImageCleanupTask; import com.magamochi.task.ImageCleanupTask;
import com.magamochi.task.MangaFollowUpdateTask; import com.magamochi.task.MangaFollowUpdateTask;
import com.magamochi.task.UpdateMangaListTask;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.*;
@RequestMapping("/management") @RequestMapping("/management")
@RequiredArgsConstructor @RequiredArgsConstructor
public class ManagementController { public class ManagementController {
private final UpdateMangaListTask updateMangaListTask; private final IngestFromContentProvidersTask ingestFromContentProvidersTask;
private final ImageCleanupTask imageCleanupTask; private final ImageCleanupTask imageCleanupTask;
private final MangaFollowUpdateTask mangaFollowUpdateTask; private final MangaFollowUpdateTask mangaFollowUpdateTask;
private final UserRepository userRepository; private final UserRepository userRepository;
@ -35,30 +35,6 @@ public class ManagementController {
return DefaultResponseDTO.ok().build(); return DefaultResponseDTO.ok().build();
} }
@Operation(
summary = "Queue update manga list",
description = "Queue the retrieval of the manga lists from the content providers",
tags = {"Management"},
operationId = "updateMangaList")
@PostMapping("update-manga-list")
public DefaultResponseDTO<Void> updateMangaList() {
updateMangaListTask.updateMangaList();
return DefaultResponseDTO.ok().build();
}
@Operation(
summary = "Queue update provider manga list",
description = "Queue the retrieval of the manga list for a specific provider",
tags = {"Management"},
operationId = "updateProviderMangaList")
@PostMapping("update-provider-manga-list")
public DefaultResponseDTO<Void> updateProviderMangaList(@RequestParam Long providerId) {
updateMangaListTask.updateProviderMangaList(providerId);
return DefaultResponseDTO.ok().build();
}
@Operation( @Operation(
summary = "Cleanup unused S3 images", summary = "Cleanup unused S3 images",
description = "Triggers the cleanup of untracked S3 images", description = "Triggers the cleanup of untracked S3 images",

View File

@ -1,4 +1,4 @@
package com.magamochi.client; package com.magamochi.ingestion.client;
import io.github.resilience4j.retry.annotation.Retry; import io.github.resilience4j.retry.annotation.Retry;
import java.util.List; import java.util.List;

View File

@ -3,18 +3,17 @@ package com.magamochi.ingestion.controller;
import com.magamochi.common.dto.DefaultResponseDTO; import com.magamochi.common.dto.DefaultResponseDTO;
import com.magamochi.ingestion.model.dto.ContentProviderListDTO; import com.magamochi.ingestion.model.dto.ContentProviderListDTO;
import com.magamochi.ingestion.service.ContentProviderService; import com.magamochi.ingestion.service.ContentProviderService;
import com.magamochi.ingestion.service.IngestionService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
@RequestMapping("/ingestion") @RequestMapping("/ingestion")
@RequiredArgsConstructor @RequiredArgsConstructor
public class IngestionController { public class IngestionController {
private final ContentProviderService contentProviderService; private final ContentProviderService contentProviderService;
private final IngestionService ingestionService;
@Operation( @Operation(
summary = "Get a list of content providers", summary = "Get a list of content providers",
@ -26,4 +25,30 @@ public class IngestionController {
@RequestParam(name = "manualImport", required = false) Boolean manualImport) { @RequestParam(name = "manualImport", required = false) Boolean manualImport) {
return DefaultResponseDTO.ok(contentProviderService.getProviders(manualImport)); return DefaultResponseDTO.ok(contentProviderService.getProviders(manualImport));
} }
@Operation(
summary = "Fetch mangas from a content provider",
description =
"Triggers the ingestion process for a specific content provider, fetching manga data and queuing it for processing.",
tags = {"Ingestion"},
operationId = "fetchContentProviderMangas")
@PostMapping("/providers/{providerId}/fetch")
public DefaultResponseDTO<Void> fetchContentProviderMangas(@PathVariable Long providerId) {
ingestionService.fetchContentProviderMangas(providerId);
return DefaultResponseDTO.ok().build();
}
@Operation(
summary = "Fetch mangas from all content providers",
description =
"Triggers the ingestion process for all content providers, fetching manga data and queuing them for processing.",
tags = {"Ingestion"},
operationId = "fetchAllContentProviderMangas")
@PostMapping("/providers/fetch")
public DefaultResponseDTO<Void> fetchAllContentProviderMangas() {
ingestionService.fetchAllContentProviderMangas();
return DefaultResponseDTO.ok().build();
}
} }

View File

@ -11,7 +11,7 @@ public record ContentProviderListDTO(@NotNull List<ContentProviderDTO> providers
contentProviders.stream().map(ContentProviderDTO::from).toList()); contentProviders.stream().map(ContentProviderDTO::from).toList());
} }
record ContentProviderDTO(long id, @NotBlank String name) { public record ContentProviderDTO(long id, @NotBlank String name) {
public static ContentProviderDTO from(ContentProvider contentProvider) { public static ContentProviderDTO from(ContentProvider contentProvider) {
return new ContentProviderDTO(contentProvider.getId(), contentProvider.getName()); return new ContentProviderDTO(contentProvider.getId(), contentProvider.getName());
} }

View File

@ -0,0 +1,6 @@
package com.magamochi.ingestion.model.dto;
import com.magamochi.model.enumeration.MangaStatus;
import jakarta.validation.constraints.NotBlank;
public record MangaInfoDTO(@NotBlank String title, @NotBlank String url, MangaStatus status) {}

View File

@ -1,4 +1,4 @@
package com.magamochi.service.providers; package com.magamochi.ingestion.providers;
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO; import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
import com.magamochi.model.entity.MangaContentProvider; import com.magamochi.model.entity.MangaContentProvider;

View File

@ -1,4 +1,4 @@
package com.magamochi.service.providers; package com.magamochi.ingestion.providers;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;

View File

@ -1,9 +1,8 @@
package com.magamochi.service.providers; package com.magamochi.ingestion.providers;
public class ContentProviders { public class ContentProviders {
public static final String MANGA_LIVRE_TO = "Manga Livre.to";
public static final String MANGA_LIVRE_BLOG = "Manga Livre Blog"; public static final String MANGA_LIVRE_BLOG = "Manga Livre Blog";
public static final String MANGA_DEX = "MangaDex"; public static final String MANGA_LIVRE_TO = "Manga Livre.to";
public static final String PINK_ROSA_SCAN = "Pink Rosa Scan"; public static final String PINK_ROSA_SCAN = "Pink Rosa Scan";
public static final String BATO = "Bato"; public static final String MANGA_DEX = "MangaDex";
} }

View File

@ -1,4 +1,4 @@
package com.magamochi.service.providers; package com.magamochi.ingestion.providers;
public interface ManualImportContentProvider { public interface ManualImportContentProvider {
String getMangaTitle(String value); String getMangaTitle(String value);

View File

@ -1,4 +1,4 @@
package com.magamochi.service.providers; package com.magamochi.ingestion.providers;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;

View File

@ -0,0 +1,10 @@
package com.magamochi.ingestion.providers;
import com.magamochi.ingestion.model.dto.MangaInfoDTO;
import java.util.List;
public interface PagedContentProvider {
int getTotalPages();
List<MangaInfoDTO> getMangasFromPage(int page);
}

View File

@ -1,4 +1,4 @@
package com.magamochi.service.providers; package com.magamochi.ingestion.providers;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;

View File

@ -1,15 +1,15 @@
package com.magamochi.service.providers.impl; 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.client.MangaDexClient; import com.magamochi.client.MangaDexClient;
import com.magamochi.common.exception.UnprocessableException; import com.magamochi.common.exception.UnprocessableException;
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.dto.ContentProviderMangaChapterResponseDTO;
import com.magamochi.model.entity.MangaContentProvider; import com.magamochi.model.entity.MangaContentProvider;
import com.magamochi.service.providers.ContentProvider;
import com.magamochi.service.providers.ContentProviders;
import com.magamochi.service.providers.ManualImportContentProvider;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;

View File

@ -1,12 +1,12 @@
package com.magamochi.service.providers.impl; package com.magamochi.ingestion.providers.impl;
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.dto.ContentProviderMangaChapterResponseDTO;
import com.magamochi.model.dto.ContentProviderMangaInfoResponseDTO;
import com.magamochi.model.entity.MangaContentProvider; import com.magamochi.model.entity.MangaContentProvider;
import com.magamochi.model.enumeration.MangaStatus; import com.magamochi.model.enumeration.MangaStatus;
import com.magamochi.service.providers.ContentProvider;
import com.magamochi.service.providers.ContentProviders;
import com.magamochi.service.providers.PagedContentProvider;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -101,7 +101,7 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
} }
@Override @Override
public List<ContentProviderMangaInfoResponseDTO> getMangasFromPage(Integer page) { public List<MangaInfoDTO> getMangasFromPage(int page) {
log.info("Getting mangas from {}, page {}", ContentProviders.MANGA_LIVRE_BLOG, page); log.info("Getting mangas from {}, page {}", ContentProviders.MANGA_LIVRE_BLOG, page);
try { try {
@ -134,7 +134,7 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
default -> MangaStatus.UNKNOWN; default -> MangaStatus.UNKNOWN;
}; };
return new ContentProviderMangaInfoResponseDTO(title, url, status); return new MangaInfoDTO(title, url, status);
} catch (Exception e) { } catch (Exception e) {
return null; return null;
} }
@ -148,7 +148,7 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
} }
@Override @Override
public Integer getTotalPages() { public int getTotalPages() {
log.info("Getting total pages for {}", ContentProviders.MANGA_LIVRE_BLOG); log.info("Getting total pages for {}", ContentProviders.MANGA_LIVRE_BLOG);
try { try {
@ -166,7 +166,7 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
return pageNumbers.stream().max(Integer::compareTo).orElse(null); return pageNumbers.stream().max(Integer::compareTo).orElse(null);
} catch (IOException | NoSuchElementException e) { } catch (IOException | NoSuchElementException e) {
log.error("Error fetching total pages from MangaLivre", e); log.error("Error fetching total pages from MangaLivre", e);
return null; return 0;
} }
} }
} }

View File

@ -1,15 +1,15 @@
package com.magamochi.service.providers.impl; package com.magamochi.ingestion.providers.impl;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
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.ingestion.service.FlareService;
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO; import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
import com.magamochi.model.dto.ContentProviderMangaInfoResponseDTO;
import com.magamochi.model.entity.MangaContentProvider; import com.magamochi.model.entity.MangaContentProvider;
import com.magamochi.model.enumeration.MangaStatus; import com.magamochi.model.enumeration.MangaStatus;
import com.magamochi.service.providers.ContentProvider;
import com.magamochi.service.providers.ContentProviders;
import com.magamochi.service.providers.PagedContentProvider;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
@ -90,7 +90,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
} }
@Override @Override
public List<ContentProviderMangaInfoResponseDTO> getMangasFromPage(Integer page) { public List<MangaInfoDTO> getMangasFromPage(int page) {
log.info("Getting mangas from {}, page {}", ContentProviders.MANGA_LIVRE_TO, page); log.info("Getting mangas from {}, page {}", ContentProviders.MANGA_LIVRE_TO, page);
try { try {
@ -116,8 +116,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
} }
} }
return new ContentProviderMangaInfoResponseDTO( return new MangaInfoDTO(title.trim(), url.trim(), MangaStatus.UNKNOWN);
title.trim(), url.trim(), MangaStatus.UNKNOWN);
}) })
.toList(); .toList();
} catch (NoSuchElementException e) { } catch (NoSuchElementException e) {
@ -127,7 +126,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
} }
@Override @Override
public Integer getTotalPages() { public int getTotalPages() {
log.info("Getting total pages for {}", ContentProviders.MANGA_LIVRE_TO); log.info("Getting total pages for {}", ContentProviders.MANGA_LIVRE_TO);
try { try {
@ -140,7 +139,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
return (int) Math.ceil((double) totalMangas / MANGAS_PER_PAGE); return (int) Math.ceil((double) totalMangas / MANGAS_PER_PAGE);
} catch (NoSuchElementException e) { } catch (NoSuchElementException e) {
log.error("Error parsing total pages from MangaLivre", e); log.error("Error parsing total pages from MangaLivre", e);
return null; return 0;
} }
} }
} }

View File

@ -1,16 +1,16 @@
package com.magamochi.service.providers.impl; 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.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.ingestion.service.FlareService;
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO; import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
import com.magamochi.model.dto.ContentProviderMangaInfoResponseDTO;
import com.magamochi.model.entity.MangaContentProvider; import com.magamochi.model.entity.MangaContentProvider;
import com.magamochi.model.enumeration.MangaStatus; import com.magamochi.model.enumeration.MangaStatus;
import com.magamochi.service.providers.ContentProvider;
import com.magamochi.service.providers.ContentProviders;
import com.magamochi.service.providers.PagedContentProvider;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
@ -110,12 +110,12 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
} }
@Override @Override
public Integer getTotalPages() { public int getTotalPages() {
return 1; return 1;
} }
@Override @Override
public List<ContentProviderMangaInfoResponseDTO> getMangasFromPage(Integer page) { public List<MangaInfoDTO> getMangasFromPage(int page) {
log.info("Getting mangas from {}", ContentProviders.PINK_ROSA_SCAN); log.info("Getting mangas from {}", ContentProviders.PINK_ROSA_SCAN);
try { try {
@ -142,7 +142,7 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
var textElement = linkElement.getElementsByTag("h3"); var textElement = linkElement.getElementsByTag("h3");
var title = textElement.text().trim(); var title = textElement.text().trim();
return new ContentProviderMangaInfoResponseDTO(title, url, MangaStatus.UNKNOWN); return new MangaInfoDTO(title, url, MangaStatus.UNKNOWN);
}) })
.toList(); .toList();
} catch (NoSuchElementException e) { } catch (NoSuchElementException e) {

View File

@ -0,0 +1,3 @@
package com.magamochi.ingestion.queue.command;
public record ProviderPageIngestCommand(long providerId, int page) {}

View File

@ -0,0 +1,21 @@
package com.magamochi.ingestion.queue.consumer;
import com.magamochi.ingestion.queue.command.ProviderPageIngestCommand;
import com.magamochi.ingestion.service.IngestionService;
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 ProviderPageIngestConsumer {
private final IngestionService ingestionService;
@RabbitListener(queues = "${queues.provider-page-ingest}")
public void receiveProviderPageIngestCommand(ProviderPageIngestCommand command) {
log.info("Received provider page ingest command: {}", command);
ingestionService.fetchProviderPageMangas(command.providerId(), command.page());
}
}

View File

@ -0,0 +1,23 @@
package com.magamochi.ingestion.queue.producer;
import com.magamochi.common.queue.command.MangaIngestCommand;
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 MangaIngestProducer {
private final RabbitTemplate rabbitTemplate;
@Value("${queues.manga-ingest}")
private String mangaIngestQueue;
public void sendMangaIngestCommand(MangaIngestCommand command) {
rabbitTemplate.convertAndSend(mangaIngestQueue, command);
log.info("Sent manga ingest command: {}", command);
}
}

View File

@ -0,0 +1,23 @@
package com.magamochi.ingestion.queue.producer;
import com.magamochi.ingestion.queue.command.ProviderPageIngestCommand;
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 ProviderPageIngestProducer {
private final RabbitTemplate rabbitTemplate;
@Value("${queues.provider-page-ingest}")
private String providerPageIngestQueue;
public void sendProviderPageIngestCommand(ProviderPageIngestCommand command) {
rabbitTemplate.convertAndSend(providerPageIngestQueue, command);
log.info("Sent provider page ingest command: {}", command);
}
}

View File

@ -2,6 +2,7 @@ package com.magamochi.ingestion.service;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
import com.magamochi.common.exception.NotFoundException;
import com.magamochi.ingestion.model.dto.ContentProviderListDTO; import com.magamochi.ingestion.model.dto.ContentProviderListDTO;
import com.magamochi.ingestion.model.entity.ContentProvider; import com.magamochi.ingestion.model.entity.ContentProvider;
import com.magamochi.ingestion.model.repository.ContentProviderRepository; import com.magamochi.ingestion.model.repository.ContentProviderRepository;
@ -22,4 +23,13 @@ public class ContentProviderService {
return ContentProviderListDTO.from(providers); return ContentProviderListDTO.from(providers);
} }
public ContentProvider find(Long contentProviderId) {
return contentProviderRepository
.findById(contentProviderId)
.orElseThrow(
() ->
new NotFoundException(
"Content Provider not found (ID: " + contentProviderId + ")."));
}
} }

View File

@ -1,6 +1,6 @@
package com.magamochi.ingestion.service; package com.magamochi.ingestion.service;
import com.magamochi.client.FlareClient; import com.magamochi.ingestion.client.FlareClient;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;

View File

@ -3,9 +3,8 @@ package com.magamochi.ingestion.service;
import static java.util.Objects.isNull; import static java.util.Objects.isNull;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
import com.magamochi.client.FlareClient; import com.magamochi.ingestion.client.FlareClient;
import com.magamochi.ingestion.model.entity.FlareSession; import com.magamochi.ingestion.model.entity.FlareSession;
import com.magamochi.registry.FlareSessionRegistry;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;

View File

@ -1,4 +1,4 @@
package com.magamochi.registry; package com.magamochi.ingestion.service;
import com.magamochi.ingestion.model.entity.FlareSession; import com.magamochi.ingestion.model.entity.FlareSession;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;

View File

@ -0,0 +1,51 @@
package com.magamochi.ingestion.service;
import com.magamochi.common.queue.command.MangaIngestCommand;
import com.magamochi.ingestion.providers.PagedContentProviderFactory;
import com.magamochi.ingestion.queue.command.ProviderPageIngestCommand;
import com.magamochi.ingestion.queue.producer.MangaIngestProducer;
import com.magamochi.ingestion.queue.producer.ProviderPageIngestProducer;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class IngestionService {
private final ContentProviderService contentProviderService;
private final PagedContentProviderFactory pagedContentProviderFactory;
private final ProviderPageIngestProducer providerPageIngestProducer;
private final MangaIngestProducer mangaIngestProducer;
public void fetchContentProviderMangas(long contentProviderId) {
var contentProvider = contentProviderService.find(contentProviderId);
var pagedContentProvider =
pagedContentProviderFactory.getPagedContentProvider(contentProvider.getName());
var pages = pagedContentProvider.getTotalPages();
IntStream.rangeClosed(1, pages)
.forEach(
page ->
providerPageIngestProducer.sendProviderPageIngestCommand(
new ProviderPageIngestCommand(contentProvider.getId(), page)));
}
public void fetchProviderPageMangas(long providerId, int page) {
var contentProvider = contentProviderService.find(providerId);
var pagedContentProvider =
pagedContentProviderFactory.getPagedContentProvider(contentProvider.getName());
var mangas = pagedContentProvider.getMangasFromPage(page);
mangas.forEach(
manga ->
mangaIngestProducer.sendMangaIngestCommand(
new MangaIngestCommand(contentProvider.getId(), manga.title(), manga.url())));
}
public void fetchAllContentProviderMangas() {
var contentProviders = contentProviderService.getProviders(null);
contentProviders.providers().forEach(dto -> fetchContentProviderMangas(dto.id()));
}
}

View File

@ -1,7 +1,7 @@
package com.magamochi.ingestion.task; package com.magamochi.ingestion.task;
import com.magamochi.client.FlareClient; import com.magamochi.ingestion.client.FlareClient;
import com.magamochi.registry.FlareSessionRegistry; import com.magamochi.ingestion.service.FlareSessionRegistry;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;

View File

@ -1,6 +1,6 @@
package com.magamochi.ingestion.task; package com.magamochi.ingestion.task;
import com.magamochi.client.FlareClient; import com.magamochi.ingestion.client.FlareClient;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.ApplicationReadyEvent;

View File

@ -0,0 +1,31 @@
package com.magamochi.ingestion.task;
import com.magamochi.ingestion.service.IngestionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Log4j2
@Component
@RequiredArgsConstructor
public class IngestFromContentProvidersTask {
@Value("${content-providers.update-enabled}")
private Boolean updateEnabled;
private final IngestionService ingestionService;
@Scheduled(cron = "${content-providers.cron-expression}")
public void updateMangaListScheduled() {
if (!updateEnabled) {
return;
}
log.info("Starting scheduling ingest from Content Providers.");
ingestionService.fetchAllContentProviderMangas();
log.info("Finished scheduling ingest from Content Providers.");
}
}

View File

@ -1,7 +0,0 @@
package com.magamochi.model.dto;
import com.magamochi.model.enumeration.MangaStatus;
import jakarta.validation.constraints.NotBlank;
public record ContentProviderMangaInfoResponseDTO(
@NotBlank String title, @NotBlank String url, MangaStatus status) {}

View File

@ -1,6 +1,7 @@
package com.magamochi.model.dto; package com.magamochi.model.dto;
import com.magamochi.ingestion.model.dto.MangaInfoDTO;
import java.util.List; import java.util.List;
public record MangaMessageDTO( public record MangaMessageDTO(
String contentProviderName, List<ContentProviderMangaInfoResponseDTO> mangaInfoResponseDTOs) {} String contentProviderName, List<MangaInfoDTO> mangaInfoResponseDTOs) {}

View File

@ -2,6 +2,7 @@ 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.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.MangaChapter;
@ -9,7 +10,6 @@ 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.MangaChapterImageRepository;
import com.magamochi.model.repository.MangaChapterRepository; import com.magamochi.model.repository.MangaChapterRepository;
import com.magamochi.service.providers.ContentProviderFactory;
import io.github.resilience4j.retry.Retry; 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.BufferedInputStream;

View File

@ -2,9 +2,9 @@ package com.magamochi.service;
import static java.util.Objects.isNull; import static java.util.Objects.isNull;
import com.magamochi.ingestion.providers.PagedContentProviderFactory;
import com.magamochi.model.entity.MangaContentProvider; import com.magamochi.model.entity.MangaContentProvider;
import com.magamochi.model.repository.MangaContentProviderRepository; import com.magamochi.model.repository.MangaContentProviderRepository;
import com.magamochi.service.providers.PagedContentProviderFactory;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;

View File

@ -4,6 +4,7 @@ import static java.util.Objects.nonNull;
import com.magamochi.client.NtfyClient; import com.magamochi.client.NtfyClient;
import com.magamochi.common.exception.NotFoundException; import com.magamochi.common.exception.NotFoundException;
import com.magamochi.ingestion.providers.ContentProviderFactory;
import com.magamochi.model.dto.*; import com.magamochi.model.dto.*;
import com.magamochi.model.entity.Manga; import com.magamochi.model.entity.Manga;
import com.magamochi.model.entity.MangaChapter; import com.magamochi.model.entity.MangaChapter;
@ -12,7 +13,6 @@ import com.magamochi.model.entity.UserMangaFollow;
import com.magamochi.model.repository.*; import com.magamochi.model.repository.*;
import com.magamochi.model.specification.MangaSpecification; import com.magamochi.model.specification.MangaSpecification;
import com.magamochi.queue.MangaChapterDownloadProducer; import com.magamochi.queue.MangaChapterDownloadProducer;
import com.magamochi.service.providers.ContentProviderFactory;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;

View File

@ -6,11 +6,11 @@ import static java.util.Objects.nonNull;
import com.magamochi.common.exception.NotFoundException; import com.magamochi.common.exception.NotFoundException;
import com.magamochi.ingestion.model.entity.ContentProvider; import com.magamochi.ingestion.model.entity.ContentProvider;
import com.magamochi.ingestion.model.repository.ContentProviderRepository; import com.magamochi.ingestion.model.repository.ContentProviderRepository;
import com.magamochi.ingestion.providers.ManualImportContentProviderFactory;
import com.magamochi.model.dto.ImportMangaResponseDTO; import com.magamochi.model.dto.ImportMangaResponseDTO;
import com.magamochi.model.dto.ImportRequestDTO; import com.magamochi.model.dto.ImportRequestDTO;
import com.magamochi.model.entity.MangaContentProvider; import com.magamochi.model.entity.MangaContentProvider;
import com.magamochi.model.repository.MangaContentProviderRepository; import com.magamochi.model.repository.MangaContentProviderRepository;
import com.magamochi.service.providers.ManualImportContentProviderFactory;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;

View File

@ -1,10 +0,0 @@
package com.magamochi.service.providers;
import com.magamochi.model.dto.ContentProviderMangaInfoResponseDTO;
import java.util.List;
public interface PagedContentProvider {
Integer getTotalPages();
List<ContentProviderMangaInfoResponseDTO> getMangasFromPage(Integer page);
}

View File

@ -1,99 +0,0 @@
package com.magamochi.service.providers.impl;
import static java.util.Objects.isNull;
import com.magamochi.common.exception.UnprocessableException;
import com.magamochi.ingestion.service.FlareService;
import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
import com.magamochi.model.entity.MangaContentProvider;
import com.magamochi.service.providers.ContentProvider;
import com.magamochi.service.providers.ContentProviders;
import com.magamochi.service.providers.ManualImportContentProvider;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
@Log4j2
@Service(ContentProviders.BATO)
@RequiredArgsConstructor
public class BatoProvider implements ContentProvider, ManualImportContentProvider {
private static final String URL = "https://battwo.com";
private final FlareService flareService;
@Override
public List<ContentProviderMangaChapterResponseDTO> getAvailableChapters(
MangaContentProvider provider) {
try {
var document =
flareService.getContentAsJsoupDocument(provider.getUrl(), ContentProviders.BATO);
// Direct selector for chapter links
var chapterLinks = document.select("div.scrollable-panel a[href*=/title/]");
// TODO: fix chapter and language code
return chapterLinks.stream()
.map(
chapterLink ->
new ContentProviderMangaChapterResponseDTO(
chapterLink.text(), chapterLink.attr("href"), null, null))
.toList();
} catch (Exception e) {
log.warn(e.getMessage());
return null;
}
}
@Override
public Map<Integer, String> getChapterImagesUrls(String chapterUrl) {
try {
var document =
flareService.getContentAsJsoupDocument(
URL + chapterUrl + "?load=2", ContentProviders.BATO);
// Select all chapter page images
var imgElements = document.select("img.z-10.w-full.h-full");
List<String> imageUrls = new ArrayList<>();
for (var img : imgElements) {
String src = img.attr("src");
// Normalize if needed
if (!src.startsWith("http")) {
src = "https://battwo.com" + src;
}
imageUrls.add(src);
}
return IntStream.range(0, imageUrls.size())
.boxed()
.collect(
Collectors.toMap(
i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new));
} catch (Exception e) {
log.warn(e.getMessage());
return null;
}
}
@Override
public String getMangaTitle(String value) {
var document = flareService.getContentAsJsoupDocument(value, ContentProviders.BATO);
var titleElement = document.selectFirst("h3 a[href*=/title/]");
if (isNull(titleElement)) {
titleElement = document.selectFirst("h3.item-title > a");
if (isNull(titleElement)) {
throw new UnprocessableException("Manga title not found for url: " + value);
}
}
return titleElement.text();
}
}

View File

@ -1,30 +0,0 @@
package com.magamochi.task;
import static java.util.Objects.isNull;
import com.magamochi.model.dto.UpdateMangaDataCommand;
import com.magamochi.model.repository.MangaRepository;
import com.magamochi.queue.UpdateMangaDataProducer;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Log4j2
@Component
@RequiredArgsConstructor
public class UpdateMangaDataTask {
private final MangaRepository mangaRepository;
private final UpdateMangaDataProducer updateMangaDataProducer;
@Scheduled(cron = "@daily")
public void updateMangaData() {
var mangas =
mangaRepository.findAll().stream().filter(manga -> isNull(manga.getScore())).toList();
mangas.forEach(
manga ->
updateMangaDataProducer.sendUpdateMangaDataCommand(
new UpdateMangaDataCommand(manga.getId())));
}
}

View File

@ -1,67 +0,0 @@
package com.magamochi.task;
import com.magamochi.common.exception.NotFoundException;
import com.magamochi.ingestion.model.repository.ContentProviderRepository;
import com.magamochi.model.dto.MangaListUpdateCommand;
import com.magamochi.queue.UpdateMangaListProducer;
import com.magamochi.service.providers.PagedContentProvider;
import com.magamochi.service.providers.PagedContentProviderFactory;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Log4j2
@Component
@RequiredArgsConstructor
public class UpdateMangaListTask {
@Value("${content-providers.update-enabled}")
private Boolean updateEnabled;
private final PagedContentProviderFactory contentProviderFactory;
private final UpdateMangaListProducer updateMangaListProducer;
private final ContentProviderRepository contentProviderRepository;
@Scheduled(cron = "${content-providers.cron-expression}")
public void updateMangaListScheduled() {
if (!updateEnabled) {
return;
}
updateMangaList();
}
public void updateMangaList() {
log.info("Queuing manga list updates...");
var contentProviders = contentProviderFactory.getContentProviders();
contentProviders.forEach(this::updateProviderMangaList);
}
public void updateProviderMangaList(Long providerId) {
var provider =
contentProviderRepository
.findById(providerId)
.orElseThrow(() -> new NotFoundException("Provider not found"));
var contentProvider = contentProviderFactory.getPagedContentProvider(provider.getName());
updateProviderMangaList(provider.getName(), contentProvider);
}
private void updateProviderMangaList(
String contentProviderName, PagedContentProvider contentProvider) {
log.info("Getting total pages for provider {}", contentProviderName);
var pages = contentProvider.getTotalPages();
IntStream.rangeClosed(1, pages)
.forEach(
page ->
updateMangaListProducer.sendUpdateMangaListCommand(
new MangaListUpdateCommand(contentProviderName, page)));
log.info("Manga list update queued for content provider {}.", contentProviderName);
}
}

View File

@ -88,6 +88,10 @@ resilience4j:
- java.io.IOException - java.io.IOException
- java.net.SocketTimeoutException - java.net.SocketTimeoutException
queues:
manga-ingest: ${MANGA_INGEST_QUEUE:mangaIngest}
provider-page-ingest: ${PROVIDER_PAGE_INGEST_QUEUE:providerPageIngest}
rabbit-mq: rabbit-mq:
queues: queues:
manga-data-update: ${MANGA_DATA_UPDATE_QUEUE:mangaDataUpdateQueue} manga-data-update: ${MANGA_DATA_UPDATE_QUEUE:mangaDataUpdateQueue}

View File

@ -42,6 +42,7 @@ CREATE TABLE content_providers
( (
id BIGSERIAL NOT NULL PRIMARY KEY, id BIGSERIAL NOT NULL PRIMARY KEY,
name VARCHAR NOT NULL, name 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_chapter_fetch BOOLEAN NOT NULL DEFAULT TRUE,
manual_import BOOLEAN NOT NULL DEFAULT FALSE, manual_import BOOLEAN NOT NULL DEFAULT FALSE,

View File

@ -0,0 +1,4 @@
INSERT INTO content_providers(name, url, active, supports_chapter_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);