feat: update Manga Livre provider

This commit is contained in:
Rodrigo Verdiani 2025-11-29 21:27:52 -03:00
parent 4b0a5ab3e5
commit 5883007591
9 changed files with 118 additions and 60 deletions

View File

@ -6,6 +6,7 @@ import com.magamochi.mangamochi.model.entity.Manga;
import com.magamochi.mangamochi.model.entity.MangaAlternativeTitle;
import com.magamochi.mangamochi.model.entity.MangaChapter;
import com.magamochi.mangamochi.model.entity.MangaProvider;
import com.magamochi.mangamochi.model.enumeration.ProviderStatus;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.OffsetDateTime;
@ -53,6 +54,7 @@ public record MangaDTO(
public record MangaProviderDTO(
@NotNull long id,
@NotBlank String providerName,
@NotNull ProviderStatus providerStatus,
@NotNull Integer chaptersAvailable,
@NotNull Integer chaptersDownloaded,
@NotNull Boolean supportsChapterFetch) {
@ -64,6 +66,7 @@ public record MangaDTO(
return new MangaProviderDTO(
mangaProvider.getId(),
mangaProvider.getProvider().getName(),
mangaProvider.getProvider().getStatus(),
chaptersAvailable,
chaptersDownloaded,
mangaProvider.getProvider().getSupportsChapterFetch());

View File

@ -4,6 +4,7 @@ import com.magamochi.mangamochi.model.dto.MangaChapterDownloadCommand;
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
@ -12,8 +13,11 @@ import org.springframework.stereotype.Service;
public class MangaChapterDownloadProducer {
private final RabbitTemplate rabbitTemplate;
@Value("${rabbit-mq.queues.manga-chapter-download}")
private String mangaChapterDownloadQueue;
public void sendMangaChapterDownloadCommand(MangaChapterDownloadCommand command) {
rabbitTemplate.convertAndSend("${rabbit-mq.queues.manga-chapter-download}", command);
rabbitTemplate.convertAndSend(mangaChapterDownloadQueue, command);
log.info("Sent manga chapter download command: {}", command);
}
}

View File

@ -4,6 +4,7 @@ import com.magamochi.mangamochi.model.dto.UpdateMangaDataCommand;
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
@ -12,8 +13,11 @@ import org.springframework.stereotype.Service;
public class UpdateMangaDataProducer {
private final RabbitTemplate rabbitTemplate;
@Value("${rabbit-mq.queues.manga-data-update}")
private String mangaDataUpdateQueue;
public void sendUpdateMangaDataCommand(UpdateMangaDataCommand command) {
rabbitTemplate.convertAndSend("${rabbit-mq.queues.manga-data-update}", command);
rabbitTemplate.convertAndSend(mangaDataUpdateQueue, command);
log.info("Sent update manga data command: {}", command);
}
}

View File

@ -4,6 +4,7 @@ import com.magamochi.mangamochi.model.dto.UpdateMangaFollowChapterListCommand;
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
@ -12,8 +13,11 @@ import org.springframework.stereotype.Service;
public class UpdateMangaFollowChapterListProducer {
private final RabbitTemplate rabbitTemplate;
@Value("${rabbit-mq.queues.manga-follow-update-chapter}")
private String mangaFollowUpdateChapterQueue;
public void sendUpdateMangaFollowChapterListCommand(UpdateMangaFollowChapterListCommand command) {
rabbitTemplate.convertAndSend("${rabbit-mq.queues.manga-follow-update-chapter}", command);
rabbitTemplate.convertAndSend(mangaFollowUpdateChapterQueue, command);
log.info("Sent update followed manga chapter list command: {}", command);
}
}

View File

@ -4,6 +4,7 @@ import com.magamochi.mangamochi.model.dto.MangaListUpdateCommand;
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
@ -12,8 +13,11 @@ import org.springframework.stereotype.Service;
public class UpdateMangaListProducer {
private final RabbitTemplate rabbitTemplate;
@Value("${rabbit-mq.queues.manga-list-update}")
private String mangaListUpdateQueue;
public void sendUpdateMangaListCommand(MangaListUpdateCommand command) {
rabbitTemplate.convertAndSend("${rabbit-mq.queues.manga-list-update}", command);
rabbitTemplate.convertAndSend(mangaListUpdateQueue, command);
log.info("Sent update manga list command: {}", command);
}
}

View File

@ -1,7 +1,7 @@
package com.magamochi.mangamochi.service.providers;
public class ContentProviders {
public static final String MANGA_LIVRE = "Manga Livre";
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_DEX = "MangaDex";
public static final String PINK_ROSA_SCAN = "Pink Rosa Scan";

View File

@ -1,5 +1,7 @@
package com.magamochi.mangamochi.service.providers.impl;
import static java.util.Objects.nonNull;
import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO;
import com.magamochi.mangamochi.model.dto.ContentProviderMangaInfoResponseDTO;
import com.magamochi.mangamochi.model.entity.MangaProvider;
@ -16,10 +18,11 @@ import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
@Log4j2
@Service(ContentProviders.MANGA_LIVRE)
@Service(ContentProviders.MANGA_LIVRE_TO)
@RequiredArgsConstructor
public class MangaLivreProvider implements ContentProvider, PagedContentProvider {
private final String url = "https://mangalivre.tv/manga/";
private final String url = "https://mangalivre.to/manga/";
private final int MANGAS_PER_PAGE = 10;
private final FlareService flareService;
@ -27,22 +30,27 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
public List<ContentProviderMangaChapterResponseDTO> getAvailableChapters(MangaProvider provider) {
log.info(
"Getting available chapters from {}, manga {}",
ContentProviders.MANGA_LIVRE,
ContentProviders.MANGA_LIVRE_TO,
provider.getManga().getTitle());
try {
var document =
flareService.getContentAsJsoupDocument(provider.getUrl(), ContentProviders.MANGA_LIVRE);
flareService.getContentAsJsoupDocument(
provider.getUrl(), ContentProviders.MANGA_LIVRE_TO);
var chapterItems = document.getElementsByClass("wp-manga-chapter");
var chapterBoxes =
document.selectFirst("div.listing-chapters-wrap").select("div.chapter-box");
return chapterItems.stream()
return chapterBoxes.stream()
.map(
chapterItemElement -> {
var linkElement = chapterItemElement.getElementsByTag("a").getFirst();
chapterBox -> {
var linkElement = chapterBox.selectFirst("a");
var url = linkElement.attr("href");
var title = linkElement.text();
return new ContentProviderMangaChapterResponseDTO(
linkElement.text(), linkElement.attr("href"), null, null);
title.trim(), url.trim(), null, null);
})
.toList();
} catch (NoSuchElementException e) {
@ -53,21 +61,19 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
@Override
public Map<Integer, String> getChapterImagesUrls(String chapterUrl) {
log.info("Getting images from {}, url {}", ContentProviders.MANGA_LIVRE, chapterUrl);
log.info("Getting images from {}, url {}", ContentProviders.MANGA_LIVRE_TO, chapterUrl);
try {
var document =
flareService.getContentAsJsoupDocument(chapterUrl, ContentProviders.MANGA_LIVRE);
flareService.getContentAsJsoupDocument(chapterUrl, ContentProviders.MANGA_LIVRE_TO);
var chapterImagesContainer = document.getElementsByClass("chapter-images").getFirst();
var chapterImagesElements = chapterImagesContainer.getElementsByClass("page-break");
var chapterImagesElements = document.select("div.reading-content img.wp-manga-chapter-img");
var imageUrls =
chapterImagesElements.stream()
.map(
chapterImagesElement -> {
var imageElement = chapterImagesElement.getElementsByTag("img").getFirst();
return imageElement.attr("src");
return chapterImagesElement.attr("src");
})
.toList();
@ -77,54 +83,40 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
Collectors.toMap(
i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new));
} catch (NoSuchElementException e) {
log.error("Error parsing mangas from MangaLivre", e);
log.error("Error parsing manga images from MangaLivre", e);
return Map.of();
}
}
@Override
public List<ContentProviderMangaInfoResponseDTO> getMangasFromPage(Integer page) {
log.info("Getting mangas from {}, page {}", ContentProviders.MANGA_LIVRE, page);
log.info("Getting mangas from {}, page {}", ContentProviders.MANGA_LIVRE_TO, page);
try {
var document =
flareService.getContentAsJsoupDocument(
url + "page/" + page, ContentProviders.MANGA_LIVRE);
url + "page/" + page, ContentProviders.MANGA_LIVRE_TO);
var mangaElements = document.getElementsByClass("manga__item");
var mangaElements = document.select("div.page-item-detail.manga");
return mangaElements.stream()
.map(
element -> {
var mangaTitleElement =
element
.getElementsByClass("manga__content")
.getFirst()
.getElementsByClass("manga__content_item")
.getFirst()
.getElementsByClass("post-title font-title")
.getFirst()
.getElementsByTag("h2")
.getFirst();
var linkElement = mangaTitleElement.getElementsByTag("a").getFirst();
var linkElement = element.selectFirst(".item-thumb > a");
var url = linkElement.attr("href");
var title = linkElement.text().trim();
var imageElement =
element
.getElementsByClass("manga__thumb")
.getFirst()
.getElementsByClass("manga__thumb_item")
.getFirst()
.getElementsByTag("a")
.getFirst()
.getElementsByTag("img")
.getFirst();
var imgUrl = imageElement.attr("src");
var title = linkElement.attr("title");
// Fallback: If 'title' attribute is empty, try the <img> tag's 'alt' or 'title'
if (title.isBlank()) {
var imgElement = linkElement.selectFirst("img");
if (nonNull(imgElement)) {
title = imgElement.attr("alt");
}
}
return new ContentProviderMangaInfoResponseDTO(
title, url, imgUrl, MangaStatus.UNKNOWN);
title.trim(), url.trim(), null, MangaStatus.UNKNOWN);
})
.toList();
} catch (NoSuchElementException e) {
@ -135,17 +127,16 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider
@Override
public Integer getTotalPages() {
log.info("Getting total pages for {}", ContentProviders.MANGA_LIVRE);
log.info("Getting total pages for {}", ContentProviders.MANGA_LIVRE_TO);
try {
var document = flareService.getContentAsJsoupDocument(url, ContentProviders.MANGA_LIVRE);
var document = flareService.getContentAsJsoupDocument(url, ContentProviders.MANGA_LIVRE_TO);
var navLinks = document.getElementsByClass("wp-pagenavi").getFirst();
var lastPageElement = navLinks.getElementsByClass("last").getFirst();
var links = lastPageElement.attr("href");
var fullText = document.selectFirst("div.h4:contains(resultados)").text();
var numberString = fullText.replace("resultados", "").trim().replaceAll("[^0-9]", "");
var totalMangas = Integer.parseInt(numberString);
var totalPages = links.replaceAll("\\D+", "");
return Integer.parseInt(totalPages);
return (int) Math.ceil((double) totalMangas / MANGAS_PER_PAGE);
} catch (NoSuchElementException e) {
log.error("Error parsing total pages from MangaLivre", e);
return null;

View File

@ -2,6 +2,7 @@ package com.magamochi.mangamochi.task;
import com.magamochi.mangamochi.model.dto.UpdateMangaFollowChapterListCommand;
import com.magamochi.mangamochi.model.entity.Manga;
import com.magamochi.mangamochi.model.enumeration.ProviderStatus;
import com.magamochi.mangamochi.model.repository.MangaRepository;
import com.magamochi.mangamochi.queue.UpdateMangaFollowChapterListProducer;
import lombok.RequiredArgsConstructor;
@ -43,10 +44,14 @@ public class MangaFollowUpdateTask {
log.info("Fetching available mangas for followed Manga {}", manga.getTitle());
var mangaProviders = manga.getMangaProviders();
mangaProviders.forEach(
mangaProvider ->
producer.sendUpdateMangaFollowChapterListCommand(
new UpdateMangaFollowChapterListCommand(mangaProvider.getId())));
mangaProviders.stream()
.filter(
mangaProvider -> mangaProvider.getProvider().getStatus().equals(ProviderStatus.ACTIVE))
.forEach(
mangaProvider ->
producer.sendUpdateMangaFollowChapterListCommand(
new UpdateMangaFollowChapterListCommand(mangaProvider.getId())));
log.info("Followed Manga ({}) chapter list update queued.", manga.getTitle());
}

View File

@ -0,0 +1,43 @@
DO
$$
DECLARE
_target_provider_name TEXT := 'Manga Livre';
_target_provider_id BIGINT;
_deleted_chapters_count INTEGER;
_deleted_manga_providers_count INTEGER;
BEGIN
SELECT id
INTO _target_provider_id
FROM providers
WHERE name = _target_provider_name;
IF _target_provider_id IS NULL THEN
RAISE EXCEPTION 'Provider with name "%" not found.', _target_provider_name;
END IF;
UPDATE providers SET status = 'INACTIVE' WHERE id = _target_provider_id;
-- Delete non-downloaded manga chapters associated with the target provider
DELETE
FROM manga_chapters mc
USING manga_provider mp
WHERE mc.manga_provider_id = mp.id
AND mp.provider_id = _target_provider_id
AND mc.downloaded = FALSE;
GET DIAGNOSTICS _deleted_chapters_count = ROW_COUNT;
RAISE NOTICE 'Deleted % non-downloaded chapters for provider ID %.', _deleted_chapters_count, _target_provider_id;
-- Delete MangaProvider records ONLY if NO chapters
DELETE
FROM manga_provider mp
WHERE mp.provider_id = _target_provider_id
AND NOT EXISTS (SELECT 1
FROM manga_chapters mc
WHERE mc.manga_provider_id = mp.id);
GET DIAGNOSTICS _deleted_manga_providers_count = ROW_COUNT;
RAISE NOTICE 'Deleted % manga_provider entries', _deleted_manga_providers_count;
END
$$;