refactor-architecture #31
@ -3,7 +3,7 @@ package com.magamochi.catalog.model.entity;
|
|||||||
import com.magamochi.catalog.model.enumeration.MangaState;
|
import com.magamochi.catalog.model.enumeration.MangaState;
|
||||||
import com.magamochi.catalog.model.enumeration.MangaStatus;
|
import com.magamochi.catalog.model.enumeration.MangaStatus;
|
||||||
import com.magamochi.image.model.entity.Image;
|
import com.magamochi.image.model.entity.Image;
|
||||||
import com.magamochi.model.entity.UserFavoriteManga;
|
import com.magamochi.userinteraction.model.entity.UserFavoriteManga;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
|
|||||||
@ -2,9 +2,13 @@ package com.magamochi.catalog.model.repository;
|
|||||||
|
|
||||||
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import java.util.Optional;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
public interface MangaContentProviderRepository extends JpaRepository<MangaContentProvider, Long> {
|
public interface MangaContentProviderRepository extends JpaRepository<MangaContentProvider, Long> {
|
||||||
boolean existsByMangaTitleIgnoreCaseAndContentProvider_Id(
|
boolean existsByMangaTitleIgnoreCaseAndContentProvider_Id(
|
||||||
@NotBlank String mangaTitle, long contentProviderId);
|
@NotBlank String mangaTitle, long contentProviderId);
|
||||||
|
|
||||||
|
Optional<MangaContentProvider> findByManga_IdAndContentProvider_Id(
|
||||||
|
long mangaId, long contentProviderId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
package com.magamochi.catalog.service;
|
package com.magamochi.catalog.service;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
|
import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
|
||||||
import com.magamochi.common.exception.NotFoundException;
|
import com.magamochi.common.exception.NotFoundException;
|
||||||
|
import com.magamochi.ingestion.model.entity.ContentProvider;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@ -19,4 +21,17 @@ public class MangaContentProviderService {
|
|||||||
new NotFoundException(
|
new NotFoundException(
|
||||||
"MangaContentProvider not found - ID: " + mangaContentProviderId));
|
"MangaContentProvider not found - ID: " + mangaContentProviderId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MangaContentProvider findOrCreate(Manga manga, ContentProvider contentProvider) {
|
||||||
|
return mangaContentProviderRepository
|
||||||
|
.findByManga_IdAndContentProvider_Id(manga.getId(), contentProvider.getId())
|
||||||
|
.orElseGet(
|
||||||
|
() ->
|
||||||
|
mangaContentProviderRepository.save(
|
||||||
|
MangaContentProvider.builder()
|
||||||
|
.manga(manga)
|
||||||
|
.mangaTitle(manga.getTitle())
|
||||||
|
.contentProvider(contentProvider)
|
||||||
|
.build()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,6 +98,10 @@ public class MangaResolutionService {
|
|||||||
return Optional.of(new ProviderResult(bestTitle, malId));
|
return Optional.of(new ProviderResult(bestTitle, malId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Manga findOrCreateManga(Long aniListId, Long malId) {
|
||||||
|
return findOrCreateManga(null, aniListId, malId);
|
||||||
|
}
|
||||||
|
|
||||||
private Manga findOrCreateManga(String canonicalTitle, Long aniListId, Long malId) {
|
private Manga findOrCreateManga(String canonicalTitle, Long aniListId, Long malId) {
|
||||||
if (nonNull(aniListId)) {
|
if (nonNull(aniListId)) {
|
||||||
var existingByAniList = mangaRepository.findByAniListId(aniListId);
|
var existingByAniList = mangaRepository.findByAniListId(aniListId);
|
||||||
@ -113,20 +117,24 @@ public class MangaResolutionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return mangaRepository
|
if (nonNull(canonicalTitle)) {
|
||||||
.findByTitleIgnoreCase(canonicalTitle)
|
var existingByTitle = mangaRepository.findByTitleIgnoreCase(canonicalTitle);
|
||||||
.orElseGet(
|
if (existingByTitle.isPresent()) {
|
||||||
() -> {
|
return existingByTitle.get();
|
||||||
var newManga =
|
}
|
||||||
Manga.builder().title(canonicalTitle).malId(malId).aniListId(aniListId).build();
|
}
|
||||||
|
|
||||||
var savedManga = mangaRepository.save(newManga);
|
return createAndNotifyManga(canonicalTitle, aniListId, malId);
|
||||||
|
}
|
||||||
|
|
||||||
mangaUpdateProducer.sendMangaUpdateCommand(
|
private Manga createAndNotifyManga(String title, Long aniListId, Long malId) {
|
||||||
new MangaUpdateCommand(savedManga.getId()));
|
var manga =
|
||||||
|
mangaRepository.save(
|
||||||
|
Manga.builder().title(title).aniListId(aniListId).malId(malId).build());
|
||||||
|
|
||||||
return savedManga;
|
mangaUpdateProducer.sendMangaUpdateCommand(new MangaUpdateCommand(manga.getId()));
|
||||||
});
|
|
||||||
|
return manga;
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ProviderResult(String title, Long externalId) {}
|
private record ProviderResult(String title, Long externalId) {}
|
||||||
|
|||||||
@ -8,10 +8,10 @@ import com.magamochi.catalog.model.dto.MangaListFilterDTO;
|
|||||||
import com.magamochi.catalog.model.entity.Manga;
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import com.magamochi.catalog.model.repository.MangaRepository;
|
import com.magamochi.catalog.model.repository.MangaRepository;
|
||||||
import com.magamochi.common.exception.NotFoundException;
|
import com.magamochi.common.exception.NotFoundException;
|
||||||
import com.magamochi.model.repository.UserFavoriteMangaRepository;
|
|
||||||
import com.magamochi.model.repository.UserMangaFollowRepository;
|
|
||||||
import com.magamochi.model.specification.MangaSpecification;
|
import com.magamochi.model.specification.MangaSpecification;
|
||||||
import com.magamochi.user.service.UserService;
|
import com.magamochi.user.service.UserService;
|
||||||
|
import com.magamochi.userinteraction.model.repository.UserFavoriteMangaRepository;
|
||||||
|
import com.magamochi.userinteraction.model.repository.UserMangaFollowRepository;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|||||||
@ -77,7 +77,7 @@ public class MyAnimeListService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private MangaStatus mapStatus(String malStatus) {
|
private MangaStatus mapStatus(String malStatus) {
|
||||||
return switch (malStatus) {
|
return switch (malStatus.toLowerCase()) {
|
||||||
case "finished" -> MangaStatus.COMPLETED;
|
case "finished" -> MangaStatus.COMPLETED;
|
||||||
case "publishing" -> MangaStatus.ONGOING;
|
case "publishing" -> MangaStatus.ONGOING;
|
||||||
case "on hiatus" -> MangaStatus.HIATUS;
|
case "on hiatus" -> MangaStatus.HIATUS;
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
package com.magamochi.ingestion.providers;
|
package com.magamochi.common;
|
||||||
|
|
||||||
public class ContentProviders {
|
public class ContentProviders {
|
||||||
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_LIVRE_TO = "Manga Livre.to";
|
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 MANGA_DEX = "MangaDex";
|
public static final String MANGA_DEX = "MangaDex";
|
||||||
|
public static final String MANUAL_IMPORT = "Manual Import";
|
||||||
}
|
}
|
||||||
@ -6,7 +6,7 @@ import org.springframework.amqp.core.Queue;
|
|||||||
import org.springframework.amqp.core.TopicExchange;
|
import org.springframework.amqp.core.TopicExchange;
|
||||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
|
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
|
||||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||||
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
|
import org.springframework.amqp.support.converter.JacksonJsonMessageConverter;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@ -37,6 +37,9 @@ public class RabbitConfig {
|
|||||||
@Value("${queues.image-fetch}")
|
@Value("${queues.image-fetch}")
|
||||||
private String imageFetchQueue;
|
private String imageFetchQueue;
|
||||||
|
|
||||||
|
@Value("${queues.file-import}")
|
||||||
|
private String fileImportQueue;
|
||||||
|
|
||||||
@Value("${topics.image-updates}")
|
@Value("${topics.image-updates}")
|
||||||
private String imageUpdatesTopic;
|
private String imageUpdatesTopic;
|
||||||
|
|
||||||
@ -68,6 +71,11 @@ public class RabbitConfig {
|
|||||||
return new Queue(mangaCoverUpdateQueue, false);
|
return new Queue(mangaCoverUpdateQueue, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Queue fileImportQueue() {
|
||||||
|
return new Queue(fileImportQueue, false);
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Binding bindingMangaCoverUpdateQueue(
|
public Binding bindingMangaCoverUpdateQueue(
|
||||||
Queue mangaCoverUpdateQueue, TopicExchange imageUpdatesExchange) {
|
Queue mangaCoverUpdateQueue, TopicExchange imageUpdatesExchange) {
|
||||||
@ -130,8 +138,8 @@ public class RabbitConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Jackson2JsonMessageConverter messageConverter() {
|
public JacksonJsonMessageConverter messageConverter() {
|
||||||
return new Jackson2JsonMessageConverter();
|
return new JacksonJsonMessageConverter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@ -1,23 +1,33 @@
|
|||||||
package com.magamochi.content.controller;
|
package com.magamochi.content.controller;
|
||||||
|
|
||||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||||
|
import com.magamochi.content.model.dto.FileImportRequestDTO;
|
||||||
import com.magamochi.content.model.dto.MangaContentDTO;
|
import com.magamochi.content.model.dto.MangaContentDTO;
|
||||||
|
import com.magamochi.content.model.dto.MangaContentImagesDTO;
|
||||||
|
import com.magamochi.content.model.enumeration.ContentArchiveFileType;
|
||||||
|
import com.magamochi.content.service.ContentDownloadService;
|
||||||
|
import com.magamochi.content.service.ContentImportService;
|
||||||
import com.magamochi.content.service.ContentService;
|
import com.magamochi.content.service.ContentService;
|
||||||
import com.magamochi.model.dto.MangaContentImagesDTO;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/content")
|
@RequestMapping("/content")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ContentController {
|
public class ContentController {
|
||||||
private final ContentService contentService;
|
private final ContentService contentService;
|
||||||
|
private final ContentDownloadService contentDownloadService;
|
||||||
|
private final ContentImportService contentImportService;
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get the content for a specific manga/content provider combination",
|
summary = "Get the content for a specific manga/content provider combination",
|
||||||
@ -41,4 +51,46 @@ public class ContentController {
|
|||||||
@PathVariable Long mangaContentId) {
|
@PathVariable Long mangaContentId) {
|
||||||
return DefaultResponseDTO.ok(contentService.getContentImages(mangaContentId));
|
return DefaultResponseDTO.ok(contentService.getContentImages(mangaContentId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Download content archive",
|
||||||
|
description = "Download content as a compressed file by its ID.",
|
||||||
|
tags = {"Content"},
|
||||||
|
operationId = "downloadContentArchive")
|
||||||
|
@ApiResponses({
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Successful download",
|
||||||
|
content =
|
||||||
|
@Content(
|
||||||
|
mediaType = "application/octet-stream",
|
||||||
|
schema = @Schema(type = "string", format = "binary"))),
|
||||||
|
})
|
||||||
|
@PostMapping(
|
||||||
|
value = "/{mangaContentId}/download",
|
||||||
|
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||||
|
public ResponseEntity<byte[]> downloadContentArchive(
|
||||||
|
@PathVariable Long mangaContentId,
|
||||||
|
@RequestParam ContentArchiveFileType contentArchiveFileType)
|
||||||
|
throws IOException {
|
||||||
|
var response = contentDownloadService.downloadContent(mangaContentId, contentArchiveFileType);
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header("Content-Disposition", "attachment; filename=\"" + response.filename() + "\"")
|
||||||
|
.body(response.content());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Import multiple files",
|
||||||
|
description = "Accepts multiple content files via multipart/form-data and processes them.",
|
||||||
|
tags = {"Content"},
|
||||||
|
operationId = "importContentFiles")
|
||||||
|
@PostMapping(value = "/import", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
|
public DefaultResponseDTO<Void> importContentFiles(
|
||||||
|
@ModelAttribute FileImportRequestDTO requestDTO) {
|
||||||
|
contentImportService.importFiles(
|
||||||
|
requestDTO.malId(), requestDTO.aniListId(), requestDTO.files());
|
||||||
|
|
||||||
|
return DefaultResponseDTO.ok().build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.magamochi.content.model.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
public record FileImportRequestDTO(String malId, String aniListId, List<MultipartFile> files) {}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
package com.magamochi.model.dto;
|
package com.magamochi.content.model.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
public record MangaChapterArchiveDTO(@NotBlank String filename, @NotNull byte[] content) {}
|
public record MangaContentArchiveDTO(@NotBlank String filename, @NotNull byte[] content) {}
|
||||||
@ -10,12 +10,12 @@ public record MangaContentDTO(
|
|||||||
@NotNull Boolean downloaded,
|
@NotNull Boolean downloaded,
|
||||||
@NotNull Boolean isRead,
|
@NotNull Boolean isRead,
|
||||||
LanguageDTO language) {
|
LanguageDTO language) {
|
||||||
public static MangaContentDTO from(MangaContent mangaContent) {
|
public static MangaContentDTO from(MangaContent mangaContent, boolean isRead) {
|
||||||
return new MangaContentDTO(
|
return new MangaContentDTO(
|
||||||
mangaContent.getId(),
|
mangaContent.getId(),
|
||||||
mangaContent.getTitle(),
|
mangaContent.getTitle(),
|
||||||
mangaContent.getDownloaded(),
|
mangaContent.getDownloaded(),
|
||||||
false,
|
isRead,
|
||||||
LanguageDTO.from(mangaContent.getLanguage()));
|
LanguageDTO.from(mangaContent.getLanguage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package com.magamochi.model.dto;
|
package com.magamochi.content.model.dto;
|
||||||
|
|
||||||
import com.magamochi.content.model.entity.MangaContent;
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
import com.magamochi.content.model.entity.MangaContentImage;
|
import com.magamochi.content.model.entity.MangaContentImage;
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.magamochi.content.model.enumeration;
|
||||||
|
|
||||||
|
public enum ContentArchiveFileType {
|
||||||
|
CBZ,
|
||||||
|
CBR
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
package com.magamochi.content.queue.command;
|
||||||
|
|
||||||
|
public record FileImportCommand(long mangaContentProviderId, String filename) {}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.magamochi.content.queue.consumer;
|
||||||
|
|
||||||
|
import com.magamochi.content.queue.command.FileImportCommand;
|
||||||
|
import com.magamochi.content.service.ContentImportService;
|
||||||
|
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 FileImportConsumer {
|
||||||
|
private final ContentImportService contentImportService;
|
||||||
|
|
||||||
|
@RabbitListener(queues = "${queues.file-import}")
|
||||||
|
public void receiveFileImportCommand(FileImportCommand command) {
|
||||||
|
log.info("Received file import command: {}", command);
|
||||||
|
contentImportService.importFile(command.mangaContentProviderId(), command.filename());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.magamochi.content.queue.producer;
|
||||||
|
|
||||||
|
import com.magamochi.content.queue.command.FileImportCommand;
|
||||||
|
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 FileImportProducer {
|
||||||
|
private final RabbitTemplate rabbitTemplate;
|
||||||
|
|
||||||
|
@Value("${queues.file-import}")
|
||||||
|
private String fileImportQueue;
|
||||||
|
|
||||||
|
public void sendFileImportCommand(FileImportCommand command) {
|
||||||
|
rabbitTemplate.convertAndSend(fileImportQueue, command);
|
||||||
|
log.info("Sent file import command: {}", command);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,10 @@
|
|||||||
package com.magamochi.service;
|
package com.magamochi.content.service;
|
||||||
|
|
||||||
import com.magamochi.common.exception.UnprocessableException;
|
import com.magamochi.common.exception.UnprocessableException;
|
||||||
import com.magamochi.content.model.entity.MangaContent;
|
import com.magamochi.content.model.dto.MangaContentArchiveDTO;
|
||||||
import com.magamochi.content.model.entity.MangaContentImage;
|
import com.magamochi.content.model.entity.MangaContentImage;
|
||||||
import com.magamochi.content.model.repository.MangaContentImageRepository;
|
import com.magamochi.content.model.enumeration.ContentArchiveFileType;
|
||||||
import com.magamochi.content.model.repository.MangaContentRepository;
|
import com.magamochi.image.service.ImageService;
|
||||||
import com.magamochi.model.dto.MangaChapterArchiveDTO;
|
|
||||||
import com.magamochi.model.enumeration.ArchiveFileType;
|
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -21,35 +19,24 @@ import org.springframework.stereotype.Service;
|
|||||||
@Log4j2
|
@Log4j2
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class MangaChapterService {
|
public class ContentDownloadService {
|
||||||
private final MangaContentRepository mangaContentRepository;
|
private final ContentService contentService;
|
||||||
private final MangaContentImageRepository mangaContentImageRepository;
|
private final ImageService imageService;
|
||||||
|
|
||||||
private final OldImageService oldImageService;
|
public MangaContentArchiveDTO downloadContent(
|
||||||
|
Long mangaContentId, ContentArchiveFileType contentArchiveFileType) throws IOException {
|
||||||
public void markAsRead(Long chapterId) {
|
var chapter = contentService.find(mangaContentId);
|
||||||
// TODO: implement this
|
var chapterImages = chapter.getMangaContentImages();
|
||||||
// var chapter = getMangaChapterThrowIfNotFound(chapterId);
|
|
||||||
// chapter.setRead(true);
|
|
||||||
//
|
|
||||||
// mangaChapterRepository.save(chapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MangaChapterArchiveDTO downloadChapter(Long chapterId, ArchiveFileType archiveFileType)
|
|
||||||
throws IOException {
|
|
||||||
var chapter = getMangaChapterThrowIfNotFound(chapterId);
|
|
||||||
|
|
||||||
var chapterImages = mangaContentImageRepository.findAllByMangaContent(chapter);
|
|
||||||
|
|
||||||
var byteArrayOutputStream =
|
var byteArrayOutputStream =
|
||||||
switch (archiveFileType) {
|
switch (contentArchiveFileType) {
|
||||||
case CBZ -> getChapterCbzArchive(chapterImages);
|
case CBZ -> getChapterCbzArchive(chapterImages);
|
||||||
default ->
|
default ->
|
||||||
throw new UnprocessableException(
|
throw new UnprocessableException(
|
||||||
"Unsupported archive file type: " + archiveFileType.name());
|
"Unsupported archive file type: " + contentArchiveFileType.name());
|
||||||
};
|
};
|
||||||
|
|
||||||
return new MangaChapterArchiveDTO(
|
return new MangaContentArchiveDTO(
|
||||||
chapter.getTitle() + ".cbz", byteArrayOutputStream.toByteArray());
|
chapter.getTitle() + ".cbz", byteArrayOutputStream.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +55,7 @@ public class MangaChapterService {
|
|||||||
var paddedFileName = String.format("%0" + paddingLength + "d.jpg", imgSrc.getPosition());
|
var paddedFileName = String.format("%0" + paddingLength + "d.jpg", imgSrc.getPosition());
|
||||||
|
|
||||||
zipOutputStream.putNextEntry(new ZipEntry(paddedFileName));
|
zipOutputStream.putNextEntry(new ZipEntry(paddedFileName));
|
||||||
IOUtils.copy(oldImageService.getImageStream(imgSrc.getImage()), zipOutputStream);
|
IOUtils.copy(imageService.getStream(imgSrc.getImage()), zipOutputStream);
|
||||||
zipOutputStream.closeEntry();
|
zipOutputStream.closeEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,10 +64,4 @@ public class MangaChapterService {
|
|||||||
IOUtils.closeQuietly(zipOutputStream);
|
IOUtils.closeQuietly(zipOutputStream);
|
||||||
return byteArrayOutputStream;
|
return byteArrayOutputStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MangaContent getMangaChapterThrowIfNotFound(Long chapterId) {
|
|
||||||
return mangaContentRepository
|
|
||||||
.findById(chapterId)
|
|
||||||
.orElseThrow(() -> new RuntimeException("Manga Chapter not found for ID: " + chapterId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -0,0 +1,151 @@
|
|||||||
|
package com.magamochi.content.service;
|
||||||
|
|
||||||
|
import static java.util.Objects.isNull;
|
||||||
|
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.service.MangaContentProviderService;
|
||||||
|
import com.magamochi.catalog.service.MangaResolutionService;
|
||||||
|
import com.magamochi.common.exception.UnprocessableException;
|
||||||
|
import com.magamochi.common.model.enumeration.ContentType;
|
||||||
|
import com.magamochi.content.model.entity.MangaContentImage;
|
||||||
|
import com.magamochi.content.model.repository.MangaContentImageRepository;
|
||||||
|
import com.magamochi.content.queue.command.FileImportCommand;
|
||||||
|
import com.magamochi.content.queue.producer.FileImportProducer;
|
||||||
|
import com.magamochi.image.service.ImageFetchService;
|
||||||
|
import com.magamochi.image.service.ImageService;
|
||||||
|
import com.magamochi.image.service.S3Service;
|
||||||
|
import com.magamochi.ingestion.service.ContentProviderService;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
@Log4j2
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ContentImportService {
|
||||||
|
private final ContentProviderService contentProviderService;
|
||||||
|
private final MangaResolutionService mangaResolutionService;
|
||||||
|
private final MangaContentProviderService mangaContentProviderService;
|
||||||
|
private final ContentIngestService contentIngestService;
|
||||||
|
private final ImageFetchService imageFetchService;
|
||||||
|
private final S3Service s3Service;
|
||||||
|
|
||||||
|
private final FileImportProducer fileImportProducer;
|
||||||
|
private final MangaContentImageRepository mangaContentImageRepository;
|
||||||
|
private final ImageService imageService;
|
||||||
|
|
||||||
|
public void importFiles(String malId, String aniListId, @NotNull List<MultipartFile> files) {
|
||||||
|
if (isBlank(malId) && isBlank(aniListId)) {
|
||||||
|
throw new UnprocessableException("Either MyAnimeList or AniList IDs are required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var manga =
|
||||||
|
mangaResolutionService.findOrCreateManga(
|
||||||
|
isBlank(aniListId) ? null : Long.parseLong(aniListId),
|
||||||
|
isBlank(malId) ? null : Long.parseLong(malId));
|
||||||
|
var contentProvider = contentProviderService.findManualImportContentProvider();
|
||||||
|
|
||||||
|
var mangaContentProvider = mangaContentProviderService.findOrCreate(manga, contentProvider);
|
||||||
|
|
||||||
|
var sortedFiles = files.stream().sorted(Comparator.comparing(MultipartFile::getName)).toList();
|
||||||
|
|
||||||
|
sortedFiles.forEach(
|
||||||
|
file -> {
|
||||||
|
try {
|
||||||
|
var filename =
|
||||||
|
s3Service.uploadFile(
|
||||||
|
file.getBytes(),
|
||||||
|
file.getContentType(),
|
||||||
|
"temp/import/" + file.getOriginalFilename());
|
||||||
|
log.info("Temp file uploaded to S3: {}", filename);
|
||||||
|
|
||||||
|
fileImportProducer.sendFileImportCommand(
|
||||||
|
new FileImportCommand(mangaContentProvider.getId(), filename));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UnprocessableException("Failed to upload file to S3");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void importFile(Long mangaContentProviderId, String filename) {
|
||||||
|
var mangaContent =
|
||||||
|
contentIngestService.ingest(
|
||||||
|
mangaContentProviderId,
|
||||||
|
removeImportPrefix(removeFileExtension(filename)),
|
||||||
|
null,
|
||||||
|
"en-US");
|
||||||
|
|
||||||
|
try {
|
||||||
|
var is = s3Service.getFileStream(filename);
|
||||||
|
var zis = new ZipInputStream(is);
|
||||||
|
|
||||||
|
ZipEntry entry;
|
||||||
|
var position = 0;
|
||||||
|
|
||||||
|
while ((entry = zis.getNextEntry()) != null) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var os = new ByteArrayOutputStream();
|
||||||
|
zis.transferTo(os);
|
||||||
|
var bytes = os.toByteArray();
|
||||||
|
|
||||||
|
var imageId = imageFetchService.uploadImage(bytes, null, ContentType.CONTENT_IMAGE);
|
||||||
|
var image = imageService.find(imageId);
|
||||||
|
|
||||||
|
mangaContentImageRepository.save(
|
||||||
|
MangaContentImage.builder()
|
||||||
|
.image(image)
|
||||||
|
.mangaContent(mangaContent)
|
||||||
|
.position(position)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
zis.closeEntry();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
mangaContent.setDownloaded(true);
|
||||||
|
s3Service.deleteObjects(Set.of(filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String removeFileExtension(String filename) {
|
||||||
|
if (isBlank(filename)) {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
int lastDotIndex = filename.lastIndexOf('.');
|
||||||
|
|
||||||
|
// No dot, or dot is the first character (like .gitignore)
|
||||||
|
if (lastDotIndex <= 0) {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filename.substring(0, lastDotIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String removeImportPrefix(String path) {
|
||||||
|
if (isNull(path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.replace("temp/import/", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package com.magamochi.content.service;
|
package com.magamochi.content.service;
|
||||||
|
|
||||||
import static java.util.Objects.isNull;
|
import static java.util.Objects.isNull;
|
||||||
|
import static java.util.Objects.nonNull;
|
||||||
|
|
||||||
import com.magamochi.catalog.service.LanguageService;
|
import com.magamochi.catalog.service.LanguageService;
|
||||||
import com.magamochi.catalog.service.MangaContentProviderService;
|
import com.magamochi.catalog.service.MangaContentProviderService;
|
||||||
@ -34,22 +35,24 @@ public class ContentIngestService {
|
|||||||
private final ImageFetchProducer imageFetchProducer;
|
private final ImageFetchProducer imageFetchProducer;
|
||||||
private final ImageService imageService;
|
private final ImageService imageService;
|
||||||
|
|
||||||
public void ingest(
|
public MangaContent ingest(
|
||||||
long mangaContentProviderId,
|
long mangaContentProviderId,
|
||||||
@NotBlank String title,
|
@NotBlank String title,
|
||||||
@NotBlank String url,
|
String url,
|
||||||
@NotBlank String languageCode) {
|
@NotBlank String languageCode) {
|
||||||
log.info("Ingesting Manga Content ({}) for provider {}", title, mangaContentProviderId);
|
log.info("Ingesting Manga Content ({}) for provider {}", title, mangaContentProviderId);
|
||||||
|
|
||||||
var mangaContentProvider = mangaContentProviderService.find(mangaContentProviderId);
|
var mangaContentProvider = mangaContentProviderService.find(mangaContentProviderId);
|
||||||
|
|
||||||
|
if (nonNull(url)) {
|
||||||
if (mangaContentRepository.existsByMangaContentProvider_IdAndUrlIgnoreCase(
|
if (mangaContentRepository.existsByMangaContentProvider_IdAndUrlIgnoreCase(
|
||||||
mangaContentProvider.getId(), url)) {
|
mangaContentProvider.getId(), url)) {
|
||||||
log.info(
|
log.info(
|
||||||
"Manga Content ({}) for provider {} already exists. Skipped.",
|
"Manga Content ({}) for provider {} already exists. Skipped.",
|
||||||
title,
|
title,
|
||||||
mangaContentProviderId);
|
mangaContentProviderId);
|
||||||
return;
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var language = languageService.find(languageCode);
|
var language = languageService.find(languageCode);
|
||||||
@ -68,6 +71,8 @@ public class ContentIngestService {
|
|||||||
title,
|
title,
|
||||||
mangaContentProviderId,
|
mangaContentProviderId,
|
||||||
mangaContent.getId());
|
mangaContent.getId());
|
||||||
|
|
||||||
|
return mangaContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|||||||
@ -3,9 +3,10 @@ package com.magamochi.content.service;
|
|||||||
import com.magamochi.catalog.service.MangaContentProviderService;
|
import com.magamochi.catalog.service.MangaContentProviderService;
|
||||||
import com.magamochi.common.exception.NotFoundException;
|
import com.magamochi.common.exception.NotFoundException;
|
||||||
import com.magamochi.content.model.dto.MangaContentDTO;
|
import com.magamochi.content.model.dto.MangaContentDTO;
|
||||||
|
import com.magamochi.content.model.dto.MangaContentImagesDTO;
|
||||||
import com.magamochi.content.model.entity.MangaContent;
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
import com.magamochi.content.model.repository.MangaContentRepository;
|
import com.magamochi.content.model.repository.MangaContentRepository;
|
||||||
import com.magamochi.model.dto.MangaContentImagesDTO;
|
import com.magamochi.userinteraction.service.UserMangaContentReadService;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -16,6 +17,7 @@ import org.springframework.stereotype.Service;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ContentService {
|
public class ContentService {
|
||||||
private final MangaContentProviderService mangaContentProviderService;
|
private final MangaContentProviderService mangaContentProviderService;
|
||||||
|
private final UserMangaContentReadService userMangaContentReadService;
|
||||||
|
|
||||||
private final MangaContentRepository mangaContentRepository;
|
private final MangaContentRepository mangaContentRepository;
|
||||||
|
|
||||||
@ -24,7 +26,11 @@ public class ContentService {
|
|||||||
|
|
||||||
return mangaContentProvider.getMangaContents().stream()
|
return mangaContentProvider.getMangaContents().stream()
|
||||||
.sorted(Comparator.comparing(MangaContent::getId))
|
.sorted(Comparator.comparing(MangaContent::getId))
|
||||||
.map(MangaContentDTO::from)
|
.map(
|
||||||
|
mangaContent -> {
|
||||||
|
var isRead = userMangaContentReadService.isRead(mangaContent.getId());
|
||||||
|
return MangaContentDTO.from(mangaContent, isRead);
|
||||||
|
})
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package com.magamochi.controller;
|
|||||||
import com.magamochi.client.NtfyClient;
|
import com.magamochi.client.NtfyClient;
|
||||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||||
import com.magamochi.image.task.ImageCleanupTask;
|
import com.magamochi.image.task.ImageCleanupTask;
|
||||||
import com.magamochi.ingestion.task.IngestFromContentProvidersTask;
|
|
||||||
import com.magamochi.task.MangaFollowUpdateTask;
|
import com.magamochi.task.MangaFollowUpdateTask;
|
||||||
import com.magamochi.user.repository.UserRepository;
|
import com.magamochi.user.repository.UserRepository;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@ -14,7 +13,6 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
@RequestMapping("/management")
|
@RequestMapping("/management")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ManagementController {
|
public class ManagementController {
|
||||||
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;
|
||||||
|
|||||||
@ -1,59 +0,0 @@
|
|||||||
package com.magamochi.controller;
|
|
||||||
|
|
||||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
|
||||||
import com.magamochi.model.enumeration.ArchiveFileType;
|
|
||||||
import com.magamochi.service.MangaChapterService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
|
||||||
import java.io.IOException;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/mangas/chapters")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class MangaChapterController {
|
|
||||||
private final MangaChapterService mangaChapterService;
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Mark a chapter as read",
|
|
||||||
description = "Mark a chapter as read by its ID.",
|
|
||||||
tags = {"Manga Chapter"},
|
|
||||||
operationId = "markAsRead")
|
|
||||||
@PostMapping("/{chapterId}/mark-as-read")
|
|
||||||
public DefaultResponseDTO<Void> markAsRead(@PathVariable Long chapterId) {
|
|
||||||
mangaChapterService.markAsRead(chapterId);
|
|
||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Download chapter archive",
|
|
||||||
description = "Download a chapter as a compressed file by its ID.",
|
|
||||||
tags = {"Manga Chapter"},
|
|
||||||
operationId = "downloadChapterArchive")
|
|
||||||
@ApiResponses({
|
|
||||||
@ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "Successful download",
|
|
||||||
content =
|
|
||||||
@Content(
|
|
||||||
mediaType = "application/octet-stream",
|
|
||||||
schema = @Schema(type = "string", format = "binary"))),
|
|
||||||
})
|
|
||||||
@PostMapping(value = "/{chapterId}/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
|
||||||
public ResponseEntity<byte[]> downloadChapterArchive(
|
|
||||||
@PathVariable Long chapterId, @RequestParam ArchiveFileType archiveFileType)
|
|
||||||
throws IOException {
|
|
||||||
|
|
||||||
var response = mangaChapterService.downloadChapter(chapterId, archiveFileType);
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.header("Content-Disposition", "attachment; filename=\"" + response.filename() + "\"")
|
|
||||||
.body(response.content());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -23,28 +23,4 @@ public class MangaController {
|
|||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
return DefaultResponseDTO.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Follow the manga specified by its ID",
|
|
||||||
description = "Follow the manga specified by its ID.",
|
|
||||||
tags = {"Manga"},
|
|
||||||
operationId = "followManga")
|
|
||||||
@PostMapping("/{mangaId}/followManga")
|
|
||||||
public DefaultResponseDTO<Void> followManga(@PathVariable Long mangaId) {
|
|
||||||
oldMangaService.follow(mangaId);
|
|
||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Unfollow the manga specified by its ID",
|
|
||||||
description = "Unfollow the manga specified by its ID.",
|
|
||||||
tags = {"Manga"},
|
|
||||||
operationId = "unfollowManga")
|
|
||||||
@PostMapping("/{mangaId}/unfollowManga")
|
|
||||||
public DefaultResponseDTO<Void> unfollowManga(@PathVariable Long mangaId) {
|
|
||||||
oldMangaService.unfollow(mangaId);
|
|
||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,15 +6,8 @@ import com.magamochi.model.dto.ImportRequestDTO;
|
|||||||
// import com.magamochi.service.MangaImportService;
|
// import com.magamochi.service.MangaImportService;
|
||||||
import com.magamochi.service.ProviderManualMangaImportService;
|
import com.magamochi.service.ProviderManualMangaImportService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/manga/import")
|
@RequestMapping("/manga/import")
|
||||||
@ -34,29 +27,4 @@ public class MangaImportController {
|
|||||||
return DefaultResponseDTO.ok(
|
return DefaultResponseDTO.ok(
|
||||||
providerManualMangaImportService.importFromProvider(providerId, requestDTO));
|
providerManualMangaImportService.importFromProvider(providerId, requestDTO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Upload multiple files",
|
|
||||||
description = "Accepts multiple files via multipart/form-data and processes them.",
|
|
||||||
tags = {"Manga Import"},
|
|
||||||
operationId = "importMultipleFiles")
|
|
||||||
@PostMapping(
|
|
||||||
value = "/upload",
|
|
||||||
consumes = {"multipart/form-data"})
|
|
||||||
public DefaultResponseDTO<Void> uploadMultipleFiles(
|
|
||||||
@RequestPart("malId") @NotBlank String malId,
|
|
||||||
@Parameter(
|
|
||||||
description = "List of files to upload",
|
|
||||||
required = true,
|
|
||||||
content =
|
|
||||||
@Content(
|
|
||||||
mediaType = "multipart/form-data",
|
|
||||||
schema = @Schema(type = "array", format = "binary")))
|
|
||||||
@RequestPart("files")
|
|
||||||
@NotNull
|
|
||||||
List<MultipartFile> files) {
|
|
||||||
// mangaImportService.importMangaFiles(malId, files);
|
|
||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
package com.magamochi.controller;
|
|
||||||
|
|
||||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
|
||||||
import com.magamochi.service.UserFavoriteMangaService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/mangas")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class UserFavoriteMangaController {
|
|
||||||
private final UserFavoriteMangaService userFavoriteMangaService;
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Favorite a manga",
|
|
||||||
description = "Set a manga as favorite for the logged user.",
|
|
||||||
tags = {"Favorite Mangas"},
|
|
||||||
operationId = "setFavorite")
|
|
||||||
@PostMapping("/{id}/favorite")
|
|
||||||
public DefaultResponseDTO<Void> setFavorite(@PathVariable Long id) {
|
|
||||||
userFavoriteMangaService.setFavorite(id);
|
|
||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Unfavorite a manga",
|
|
||||||
description = "Remove a manga from favorites for the logged user.",
|
|
||||||
tags = {"Favorite Mangas"},
|
|
||||||
operationId = "setUnfavorite")
|
|
||||||
@PostMapping("/{id}/unfavorite")
|
|
||||||
public DefaultResponseDTO<Void> setUnfavorite(@PathVariable Long id) {
|
|
||||||
userFavoriteMangaService.setUnfavorite(id);
|
|
||||||
|
|
||||||
return DefaultResponseDTO.ok().build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -37,19 +37,26 @@ public class ImageFetchService {
|
|||||||
|
|
||||||
var imageBytes = response.body();
|
var imageBytes = response.body();
|
||||||
|
|
||||||
var fileContentType = resolveContentType(response, imageBytes);
|
return uploadImage(imageBytes, response, contentType);
|
||||||
|
|
||||||
var fileHash = computeHash(imageBytes);
|
|
||||||
|
|
||||||
return imageManagerService.upload(
|
|
||||||
imageBytes, fileContentType, contentType.name().toLowerCase(), fileHash);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to fetch image from URL: {}", imageUrl, e);
|
log.error("Failed to fetch image from URL: {}", imageUrl, e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UUID uploadImage(
|
||||||
|
byte[] imageBytes, HttpResponse<byte[]> httpResponse, ContentType contentType)
|
||||||
|
throws NoSuchAlgorithmException {
|
||||||
|
var fileContentType = resolveContentType(httpResponse, imageBytes);
|
||||||
|
|
||||||
|
var fileHash = computeHash(imageBytes);
|
||||||
|
|
||||||
|
return imageManagerService.upload(
|
||||||
|
imageBytes, fileContentType, contentType.name().toLowerCase(), fileHash);
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveContentType(HttpResponse<byte[]> response, byte[] fileBytes) {
|
private String resolveContentType(HttpResponse<byte[]> response, byte[] fileBytes) {
|
||||||
|
if (nonNull(response)) {
|
||||||
var headerType =
|
var headerType =
|
||||||
response
|
response
|
||||||
.headers()
|
.headers()
|
||||||
@ -60,6 +67,7 @@ public class ImageFetchService {
|
|||||||
if (nonNull(headerType) && headerType.startsWith("image/")) {
|
if (nonNull(headerType) && headerType.startsWith("image/")) {
|
||||||
return headerType;
|
return headerType;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return tika.detect(fileBytes);
|
return tika.detect(fileBytes);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.magamochi.image.service;
|
|||||||
import com.magamochi.common.exception.NotFoundException;
|
import com.magamochi.common.exception.NotFoundException;
|
||||||
import com.magamochi.image.model.entity.Image;
|
import com.magamochi.image.model.entity.Image;
|
||||||
import com.magamochi.image.model.repository.ImageRepository;
|
import com.magamochi.image.model.repository.ImageRepository;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -51,4 +52,8 @@ public class ImageService {
|
|||||||
public List<Image> findAll() {
|
public List<Image> findAll() {
|
||||||
return imageRepository.findAll();
|
return imageRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public InputStream getStream(Image image) {
|
||||||
|
return s3Service.getFileStream(image.getObjectKey());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.magamochi.image.service;
|
|||||||
|
|
||||||
import static java.util.Objects.nonNull;
|
import static java.util.Objects.nonNull;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -103,4 +104,10 @@ public class S3Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public InputStream getFileStream(String key) {
|
||||||
|
var request = GetObjectRequest.builder().bucket(bucket).key(key).build();
|
||||||
|
|
||||||
|
return s3Client.getObject(request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
package com.magamochi.ingestion.model.repository;
|
package com.magamochi.ingestion.model.repository;
|
||||||
|
|
||||||
import com.magamochi.ingestion.model.entity.ContentProvider;
|
import com.magamochi.ingestion.model.entity.ContentProvider;
|
||||||
|
import java.util.Optional;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
public interface ContentProviderRepository extends JpaRepository<ContentProvider, Long> {}
|
public interface ContentProviderRepository extends JpaRepository<ContentProvider, Long> {
|
||||||
|
Optional<ContentProvider> findByNameIgnoreCase(String name);
|
||||||
|
}
|
||||||
|
|||||||
@ -5,11 +5,11 @@ import static java.util.Objects.isNull;
|
|||||||
import com.google.common.util.concurrent.RateLimiter;
|
import com.google.common.util.concurrent.RateLimiter;
|
||||||
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
import com.magamochi.client.MangaDexClient;
|
import com.magamochi.client.MangaDexClient;
|
||||||
|
import com.magamochi.common.ContentProviders;
|
||||||
import com.magamochi.common.exception.UnprocessableException;
|
import com.magamochi.common.exception.UnprocessableException;
|
||||||
import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
|
import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
|
||||||
import com.magamochi.ingestion.model.dto.ContentInfoDTO;
|
import com.magamochi.ingestion.model.dto.ContentInfoDTO;
|
||||||
import com.magamochi.ingestion.providers.ContentProvider;
|
import com.magamochi.ingestion.providers.ContentProvider;
|
||||||
import com.magamochi.ingestion.providers.ContentProviders;
|
|
||||||
import com.magamochi.ingestion.providers.ManualImportContentProvider;
|
import com.magamochi.ingestion.providers.ManualImportContentProvider;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
package com.magamochi.ingestion.providers.impl;
|
package com.magamochi.ingestion.providers.impl;
|
||||||
|
|
||||||
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
|
import com.magamochi.common.ContentProviders;
|
||||||
import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
|
import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
|
||||||
import com.magamochi.ingestion.model.dto.ContentInfoDTO;
|
import com.magamochi.ingestion.model.dto.ContentInfoDTO;
|
||||||
import com.magamochi.ingestion.model.dto.MangaInfoDTO;
|
import com.magamochi.ingestion.model.dto.MangaInfoDTO;
|
||||||
import com.magamochi.ingestion.providers.ContentProvider;
|
import com.magamochi.ingestion.providers.ContentProvider;
|
||||||
import com.magamochi.ingestion.providers.ContentProviders;
|
|
||||||
import com.magamochi.ingestion.providers.PagedContentProvider;
|
import com.magamochi.ingestion.providers.PagedContentProvider;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|||||||
@ -3,11 +3,11 @@ package com.magamochi.ingestion.providers.impl;
|
|||||||
import static java.util.Objects.nonNull;
|
import static java.util.Objects.nonNull;
|
||||||
|
|
||||||
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
|
import com.magamochi.common.ContentProviders;
|
||||||
import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
|
import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
|
||||||
import com.magamochi.ingestion.model.dto.ContentInfoDTO;
|
import com.magamochi.ingestion.model.dto.ContentInfoDTO;
|
||||||
import com.magamochi.ingestion.model.dto.MangaInfoDTO;
|
import com.magamochi.ingestion.model.dto.MangaInfoDTO;
|
||||||
import com.magamochi.ingestion.providers.ContentProvider;
|
import com.magamochi.ingestion.providers.ContentProvider;
|
||||||
import com.magamochi.ingestion.providers.ContentProviders;
|
|
||||||
import com.magamochi.ingestion.providers.PagedContentProvider;
|
import com.magamochi.ingestion.providers.PagedContentProvider;
|
||||||
import com.magamochi.ingestion.service.FlareService;
|
import com.magamochi.ingestion.service.FlareService;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|||||||
@ -4,11 +4,11 @@ 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.catalog.model.entity.MangaContentProvider;
|
import com.magamochi.catalog.model.entity.MangaContentProvider;
|
||||||
|
import com.magamochi.common.ContentProviders;
|
||||||
import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
|
import com.magamochi.ingestion.model.dto.ContentImageInfoDTO;
|
||||||
import com.magamochi.ingestion.model.dto.ContentInfoDTO;
|
import com.magamochi.ingestion.model.dto.ContentInfoDTO;
|
||||||
import com.magamochi.ingestion.model.dto.MangaInfoDTO;
|
import com.magamochi.ingestion.model.dto.MangaInfoDTO;
|
||||||
import com.magamochi.ingestion.providers.ContentProvider;
|
import com.magamochi.ingestion.providers.ContentProvider;
|
||||||
import com.magamochi.ingestion.providers.ContentProviders;
|
|
||||||
import com.magamochi.ingestion.providers.PagedContentProvider;
|
import com.magamochi.ingestion.providers.PagedContentProvider;
|
||||||
import com.magamochi.ingestion.service.FlareService;
|
import com.magamochi.ingestion.service.FlareService;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|||||||
@ -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.ContentProviders;
|
||||||
import com.magamochi.common.exception.NotFoundException;
|
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;
|
||||||
@ -32,4 +33,10 @@ public class ContentProviderService {
|
|||||||
new NotFoundException(
|
new NotFoundException(
|
||||||
"Content Provider not found (ID: " + contentProviderId + ")."));
|
"Content Provider not found (ID: " + contentProviderId + ")."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ContentProvider findManualImportContentProvider() {
|
||||||
|
return contentProviderRepository
|
||||||
|
.findByNameIgnoreCase(ContentProviders.MANUAL_IMPORT)
|
||||||
|
.orElseThrow(() -> new NotFoundException("Manual Import Content Provider not found"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
package com.magamochi.model.enumeration;
|
|
||||||
|
|
||||||
public enum ArchiveFileType {
|
|
||||||
CBZ,
|
|
||||||
CBR
|
|
||||||
}
|
|
||||||
@ -1,438 +0,0 @@
|
|||||||
// package com.magamochi.service;
|
|
||||||
//
|
|
||||||
// import static java.util.Objects.isNull;
|
|
||||||
// import static java.util.Objects.nonNull;
|
|
||||||
//
|
|
||||||
// import com.google.common.util.concurrent.RateLimiter;
|
|
||||||
// import com.magamochi.catalog.model.entity.Genre;
|
|
||||||
// import com.magamochi.catalog.model.repository.GenreRepository;
|
|
||||||
// import com.magamochi.catalog.client.AniListClient;
|
|
||||||
// import com.magamochi.catalog.client.JikanClient;
|
|
||||||
// import com.magamochi.common.exception.NotFoundException;
|
|
||||||
// import com.magamochi.ingestion.model.entity.ContentProvider;
|
|
||||||
// import com.magamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
|
||||||
// import com.magamochi.model.entity.*;
|
|
||||||
// import com.magamochi.model.repository.*;
|
|
||||||
// import com.magamochi.catalog.util.DoubleUtil;
|
|
||||||
// import java.io.*;
|
|
||||||
// import java.net.URI;
|
|
||||||
// import java.net.URISyntaxException;
|
|
||||||
// import java.net.URL;
|
|
||||||
// import java.time.OffsetDateTime;
|
|
||||||
// import java.time.ZoneOffset;
|
|
||||||
// import java.util.ArrayList;
|
|
||||||
// import java.util.Comparator;
|
|
||||||
// import java.util.List;
|
|
||||||
// import java.util.stream.IntStream;
|
|
||||||
// import java.util.zip.ZipEntry;
|
|
||||||
// import java.util.zip.ZipInputStream;
|
|
||||||
// import lombok.RequiredArgsConstructor;
|
|
||||||
// import lombok.extern.log4j.Log4j2;
|
|
||||||
// import org.apache.commons.lang3.StringUtils;
|
|
||||||
// import org.springframework.stereotype.Service;
|
|
||||||
// import org.springframework.web.multipart.MultipartFile;
|
|
||||||
//
|
|
||||||
// @Log4j2
|
|
||||||
//// @Service
|
|
||||||
// @RequiredArgsConstructor
|
|
||||||
// public class MangaImportService {
|
|
||||||
// private final ProviderService providerService;
|
|
||||||
// private final MangaCreationService mangaCreationService;
|
|
||||||
// private final ImageService imageService;
|
|
||||||
// private final LanguageService languageService;
|
|
||||||
//
|
|
||||||
// private final GenreRepository genreRepository;
|
|
||||||
// private final MangaGenreRepository mangaGenreRepository;
|
|
||||||
// private final MangaContentProviderRepository mangaContentProviderRepository;
|
|
||||||
// private final AuthorRepository authorRepository;
|
|
||||||
// private final MangaAuthorRepository mangaAuthorRepository;
|
|
||||||
// private final MangaChapterRepository mangaChapterRepository;
|
|
||||||
// private final MangaRepository mangaRepository;
|
|
||||||
//
|
|
||||||
// private final JikanClient jikanClient;
|
|
||||||
// private final AniListClient aniListClient;
|
|
||||||
// private final MangaChapterImageRepository mangaChapterImageRepository;
|
|
||||||
// private final MangaAlternativeTitlesRepository mangaAlternativeTitlesRepository;
|
|
||||||
//
|
|
||||||
// private final RateLimiter jikanRateLimiter;
|
|
||||||
//
|
|
||||||
// public void importMangaFiles(String malId, List<MultipartFile> files) {
|
|
||||||
// log.info("Importing manga files for MAL ID {}", malId);
|
|
||||||
// var provider = providerService.getOrCreateProvider("Manual Import", false);
|
|
||||||
//
|
|
||||||
// jikanRateLimiter.acquire();
|
|
||||||
// var mangaData = jikanClient.getMangaById(Long.parseLong(malId));
|
|
||||||
//
|
|
||||||
// var mangaProvider = getOrCreateMangaProvider(mangaData.data().title(), provider);
|
|
||||||
//
|
|
||||||
// var sortedFiles =
|
|
||||||
// files.stream().sorted(Comparator.comparing(MultipartFile::getName)).toList();
|
|
||||||
//
|
|
||||||
// IntStream.rangeClosed(1, sortedFiles.size())
|
|
||||||
// .forEach(
|
|
||||||
// fileIndex -> {
|
|
||||||
// var file = sortedFiles.get(fileIndex - 1);
|
|
||||||
// log.info(
|
|
||||||
// "Importing file {}/{}: {}, for Mangá {}",
|
|
||||||
// fileIndex,
|
|
||||||
// sortedFiles.size(),
|
|
||||||
// file.getOriginalFilename(),
|
|
||||||
// mangaProvider.getManga().getTitle());
|
|
||||||
//
|
|
||||||
// var chapter =
|
|
||||||
// persistMangaChapter(
|
|
||||||
// mangaProvider,
|
|
||||||
// new ContentProviderMangaChapterResponseDTO(
|
|
||||||
// removeFileExtension(file.getOriginalFilename()),
|
|
||||||
// "manual_" + file.getOriginalFilename(),
|
|
||||||
// file.getOriginalFilename(),
|
|
||||||
// "en-US"));
|
|
||||||
//
|
|
||||||
// List<MangaChapterImage> allChapterImages = new ArrayList<>();
|
|
||||||
// try (InputStream is = file.getInputStream();
|
|
||||||
// ZipInputStream zis = new ZipInputStream(is)) {
|
|
||||||
// ZipEntry entry;
|
|
||||||
// var position = 0;
|
|
||||||
//
|
|
||||||
// while ((entry = zis.getNextEntry()) != null) {
|
|
||||||
// if (entry.isDirectory()) {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// var os = new ByteArrayOutputStream();
|
|
||||||
// zis.transferTo(os);
|
|
||||||
// var bytes = os.toByteArray();
|
|
||||||
//
|
|
||||||
// var image =
|
|
||||||
// imageService.uploadImage(bytes, "image/jpeg", "chapter/" + chapter.getId());
|
|
||||||
//
|
|
||||||
// var chapterImage =
|
|
||||||
// MangaChapterImage.builder()
|
|
||||||
// .position(position++)
|
|
||||||
// .image(image)
|
|
||||||
// .mangaChapter(chapter)
|
|
||||||
// .build();
|
|
||||||
//
|
|
||||||
// allChapterImages.add(chapterImage);
|
|
||||||
// zis.closeEntry();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// log.info("Chapter images added for chapter {}", chapter.getTitle());
|
|
||||||
// } catch (IOException e) {
|
|
||||||
// throw new RuntimeException(e);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// mangaChapterImageRepository.saveAll(allChapterImages);
|
|
||||||
// chapter.setDownloaded(true);
|
|
||||||
// mangaChapterRepository.save(chapter);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// log.info("Import manga files for MAL ID {} completed.", malId);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public void updateMangaData(Long mangaId) {
|
|
||||||
// var manga =
|
|
||||||
// mangaRepository
|
|
||||||
// .findById(mangaId)
|
|
||||||
// .orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
|
|
||||||
//
|
|
||||||
// updateMangaData(manga);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public void updateMangaData(Manga manga) {
|
|
||||||
// log.info("Updating manga {}", manga.getTitle());
|
|
||||||
//
|
|
||||||
// if (nonNull(manga.getMalId())) {
|
|
||||||
// try {
|
|
||||||
// updateFromJikan(manga);
|
|
||||||
// return;
|
|
||||||
// } catch (Exception e) {
|
|
||||||
// log.warn(
|
|
||||||
// "Error updating manga data from Jikan for manga {}. Trying AniList... Error: {}",
|
|
||||||
// manga.getTitle(),
|
|
||||||
// e.getMessage());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (nonNull(manga.getAniListId())) {
|
|
||||||
// try {
|
|
||||||
// updateFromAniList(manga);
|
|
||||||
// return;
|
|
||||||
// } catch (Exception e) {
|
|
||||||
// log.warn(
|
|
||||||
// "Error updating manga data from AniList for manga {}. Error: {}",
|
|
||||||
// manga.getTitle(),
|
|
||||||
// e.getMessage());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// log.warn(
|
|
||||||
// "Could not update manga data for {}. No provider data available/found.",
|
|
||||||
// manga.getTitle());
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private void updateFromJikan(Manga manga) throws IOException, URISyntaxException {
|
|
||||||
// jikanRateLimiter.acquire();
|
|
||||||
// var mangaData = jikanClient.getMangaById(manga.getMalId());
|
|
||||||
//
|
|
||||||
// manga.setSynopsis(mangaData.data().synopsis());
|
|
||||||
// manga.setStatus(mangaData.data().status());
|
|
||||||
// manga.setScore(DoubleUtil.round((double) mangaData.data().score(), 2));
|
|
||||||
// manga.setPublishedFrom(mangaData.data().published().from());
|
|
||||||
// manga.setPublishedTo(mangaData.data().published().to());
|
|
||||||
// manga.setChapterCount(mangaData.data().chapters());
|
|
||||||
//
|
|
||||||
// var authors =
|
|
||||||
// mangaData.data().authors().stream()
|
|
||||||
// .map(
|
|
||||||
// authorData ->
|
|
||||||
// authorRepository
|
|
||||||
// .findByMalId(authorData.mal_id())
|
|
||||||
// .orElseGet(
|
|
||||||
// () ->
|
|
||||||
// authorRepository.save(
|
|
||||||
// Author.builder()
|
|
||||||
// .malId(authorData.mal_id())
|
|
||||||
// .name(authorData.name())
|
|
||||||
// .build())))
|
|
||||||
// .toList();
|
|
||||||
//
|
|
||||||
// updateMangaAuthors(manga, authors);
|
|
||||||
//
|
|
||||||
// var genres =
|
|
||||||
// mangaData.data().genres().stream()
|
|
||||||
// .map(
|
|
||||||
// genreData ->
|
|
||||||
// genreRepository
|
|
||||||
// .findByMalId(genreData.mal_id())
|
|
||||||
// .orElseGet(
|
|
||||||
// () ->
|
|
||||||
// genreRepository.save(
|
|
||||||
// Genre.builder()
|
|
||||||
// .malId(genreData.mal_id())
|
|
||||||
// .name(genreData.name())
|
|
||||||
// .build())))
|
|
||||||
// .toList();
|
|
||||||
//
|
|
||||||
// updateMangaGenres(manga, genres);
|
|
||||||
//
|
|
||||||
// if (isNull(manga.getCoverImage())) {
|
|
||||||
// downloadCoverImage(manga, mangaData.data().images().jpg().large_image_url());
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// var mangaEntity = mangaRepository.save(manga);
|
|
||||||
// var alternativeTitles =
|
|
||||||
// mangaData.data().title_synonyms().stream()
|
|
||||||
// .map(at -> MangaAlternativeTitle.builder().manga(mangaEntity).title(at).build())
|
|
||||||
// .toList();
|
|
||||||
// mangaAlternativeTitlesRepository.saveAll(alternativeTitles);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private void updateFromAniList(Manga manga) throws IOException, URISyntaxException {
|
|
||||||
// var query =
|
|
||||||
// """
|
|
||||||
// query ($id: Int) {
|
|
||||||
// Media (id: $id, type: MANGA) {
|
|
||||||
// startDate { year month day }
|
|
||||||
// endDate { year month day }
|
|
||||||
// description
|
|
||||||
// status
|
|
||||||
// averageScore
|
|
||||||
// chapters
|
|
||||||
// coverImage { large }
|
|
||||||
// genres
|
|
||||||
// staff {
|
|
||||||
// edges {
|
|
||||||
// role
|
|
||||||
// node {
|
|
||||||
// name {
|
|
||||||
// full
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// """;
|
|
||||||
// var request =
|
|
||||||
// new AniListClient.GraphQLRequest(
|
|
||||||
// query, new AniListClient.GraphQLRequest.Variables(manga.getAniListId()));
|
|
||||||
// var media = aniListClient.getManga(request).data().Media();
|
|
||||||
//
|
|
||||||
// manga.setSynopsis(media.description());
|
|
||||||
// manga.setStatus(mapAniListStatus(media.status()));
|
|
||||||
// manga.setScore(DoubleUtil.round((double) media.averageScore() / 10, 2)); // 0-100 -> 0-10
|
|
||||||
// manga.setPublishedFrom(convertFuzzyDate(media.startDate()));
|
|
||||||
// manga.setPublishedTo(convertFuzzyDate(media.endDate()));
|
|
||||||
// manga.setChapterCount(media.chapters());
|
|
||||||
//
|
|
||||||
// var authors =
|
|
||||||
// media.staff().edges().stream()
|
|
||||||
// .filter(edge -> isAuthorRole(edge.role()))
|
|
||||||
// .map(edge -> edge.node().name().full())
|
|
||||||
// .distinct()
|
|
||||||
// .map(
|
|
||||||
// name ->
|
|
||||||
// authorRepository
|
|
||||||
// .findByName(name)
|
|
||||||
// .orElseGet(
|
|
||||||
// () -> authorRepository.save(Author.builder().name(name).build())))
|
|
||||||
// .toList();
|
|
||||||
//
|
|
||||||
// updateMangaAuthors(manga, authors);
|
|
||||||
//
|
|
||||||
// var genres =
|
|
||||||
// media.genres().stream()
|
|
||||||
// .map(
|
|
||||||
// name ->
|
|
||||||
// genreRepository
|
|
||||||
// .findByName(name)
|
|
||||||
// .orElseGet(() ->
|
|
||||||
// genreRepository.save(Genre.builder().name(name).build())))
|
|
||||||
// .toList();
|
|
||||||
//
|
|
||||||
// updateMangaGenres(manga, genres);
|
|
||||||
//
|
|
||||||
// if (isNull(manga.getCoverImage())) {
|
|
||||||
// downloadCoverImage(manga, media.coverImage().large());
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// mangaRepository.save(manga);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private boolean isAuthorRole(String role) {
|
|
||||||
// return role.equalsIgnoreCase("Story & Art")
|
|
||||||
// || role.equalsIgnoreCase("Story")
|
|
||||||
// || role.equalsIgnoreCase("Art");
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private String mapAniListStatus(String status) {
|
|
||||||
// return switch (status) {
|
|
||||||
// case "RELEASING" -> "Publishing";
|
|
||||||
// case "FINISHED" -> "Finished";
|
|
||||||
// case "NOT_YET_RELEASED" -> "Not yet published";
|
|
||||||
// default -> "Unknown";
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private OffsetDateTime convertFuzzyDate(AniListClient.MangaResponse.Manga.FuzzyDate date) {
|
|
||||||
// if (isNull(date) || isNull(date.year())) {
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// return OffsetDateTime.of(
|
|
||||||
// date.year(),
|
|
||||||
// isNull(date.month()) ? 1 : date.month(),
|
|
||||||
// isNull(date.day()) ? 1 : date.day(),
|
|
||||||
// 0,
|
|
||||||
// 0,
|
|
||||||
// 0,
|
|
||||||
// 0,
|
|
||||||
// ZoneOffset.UTC);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private void updateMangaAuthors(Manga manga, List<Author> authors) {
|
|
||||||
// var mangaAuthors =
|
|
||||||
// authors.stream()
|
|
||||||
// .map(
|
|
||||||
// author ->
|
|
||||||
// mangaAuthorRepository
|
|
||||||
// .findByMangaAndAuthor(manga, author)
|
|
||||||
// .orElseGet(
|
|
||||||
// () ->
|
|
||||||
// mangaAuthorRepository.save(
|
|
||||||
// MangaAuthor.builder().manga(manga).author(author).build())))
|
|
||||||
// .toList();
|
|
||||||
// manga.setMangaAuthors(mangaAuthors);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private void updateMangaGenres(Manga manga, List<Genre> genres) {
|
|
||||||
// var mangaGenres =
|
|
||||||
// genres.stream()
|
|
||||||
// .map(
|
|
||||||
// genre ->
|
|
||||||
// mangaGenreRepository
|
|
||||||
// .findByMangaAndGenre(manga, genre)
|
|
||||||
// .orElseGet(
|
|
||||||
// () ->
|
|
||||||
// mangaGenreRepository.save(
|
|
||||||
// MangaGenre.builder().manga(manga).genre(genre).build())))
|
|
||||||
// .toList();
|
|
||||||
// manga.setMangaGenres(mangaGenres);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private void downloadCoverImage(Manga manga, String imageUrl)
|
|
||||||
// throws IOException, URISyntaxException {
|
|
||||||
// var inputStream =
|
|
||||||
// new BufferedInputStream(new URL(new URI(imageUrl).toASCIIString()).openStream());
|
|
||||||
//
|
|
||||||
// var bytes = inputStream.readAllBytes();
|
|
||||||
//
|
|
||||||
// inputStream.close();
|
|
||||||
// var image = imageService.uploadImage(bytes, "image/jpeg", "cover");
|
|
||||||
//
|
|
||||||
// manga.setCoverImage(image);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public MangaChapter persistMangaChapter(
|
|
||||||
// MangaContentProvider mangaContentProvider, ContentProviderMangaChapterResponseDTO chapter) {
|
|
||||||
// var mangaChapter =
|
|
||||||
// mangaChapterRepository
|
|
||||||
// .findByMangaContentProviderAndUrlIgnoreCase(mangaContentProvider,
|
|
||||||
// chapter.url())
|
|
||||||
// .orElseGet(MangaChapter::new);
|
|
||||||
//
|
|
||||||
// mangaChapter.setMangaContentProvider(mangaContentProvider);
|
|
||||||
// mangaChapter.setTitle(chapter.title());
|
|
||||||
// mangaChapter.setUrl(chapter.url());
|
|
||||||
//
|
|
||||||
// var language = languageService.getOrThrow(chapter.languageCode());
|
|
||||||
// mangaChapter.setLanguage(language);
|
|
||||||
//
|
|
||||||
// if (nonNull(chapter.chapter())) {
|
|
||||||
// try {
|
|
||||||
// mangaChapter.setChapterNumber(Integer.parseInt(chapter.chapter()));
|
|
||||||
// } catch (NumberFormatException e) {
|
|
||||||
// log.warn(
|
|
||||||
// "Could not parse chapter number {} from manga {}",
|
|
||||||
// chapter.chapter(),
|
|
||||||
// mangaContentProvider.getManga().getTitle());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return mangaChapterRepository.save(mangaChapter);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private MangaContentProvider getOrCreateMangaProvider(
|
|
||||||
// String title, ContentProvider contentProvider) {
|
|
||||||
// return mangaContentProviderRepository
|
|
||||||
// .findByMangaTitleIgnoreCaseAndContentProvider(title, contentProvider)
|
|
||||||
// .orElseGet(
|
|
||||||
// () -> {
|
|
||||||
// jikanRateLimiter.acquire();
|
|
||||||
// var manga = mangaCreationService.getOrCreateManga(title, "manual", contentProvider);
|
|
||||||
//
|
|
||||||
// return mangaContentProviderRepository.save(
|
|
||||||
// MangaContentProvider.builder()
|
|
||||||
// .manga(manga)
|
|
||||||
// .mangaTitle(manga.getTitle())
|
|
||||||
// .contentProvider(contentProvider)
|
|
||||||
// .url("manual")
|
|
||||||
// .build());
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private String removeFileExtension(String filename) {
|
|
||||||
// if (StringUtils.isBlank(filename)) {
|
|
||||||
// return filename;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// int lastDotIndex = filename.lastIndexOf('.');
|
|
||||||
//
|
|
||||||
// // No dot, or dot is the first character (like .gitignore)
|
|
||||||
// if (lastDotIndex <= 0) {
|
|
||||||
// return filename;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return filename.substring(0, lastDotIndex);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
package com.magamochi.service;
|
|
||||||
|
|
||||||
import com.magamochi.image.model.entity.Image;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.log4j.Log4j2;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Log4j2
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class OldImageService {
|
|
||||||
private final OldS3Service oldS3Service;
|
|
||||||
|
|
||||||
public InputStream getImageStream(Image image) {
|
|
||||||
return oldS3Service.getFile(image.getObjectKey());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +1,21 @@
|
|||||||
package com.magamochi.service;
|
package com.magamochi.service;
|
||||||
|
|
||||||
import com.magamochi.catalog.model.entity.Manga;
|
|
||||||
import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
|
import com.magamochi.catalog.model.repository.MangaContentProviderRepository;
|
||||||
import com.magamochi.catalog.model.repository.MangaRepository;
|
|
||||||
import com.magamochi.common.exception.NotFoundException;
|
import com.magamochi.common.exception.NotFoundException;
|
||||||
import com.magamochi.content.model.entity.MangaContent;
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
import com.magamochi.model.dto.*;
|
import com.magamochi.model.dto.*;
|
||||||
import com.magamochi.model.entity.UserMangaFollow;
|
|
||||||
import com.magamochi.model.repository.*;
|
|
||||||
import com.magamochi.queue.MangaChapterDownloadProducer;
|
import com.magamochi.queue.MangaChapterDownloadProducer;
|
||||||
import com.magamochi.user.service.UserService;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
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;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
@Log4j2
|
@Log4j2
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class OldMangaService {
|
public class OldMangaService {
|
||||||
private final UserService userService;
|
|
||||||
private final MangaRepository mangaRepository;
|
|
||||||
private final MangaContentProviderRepository mangaContentProviderRepository;
|
private final MangaContentProviderRepository mangaContentProviderRepository;
|
||||||
|
|
||||||
private final UserMangaFollowRepository userMangaFollowRepository;
|
|
||||||
|
|
||||||
private final MangaChapterDownloadProducer mangaChapterDownloadProducer;
|
private final MangaChapterDownloadProducer mangaChapterDownloadProducer;
|
||||||
|
|
||||||
public void fetchAllNotDownloadedChapters(Long mangaProviderId) {
|
public void fetchAllNotDownloadedChapters(Long mangaProviderId) {
|
||||||
@ -81,36 +71,4 @@ public class OldMangaService {
|
|||||||
// mangaProvider.getContentProvider().getName())));
|
// mangaProvider.getContentProvider().getName())));
|
||||||
// }
|
// }
|
||||||
|
|
||||||
public Manga findMangaByIdThrowIfNotFound(Long mangaId) {
|
|
||||||
return mangaRepository
|
|
||||||
.findById(mangaId)
|
|
||||||
.orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void follow(Long mangaId) {
|
|
||||||
var user = userService.getLoggedUserThrowIfNotFound();
|
|
||||||
|
|
||||||
var manga = findMangaByIdThrowIfNotFound(mangaId);
|
|
||||||
manga.setFollow(true);
|
|
||||||
|
|
||||||
if (userMangaFollowRepository.existsByUserAndManga(user, manga)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
userMangaFollowRepository.save(UserMangaFollow.builder().user(user).manga(manga).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void unfollow(Long mangaId) {
|
|
||||||
var user = userService.getLoggedUserThrowIfNotFound();
|
|
||||||
var manga = findMangaByIdThrowIfNotFound(mangaId);
|
|
||||||
|
|
||||||
var userMangaFollow = userMangaFollowRepository.findByUserAndManga(user, manga);
|
|
||||||
userMangaFollow.ifPresent(userMangaFollowRepository::delete);
|
|
||||||
|
|
||||||
if (!userMangaFollowRepository.existsByManga(manga)) {
|
|
||||||
manga.setFollow(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
package com.magamochi.service;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import software.amazon.awssdk.services.s3.S3Client;
|
|
||||||
import software.amazon.awssdk.services.s3.model.*;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class OldS3Service {
|
|
||||||
@Value("${minio.bucket}")
|
|
||||||
private String bucket;
|
|
||||||
|
|
||||||
private final S3Client s3Client;
|
|
||||||
|
|
||||||
public InputStream getFile(String key) {
|
|
||||||
var request = GetObjectRequest.builder().bucket(bucket).key(key).build();
|
|
||||||
|
|
||||||
return s3Client.getObject(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
package com.magamochi.service;
|
|
||||||
|
|
||||||
import com.magamochi.catalog.model.entity.Manga;
|
|
||||||
import com.magamochi.catalog.model.repository.MangaRepository;
|
|
||||||
import com.magamochi.common.exception.NotFoundException;
|
|
||||||
import com.magamochi.model.entity.UserFavoriteManga;
|
|
||||||
import com.magamochi.model.repository.UserFavoriteMangaRepository;
|
|
||||||
import com.magamochi.user.service.UserService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class UserFavoriteMangaService {
|
|
||||||
private final UserService userService;
|
|
||||||
private final MangaRepository mangaRepository;
|
|
||||||
private final UserFavoriteMangaRepository userFavoriteMangaRepository;
|
|
||||||
|
|
||||||
public void setFavorite(Long id) {
|
|
||||||
var user = userService.getLoggedUserThrowIfNotFound();
|
|
||||||
var manga = findMangaByIdThrowIfNotFound(id);
|
|
||||||
|
|
||||||
if (userFavoriteMangaRepository.existsByUserAndManga(user, manga)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
userFavoriteMangaRepository.save(UserFavoriteManga.builder().user(user).manga(manga).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUnfavorite(Long id) {
|
|
||||||
var user = userService.getLoggedUserThrowIfNotFound();
|
|
||||||
var manga = findMangaByIdThrowIfNotFound(id);
|
|
||||||
|
|
||||||
var favoriteManga =
|
|
||||||
userFavoriteMangaRepository
|
|
||||||
.findByUserAndManga(user, manga)
|
|
||||||
.orElseThrow(
|
|
||||||
() ->
|
|
||||||
new NotFoundException(
|
|
||||||
"Error while trying to unfavorite manga. Please try again later."));
|
|
||||||
|
|
||||||
userFavoriteMangaRepository.delete(favoriteManga);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Manga findMangaByIdThrowIfNotFound(Long mangaId) {
|
|
||||||
return mangaRepository
|
|
||||||
.findById(mangaId)
|
|
||||||
.orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,7 +7,6 @@ import com.magamochi.queue.UpdateMangaFollowChapterListProducer;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@ -22,7 +21,7 @@ public class MangaFollowUpdateTask {
|
|||||||
|
|
||||||
private final UpdateMangaFollowChapterListProducer producer;
|
private final UpdateMangaFollowChapterListProducer producer;
|
||||||
|
|
||||||
@Scheduled(cron = "${manga-follow.cron-expression}")
|
// @Scheduled(cron = "${manga-follow.cron-expression}")
|
||||||
@Transactional
|
@Transactional
|
||||||
public void updateMangaFollow() {
|
public void updateMangaFollow() {
|
||||||
if (!updateEnabled) {
|
if (!updateEnabled) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package com.magamochi.user.model.entity;
|
package com.magamochi.user.model.entity;
|
||||||
|
|
||||||
import com.magamochi.model.entity.UserFavoriteManga;
|
|
||||||
import com.magamochi.user.model.enumeration.UserRole;
|
import com.magamochi.user.model.enumeration.UserRole;
|
||||||
|
import com.magamochi.userinteraction.model.entity.UserFavoriteManga;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
|||||||
@ -0,0 +1,81 @@
|
|||||||
|
package com.magamochi.userinteraction.controller;
|
||||||
|
|
||||||
|
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||||
|
import com.magamochi.userinteraction.service.UserFavoriteMangaService;
|
||||||
|
import com.magamochi.userinteraction.service.UserMangaContentReadService;
|
||||||
|
import com.magamochi.userinteraction.service.UserMangaFollowService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/user-interaction")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserInteractionController {
|
||||||
|
private final UserFavoriteMangaService userFavoriteMangaService;
|
||||||
|
private final UserMangaContentReadService userMangaContentReadService;
|
||||||
|
private final UserMangaFollowService userMangaFollowService;
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Favorite a manga",
|
||||||
|
description = "Set a manga as favorite for the logged user.",
|
||||||
|
tags = {"User Interaction"},
|
||||||
|
operationId = "setFavorite")
|
||||||
|
@PostMapping("/manga/{mangaId}/favorite")
|
||||||
|
public DefaultResponseDTO<Void> setFavorite(@PathVariable Long mangaId) {
|
||||||
|
userFavoriteMangaService.setFavorite(mangaId);
|
||||||
|
|
||||||
|
return DefaultResponseDTO.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Unfavorite a manga",
|
||||||
|
description = "Remove a manga from favorites for the logged user.",
|
||||||
|
tags = {"User Interaction"},
|
||||||
|
operationId = "setUnfavorite")
|
||||||
|
@PostMapping("/manga/{mangaId}/unfavorite")
|
||||||
|
public DefaultResponseDTO<Void> setUnfavorite(@PathVariable Long mangaId) {
|
||||||
|
userFavoriteMangaService.setUnfavorite(mangaId);
|
||||||
|
|
||||||
|
return DefaultResponseDTO.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Mark content as read",
|
||||||
|
description = "Mark content as read by its ID.",
|
||||||
|
tags = {"User Interaction"},
|
||||||
|
operationId = "markContentAsRead")
|
||||||
|
@PostMapping("/content/{mangaContentId}/read")
|
||||||
|
public DefaultResponseDTO<Void> markContentAsRead(@PathVariable Long mangaContentId) {
|
||||||
|
userMangaContentReadService.setRead(mangaContentId);
|
||||||
|
|
||||||
|
return DefaultResponseDTO.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Follow the manga specified by its ID",
|
||||||
|
description = "Follow the manga specified by its ID.",
|
||||||
|
tags = {"User Interaction"},
|
||||||
|
operationId = "followManga")
|
||||||
|
@PostMapping("/manga/{mangaId}/follow")
|
||||||
|
public DefaultResponseDTO<Void> followManga(@PathVariable Long mangaId) {
|
||||||
|
userMangaFollowService.follow(mangaId);
|
||||||
|
|
||||||
|
return DefaultResponseDTO.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Unfollow the manga specified by its ID",
|
||||||
|
description = "Unfollow the manga specified by its ID.",
|
||||||
|
tags = {"User Interaction"},
|
||||||
|
operationId = "unfollowManga")
|
||||||
|
@PostMapping("/manga/{mangaId}/unfollow")
|
||||||
|
public DefaultResponseDTO<Void> unfollowManga(@PathVariable Long mangaId) {
|
||||||
|
userMangaFollowService.unfollow(mangaId);
|
||||||
|
|
||||||
|
return DefaultResponseDTO.ok().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.magamochi.model.entity;
|
package com.magamochi.userinteraction.model.entity;
|
||||||
|
|
||||||
import com.magamochi.catalog.model.entity.Manga;
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import com.magamochi.user.model.entity.User;
|
import com.magamochi.user.model.entity.User;
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package com.magamochi.userinteraction.model.entity;
|
||||||
|
|
||||||
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
|
import com.magamochi.user.model.entity.User;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.Instant;
|
||||||
|
import lombok.*;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "user_manga_content_read")
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class UserMangaContentRead {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "user_id")
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "manga_content_id")
|
||||||
|
private MangaContent mangaContent;
|
||||||
|
|
||||||
|
@CreationTimestamp private Instant createdAt;
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.magamochi.model.entity;
|
package com.magamochi.userinteraction.model.entity;
|
||||||
|
|
||||||
import com.magamochi.catalog.model.entity.Manga;
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import com.magamochi.user.model.entity.User;
|
import com.magamochi.user.model.entity.User;
|
||||||
@ -1,8 +1,8 @@
|
|||||||
package com.magamochi.model.repository;
|
package com.magamochi.userinteraction.model.repository;
|
||||||
|
|
||||||
import com.magamochi.catalog.model.entity.Manga;
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import com.magamochi.model.entity.UserFavoriteManga;
|
|
||||||
import com.magamochi.user.model.entity.User;
|
import com.magamochi.user.model.entity.User;
|
||||||
|
import com.magamochi.userinteraction.model.entity.UserFavoriteManga;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.magamochi.userinteraction.model.repository;
|
||||||
|
|
||||||
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
|
import com.magamochi.user.model.entity.User;
|
||||||
|
import com.magamochi.userinteraction.model.entity.UserMangaContentRead;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface UserMangaContentReadRepository extends JpaRepository<UserMangaContentRead, Long> {
|
||||||
|
boolean existsByUserAndMangaContent(User user, MangaContent mangaContent);
|
||||||
|
|
||||||
|
Optional<UserMangaContentRead> findByUserAndMangaContent(User user, MangaContent mangaContent);
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
package com.magamochi.model.repository;
|
package com.magamochi.userinteraction.model.repository;
|
||||||
|
|
||||||
import com.magamochi.catalog.model.entity.Manga;
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
import com.magamochi.model.entity.UserMangaFollow;
|
|
||||||
import com.magamochi.user.model.entity.User;
|
import com.magamochi.user.model.entity.User;
|
||||||
|
import com.magamochi.userinteraction.model.entity.UserMangaFollow;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package com.magamochi.userinteraction.service;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.service.MangaService;
|
||||||
|
import com.magamochi.user.service.UserService;
|
||||||
|
import com.magamochi.userinteraction.model.entity.UserFavoriteManga;
|
||||||
|
import com.magamochi.userinteraction.model.repository.UserFavoriteMangaRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserFavoriteMangaService {
|
||||||
|
private final UserService userService;
|
||||||
|
private final MangaService mangaService;
|
||||||
|
|
||||||
|
private final UserFavoriteMangaRepository userFavoriteMangaRepository;
|
||||||
|
|
||||||
|
public void setFavorite(Long id) {
|
||||||
|
var user = userService.getLoggedUserThrowIfNotFound();
|
||||||
|
var manga = mangaService.find(id);
|
||||||
|
|
||||||
|
if (userFavoriteMangaRepository.existsByUserAndManga(user, manga)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
userFavoriteMangaRepository.save(UserFavoriteManga.builder().user(user).manga(manga).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUnfavorite(Long id) {
|
||||||
|
var user = userService.getLoggedUserThrowIfNotFound();
|
||||||
|
var manga = mangaService.find(id);
|
||||||
|
|
||||||
|
var favoriteManga = userFavoriteMangaRepository.findByUserAndManga(user, manga);
|
||||||
|
|
||||||
|
favoriteManga.ifPresent(userFavoriteMangaRepository::delete);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
package com.magamochi.userinteraction.service;
|
||||||
|
|
||||||
|
import com.magamochi.common.exception.NotFoundException;
|
||||||
|
import com.magamochi.content.model.entity.MangaContent;
|
||||||
|
import com.magamochi.content.model.repository.MangaContentRepository;
|
||||||
|
import com.magamochi.user.service.UserService;
|
||||||
|
import com.magamochi.userinteraction.model.entity.UserMangaContentRead;
|
||||||
|
import com.magamochi.userinteraction.model.repository.UserMangaContentReadRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Log4j2
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserMangaContentReadService {
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
private final UserMangaContentReadRepository userMangaContentReadRepository;
|
||||||
|
private final MangaContentRepository mangaContentRepository;
|
||||||
|
|
||||||
|
public void setRead(Long id) {
|
||||||
|
try {
|
||||||
|
var user = userService.getLoggedUserThrowIfNotFound();
|
||||||
|
var mangaContent = findMangaContent(id);
|
||||||
|
|
||||||
|
if (userMangaContentReadRepository.existsByUserAndMangaContent(user, mangaContent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
userMangaContentReadRepository.save(
|
||||||
|
UserMangaContentRead.builder().user(user).mangaContent(mangaContent).build());
|
||||||
|
} catch (NotFoundException _) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUnread(Long id) {
|
||||||
|
var user = userService.getLoggedUserThrowIfNotFound();
|
||||||
|
var mangaContent = findMangaContent(id);
|
||||||
|
|
||||||
|
var mangaContentRead =
|
||||||
|
userMangaContentReadRepository.findByUserAndMangaContent(user, mangaContent);
|
||||||
|
|
||||||
|
mangaContentRead.ifPresent(userMangaContentReadRepository::delete);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRead(Long id) {
|
||||||
|
try {
|
||||||
|
var user = userService.getLoggedUserThrowIfNotFound();
|
||||||
|
var mangaContent = findMangaContent(id);
|
||||||
|
|
||||||
|
return userMangaContentReadRepository.existsByUserAndMangaContent(user, mangaContent);
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MangaContent findMangaContent(Long id) {
|
||||||
|
return mangaContentRepository
|
||||||
|
.findById(id)
|
||||||
|
.orElseThrow(() -> new NotFoundException("MangaContent not found for ID: " + id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
package com.magamochi.userinteraction.service;
|
||||||
|
|
||||||
|
import com.magamochi.catalog.model.entity.Manga;
|
||||||
|
import com.magamochi.catalog.model.repository.MangaRepository;
|
||||||
|
import com.magamochi.common.exception.NotFoundException;
|
||||||
|
import com.magamochi.user.service.UserService;
|
||||||
|
import com.magamochi.userinteraction.model.entity.UserMangaFollow;
|
||||||
|
import com.magamochi.userinteraction.model.repository.UserMangaFollowRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserMangaFollowService {
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
private final MangaRepository mangaRepository;
|
||||||
|
private final UserMangaFollowRepository userMangaFollowRepository;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void follow(Long mangaId) {
|
||||||
|
var user = userService.getLoggedUserThrowIfNotFound();
|
||||||
|
|
||||||
|
var manga = findMangaByIdThrowIfNotFound(mangaId);
|
||||||
|
manga.setFollow(true);
|
||||||
|
|
||||||
|
if (userMangaFollowRepository.existsByUserAndManga(user, manga)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
userMangaFollowRepository.save(UserMangaFollow.builder().user(user).manga(manga).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void unfollow(Long mangaId) {
|
||||||
|
var user = userService.getLoggedUserThrowIfNotFound();
|
||||||
|
var manga = findMangaByIdThrowIfNotFound(mangaId);
|
||||||
|
|
||||||
|
var userMangaFollow = userMangaFollowRepository.findByUserAndManga(user, manga);
|
||||||
|
userMangaFollow.ifPresent(userMangaFollowRepository::delete);
|
||||||
|
|
||||||
|
if (!userMangaFollowRepository.existsByManga(manga)) {
|
||||||
|
manga.setFollow(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Manga findMangaByIdThrowIfNotFound(Long mangaId) {
|
||||||
|
return mangaRepository
|
||||||
|
.findById(mangaId)
|
||||||
|
.orElseThrow(() -> new NotFoundException("Manga not found for ID: " + mangaId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -68,6 +68,7 @@ queues:
|
|||||||
provider-page-ingest: ${PROVIDER_PAGE_INGEST_QUEUE:mangamochi.provider.page.ingest}
|
provider-page-ingest: ${PROVIDER_PAGE_INGEST_QUEUE:mangamochi.provider.page.ingest}
|
||||||
image-fetch: ${IMAGE_FETCH_QUEUE:mangamochi.image.fetch}
|
image-fetch: ${IMAGE_FETCH_QUEUE:mangamochi.image.fetch}
|
||||||
manga-cover-update: ${MANGA_COVER_UDPATE_QUEUE:mangamochi.manga.cover.update}
|
manga-cover-update: ${MANGA_COVER_UDPATE_QUEUE:mangamochi.manga.cover.update}
|
||||||
|
file-import: ${FILE_IMPORT_QUEUE:mangamochi.file.import}
|
||||||
|
|
||||||
routing-key:
|
routing-key:
|
||||||
image-update: ${IMAGE_UPDATE_ROUTING_KEY:mangamochi.image.update}
|
image-update: ${IMAGE_UPDATE_ROUTING_KEY:mangamochi.image.update}
|
||||||
|
|||||||
8
src/main/resources/db/migration/V0003__CONTENT_READ.sql
Normal file
8
src/main/resources/db/migration/V0003__CONTENT_READ.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE user_manga_content_read
|
||||||
|
(
|
||||||
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
|
manga_content_id BIGINT NOT NULL REFERENCES manga_contents (id) ON DELETE CASCADE,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE (user_id, manga_content_id)
|
||||||
|
);
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE content_providers
|
||||||
|
ALTER COLUMN url DROP NOT NULL;
|
||||||
|
|
||||||
|
INSERT INTO content_providers(name, url, active, supports_content_fetch, manual_import)
|
||||||
|
VALUES ('MangaDex', NULL, TRUE, TRUE, TRUE),
|
||||||
|
('Manual Import', NULL, TRUE, FALSE, FALSE);
|
||||||
2
src/main/resources/db/migration/V0005__MANGA_CONTENT.sql
Normal file
2
src/main/resources/db/migration/V0005__MANGA_CONTENT.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE manga_contents
|
||||||
|
ALTER COLUMN url DROP NOT NULL;
|
||||||
3
src/main/resources/db/migration/V0006__MANGA_CONTENT.sql
Normal file
3
src/main/resources/db/migration/V0006__MANGA_CONTENT.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE manga_content_provider
|
||||||
|
ALTER COLUMN url DROP NOT NULL,
|
||||||
|
ALTER COLUMN manga_title DROP NOT NULL;
|
||||||
Loading…
x
Reference in New Issue
Block a user