Merge pull request 'feat: implement manga follow update task with RabbitMQ integration and notification system' (#55) from feat/manga-follow into main

Reviewed-on: #55
This commit is contained in:
rov 2026-05-01 15:12:37 -03:00
commit ebd38e06ee
12 changed files with 47 additions and 69 deletions

View File

@ -1,4 +1,4 @@
package com.magamochi.model.specification;
package com.magamochi.catalog.model.specification;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;

View File

@ -7,8 +7,8 @@ import com.magamochi.catalog.model.dto.MangaListDTO;
import com.magamochi.catalog.model.dto.MangaListFilterDTO;
import com.magamochi.catalog.model.entity.Manga;
import com.magamochi.catalog.model.repository.MangaRepository;
import com.magamochi.catalog.model.specification.MangaSpecification;
import com.magamochi.common.exception.NotFoundException;
import com.magamochi.model.specification.MangaSpecification;
import com.magamochi.user.service.UserService;
import com.magamochi.userinteraction.model.repository.UserFavoriteMangaRepository;
import com.magamochi.userinteraction.model.repository.UserMangaFollowRepository;

View File

@ -52,6 +52,9 @@ public class RabbitConfig {
@Value("${queues.manga-content-download}")
private String mangaContentDownloadQueue;
@Value("${queues.manga-follow-update-chapter}")
private String mangaFollowUpdateChapterQueue;
@Bean
public TopicExchange imageUpdatesExchange() {
return new TopicExchange(imageUpdatesTopic);
@ -220,11 +223,6 @@ public class RabbitConfig {
return QueueBuilder.durable(mangaContentDownloadQueue + ".dlq").quorum().build();
}
// TODO: remove unused queues
@Value("${rabbit-mq.queues.manga-follow-update-chapter}")
private String mangaFollowUpdateChapterQueue;
@Bean
public Queue mangaFollowUpdateChapterQueue() {
return QueueBuilder.durable(mangaFollowUpdateChapterQueue)

View File

@ -14,6 +14,7 @@ import com.magamochi.content.model.entity.MangaContentImage;
import com.magamochi.content.model.repository.MangaContentImageRepository;
import com.magamochi.content.model.repository.MangaContentRepository;
import com.magamochi.image.service.ImageService;
import com.magamochi.userinteraction.service.UserMangaFollowService;
import jakarta.validation.constraints.NotBlank;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
@ -28,6 +29,7 @@ public class ContentIngestService {
private final ContentService contentService;
private final MangaContentProviderService mangaContentProviderService;
private final LanguageService languageService;
private final UserMangaFollowService userMangaFollowService;
private final MangaContentRepository mangaContentRepository;
private final MangaContentImageRepository mangaContentImageRepository;
@ -66,6 +68,8 @@ public class ContentIngestService {
.language(language)
.build());
userMangaFollowService.notifyNewMangaContent(mangaContent);
log.info(
"Ingested Manga Content ({}) for provider {}: {}",
title,

View File

@ -3,8 +3,8 @@ package com.magamochi.controller;
import com.magamochi.client.NtfyClient;
import com.magamochi.common.model.dto.DefaultResponseDTO;
import com.magamochi.image.task.ImageCleanupTask;
import com.magamochi.task.MangaFollowUpdateTask;
import com.magamochi.user.repository.UserRepository;
import com.magamochi.userinteraction.task.MangaFollowUpdateTask;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

View File

@ -1,45 +0,0 @@
package com.magamochi.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
@Log4j2
@Service
@RequiredArgsConstructor
public class OldMangaService {
// public void fetchFollowedMangaChapters(Long mangaProviderId) {
// var mangaProvider =
// mangaContentProviderRepository
// .findById(mangaProviderId)
// .orElseThrow(
// () -> new NotFoundException("Manga Provider not found for ID: " +
// mangaProviderId));
//
// var currentAvailableChapterCount =
// mangaChapterRepository.findByMangaContentProviderId(mangaProviderId).size();
//
// fetchMangaChapters(mangaProviderId);
// mangaChapterRepository.flush();
//
// var availableChapterCount =
// mangaChapterRepository.findByMangaContentProviderId(mangaProviderId).size();
//
// if (availableChapterCount <= currentAvailableChapterCount) {
// return;
// }
//
// log.info("New chapters found for Manga Provider {}", mangaProviderId);
//
// var userMangaFollows = userMangaFollowRepository.findByManga(mangaProvider.getManga());
// userMangaFollows.forEach(
// umf ->
// ntfyClient.notify(
// new NtfyClient.Request(
// "mangamochi-" + umf.getUser().getId().toString(),
// umf.getManga().getTitle(),
// "New chapter available on " +
// mangaProvider.getContentProvider().getName())));
// }
}

View File

@ -1,3 +1,3 @@
package com.magamochi.model.dto;
package com.magamochi.userinteraction.queue.command;
public record UpdateMangaFollowChapterListCommand(Long mangaProviderId) {}

View File

@ -1,7 +1,7 @@
package com.magamochi.queue;
package com.magamochi.userinteraction.queue.consumer;
import com.magamochi.model.dto.UpdateMangaFollowChapterListCommand;
import com.magamochi.service.OldMangaService;
import com.magamochi.userinteraction.queue.command.UpdateMangaFollowChapterListCommand;
import com.magamochi.userinteraction.service.UserMangaFollowService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
@ -11,11 +11,11 @@ import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UpdateMangaFollowChapterListConsumer {
private final OldMangaService oldMangaService;
private final UserMangaFollowService userMangaFollowService;
@RabbitListener(queues = "${rabbit-mq.queues.manga-follow-update-chapter}")
public void receiveMangaFollowUpdateChapterCommand(UpdateMangaFollowChapterListCommand command) {
log.info("Received update followed manga chapter list command: {}", command);
// oldMangaService.fetchFollowedMangaChapters(command.mangaProviderId());
userMangaFollowService.checkForMangaUpdates(command.mangaProviderId());
}
}

View File

@ -1,6 +1,6 @@
package com.magamochi.queue;
package com.magamochi.userinteraction.queue.producer;
import com.magamochi.model.dto.UpdateMangaFollowChapterListCommand;
import com.magamochi.userinteraction.queue.command.UpdateMangaFollowChapterListCommand;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.rabbit.core.RabbitTemplate;

View File

@ -2,7 +2,10 @@ package com.magamochi.userinteraction.service;
import com.magamochi.catalog.model.entity.Manga;
import com.magamochi.catalog.model.repository.MangaRepository;
import com.magamochi.client.NtfyClient;
import com.magamochi.common.exception.NotFoundException;
import com.magamochi.content.model.entity.MangaContent;
import com.magamochi.ingestion.service.IngestionService;
import com.magamochi.user.service.UserService;
import com.magamochi.userinteraction.model.entity.UserMangaFollow;
import com.magamochi.userinteraction.model.repository.UserMangaFollowRepository;
@ -14,10 +17,13 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class UserMangaFollowService {
private final UserService userService;
private final IngestionService ingestionService;
private final MangaRepository mangaRepository;
private final UserMangaFollowRepository userMangaFollowRepository;
private final NtfyClient ntfyClient;
@Transactional
public void follow(Long mangaId) {
var user = userService.getLoggedUserThrowIfNotFound();
@ -50,4 +56,21 @@ public class UserMangaFollowService {
.findById(mangaId)
.orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
}
public void checkForMangaUpdates(Long contentProviderId) {
ingestionService.fetchMangaContentProviderContentList(contentProviderId);
}
public void notifyNewMangaContent(MangaContent mangaContent) {
var userMangaFollows =
userMangaFollowRepository.findByManga(mangaContent.getMangaContentProvider().getManga());
userMangaFollows.forEach(
umf ->
ntfyClient.notify(
new NtfyClient.Request(
"mangamochi-" + umf.getUser().getId().toString(),
umf.getManga().getTitle(),
"New content available on "
+ mangaContent.getMangaContentProvider().getContentProvider().getName())));
}
}

View File

@ -1,12 +1,13 @@
package com.magamochi.task;
package com.magamochi.userinteraction.task;
import com.magamochi.catalog.model.entity.Manga;
import com.magamochi.catalog.model.repository.MangaRepository;
import com.magamochi.model.dto.UpdateMangaFollowChapterListCommand;
import com.magamochi.queue.UpdateMangaFollowChapterListProducer;
import com.magamochi.userinteraction.queue.command.UpdateMangaFollowChapterListCommand;
import com.magamochi.userinteraction.queue.producer.UpdateMangaFollowChapterListProducer;
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;
import org.springframework.transaction.annotation.Transactional;
@ -21,7 +22,7 @@ public class MangaFollowUpdateTask {
private final UpdateMangaFollowChapterListProducer producer;
// @Scheduled(cron = "${manga-follow.cron-expression}")
@Scheduled(cron = "${manga-follow.cron-expression}")
@Transactional
public void updateMangaFollow() {
if (!updateEnabled) {

View File

@ -82,14 +82,11 @@ queues:
manga-cover-update: ${MANGA_COVER_UDPATE_QUEUE:mangamochi.manga.cover.update}
file-import: ${FILE_IMPORT_QUEUE:mangamochi.file.import}
manga-content-download: ${MANGA_CONTENT_DOWNLOAD_QUEUE:mangamochi.manga.content.download}
manga-follow-update-chapter: ${MANGA_FOLLOW_UPDATE_CHAPTER_QUEUE:mangamochi.follow.update}
routing-key:
image-update: ${IMAGE_UPDATE_ROUTING_KEY:mangamochi.image.update}
rabbit-mq:
queues:
manga-follow-update-chapter: ${MANGA_FOLLOW_UPDATE_CHAPTER_QUEUE:mangaFollowUpdateChapterQueue}
image-service:
clean-up-enabled: ${IMAGE_SERVICE_CLEAN_UP_ENABLED:false}
cron-expression: "@weekly"