Merge pull request 'improvements' (#25) from improvements into main

Reviewed-on: #25
This commit is contained in:
rov 2025-12-31 14:24:45 -03:00
commit 7dbb019345
16 changed files with 149 additions and 29 deletions

View File

@ -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<String> 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<String> contentRating);
@GetMapping("/at-home/server/{chapterId}")
MangaChapterDataDTO getMangaChapter(@PathVariable UUID chapterId);

View File

@ -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) {}

View File

@ -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) {}

View File

@ -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());
}
}

View File

@ -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()));
}
}

View File

@ -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;
}

View File

@ -38,7 +38,9 @@ public class MangaChapter {
@OneToMany(mappedBy = "mangaChapter")
private List<MangaChapterImage> mangaChapterImages;
private String language;
private Integer chapterNumber;
@ManyToOne
@JoinColumn(name = "language_id")
private Language language;
}

View File

@ -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<Language, Long> {
Optional<Language> findByCodeIgnoreCase(String code);
}

View File

@ -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"));
}
}

View File

@ -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<MangaChapterImage> 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 {

View File

@ -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 ->

View File

@ -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<ContentProviderMangaChapterResponseDTO> 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());
});
@ -49,16 +50,22 @@ 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) -> {
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());
@ -73,7 +80,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());

View File

@ -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) {
@ -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;
}

View File

@ -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) {
@ -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) {

View File

@ -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) {
@ -138,8 +141,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) {

View File

@ -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;