refactor(catalog): move classes to new package

This commit is contained in:
Rodrigo Verdiani 2026-03-08 17:54:08 -03:00
parent 60a4f2cff6
commit 4d55f97298
23 changed files with 222 additions and 171 deletions

View File

@ -1,8 +1,8 @@
package com.magamochi.controller; package com.magamochi.catalog.controller;
import com.magamochi.catalog.model.dto.GenreDTO;
import com.magamochi.catalog.service.GenreService;
import com.magamochi.model.dto.DefaultResponseDTO; import com.magamochi.model.dto.DefaultResponseDTO;
import com.magamochi.model.dto.GenreDTO;
import com.magamochi.service.GenreService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;

View File

@ -1,6 +1,6 @@
package com.magamochi.model.dto; package com.magamochi.catalog.model.dto;
import com.magamochi.model.entity.Genre; import com.magamochi.catalog.model.entity.Genre;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;

View File

@ -1,8 +1,7 @@
package com.magamochi.model.entity; package com.magamochi.catalog.model.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.time.Instant; import java.time.Instant;
import java.util.List;
import lombok.*; import lombok.*;
import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp; import org.hibernate.annotations.UpdateTimestamp;
@ -26,7 +25,4 @@ public class Author {
@CreationTimestamp private Instant createdAt; @CreationTimestamp private Instant createdAt;
@UpdateTimestamp private Instant updatedAt; @UpdateTimestamp private Instant updatedAt;
@OneToMany(mappedBy = "author")
private List<MangaAuthor> mangaAuthors;
} }

View File

@ -1,7 +1,6 @@
package com.magamochi.model.entity; package com.magamochi.catalog.model.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.util.List;
import lombok.*; import lombok.*;
@Entity @Entity
@ -19,7 +18,4 @@ public class Genre {
private Long malId; private Long malId;
private String name; private String name;
@OneToMany(mappedBy = "genre")
private List<MangaGenre> mangaGenres;
} }

View File

@ -1,4 +1,4 @@
package com.magamochi.model.entity; package com.magamochi.catalog.model.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
@ -15,11 +15,7 @@ public class MangaAuthor {
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@ManyToOne private Long mangaId;
@JoinColumn(name = "manga_id")
private Manga manga;
@ManyToOne private Long authorId;
@JoinColumn(name = "author_id")
private Author author;
} }

View File

@ -1,4 +1,4 @@
package com.magamochi.model.entity; package com.magamochi.catalog.model.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
@ -15,11 +15,7 @@ public class MangaGenre {
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@ManyToOne private Long mangaId;
@JoinColumn(name = "manga_id")
private Manga manga;
@ManyToOne private Long genreId;
@JoinColumn(name = "genre_id")
private Genre genre;
} }

View File

@ -1,11 +1,11 @@
package com.magamochi.model.repository; package com.magamochi.catalog.model.repository;
import com.magamochi.model.entity.Author; import com.magamochi.catalog.model.entity.Author;
import java.util.Optional; import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
public interface AuthorRepository extends JpaRepository<Author, Long> { public interface AuthorRepository extends JpaRepository<Author, Long> {
Optional<Author> findByMalId(Long aLong); Optional<Author> findByMalId(Long malId);
Optional<Author> findByName(String name); Optional<Author> findByName(String name);
} }

View File

@ -1,6 +1,6 @@
package com.magamochi.model.repository; package com.magamochi.catalog.model.repository;
import com.magamochi.model.entity.Genre; import com.magamochi.catalog.model.entity.Genre;
import java.util.Optional; import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;

View File

@ -0,0 +1,11 @@
package com.magamochi.catalog.model.repository;
import com.magamochi.catalog.model.entity.MangaAuthor;
import java.util.Set;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MangaAuthorRepository extends JpaRepository<MangaAuthor, Long> {
void deleteAllByMangaId(Long mangaId);
Set<MangaAuthor> findAllByMangaId(Long mangaId);
}

View File

@ -0,0 +1,11 @@
package com.magamochi.catalog.model.repository;
import com.magamochi.catalog.model.entity.MangaGenre;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MangaGenreRepository extends JpaRepository<MangaGenre, Long> {
void deleteAllByMangaId(Long mangaId);
List<MangaGenre> findAllByMangaId(Long mangaId);
}

View File

@ -0,0 +1,29 @@
package com.magamochi.catalog.service;
import com.magamochi.catalog.model.entity.Author;
import com.magamochi.catalog.model.repository.AuthorRepository;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AuthorService {
private final AuthorRepository authorRepository;
public Long findOrCreateAuthor(Long malAuthorId, String authorName) {
return authorRepository
.findByName(authorName)
.orElseGet(
() ->
authorRepository.save(Author.builder().malId(malAuthorId).name(authorName).build()))
.getId();
}
public List<String> getAuthorNamesByIds(Set<Long> genreIds) {
var genres = authorRepository.findAllById(genreIds);
return genres.stream().map(Author::getName).toList();
}
}

View File

@ -0,0 +1,35 @@
package com.magamochi.catalog.service;
import com.magamochi.catalog.model.dto.GenreDTO;
import com.magamochi.catalog.model.entity.Genre;
import com.magamochi.catalog.model.repository.GenreRepository;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class GenreService {
private final GenreRepository genreRepository;
public Long findOrCreateGenre(Long malGenreId, String genreName) {
return genreRepository
.findByName(genreName)
.orElseGet(
() -> genreRepository.save(Genre.builder().malId(malGenreId).name(genreName).build()))
.getId();
}
public List<String> getGenreNamesByIds(Set<Long> genreIds) {
var genres = genreRepository.findAllById(genreIds);
return genres.stream().map(Genre::getName).toList();
}
public List<GenreDTO> getGenres() {
var genres = genreRepository.findAll();
return genres.stream().map(GenreDTO::from).toList();
}
}

View File

@ -0,0 +1,34 @@
package com.magamochi.catalog.service;
import com.magamochi.catalog.model.entity.MangaAuthor;
import com.magamochi.catalog.model.repository.MangaAuthorRepository;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class MangaAuthorService {
private final AuthorService authorService;
private final MangaAuthorRepository mangaAuthorRepository;
public void saveOrUpdateMangaAuthors(Long mangaId, Set<Long> authorIds) {
mangaAuthorRepository.deleteAllByMangaId(mangaId);
authorIds.forEach(
authorId ->
mangaAuthorRepository.save(
MangaAuthor.builder().mangaId(mangaId).authorId(authorId).build()));
}
public List<String> getMangaAuthors(Long mangaId) {
var mangaAuthorIds =
mangaAuthorRepository.findAllByMangaId(mangaId).stream()
.map(MangaAuthor::getAuthorId)
.collect(Collectors.toSet());
return authorService.getAuthorNamesByIds(mangaAuthorIds);
}
}

View File

@ -0,0 +1,34 @@
package com.magamochi.catalog.service;
import com.magamochi.catalog.model.entity.MangaGenre;
import com.magamochi.catalog.model.repository.MangaGenreRepository;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class MangaGenreService {
private final GenreService genreService;
private final MangaGenreRepository mangaGenreRepository;
public void saveOrUpdateMangaGenres(Long mangaId, Set<Long> genreIds) {
mangaGenreRepository.deleteAllByMangaId(mangaId);
genreIds.forEach(
genreId ->
mangaGenreRepository.save(
MangaGenre.builder().mangaId(mangaId).genreId(genreId).build()));
}
public List<String> getMangaGenres(Long mangaId) {
var mangaGenreIds =
mangaGenreRepository.findAllByMangaId(mangaId).stream()
.map(MangaGenre::getGenreId)
.collect(Collectors.toSet());
return genreService.getGenreNamesByIds(mangaGenreIds);
}
}

View File

@ -29,7 +29,8 @@ public record MangaDTO(
@NotNull Integer chapterCount, @NotNull Integer chapterCount,
@NotNull Boolean favorite, @NotNull Boolean favorite,
@NotNull Boolean following) { @NotNull Boolean following) {
public static MangaDTO from(Manga manga, Boolean favorite, Boolean following) { public static MangaDTO from(
Manga manga, Boolean favorite, Boolean following, List<String> genres, List<String> authors) {
return new MangaDTO( return new MangaDTO(
manga.getId(), manga.getId(),
manga.getTitle(), manga.getTitle(),
@ -40,10 +41,8 @@ public record MangaDTO(
manga.getSynopsis(), manga.getSynopsis(),
manga.getMangaProviders().size(), manga.getMangaProviders().size(),
manga.getAlternativeTitles().stream().map(MangaAlternativeTitle::getTitle).toList(), manga.getAlternativeTitles().stream().map(MangaAlternativeTitle::getTitle).toList(),
manga.getMangaGenres().stream().map(mangaGenre -> mangaGenre.getGenre().getName()).toList(), genres,
manga.getMangaAuthors().stream() authors,
.map(mangaAuthor -> mangaAuthor.getAuthor().getName())
.toList(),
manga.getScore(), manga.getScore(),
manga.getMangaProviders().stream().map(MangaProviderDTO::from).toList(), manga.getMangaProviders().stream().map(MangaProviderDTO::from).toList(),
manga.getChapterCount(), manga.getChapterCount(),

View File

@ -20,7 +20,8 @@ public record MangaListDTO(
@NotNull List<String> authors, @NotNull List<String> authors,
@NotNull Double score, @NotNull Double score,
@NotNull Boolean favorite) { @NotNull Boolean favorite) {
public static MangaListDTO from(Manga manga, boolean favorite) { public static MangaListDTO from(
Manga manga, boolean favorite, List<String> genres, List<String> authors) {
return new MangaListDTO( return new MangaListDTO(
manga.getId(), manga.getId(),
manga.getTitle(), manga.getTitle(),
@ -29,10 +30,8 @@ public record MangaListDTO(
manga.getPublishedFrom(), manga.getPublishedFrom(),
manga.getPublishedTo(), manga.getPublishedTo(),
manga.getMangaProviders().size(), manga.getMangaProviders().size(),
manga.getMangaGenres().stream().map(mangaGenre -> mangaGenre.getGenre().getName()).toList(), genres,
manga.getMangaAuthors().stream() authors,
.map(mangaAuthor -> mangaAuthor.getAuthor().getName())
.toList(),
manga.getScore(), manga.getScore(),
favorite); favorite);
} }

View File

@ -47,12 +47,6 @@ public class Manga {
private OffsetDateTime publishedTo; private OffsetDateTime publishedTo;
@OneToMany(mappedBy = "manga")
private List<MangaAuthor> mangaAuthors;
@OneToMany(mappedBy = "manga")
private List<MangaGenre> mangaGenres;
@OneToMany(mappedBy = "manga") @OneToMany(mappedBy = "manga")
private List<MangaAlternativeTitle> alternativeTitles; private List<MangaAlternativeTitle> alternativeTitles;

View File

@ -1,11 +0,0 @@
package com.magamochi.model.repository;
import com.magamochi.model.entity.Author;
import com.magamochi.model.entity.Manga;
import com.magamochi.model.entity.MangaAuthor;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MangaAuthorRepository extends JpaRepository<MangaAuthor, Long> {
Optional<MangaAuthor> findByMangaAndAuthor(Manga manga, Author author);
}

View File

@ -1,11 +0,0 @@
package com.magamochi.model.repository;
import com.magamochi.model.entity.Genre;
import com.magamochi.model.entity.Manga;
import com.magamochi.model.entity.MangaGenre;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MangaGenreRepository extends JpaRepository<MangaGenre, Long> {
Optional<MangaGenre> findByMangaAndGenre(Manga manga, Genre genre);
}

View File

@ -2,8 +2,8 @@ package com.magamochi.model.specification;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
import com.magamochi.catalog.model.entity.Author;
import com.magamochi.model.dto.MangaListFilterDTO; import com.magamochi.model.dto.MangaListFilterDTO;
import com.magamochi.model.entity.Author;
import com.magamochi.model.entity.Manga; import com.magamochi.model.entity.Manga;
import com.magamochi.user.model.entity.User; import com.magamochi.user.model.entity.User;
import jakarta.persistence.criteria.*; import jakarta.persistence.criteria.*;

View File

@ -1,19 +0,0 @@
package com.magamochi.service;
import com.magamochi.model.dto.GenreDTO;
import com.magamochi.model.repository.GenreRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class GenreService {
private final GenreRepository genreRepository;
public List<GenreDTO> getGenres() {
var genres = genreRepository.findAll();
return genres.stream().map(GenreDTO::from).toList();
}
}

View File

@ -4,6 +4,10 @@ import static java.util.Objects.isNull;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
import com.google.common.util.concurrent.RateLimiter; import com.google.common.util.concurrent.RateLimiter;
import com.magamochi.catalog.service.AuthorService;
import com.magamochi.catalog.service.GenreService;
import com.magamochi.catalog.service.MangaAuthorService;
import com.magamochi.catalog.service.MangaGenreService;
import com.magamochi.client.AniListClient; import com.magamochi.client.AniListClient;
import com.magamochi.client.JikanClient; import com.magamochi.client.JikanClient;
import com.magamochi.exception.NotFoundException; import com.magamochi.exception.NotFoundException;
@ -20,6 +24,7 @@ import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
@ -38,11 +43,13 @@ public class MangaImportService {
private final ImageService imageService; private final ImageService imageService;
private final LanguageService languageService; private final LanguageService languageService;
private final GenreRepository genreRepository; private final GenreService genreService;
private final MangaGenreRepository mangaGenreRepository; private final MangaGenreService mangaGenreService;
private final AuthorService authorService;
private final MangaAuthorService mangaAuthorService;
private final MangaProviderRepository mangaProviderRepository; private final MangaProviderRepository mangaProviderRepository;
private final AuthorRepository authorRepository;
private final MangaAuthorRepository mangaAuthorRepository;
private final MangaChapterRepository mangaChapterRepository; private final MangaChapterRepository mangaChapterRepository;
private final MangaRepository mangaRepository; private final MangaRepository mangaRepository;
@ -177,39 +184,21 @@ public class MangaImportService {
manga.setPublishedTo(mangaData.data().published().to()); manga.setPublishedTo(mangaData.data().published().to());
manga.setChapterCount(mangaData.data().chapters()); manga.setChapterCount(mangaData.data().chapters());
var authors = var authorIds =
mangaData.data().authors().stream() mangaData.data().authors().stream()
.map( .map(
authorData -> authorData ->
authorRepository authorService.findOrCreateAuthor(authorData.mal_id(), authorData.name()))
.findByMalId(authorData.mal_id()) .collect(Collectors.toSet());
.orElseGet(
() ->
authorRepository.save(
Author.builder()
.malId(authorData.mal_id())
.name(authorData.name())
.build())))
.toList();
updateMangaAuthors(manga, authors); mangaAuthorService.saveOrUpdateMangaAuthors(manga.getId(), authorIds);
var genres = var genreIds =
mangaData.data().genres().stream() mangaData.data().genres().stream()
.map( .map(genreData -> genreService.findOrCreateGenre(genreData.mal_id(), genreData.name()))
genreData -> .collect(Collectors.toSet());
genreRepository
.findByMalId(genreData.mal_id())
.orElseGet(
() ->
genreRepository.save(
Genre.builder()
.malId(genreData.mal_id())
.name(genreData.name())
.build())))
.toList();
updateMangaGenres(manga, genres); mangaGenreService.saveOrUpdateMangaGenres(manga.getId(), genreIds);
if (isNull(manga.getCoverImage())) { if (isNull(manga.getCoverImage())) {
downloadCoverImage(manga, mangaData.data().images().jpg().large_image_url()); downloadCoverImage(manga, mangaData.data().images().jpg().large_image_url());
@ -261,31 +250,22 @@ public class MangaImportService {
manga.setPublishedTo(convertFuzzyDate(media.endDate())); manga.setPublishedTo(convertFuzzyDate(media.endDate()));
manga.setChapterCount(media.chapters()); manga.setChapterCount(media.chapters());
var authors = var authorIds =
media.staff().edges().stream() media.staff().edges().stream()
.filter(edge -> isAuthorRole(edge.role())) .filter(edge -> isAuthorRole(edge.role()))
.map(edge -> edge.node().name().full()) .map(edge -> edge.node().name().full())
.distinct() .distinct()
.map( .map(name -> authorService.findOrCreateAuthor(null, name))
name -> .collect(Collectors.toSet());
authorRepository
.findByName(name)
.orElseGet(
() -> authorRepository.save(Author.builder().name(name).build())))
.toList();
updateMangaAuthors(manga, authors); mangaAuthorService.saveOrUpdateMangaAuthors(manga.getId(), authorIds);
var genres = var genreIds =
media.genres().stream() media.genres().stream()
.map( .map(name -> genreService.findOrCreateGenre(null, name))
name -> .collect(Collectors.toSet());
genreRepository
.findByName(name)
.orElseGet(() -> genreRepository.save(Genre.builder().name(name).build())))
.toList();
updateMangaGenres(manga, genres); mangaGenreService.saveOrUpdateMangaGenres(manga.getId(), genreIds);
if (isNull(manga.getCoverImage())) { if (isNull(manga.getCoverImage())) {
downloadCoverImage(manga, media.coverImage().large()); downloadCoverImage(manga, media.coverImage().large());
@ -324,36 +304,6 @@ public class MangaImportService {
ZoneOffset.UTC); 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) private void downloadCoverImage(Manga manga, String imageUrl)
throws IOException, URISyntaxException { throws IOException, URISyntaxException {
var inputStream = var inputStream =

View File

@ -2,6 +2,8 @@ package com.magamochi.service;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
import com.magamochi.catalog.service.MangaAuthorService;
import com.magamochi.catalog.service.MangaGenreService;
import com.magamochi.client.NtfyClient; import com.magamochi.client.NtfyClient;
import com.magamochi.exception.NotFoundException; import com.magamochi.exception.NotFoundException;
import com.magamochi.model.dto.*; import com.magamochi.model.dto.*;
@ -35,6 +37,9 @@ public class MangaService {
private final MangaRepository mangaRepository; private final MangaRepository mangaRepository;
private final MangaProviderRepository mangaProviderRepository; private final MangaProviderRepository mangaProviderRepository;
private final MangaGenreService mangaGenreService;
private final MangaAuthorService mangaAuthorService;
private final ContentProviderFactory contentProviderFactory; private final ContentProviderFactory contentProviderFactory;
private final MangaChapterDownloadProducer mangaChapterDownloadProducer; private final MangaChapterDownloadProducer mangaChapterDownloadProducer;
@ -73,8 +78,10 @@ public class MangaService {
.findAll(specification, pageable) .findAll(specification, pageable)
.map( .map(
manga -> { manga -> {
var genres = mangaGenreService.getMangaGenres(manga.getId());
var authors = mangaAuthorService.getMangaAuthors(manga.getId());
var favorite = favoriteMangasIds.contains(manga.getId()); var favorite = favoriteMangasIds.contains(manga.getId());
return MangaListDTO.from(manga, favorite); return MangaListDTO.from(manga, favorite, genres, authors);
}); });
} }
@ -97,10 +104,15 @@ public class MangaService {
var followingMangaIds = var followingMangaIds =
nonNull(user) ? userFollowMangaService.getFollowedMangaIdsForLoggedUser() : Set.of(); nonNull(user) ? userFollowMangaService.getFollowedMangaIdsForLoggedUser() : Set.of();
var genres = mangaGenreService.getMangaGenres(manga.getId());
var authors = mangaAuthorService.getMangaAuthors(manga.getId());
return MangaDTO.from( return MangaDTO.from(
manga, manga,
favoriteMangasIds.contains(manga.getId()), favoriteMangasIds.contains(manga.getId()),
followingMangaIds.contains(manga.getId())); followingMangaIds.contains(manga.getId()),
genres,
authors);
} }
public void fetchFollowedMangaChapters(Long mangaProviderId) { public void fetchFollowedMangaChapters(Long mangaProviderId) {