From 1bb04e5d75cf4f74e9ab80a4d16e6c5d93082572 Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Wed, 31 Dec 2025 11:50:58 -0300 Subject: [PATCH 1/4] refactor(provider): remove image URL from ContentProviderMangaInfoResponseDTO --- .../model/dto/ContentProviderMangaInfoResponseDTO.java | 2 +- .../service/providers/impl/MangaLivreBlogProvider.java | 8 +------- .../service/providers/impl/MangaLivreProvider.java | 2 +- .../service/providers/impl/PinkRosaScanProvider.java | 3 +-- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/ContentProviderMangaInfoResponseDTO.java b/src/main/java/com/magamochi/mangamochi/model/dto/ContentProviderMangaInfoResponseDTO.java index bd6b5bc..f91ee58 100644 --- a/src/main/java/com/magamochi/mangamochi/model/dto/ContentProviderMangaInfoResponseDTO.java +++ b/src/main/java/com/magamochi/mangamochi/model/dto/ContentProviderMangaInfoResponseDTO.java @@ -4,4 +4,4 @@ import com.magamochi.mangamochi.model.enumeration.MangaStatus; import jakarta.validation.constraints.NotBlank; public record ContentProviderMangaInfoResponseDTO( - @NotBlank String title, @NotBlank String url, String imgUrl, MangaStatus status) {} + @NotBlank String title, @NotBlank String url, MangaStatus status) {} diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreBlogProvider.java b/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreBlogProvider.java index 5f89816..ba74855 100644 --- a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreBlogProvider.java +++ b/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreBlogProvider.java @@ -121,12 +121,6 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv var contentContainer = linkElement.getElementsByClass("manga-card-content").getFirst(); - var imageUrl = - imageContainer - .getElementsByClass("attachment-manga-cover") - .getFirst() - .attr("data-lazy-src"); - var title = contentContainer.getElementsByTag("h3").text(); var url = linkElement.attr("href"); var status = @@ -140,7 +134,7 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv default -> MangaStatus.UNKNOWN; }; - return new ContentProviderMangaInfoResponseDTO(title, url, imageUrl, status); + return new ContentProviderMangaInfoResponseDTO(title, url, status); } catch (Exception e) { return null; } diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreProvider.java b/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreProvider.java index 7533d3d..8a6170b 100644 --- a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreProvider.java +++ b/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreProvider.java @@ -116,7 +116,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider } return new ContentProviderMangaInfoResponseDTO( - title.trim(), url.trim(), null, MangaStatus.UNKNOWN); + title.trim(), url.trim(), MangaStatus.UNKNOWN); }) .toList(); } catch (NoSuchElementException e) { diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/impl/PinkRosaScanProvider.java b/src/main/java/com/magamochi/mangamochi/service/providers/impl/PinkRosaScanProvider.java index 0883c8c..3011444 100644 --- a/src/main/java/com/magamochi/mangamochi/service/providers/impl/PinkRosaScanProvider.java +++ b/src/main/java/com/magamochi/mangamochi/service/providers/impl/PinkRosaScanProvider.java @@ -138,8 +138,7 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid var textElement = linkElement.getElementsByTag("h3"); var title = textElement.text().trim(); - return new ContentProviderMangaInfoResponseDTO( - title, url, null, MangaStatus.UNKNOWN); + return new ContentProviderMangaInfoResponseDTO(title, url, MangaStatus.UNKNOWN); }) .toList(); } catch (NoSuchElementException e) { From 9e44f031d2c0c1f7e46f87d4f4b0ee32753d38cd Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Wed, 31 Dec 2025 12:51:36 -0300 Subject: [PATCH 2/4] feat(language): add Language entity and update manga chapter language handling --- ...ontentProviderMangaChapterResponseDTO.java | 5 ++- .../mangamochi/model/dto/LanguageDTO.java | 17 ++++++++ .../mangamochi/model/dto/MangaChapterDTO.java | 6 ++- .../mangamochi/model/entity/Language.java | 21 ++++++++++ .../mangamochi/model/entity/MangaChapter.java | 6 ++- .../model/repository/LanguageRepository.java | 9 +++++ .../mangamochi/service/LanguageService.java | 19 +++++++++ .../service/MangaImportService.java | 7 +++- .../service/providers/impl/BatoProvider.java | 1 + .../impl/MangaLivreBlogProvider.java | 2 +- .../providers/impl/MangaLivreProvider.java | 2 +- .../providers/impl/PinkRosaScanProvider.java | 5 ++- .../db/migration/V0019__CHAPTER_LANGUAGE.sql | 40 +++++++++++++++++++ 13 files changed, 130 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/magamochi/mangamochi/model/dto/LanguageDTO.java create mode 100644 src/main/java/com/magamochi/mangamochi/model/entity/Language.java create mode 100644 src/main/java/com/magamochi/mangamochi/model/repository/LanguageRepository.java create mode 100644 src/main/java/com/magamochi/mangamochi/service/LanguageService.java create mode 100644 src/main/resources/db/migration/V0019__CHAPTER_LANGUAGE.sql diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/ContentProviderMangaChapterResponseDTO.java b/src/main/java/com/magamochi/mangamochi/model/dto/ContentProviderMangaChapterResponseDTO.java index 950dcd3..5fe280f 100644 --- a/src/main/java/com/magamochi/mangamochi/model/dto/ContentProviderMangaChapterResponseDTO.java +++ b/src/main/java/com/magamochi/mangamochi/model/dto/ContentProviderMangaChapterResponseDTO.java @@ -3,4 +3,7 @@ package com.magamochi.mangamochi.model.dto; import jakarta.validation.constraints.NotBlank; public record ContentProviderMangaChapterResponseDTO( - @NotBlank String chapterTitle, @NotBlank String chapterUrl, String chapter, String language) {} + @NotBlank String chapterTitle, + @NotBlank String chapterUrl, + String chapter, + String languageCode) {} diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/LanguageDTO.java b/src/main/java/com/magamochi/mangamochi/model/dto/LanguageDTO.java new file mode 100644 index 0000000..e2d9163 --- /dev/null +++ b/src/main/java/com/magamochi/mangamochi/model/dto/LanguageDTO.java @@ -0,0 +1,17 @@ +package com.magamochi.mangamochi.model.dto; + +import static java.util.Objects.isNull; + +import com.magamochi.mangamochi.model.entity.Language; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record LanguageDTO(@NotNull Long id, @NotBlank String code, @NotBlank String name) { + public static LanguageDTO from(Language language) { + if (isNull(language)) { + return null; + } + + return new LanguageDTO(language.getId(), language.getCode(), language.getName()); + } +} diff --git a/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterDTO.java b/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterDTO.java index dff52ef..9da55db 100644 --- a/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterDTO.java +++ b/src/main/java/com/magamochi/mangamochi/model/dto/MangaChapterDTO.java @@ -8,12 +8,14 @@ public record MangaChapterDTO( @NotNull Long id, @NotBlank String title, @NotNull Boolean downloaded, - @NotNull Boolean isRead) { + @NotNull Boolean isRead, + LanguageDTO language) { public static MangaChapterDTO from(MangaChapter mangaChapter) { return new MangaChapterDTO( mangaChapter.getId(), mangaChapter.getTitle(), mangaChapter.getDownloaded(), - mangaChapter.getRead()); + mangaChapter.getRead(), + LanguageDTO.from(mangaChapter.getLanguage())); } } diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/Language.java b/src/main/java/com/magamochi/mangamochi/model/entity/Language.java new file mode 100644 index 0000000..e09c187 --- /dev/null +++ b/src/main/java/com/magamochi/mangamochi/model/entity/Language.java @@ -0,0 +1,21 @@ +package com.magamochi.mangamochi.model.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "languages") +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class Language { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String code; + + private String name; +} diff --git a/src/main/java/com/magamochi/mangamochi/model/entity/MangaChapter.java b/src/main/java/com/magamochi/mangamochi/model/entity/MangaChapter.java index a60b849..0427f9e 100644 --- a/src/main/java/com/magamochi/mangamochi/model/entity/MangaChapter.java +++ b/src/main/java/com/magamochi/mangamochi/model/entity/MangaChapter.java @@ -38,7 +38,9 @@ public class MangaChapter { @OneToMany(mappedBy = "mangaChapter") private List mangaChapterImages; - private String language; - private Integer chapterNumber; + + @ManyToOne + @JoinColumn(name = "language_id") + private Language language; } diff --git a/src/main/java/com/magamochi/mangamochi/model/repository/LanguageRepository.java b/src/main/java/com/magamochi/mangamochi/model/repository/LanguageRepository.java new file mode 100644 index 0000000..5411c94 --- /dev/null +++ b/src/main/java/com/magamochi/mangamochi/model/repository/LanguageRepository.java @@ -0,0 +1,9 @@ +package com.magamochi.mangamochi.model.repository; + +import com.magamochi.mangamochi.model.entity.Language; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LanguageRepository extends JpaRepository { + Optional findByCodeIgnoreCase(String code); +} diff --git a/src/main/java/com/magamochi/mangamochi/service/LanguageService.java b/src/main/java/com/magamochi/mangamochi/service/LanguageService.java new file mode 100644 index 0000000..69d048c --- /dev/null +++ b/src/main/java/com/magamochi/mangamochi/service/LanguageService.java @@ -0,0 +1,19 @@ +package com.magamochi.mangamochi.service; + +import com.magamochi.mangamochi.exception.NotFoundException; +import com.magamochi.mangamochi.model.entity.Language; +import com.magamochi.mangamochi.model.repository.LanguageRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LanguageService { + public final LanguageRepository languageRepository; + + public Language getOrThrow(String code) { + return languageRepository + .findByCodeIgnoreCase(code) + .orElseThrow(() -> new NotFoundException("Language with code " + code + " not found")); + } +} diff --git a/src/main/java/com/magamochi/mangamochi/service/MangaImportService.java b/src/main/java/com/magamochi/mangamochi/service/MangaImportService.java index bfb0e20..92911c3 100644 --- a/src/main/java/com/magamochi/mangamochi/service/MangaImportService.java +++ b/src/main/java/com/magamochi/mangamochi/service/MangaImportService.java @@ -32,6 +32,7 @@ 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; @@ -76,7 +77,7 @@ public class MangaImportService { removeFileExtension(file.getOriginalFilename()), "manual_" + file.getOriginalFilename(), file.getOriginalFilename(), - "pt-br")); + "en-US")); List allChapterImages = new ArrayList<>(); try (InputStream is = file.getInputStream(); @@ -237,7 +238,9 @@ public class MangaImportService { mangaChapter.setMangaProvider(mangaProvider); mangaChapter.setTitle(chapter.chapterTitle()); mangaChapter.setUrl(chapter.chapterUrl()); - mangaChapter.setLanguage(chapter.language()); + + var language = languageService.getOrThrow(chapter.languageCode()); + mangaChapter.setLanguage(language); if (nonNull(chapter.chapter())) { try { diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/impl/BatoProvider.java b/src/main/java/com/magamochi/mangamochi/service/providers/impl/BatoProvider.java index aef0913..d3bd1e1 100644 --- a/src/main/java/com/magamochi/mangamochi/service/providers/impl/BatoProvider.java +++ b/src/main/java/com/magamochi/mangamochi/service/providers/impl/BatoProvider.java @@ -33,6 +33,7 @@ public class BatoProvider implements ContentProvider, ManualImportContentProvide // Direct selector for chapter links var chapterLinks = document.select("div.scrollable-panel a[href*=/title/]"); + // TODO: fix chapter and language code return chapterLinks.stream() .map( chapterLink -> diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreBlogProvider.java b/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreBlogProvider.java index ba74855..648edb4 100644 --- a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreBlogProvider.java +++ b/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreBlogProvider.java @@ -51,7 +51,7 @@ public class MangaLivreBlogProvider implements ContentProvider, PagedContentProv linkElement.getElementsByClass("chapter-number").getFirst(); return new ContentProviderMangaChapterResponseDTO( - chapterNumberElement.text(), linkElement.attr("href"), null, null); + chapterNumberElement.text(), linkElement.attr("href"), null, "pt-BR"); }) .toList(); } catch (IOException | NoSuchElementException e) { diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreProvider.java b/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreProvider.java index 8a6170b..d133ac5 100644 --- a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreProvider.java +++ b/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaLivreProvider.java @@ -50,7 +50,7 @@ public class MangaLivreProvider implements ContentProvider, PagedContentProvider var title = linkElement.text(); return new ContentProviderMangaChapterResponseDTO( - title.trim(), url.trim(), null, null); + title.trim(), url.trim(), null, "pt-BR"); }) .toList(); } catch (NoSuchElementException e) { diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/impl/PinkRosaScanProvider.java b/src/main/java/com/magamochi/mangamochi/service/providers/impl/PinkRosaScanProvider.java index 3011444..6e30086 100644 --- a/src/main/java/com/magamochi/mangamochi/service/providers/impl/PinkRosaScanProvider.java +++ b/src/main/java/com/magamochi/mangamochi/service/providers/impl/PinkRosaScanProvider.java @@ -58,7 +58,10 @@ public class PinkRosaScanProvider implements ContentProvider, PagedContentProvid .getElementsByClass("text-sm truncate") .getFirst(); return new ContentProviderMangaChapterResponseDTO( - chapterTitleElement.text().trim(), chapterItemElement.attr("href"), null, null); + chapterTitleElement.text().trim(), + chapterItemElement.attr("href"), + null, + "pt-BR"); }) .toList(); } catch (NoSuchElementException e) { diff --git a/src/main/resources/db/migration/V0019__CHAPTER_LANGUAGE.sql b/src/main/resources/db/migration/V0019__CHAPTER_LANGUAGE.sql new file mode 100644 index 0000000..225e023 --- /dev/null +++ b/src/main/resources/db/migration/V0019__CHAPTER_LANGUAGE.sql @@ -0,0 +1,40 @@ +CREATE TABLE IF NOT EXISTS languages +( + id SERIAL PRIMARY KEY, + code VARCHAR(12) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL +); + +INSERT INTO languages (code, name) +VALUES ('en-US', 'English'), + ('es', 'Spanish'), + ('ja-JP', 'Japanese'), + ('pt-BR', 'Portuguese (Brazil)') +ON CONFLICT DO NOTHING; + +ALTER TABLE manga_chapters + ADD COLUMN IF NOT EXISTS language_id BIGINT REFERENCES languages (id); + +UPDATE manga_chapters +SET language = NULL; + +UPDATE manga_chapters mc +SET language_id = (SELECT id FROM languages WHERE code = 'pt-BR') +FROM manga_provider mp + JOIN providers p ON mp.provider_id = p.id +WHERE mc.manga_provider_id = mp.id + AND mc.language IS NULL + AND p.name ILIKE ANY + (ARRAY ['Manga Livre Blog', 'Pink Rosa Scan', 'Manga Livre.to', 'Manga Livre', 'MangaDex', 'Bato', 'Taimu']); + +UPDATE manga_chapters mc +SET language_id = (SELECT id FROM languages WHERE code = 'en-US') +FROM manga_provider mp + JOIN providers p ON mp.provider_id = p.id +WHERE mc.manga_provider_id = mp.id + AND mc.language IS NULL + AND p.name ILIKE ANY + (ARRAY ['Manual Import']); + +ALTER TABLE manga_chapters + DROP COLUMN IF EXISTS language; \ No newline at end of file From b238f66e085cbcd1bfc0207820f2f8af47db79cd Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Wed, 31 Dec 2025 13:47:01 -0300 Subject: [PATCH 3/4] feat(MangaDexProvider): support multiple languages for chapter filtering --- .../service/providers/impl/MangaDexProvider.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaDexProvider.java b/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaDexProvider.java index e2835e9..9dc2d0d 100644 --- a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaDexProvider.java +++ b/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaDexProvider.java @@ -49,14 +49,16 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro log.warn(e.getMessage()); } - // TODO this is filtering only pt-br chapters for now, we may want to make this configurable + // NOTE: this is getting only pt-br and en chapters for now, we may want to make this configurable // later + var languagesToImport = Map.of("pt-br", "pt-BR", "en", "en-US"); + return mangas.stream() .filter( c -> c.type().equals("chapter") && c.attributes().isUnavailable().equals(Boolean.FALSE) - && c.attributes().translatedLanguage().equals("pt-br")) + && languagesToImport.containsKey(c.attributes().translatedLanguage())) .sorted( (o1, o2) -> { try { @@ -73,7 +75,7 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro c.attributes().chapter() + " - " + c.attributes().title(), c.id().toString(), c.attributes().chapter(), - c.attributes().translatedLanguage())) + languagesToImport.get(c.attributes().translatedLanguage()))) .toList(); } catch (Exception e) { log.warn(e.getMessage()); From f98b76bc762d6c381700e8340f9dc0d5a3c35a30 Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Wed, 31 Dec 2025 14:23:47 -0300 Subject: [PATCH 4/4] feat(MangaDexProvider): add content rating filtering to manga feed retrieval --- .../magamochi/mangamochi/client/MangaDexClient.java | 4 ++-- .../service/providers/impl/MangaDexProvider.java | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/magamochi/mangamochi/client/MangaDexClient.java b/src/main/java/com/magamochi/mangamochi/client/MangaDexClient.java index 40a3ed7..e4b447f 100644 --- a/src/main/java/com/magamochi/mangamochi/client/MangaDexClient.java +++ b/src/main/java/com/magamochi/mangamochi/client/MangaDexClient.java @@ -14,11 +14,11 @@ public interface MangaDexClient { MangaDexMangaDTO getManga(@PathVariable UUID id); @GetMapping("/manga/{id}/feed") - MangaDexMangaFeedDTO getMangaFeed(@PathVariable UUID id); + MangaDexMangaFeedDTO getMangaFeed(@PathVariable UUID id, @RequestParam("contentRating[]") List contentRating); @GetMapping("/manga/{id}/feed") MangaDexMangaFeedDTO getMangaFeed( - @PathVariable UUID id, @RequestParam int limit, @RequestParam int offset); + @PathVariable UUID id, @RequestParam int limit, @RequestParam int offset, @RequestParam("contentRating[]") List contentRating); @GetMapping("/at-home/server/{chapterId}") MangaChapterDataDTO getMangaChapter(@PathVariable UUID chapterId); diff --git a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaDexProvider.java b/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaDexProvider.java index 9dc2d0d..34ed713 100644 --- a/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaDexProvider.java +++ b/src/main/java/com/magamochi/mangamochi/service/providers/impl/MangaDexProvider.java @@ -15,6 +15,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; +import static java.util.Objects.isNull; + @Log4j2 @Service(ContentProviders.MANGA_DEX) @RequiredArgsConstructor @@ -27,13 +29,12 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro public List getAvailableChapters(MangaProvider provider) { try { mangaDexRateLimiter.acquire(); - var response = mangaDexClient.getMangaFeed(UUID.fromString(provider.getUrl())); + var response = mangaDexClient.getMangaFeed(UUID.fromString(provider.getUrl()), List.of("safe", "suggestive", "erotica", "pornographic")); var mangas = new ArrayList<>(response.data()); var totalPages = (int) Math.ceil((double) response.total() / 500); try { - IntStream.range(1, totalPages) .parallel() .forEach( @@ -41,7 +42,7 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro mangaDexRateLimiter.acquire(); var pagedResponse = - mangaDexClient.getMangaFeed(UUID.fromString(provider.getUrl()), 500, i * 500); + mangaDexClient.getMangaFeed(UUID.fromString(provider.getUrl()), 500, i * 500, List.of("safe", "suggestive", "erotica", "pornographic")); mangas.addAll(pagedResponse.data()); }); @@ -61,6 +62,10 @@ public class MangaDexProvider implements ContentProvider, ManualImportContentPro && languagesToImport.containsKey(c.attributes().translatedLanguage())) .sorted( (o1, o2) -> { + if (isNull(o1.attributes().chapter()) || isNull(o2.attributes().chapter())) { + return 0; + } + try { Float chapter1 = Float.parseFloat(o1.attributes().chapter()); Float chapter2 = Float.parseFloat(o2.attributes().chapter());