Compare commits
5 Commits
3e6bbdc723
...
247788c3d0
| Author | SHA1 | Date | |
|---|---|---|---|
| 247788c3d0 | |||
| 1aa6a39c96 | |||
| 08ad47a875 | |||
| 42f0b9ec4d | |||
| 3f3a3739b6 |
1
.env
1
.env
@ -6,6 +6,7 @@ MINIO_ENDPOINT=http://omv2.badger-pirarucu.ts.net:9000
|
||||
MINIO_USER=rov
|
||||
MINIO_PASS=!E9v4i0v3
|
||||
|
||||
FLARESOLVERR_ENDPOINT=https://flare-solverr.badger-pirarucu.ts.net
|
||||
WEBSCRAPPER_ENDPOINT=http://mangamochi.badger-pirarucu.ts.net:8090/url
|
||||
MANGAMATCHER_ENDPOINT=http://mangamochi.badger-pirarucu.ts.net:8000/match-title
|
||||
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
package com.magamochi.mangamochi.client;
|
||||
|
||||
import io.github.resilience4j.retry.annotation.Retry;
|
||||
import java.util.List;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
@FeignClient(name = "flare-solverr", url = "${flare-solverr.endpoint}/v1")
|
||||
@Retry(name = "FlareSolverrRetry")
|
||||
public interface FlareClient {
|
||||
@PostMapping(
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
GetResponse get(@RequestBody GetRequest request);
|
||||
|
||||
@PostMapping(
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
SessionCreateResponse createSession(@RequestBody SessionCreateRequest request);
|
||||
|
||||
@PostMapping(
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
SessionDestroyResponse destroySession(@RequestBody SessionDestroyRequest request);
|
||||
|
||||
@PostMapping(
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
SessionListResponse listSessions(@RequestBody SessionListRequest request);
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
class GetRequest {
|
||||
@Builder.Default private final String cmd = "request.get";
|
||||
@Builder.Default private final Integer maxTimeout = 120000;
|
||||
|
||||
private final String url;
|
||||
private final String session;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
class SessionCreateRequest {
|
||||
@Builder.Default private final String cmd = "sessions.create";
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
class SessionDestroyRequest {
|
||||
@Builder.Default private final String cmd = "sessions.destroy";
|
||||
private final String session;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
class SessionListRequest {
|
||||
@Builder.Default private final String cmd = "sessions.list";
|
||||
}
|
||||
|
||||
record GetResponse(String status, String message, Solution solution) {
|
||||
public record Solution(String url, Integer status, String response) {}
|
||||
}
|
||||
|
||||
record SessionCreateResponse(String status, String message, String session) {}
|
||||
|
||||
record SessionDestroyResponse(String status, String message) {}
|
||||
|
||||
record SessionListResponse(String status, String message, List<String> sessions) {}
|
||||
}
|
||||
@ -33,6 +33,18 @@ public class ManagementController {
|
||||
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(
|
||||
summary = "Cleanup unused S3 images",
|
||||
description = "Triggers the cleanup of untracked S3 images",
|
||||
|
||||
@ -2,6 +2,7 @@ package com.magamochi.mangamochi.controller;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.*;
|
||||
import com.magamochi.mangamochi.service.MangaImportService;
|
||||
import com.magamochi.mangamochi.service.providers.impl.BatoProvider;
|
||||
import com.magamochi.mangamochi.service.providers.impl.MangaDexProvider;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@ -19,6 +20,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
@RequiredArgsConstructor
|
||||
public class MangaImportController {
|
||||
private final MangaDexProvider mangaDexProvider;
|
||||
private final BatoProvider batoProvider;
|
||||
private final MangaImportService mangaImportService;
|
||||
|
||||
@Operation(
|
||||
@ -27,11 +29,22 @@ public class MangaImportController {
|
||||
tags = {"Manga Import"},
|
||||
operationId = "importFromMangaDex")
|
||||
@PostMapping("/manga-dex")
|
||||
public DefaultResponseDTO<ImportMangaDexResponseDTO> importFromMangaDex(
|
||||
@RequestBody ImportMangaDexRequestDTO requestDTO) {
|
||||
public DefaultResponseDTO<ImportMangaResponseDTO> importFromMangaDex(
|
||||
@RequestBody ImportRequestDTO requestDTO) {
|
||||
return DefaultResponseDTO.ok(mangaDexProvider.importManga(requestDTO.id()));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Import manga from Bato",
|
||||
description = "Imports manga data from Bato into the local database.",
|
||||
tags = {"Manga Import"},
|
||||
operationId = "importFromBato")
|
||||
@PostMapping("/bato")
|
||||
public DefaultResponseDTO<ImportMangaResponseDTO> importFromBato(
|
||||
@RequestBody ImportRequestDTO requestDTO) {
|
||||
return DefaultResponseDTO.ok(batoProvider.importManga(requestDTO.id()));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Upload multiple files",
|
||||
description = "Accepts multiple files via multipart/form-data and processes them.",
|
||||
|
||||
@ -2,4 +2,4 @@ package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record ImportMangaDexResponseDTO(@NotNull Long id) {}
|
||||
public record ImportMangaResponseDTO(@NotNull Long id) {}
|
||||
@ -1,6 +1,5 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.UUID;
|
||||
|
||||
public record ImportMangaDexRequestDTO(@NotNull UUID id) {}
|
||||
public record ImportRequestDTO(@NotNull String id) {}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.magamochi.mangamochi.model.entity;
|
||||
|
||||
import java.time.Instant;
|
||||
import lombok.Builder;
|
||||
|
||||
@Builder
|
||||
public record FlareSession(String sessionId, Instant lastAccess) {
|
||||
public FlareSession updateLastAccess() {
|
||||
return new FlareSession(this.sessionId, Instant.now());
|
||||
}
|
||||
}
|
||||
@ -57,7 +57,7 @@ public class Manga {
|
||||
@OneToMany(mappedBy = "manga")
|
||||
private List<MangaAlternativeTitle> alternativeTitles;
|
||||
|
||||
private Integer chapterCount;
|
||||
@Builder.Default private Integer chapterCount = 0;
|
||||
|
||||
private Boolean follow;
|
||||
@Builder.Default private Boolean follow = false;
|
||||
}
|
||||
|
||||
@ -32,5 +32,5 @@ public class Provider {
|
||||
@OneToMany(mappedBy = "provider")
|
||||
private List<MangaProvider> mangaProviders;
|
||||
|
||||
private Boolean supportsChapterFetch;
|
||||
@Builder.Default private Boolean supportsChapterFetch = true;
|
||||
}
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
package com.magamochi.mangamochi.registry;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.FlareSession;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import lombok.Getter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@Getter
|
||||
public class FlareSessionRegistry {
|
||||
private final ConcurrentHashMap<String, FlareSession> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
public FlareSession get(String provider) {
|
||||
return sessions.get(provider);
|
||||
}
|
||||
|
||||
public void put(String provider, FlareSession session) {
|
||||
sessions.put(provider, session);
|
||||
}
|
||||
|
||||
public void remove(String provider) {
|
||||
sessions.remove(provider);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.magamochi.mangamochi.service;
|
||||
|
||||
import com.magamochi.mangamochi.client.FlareClient;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FlareService {
|
||||
private final FlareClient client;
|
||||
private final FlareSessionManager sessionManager;
|
||||
|
||||
public Document getContentAsJsoupDocument(String url, String providerName) {
|
||||
return Jsoup.parse(getContent(url, providerName));
|
||||
}
|
||||
|
||||
private String getContent(String url, String providerName) {
|
||||
var session = sessionManager.getOrCreateSession(providerName);
|
||||
|
||||
return client
|
||||
.get(FlareClient.GetRequest.builder().url(url).session(session).build())
|
||||
.solution()
|
||||
.response();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package com.magamochi.mangamochi.service;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import com.magamochi.mangamochi.client.FlareClient;
|
||||
import com.magamochi.mangamochi.model.entity.FlareSession;
|
||||
import com.magamochi.mangamochi.registry.FlareSessionRegistry;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Log4j2
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FlareSessionManager {
|
||||
private static final Duration TIMEOUT = Duration.ofMinutes(15);
|
||||
|
||||
private final FlareClient flareClient;
|
||||
private final FlareSessionRegistry registry;
|
||||
|
||||
public String getOrCreateSession(String provider) {
|
||||
log.info("Getting session for {}", provider);
|
||||
|
||||
var session = registry.get(provider);
|
||||
|
||||
if (isNull(session) || isExpired(session)) {
|
||||
if (nonNull(session)) {
|
||||
log.info("Session expired for {}", provider);
|
||||
|
||||
flareClient.destroySession(
|
||||
FlareClient.SessionDestroyRequest.builder().session(session.sessionId()).build());
|
||||
|
||||
registry.remove(provider);
|
||||
} else {
|
||||
log.info("Session not found for {}", provider);
|
||||
}
|
||||
|
||||
log.info("Creating session for {}", provider);
|
||||
|
||||
var newId =
|
||||
flareClient.createSession(FlareClient.SessionCreateRequest.builder().build()).session();
|
||||
|
||||
session = new FlareSession(newId, Instant.now());
|
||||
registry.put(provider, session);
|
||||
} else {
|
||||
log.info("Got session for {}", provider);
|
||||
}
|
||||
|
||||
registry.put(provider, session.updateLastAccess());
|
||||
|
||||
return session.sessionId();
|
||||
}
|
||||
|
||||
private boolean isExpired(FlareSession session) {
|
||||
return Duration.between(session.lastAccess(), Instant.now()).compareTo(TIMEOUT) > 0;
|
||||
}
|
||||
}
|
||||
@ -6,12 +6,14 @@ import com.google.common.util.concurrent.RateLimiter;
|
||||
import com.magamochi.mangamochi.client.JikanClient;
|
||||
import com.magamochi.mangamochi.exception.NotFoundException;
|
||||
import com.magamochi.mangamochi.model.dto.ImportReviewDTO;
|
||||
import com.magamochi.mangamochi.model.dto.UpdateMangaDataCommand;
|
||||
import com.magamochi.mangamochi.model.entity.Manga;
|
||||
import com.magamochi.mangamochi.model.entity.MangaImportReview;
|
||||
import com.magamochi.mangamochi.model.entity.MangaProvider;
|
||||
import com.magamochi.mangamochi.model.repository.MangaImportReviewRepository;
|
||||
import com.magamochi.mangamochi.model.repository.MangaProviderRepository;
|
||||
import com.magamochi.mangamochi.model.repository.MangaRepository;
|
||||
import com.magamochi.mangamochi.queue.UpdateMangaDataProducer;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -27,6 +29,8 @@ public class MangaImportReviewService {
|
||||
|
||||
private final RateLimiter jikanRateLimiter;
|
||||
|
||||
private final UpdateMangaDataProducer updateMangaDataProducer;
|
||||
|
||||
public List<ImportReviewDTO> getImportReviews() {
|
||||
return mangaImportReviewRepository.findAll().stream().map(ImportReviewDTO::from).toList();
|
||||
}
|
||||
@ -67,6 +71,8 @@ public class MangaImportReviewService {
|
||||
.build());
|
||||
|
||||
mangaImportReviewRepository.delete(importReview);
|
||||
|
||||
updateMangaDataProducer.sendUpdateMangaDataCommand(new UpdateMangaDataCommand(manga.getId()));
|
||||
}
|
||||
|
||||
private MangaImportReview getImportReviewThrowIfNotFound(Long id) {
|
||||
|
||||
@ -5,4 +5,5 @@ public class ContentProviders {
|
||||
public static final String MANGA_LIVRE_BLOG = "Manga Livre Blog";
|
||||
public static final String MANGA_DEX = "MangaDex";
|
||||
public static final String PINK_ROSA_SCAN = "Pink Rosa Scan";
|
||||
public static final String BATO = "Bato";
|
||||
}
|
||||
|
||||
@ -0,0 +1,126 @@
|
||||
package com.magamochi.mangamochi.service.providers.impl;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
|
||||
import com.magamochi.mangamochi.exception.NotFoundException;
|
||||
import com.magamochi.mangamochi.exception.UnprocessableException;
|
||||
import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
||||
import com.magamochi.mangamochi.model.dto.ImportMangaResponseDTO;
|
||||
import com.magamochi.mangamochi.model.entity.MangaProvider;
|
||||
import com.magamochi.mangamochi.model.entity.Provider;
|
||||
import com.magamochi.mangamochi.model.enumeration.ProviderStatus;
|
||||
import com.magamochi.mangamochi.model.repository.MangaProviderRepository;
|
||||
import com.magamochi.mangamochi.model.repository.ProviderRepository;
|
||||
import com.magamochi.mangamochi.service.FlareService;
|
||||
import com.magamochi.mangamochi.service.MangaCreationService;
|
||||
import com.magamochi.mangamochi.service.providers.ContentProvider;
|
||||
import com.magamochi.mangamochi.service.providers.ContentProviders;
|
||||
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 {
|
||||
private static final String URL = "https://battwo.com";
|
||||
|
||||
private final FlareService flareService;
|
||||
private final MangaCreationService mangaCreationService;
|
||||
private final ProviderRepository providerRepository;
|
||||
private final MangaProviderRepository mangaProviderRepository;
|
||||
|
||||
@Override
|
||||
public List<ContentProviderMangaChapterResponseDTO> getAvailableChapters(MangaProvider 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/]");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public ImportMangaResponseDTO importManga(String url) {
|
||||
var document = flareService.getContentAsJsoupDocument(url, ContentProviders.BATO);
|
||||
|
||||
// Method 1: Look for the main title in the manga info section
|
||||
var titleElement = document.selectFirst("h3 a[href*=/title/]");
|
||||
if (isNull(titleElement)) {
|
||||
throw new UnprocessableException("Manga title not found for url: " + url);
|
||||
}
|
||||
|
||||
var mangaTitle = titleElement.text();
|
||||
|
||||
var provider =
|
||||
providerRepository
|
||||
.findByNameIgnoreCase("Bato")
|
||||
.orElseGet(
|
||||
() ->
|
||||
providerRepository.save(
|
||||
Provider.builder().name("Bato").status(ProviderStatus.ACTIVE).build()));
|
||||
|
||||
var manga = mangaCreationService.getOrCreateManga(mangaTitle, url, provider);
|
||||
|
||||
if (isNull(manga)) {
|
||||
throw new NotFoundException("Manga could not be found or created for url: " + url);
|
||||
}
|
||||
|
||||
mangaProviderRepository.save(
|
||||
MangaProvider.builder()
|
||||
.manga(manga)
|
||||
.mangaTitle(mangaTitle)
|
||||
.provider(provider)
|
||||
.url(url)
|
||||
.build());
|
||||
|
||||
return new ImportMangaResponseDTO(manga.getId());
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@ import com.magamochi.mangamochi.client.MangaDexClient;
|
||||
import com.magamochi.mangamochi.exception.NotFoundException;
|
||||
import com.magamochi.mangamochi.exception.UnprocessableException;
|
||||
import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
||||
import com.magamochi.mangamochi.model.dto.ImportMangaDexResponseDTO;
|
||||
import com.magamochi.mangamochi.model.dto.ImportMangaResponseDTO;
|
||||
import com.magamochi.mangamochi.model.entity.MangaProvider;
|
||||
import com.magamochi.mangamochi.model.entity.Provider;
|
||||
import com.magamochi.mangamochi.model.enumeration.ProviderStatus;
|
||||
@ -112,9 +112,9 @@ public class MangaDexProvider implements ContentProvider {
|
||||
LinkedHashMap::new));
|
||||
}
|
||||
|
||||
public ImportMangaDexResponseDTO importManga(UUID id) {
|
||||
public ImportMangaResponseDTO importManga(String id) {
|
||||
mangaDexRateLimiter.acquire();
|
||||
var resultData = mangaDexClient.getManga(id).data();
|
||||
var resultData = mangaDexClient.getManga(UUID.fromString(id)).data();
|
||||
|
||||
if (resultData.attributes().title().isEmpty()) {
|
||||
throw new UnprocessableException("Manga title not found for ID: " + id);
|
||||
@ -152,6 +152,6 @@ public class MangaDexProvider implements ContentProvider {
|
||||
.url(id.toString())
|
||||
.build());
|
||||
|
||||
return new ImportMangaDexResponseDTO(manga.getId());
|
||||
return new ImportMangaResponseDTO(manga.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,6 +32,11 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
|
||||
@Override
|
||||
public List<ContentProviderMangaChapterResponseDTO> getAvailableChapters(
|
||||
MangaProvider mangaProvider) {
|
||||
log.info(
|
||||
"Getting available chapters from {}, manga {}",
|
||||
ContentProviders.MANGA_LIVRE_BLOG,
|
||||
mangaProvider.getManga().getTitle());
|
||||
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(mangaProvider.getUrl());
|
||||
|
||||
@ -59,6 +64,8 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
|
||||
|
||||
@Override
|
||||
public Map<Integer, String> getChapterImagesUrls(String chapterUrl) {
|
||||
log.info("Getting images from {}, url {}", ContentProviders.MANGA_LIVRE_BLOG, chapterUrl);
|
||||
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(chapterUrl);
|
||||
|
||||
@ -97,6 +104,8 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
|
||||
|
||||
@Override
|
||||
public List<ContentProviderMangaInfoResponseDTO> getMangasFromPage(Integer page) {
|
||||
log.info("Getting mangas from {}, page {}", ContentProviders.MANGA_LIVRE_BLOG, page);
|
||||
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(url + "page/" + page);
|
||||
|
||||
@ -148,6 +157,8 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv
|
||||
|
||||
@Override
|
||||
public Integer getTotalPages() {
|
||||
log.info("Getting total pages for {}", ContentProviders.MANGA_LIVRE_BLOG);
|
||||
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(url);
|
||||
|
||||
|
||||
@ -4,11 +4,10 @@ import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO
|
||||
import com.magamochi.mangamochi.model.dto.ContentProviderMangaInfoResponseDTO;
|
||||
import com.magamochi.mangamochi.model.entity.MangaProvider;
|
||||
import com.magamochi.mangamochi.model.enumeration.MangaStatus;
|
||||
import com.magamochi.mangamochi.service.WebScrapperClientProxyService;
|
||||
import com.magamochi.mangamochi.service.FlareService;
|
||||
import com.magamochi.mangamochi.service.providers.ContentProvider;
|
||||
import com.magamochi.mangamochi.service.providers.ContentProviders;
|
||||
import com.magamochi.mangamochi.service.providers.PagedContentProvider;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
@ -22,12 +21,18 @@ import org.springframework.stereotype.Service;
|
||||
public class MangaLivreProvider implements ContentProvider, PagedContentProvider {
|
||||
private final String url = "https://mangalivre.tv/manga/";
|
||||
|
||||
private final WebScrapperClientProxyService webScrapperClientProxyService;
|
||||
private final FlareService flareService;
|
||||
|
||||
@Override
|
||||
public List<ContentProviderMangaChapterResponseDTO> getAvailableChapters(MangaProvider provider) {
|
||||
log.info(
|
||||
"Getting available chapters from {}, manga {}",
|
||||
ContentProviders.MANGA_LIVRE,
|
||||
provider.getManga().getTitle());
|
||||
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(provider.getUrl());
|
||||
var document =
|
||||
flareService.getContentAsJsoupDocument(provider.getUrl(), ContentProviders.MANGA_LIVRE);
|
||||
|
||||
var chapterItems = document.getElementsByClass("wp-manga-chapter");
|
||||
|
||||
@ -40,7 +45,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
|
||||
linkElement.text(), linkElement.attr("href"), null, null);
|
||||
})
|
||||
.toList();
|
||||
} catch (NoSuchElementException | IOException e) {
|
||||
} catch (NoSuchElementException e) {
|
||||
log.error("Error parsing mangas from MangaLivre", e);
|
||||
return List.of();
|
||||
}
|
||||
@ -48,8 +53,11 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
|
||||
|
||||
@Override
|
||||
public Map<Integer, String> getChapterImagesUrls(String chapterUrl) {
|
||||
log.info("Getting images from {}, url {}", ContentProviders.MANGA_LIVRE, chapterUrl);
|
||||
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(chapterUrl);
|
||||
var document =
|
||||
flareService.getContentAsJsoupDocument(chapterUrl, ContentProviders.MANGA_LIVRE);
|
||||
|
||||
var chapterImagesContainer = document.getElementsByClass("chapter-images").getFirst();
|
||||
var chapterImagesElements = chapterImagesContainer.getElementsByClass("page-break");
|
||||
@ -68,7 +76,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new));
|
||||
} catch (NoSuchElementException | IOException e) {
|
||||
} catch (NoSuchElementException e) {
|
||||
log.error("Error parsing mangas from MangaLivre", e);
|
||||
return Map.of();
|
||||
}
|
||||
@ -76,8 +84,12 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
|
||||
|
||||
@Override
|
||||
public List<ContentProviderMangaInfoResponseDTO> getMangasFromPage(Integer page) {
|
||||
log.info("Getting mangas from {}, page {}", ContentProviders.MANGA_LIVRE, page);
|
||||
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(url + "page/" + page);
|
||||
var document =
|
||||
flareService.getContentAsJsoupDocument(
|
||||
url + "page/" + page, ContentProviders.MANGA_LIVRE);
|
||||
|
||||
var mangaElements = document.getElementsByClass("manga__item");
|
||||
|
||||
@ -115,7 +127,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
|
||||
title, url, imgUrl, MangaStatus.UNKNOWN);
|
||||
})
|
||||
.toList();
|
||||
} catch (NoSuchElementException | IOException e) {
|
||||
} catch (NoSuchElementException e) {
|
||||
log.error("Error parsing mangas from MangaLivre", e);
|
||||
return List.of();
|
||||
}
|
||||
@ -123,8 +135,10 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
|
||||
|
||||
@Override
|
||||
public Integer getTotalPages() {
|
||||
log.info("Getting total pages for {}", ContentProviders.MANGA_LIVRE);
|
||||
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(url);
|
||||
var document = flareService.getContentAsJsoupDocument(url, ContentProviders.MANGA_LIVRE);
|
||||
|
||||
var navLinks = document.getElementsByClass("wp-pagenavi").getFirst();
|
||||
var lastPageElement = navLinks.getElementsByClass("last").getFirst();
|
||||
@ -132,7 +146,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
|
||||
|
||||
var totalPages = links.replaceAll("\\D+", "");
|
||||
return Integer.parseInt(totalPages);
|
||||
} catch (NoSuchElementException | IOException e) {
|
||||
} catch (NoSuchElementException e) {
|
||||
log.error("Error parsing total pages from MangaLivre", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -7,11 +7,10 @@ import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO
|
||||
import com.magamochi.mangamochi.model.dto.ContentProviderMangaInfoResponseDTO;
|
||||
import com.magamochi.mangamochi.model.entity.MangaProvider;
|
||||
import com.magamochi.mangamochi.model.enumeration.MangaStatus;
|
||||
import com.magamochi.mangamochi.service.WebScrapperClientProxyService;
|
||||
import com.magamochi.mangamochi.service.FlareService;
|
||||
import com.magamochi.mangamochi.service.providers.ContentProvider;
|
||||
import com.magamochi.mangamochi.service.providers.ContentProviders;
|
||||
import com.magamochi.mangamochi.service.providers.PagedContentProvider;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
@ -23,12 +22,22 @@ import org.springframework.stereotype.Service;
|
||||
@Service(ContentProviders.PINK_ROSA_SCAN)
|
||||
@RequiredArgsConstructor
|
||||
public class PinkRosaScanProvider implements ContentProvider, PagedContentProvider {
|
||||
private final WebScrapperClientProxyService webScrapperClientProxyService;
|
||||
private static final String URL =
|
||||
"https://scanpinkrosa.blogspot.com/search/label/Series?max-results=1000";
|
||||
|
||||
private final FlareService flareService;
|
||||
|
||||
@Override
|
||||
public List<ContentProviderMangaChapterResponseDTO> getAvailableChapters(MangaProvider provider) {
|
||||
log.info(
|
||||
"Getting available chapters from {}, manga {}",
|
||||
ContentProviders.PINK_ROSA_SCAN,
|
||||
provider.getManga().getTitle());
|
||||
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(provider.getUrl());
|
||||
var document =
|
||||
flareService.getContentAsJsoupDocument(
|
||||
provider.getUrl(), ContentProviders.PINK_ROSA_SCAN);
|
||||
|
||||
var chapterList = document.getElementById("chapter-list");
|
||||
if (isNull(chapterList)) {
|
||||
@ -52,7 +61,7 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
|
||||
chapterTitleElement.text().trim(), chapterItemElement.attr("href"), null, null);
|
||||
})
|
||||
.toList();
|
||||
} catch (NoSuchElementException | IOException e) {
|
||||
} catch (NoSuchElementException e) {
|
||||
log.error("Error parsing mangas from Pink Rosa Scan", e);
|
||||
return List.of();
|
||||
}
|
||||
@ -60,8 +69,11 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
|
||||
|
||||
@Override
|
||||
public Map<Integer, String> getChapterImagesUrls(String chapterUrl) {
|
||||
log.info("Getting images from {}, url {}", ContentProviders.PINK_ROSA_SCAN, chapterUrl);
|
||||
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(chapterUrl);
|
||||
var document =
|
||||
flareService.getContentAsJsoupDocument(chapterUrl, ContentProviders.PINK_ROSA_SCAN);
|
||||
|
||||
var chapterImagesContainer = document.getElementById("pages");
|
||||
if (isNull(chapterImagesContainer)) {
|
||||
@ -87,7 +99,7 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new));
|
||||
} catch (NoSuchElementException | IOException e) {
|
||||
} catch (NoSuchElementException e) {
|
||||
log.error("Error parsing mangas from Pink Rosa Scan", e);
|
||||
return Map.of();
|
||||
}
|
||||
@ -100,10 +112,10 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
|
||||
|
||||
@Override
|
||||
public List<ContentProviderMangaInfoResponseDTO> getMangasFromPage(Integer page) {
|
||||
log.info("Getting mangas from {}", ContentProviders.PINK_ROSA_SCAN);
|
||||
|
||||
try {
|
||||
var document =
|
||||
webScrapperClientProxyService.scrapeToJsoupDocument(
|
||||
"https://scanpinkrosa.blogspot.com/search/label/Series?max-results=1000");
|
||||
var document = flareService.getContentAsJsoupDocument(URL, ContentProviders.PINK_ROSA_SCAN);
|
||||
|
||||
var mangaElements =
|
||||
document.getElementsByClass("grid relative sm:gap-3.5 gap-[2.5vw] w-full h-fit");
|
||||
@ -130,7 +142,7 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid
|
||||
title, url, null, MangaStatus.UNKNOWN);
|
||||
})
|
||||
.toList();
|
||||
} catch (NoSuchElementException | IOException e) {
|
||||
} catch (NoSuchElementException e) {
|
||||
log.error("Error parsing mangas from Pink Rosa Scan", e);
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
package com.magamochi.mangamochi.task;
|
||||
|
||||
import com.magamochi.mangamochi.client.FlareClient;
|
||||
import com.magamochi.mangamochi.registry.FlareSessionRegistry;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FlareSessionCleanupTask {
|
||||
private static final Duration TIMEOUT = Duration.ofMinutes(15);
|
||||
|
||||
private final FlareClient client;
|
||||
private final FlareSessionRegistry registry;
|
||||
|
||||
@Scheduled(fixedDelayString = "1m")
|
||||
public void cleanExpiredSessions() {
|
||||
registry
|
||||
.getSessions()
|
||||
.forEach(
|
||||
(provider, session) -> {
|
||||
if (Duration.between(session.lastAccess(), Instant.now()).compareTo(TIMEOUT) <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.destroySession(
|
||||
FlareClient.SessionDestroyRequest.builder().session(session.sessionId()).build());
|
||||
|
||||
registry.remove(provider);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.magamochi.mangamochi.task;
|
||||
|
||||
import com.magamochi.mangamochi.client.FlareClient;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Log4j2
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FlareStartupCleanupTask {
|
||||
private final FlareClient client;
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void cleanupExistingSessions() {
|
||||
var sessions = client.listSessions(FlareClient.SessionListRequest.builder().build()).sessions();
|
||||
|
||||
for (var sessionId : sessions) {
|
||||
client.destroySession(FlareClient.SessionDestroyRequest.builder().session(sessionId).build());
|
||||
}
|
||||
|
||||
log.info("FlareSolverr session cleanup completed on startup.");
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
package com.magamochi.mangamochi.task;
|
||||
|
||||
import com.magamochi.mangamochi.exception.NotFoundException;
|
||||
import com.magamochi.mangamochi.model.dto.MangaListUpdateCommand;
|
||||
import com.magamochi.mangamochi.model.repository.ProviderRepository;
|
||||
import com.magamochi.mangamochi.queue.UpdateMangaListProducer;
|
||||
import com.magamochi.mangamochi.service.providers.PagedContentProvider;
|
||||
import com.magamochi.mangamochi.service.providers.PagedContentProviderFactory;
|
||||
@ -20,6 +22,7 @@ public class UpdateMangaListTask {
|
||||
|
||||
private final PagedContentProviderFactory contentProviderFactory;
|
||||
private final UpdateMangaListProducer updateMangaListProducer;
|
||||
private final ProviderRepository providerRepository;
|
||||
|
||||
@Scheduled(cron = "${content-providers.cron-expression}")
|
||||
public void updateMangaListScheduled() {
|
||||
@ -37,6 +40,16 @@ public class UpdateMangaListTask {
|
||||
contentProviders.forEach(this::updateProviderMangaList);
|
||||
}
|
||||
|
||||
public void updateProviderMangaList(Long providerId) {
|
||||
var provider =
|
||||
providerRepository
|
||||
.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);
|
||||
@ -45,10 +58,9 @@ public class UpdateMangaListTask {
|
||||
|
||||
IntStream.rangeClosed(1, pages)
|
||||
.forEach(
|
||||
page -> {
|
||||
updateMangaListProducer.sendUpdateMangaListCommand(
|
||||
new MangaListUpdateCommand(contentProviderName, page));
|
||||
});
|
||||
page ->
|
||||
updateMangaListProducer.sendUpdateMangaListCommand(
|
||||
new MangaListUpdateCommand(contentProviderName, page)));
|
||||
|
||||
log.info("Manga list update queued for content provider {}.", contentProviderName);
|
||||
}
|
||||
|
||||
@ -38,6 +38,9 @@ springdoc:
|
||||
web-scrapper:
|
||||
endpoint: ${WEBSCRAPPER_ENDPOINT}
|
||||
|
||||
flare-solverr:
|
||||
endpoint: ${FLARESOLVERR_ENDPOINT}
|
||||
|
||||
minio:
|
||||
endpoint: ${MINIO_ENDPOINT}
|
||||
accessKey: ${MINIO_USER}
|
||||
@ -59,6 +62,12 @@ manga-matcher:
|
||||
resilience4j:
|
||||
retry:
|
||||
instances:
|
||||
FlareSolverrRetry:
|
||||
max-attempts: 2
|
||||
wait-duration:
|
||||
seconds: 5
|
||||
retry-exceptions:
|
||||
- feign.FeignException
|
||||
MangaDexRetry:
|
||||
max-attempts: 5
|
||||
wait-duration:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user