Compare commits

..

1 Commits

Author SHA1 Message Date
098612dbb4 feat: content service 2026-03-18 14:31:45 -03:00
13 changed files with 126 additions and 132 deletions

View File

@ -31,6 +31,9 @@ public class RabbitConfig {
@Value("${queues.manga-cover-update}")
private String mangaCoverUpdateQueue;
@Value("${queues.manga-content-image-update}")
private String mangaContentImageUpdateQueue;
@Value("${queues.image-fetch}")
private String imageFetchQueue;
@ -52,6 +55,11 @@ public class RabbitConfig {
return new Queue(mangaUpdateQueue, false);
}
@Bean
public Queue mangaContentImageUpdateQueue() {
return new Queue(mangaContentImageUpdateQueue, false);
}
@Bean
public Queue mangaCoverUpdateQueue() {
return new Queue(mangaCoverUpdateQueue, false);
@ -68,6 +76,17 @@ public class RabbitConfig {
null);
}
@Bean
public Binding bindingMangaContentImageUpdateQueue(
Queue mangaContentImageUpdateQueue, TopicExchange imageUpdatesExchange) {
return new Binding(
mangaContentImageUpdateQueue.getName(),
Binding.DestinationType.QUEUE,
imageUpdatesExchange.getName(),
String.format("image.update.%s", ContentType.CONTENT_IMAGE.name().toLowerCase()),
null);
}
@Bean
public Queue mangaContentIngestQueue() {
return new Queue(mangaContentIngestQueue, false);

View File

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

View File

@ -5,6 +5,8 @@ import com.magamochi.content.model.entity.MangaContentImage;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MangaChapterImageRepository extends JpaRepository<MangaContentImage, Long> {
public interface MangaContentImageRepository extends JpaRepository<MangaContentImage, Long> {
List<MangaContentImage> findAllByMangaContent(MangaContent mangaContent);
boolean existsByMangaContent_IdAndPosition(Long mangaContentId, int position);
}

View File

@ -0,0 +1,22 @@
package com.magamochi.content.queue.consumer;
import com.magamochi.common.queue.command.MangaContentImageIngestCommand;
import com.magamochi.content.service.ContentIngestService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
@Log4j2
@Service
@RequiredArgsConstructor
public class MangaContentImageIngestConsumer {
private final ContentIngestService contentIngestService;
@RabbitListener(queues = "${queues.manga-content-image-ingest}")
public void receiveMangaContentImageIngestCommand(MangaContentImageIngestCommand command) {
log.info("Received manga content ingest command: {}", command);
contentIngestService.ingestImages(
command.mangaContentId(), command.url(), command.position(), command.isLast());
}
}

View File

@ -0,0 +1,21 @@
package com.magamochi.content.queue.consumer;
import com.magamochi.common.queue.command.ImageUpdateCommand;
import com.magamochi.content.service.ContentIngestService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
@Log4j2
@Service
@RequiredArgsConstructor
public class MangaContentImageUpdateConsumer {
private final ContentIngestService contentIngestService;
@RabbitListener(queues = "${queues.manga-content-image-update}")
public void receiveMangaContentImageUpdateCommand(ImageUpdateCommand command) {
log.info("Received manga content image update command: {}", command);
contentIngestService.updateMangaContentImage(command.entityId(), command.imageId());
}
}

View File

@ -2,21 +2,35 @@ package com.magamochi.content.service;
import com.magamochi.catalog.service.LanguageService;
import com.magamochi.catalog.service.MangaContentProviderService;
import com.magamochi.common.exception.NotFoundException;
import com.magamochi.common.model.enumeration.ContentType;
import com.magamochi.common.queue.command.ImageFetchCommand;
import com.magamochi.common.queue.producer.ImageFetchProducer;
import com.magamochi.content.model.entity.MangaContent;
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 jakarta.validation.constraints.NotBlank;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Log4j2
@Service
@RequiredArgsConstructor
public class ContentIngestService {
private final ContentService contentService;
private final MangaContentProviderService mangaContentProviderService;
private final LanguageService languageService;
private final MangaContentRepository mangaContentRepository;
private final MangaContentImageRepository mangaContentImageRepository;
private final ImageFetchProducer imageFetchProducer;
private final ImageService imageService;
public void ingest(
long mangaContentProviderId,
@ -53,4 +67,40 @@ public class ContentIngestService {
mangaContentProviderId,
mangaContent.getId());
}
@Transactional
public void ingestImages(
long mangaContentId, @NotBlank String url, int position, boolean isLast) {
log.info(
"Ingesting Manga Content Image for MangaContent {}, position {}", mangaContentId, position);
var mangaContent = contentService.find(mangaContentId);
if (mangaContentImageRepository.existsByMangaContent_IdAndPosition(mangaContentId, position)) {
return;
}
var mangaContentImage =
mangaContentImageRepository.save(
MangaContentImage.builder().mangaContent(mangaContent).position(position).build());
imageFetchProducer.sendImageFetchCommand(
new ImageFetchCommand(mangaContentImage.getId(), ContentType.CONTENT_IMAGE, url));
if (isLast) {
mangaContent.setDownloaded(true);
}
}
@Transactional
public void updateMangaContentImage(long mangaContentImageId, UUID imageId) {
var mangaContentImage =
mangaContentImageRepository
.findById(mangaContentImageId)
.orElseThrow(
() -> new NotFoundException("Image not found for ID: " + mangaContentImageId));
var image = imageService.find(imageId);
mangaContentImage.setImage(image);
}
}

View File

@ -2,6 +2,7 @@ package com.magamochi.image.service;
import static java.util.Objects.nonNull;
import com.google.common.util.concurrent.RateLimiter;
import com.magamochi.common.model.enumeration.ContentType;
import java.net.URI;
import java.net.http.HttpClient;
@ -21,6 +22,8 @@ import org.springframework.stereotype.Service;
public class ImageFetchService {
private final ImageService imageManagerService;
private final RateLimiter imageDownloadRateLimiter;
private final HttpClient httpClient =
HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build();
private final Tika tika = new Tika();
@ -28,6 +31,8 @@ public class ImageFetchService {
public UUID fetchImage(String imageUrl, ContentType contentType) {
try {
var request = HttpRequest.newBuilder(URI.create(imageUrl)).GET().build();
imageDownloadRateLimiter.acquire();
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
var imageBytes = response.body();

View File

@ -103,85 +103,4 @@ public class IngestionService {
mangaContent.getId(), item.url(), item.position(), isLast));
});
}
// @Transactional
// public void fetchChapter(Long chapterId) {
//
// var retryConfig = retryRegistry.retry("ImageDownloadRetry").getRetryConfig();
//
// var chapterImages =
// chapterImagesUrls.entrySet().parallelStream()
// .map(
// entry -> {
// imageDownloadRateLimiter.acquire();
//
// try {
// var finalUrl = new
// URI(entry.getValue().trim()).toASCIIString().trim();
// var retry =
// Retry.of("image-download-" + chapterId + "-" +
// entry.getKey(), retryConfig);
//
// retry
// .getEventPublisher()
// .onRetry(
// event ->
// log.warn(
// "Retrying image download {}/{}
// for chapter {}. Attempt #{}. Error: {}",
// entry.getKey() + 1,
// chapterImagesUrls.size(),
// chapterId,
//
// event.getNumberOfRetryAttempts(),
//
// event.getLastThrowable().getMessage()));
//
// return retry.executeCheckedSupplier(
// () -> {
// var url = new URL(finalUrl);
// var connection = url.openConnection();
// connection.setConnectTimeout(5000);
// connection.setReadTimeout(5000);
//
// try (var inputStream =
// new
// BufferedInputStream(connection.getInputStream())) {
// var bytes = inputStream.readAllBytes();
//
// var image =
// oldImageService.uploadImage(
// bytes, "image/jpeg", "chapter/" +
// chapterId);
//
// log.info(
// "Downloaded image {}/{} for manga {} chapter
// {}: {}",
// entry.getKey() + 1,
// chapterImagesUrls.size(),
//
// chapter.getMangaContentProvider().getManga().getTitle(),
// chapterId,
// entry.getValue());
//
// return MangaContentImage.builder()
// .mangaContent(chapter)
// .position(entry.getKey())
// .image(image)
// .build();
// }
// });
// } catch (Throwable e) {
// throw new UnprocessableException(
// "Could not download image for chapter ID: " + chapterId,
// e);
// }
// })
// .toList();
//
// mangaChapterImageRepository.saveAll(chapterImages);
//
// chapter.setDownloaded(true);
// mangaContentRepository.save(chapter);
// }
}

View File

@ -1,16 +1,13 @@
package com.magamochi.service;
import com.google.common.util.concurrent.RateLimiter;
import com.magamochi.common.exception.UnprocessableException;
import com.magamochi.content.model.entity.MangaContent;
import com.magamochi.content.model.entity.MangaContentImage;
import com.magamochi.content.model.repository.MangaChapterImageRepository;
import com.magamochi.content.model.repository.MangaContentImageRepository;
import com.magamochi.content.model.repository.MangaContentRepository;
import com.magamochi.ingestion.providers.ContentProviderFactory;
import com.magamochi.model.dto.MangaChapterArchiveDTO;
import com.magamochi.model.dto.MangaChapterImagesDTO;
import com.magamochi.model.enumeration.ArchiveFileType;
import io.github.resilience4j.retry.RetryRegistry;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -28,15 +25,10 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor
public class MangaChapterService {
private final MangaContentRepository mangaContentRepository;
private final MangaChapterImageRepository mangaChapterImageRepository;
private final MangaContentImageRepository mangaContentImageRepository;
private final OldImageService oldImageService;
private final ContentProviderFactory contentProviderFactory;
private final RateLimiter imageDownloadRateLimiter;
private final RetryRegistry retryRegistry;
public MangaChapterImagesDTO getMangaChapterImages(Long chapterId) {
var chapter = getMangaChapterThrowIfNotFound(chapterId);
@ -77,7 +69,7 @@ public class MangaChapterService {
throws IOException {
var chapter = getMangaChapterThrowIfNotFound(chapterId);
var chapterImages = mangaChapterImageRepository.findAllByMangaContent(chapter);
var chapterImages = mangaContentImageRepository.findAllByMangaContent(chapter);
var byteArrayOutputStream =
switch (archiveFileType) {

View File

@ -1,7 +1,6 @@
package com.magamochi.service;
import com.magamochi.image.model.entity.Image;
import com.magamochi.image.model.repository.ImageRepository;
import java.io.InputStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@ -12,14 +11,6 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor
public class OldImageService {
private final OldS3Service oldS3Service;
private final ImageRepository imageRepository;
public Image uploadImage(byte[] data, String contentType, String path) {
log.info("Uploading image {} to S3", path);
var fileKey = oldS3Service.uploadFile(data, contentType, path);
return imageRepository.save(Image.builder().objectKey(fileKey).build());
}
public InputStream getImageStream(Image image) {
return oldS3Service.getFile(image.getObjectKey());

View File

@ -1,14 +1,10 @@
package com.magamochi.service;
import com.magamochi.catalog.model.entity.Manga;
import com.magamochi.catalog.model.entity.MangaContentProvider;
import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
import com.magamochi.catalog.model.repository.MangaRepository;
import com.magamochi.client.NtfyClient;
import com.magamochi.common.exception.NotFoundException;
import com.magamochi.content.model.entity.MangaContent;
import com.magamochi.content.model.repository.MangaContentRepository;
import com.magamochi.ingestion.providers.ContentProviderFactory;
import com.magamochi.model.dto.*;
import com.magamochi.model.entity.UserMangaFollow;
import com.magamochi.model.repository.*;
@ -28,14 +24,9 @@ public class OldMangaService {
private final MangaRepository mangaRepository;
private final MangaContentProviderRepository mangaContentProviderRepository;
private final ContentProviderFactory contentProviderFactory;
private final UserMangaFollowRepository userMangaFollowRepository;
private final MangaChapterDownloadProducer mangaChapterDownloadProducer;
private final MangaContentRepository mangaContentRepository;
private final NtfyClient ntfyClient;
public void fetchAllNotDownloadedChapters(Long mangaProviderId) {
var mangaProvider =
@ -96,13 +87,6 @@ public class OldMangaService {
.orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
}
private MangaContentProvider getMangaProviderThrowIfNotFound(Long mangaProviderId) {
return mangaContentProviderRepository
.findById(mangaProviderId)
.orElseThrow(
() -> new NotFoundException("Manga Provider not found for ID: " + mangaProviderId));
}
@Transactional
public void follow(Long mangaId) {
var user = userService.getLoggedUserThrowIfNotFound();

View File

@ -1,11 +1,9 @@
package com.magamochi.service;
import java.io.InputStream;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
@ -17,17 +15,6 @@ public class OldS3Service {
private final S3Client s3Client;
public String uploadFile(byte[] data, String contentType, String path) {
var filename = "manga/" + path + "/" + UUID.randomUUID();
var request =
PutObjectRequest.builder().bucket(bucket).key(filename).contentType(contentType).build();
s3Client.putObject(request, RequestBody.fromBytes(data));
return filename;
}
public InputStream getFile(String key) {
var request = GetObjectRequest.builder().bucket(bucket).key(key).build();

View File

@ -100,6 +100,7 @@ queues:
manga-content-image-ingest: ${MANGA_CONTENT_IMAGE_INGEST_QUEUE:mangaContentImageIngest}
provider-page-ingest: ${PROVIDER_PAGE_INGEST_QUEUE:providerPageIngest}
manga-update: ${MANGA_UPDATE_QUEUE:mangaUpdate}
manga-content-image-update: ${MANGA_CONTENT_IMAGE_UPDATE_QUEUE:mangaContentImageUpdate}
image-fetch: ${IMAGE_FETCH_QUEUE:imageFetch}
manga-cover-update: ${MANGA_COVER_UDPATE_QUEUE:mangaCoverUpdate}