diff --git a/multisrc/overrides/zeistmanga/datgarscanlation/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/zeistmanga/datgarscanlation/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..2c043a5d9 Binary files /dev/null and b/multisrc/overrides/zeistmanga/datgarscanlation/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/zeistmanga/datgarscanlation/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/zeistmanga/datgarscanlation/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..90af3d5d4 Binary files /dev/null and b/multisrc/overrides/zeistmanga/datgarscanlation/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/zeistmanga/datgarscanlation/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/zeistmanga/datgarscanlation/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..a3d613f21 Binary files /dev/null and b/multisrc/overrides/zeistmanga/datgarscanlation/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/zeistmanga/datgarscanlation/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/zeistmanga/datgarscanlation/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..f2a1acae9 Binary files /dev/null and b/multisrc/overrides/zeistmanga/datgarscanlation/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/zeistmanga/datgarscanlation/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/zeistmanga/datgarscanlation/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..2d1bb2708 Binary files /dev/null and b/multisrc/overrides/zeistmanga/datgarscanlation/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/zeistmanga/datgarscanlation/res/web_hi_res_512.png b/multisrc/overrides/zeistmanga/datgarscanlation/res/web_hi_res_512.png new file mode 100644 index 000000000..7795824b8 Binary files /dev/null and b/multisrc/overrides/zeistmanga/datgarscanlation/res/web_hi_res_512.png differ diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/zeistmanga/ZeistManga.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/zeistmanga/ZeistManga.kt new file mode 100644 index 000000000..8204f5b89 --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/zeistmanga/ZeistManga.kt @@ -0,0 +1,172 @@ +package eu.kanade.tachiyomi.multisrc.zeistmanga + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import uy.kohesive.injekt.injectLazy + +abstract class ZeistManga( + override val name: String, + override val baseUrl: String, + override val lang: String +) : ParsedHttpSource() { + + override val supportsLatest = false + private val json: Json by injectLazy() + + override fun chapterListParse(response: Response): List { + val document = response.asJsoup() + + // Find chapter feed name (ZeistManga v5) + val script = document.selectFirst("#clwd > script") + val feed = chapterFeedRegex + .find(script.html()) + ?.groupValues?.get(1) + ?: throw Exception("Failed to find chapter feed") + + val url = apiUrl(feed) + .addQueryParameter("start-index", "2") // Only get chapters + .addQueryParameter("max-results", "999999") // Get all chapters + .build() + + // Call JSON API + val req = GET(url.toString(), headers) + val res = client.newCall(req).execute() + + // Parse JSON API response + val jsonString = res.body!!.string() + val result = json.decodeFromString(jsonString) + + // Transform JSON response into List + return result.feed?.entry?.map { it.toSChapter(baseUrl) } + ?: throw Exception("Failed to parse from chapter API") + } + + override fun imageUrlParse(document: Document): String { + throw UnsupportedOperationException("Not used.") + } + + override fun latestUpdatesParse(response: Response): MangasPage { + throw UnsupportedOperationException("Not used.") + } + + override fun latestUpdatesRequest(page: Int): Request { + throw UnsupportedOperationException("Not used.") + } + + override fun chapterFromElement(element: Element): SChapter { + throw UnsupportedOperationException("Not used.") + } + + override fun chapterListSelector(): String { + throw UnsupportedOperationException("Not used.") + } + + override fun latestUpdatesFromElement(element: Element): SManga { + throw UnsupportedOperationException("Not used.") + } + + override fun latestUpdatesNextPageSelector(): String? { + throw UnsupportedOperationException("Not used.") + } + + override fun latestUpdatesSelector(): String { + throw UnsupportedOperationException("Not used.") + } + + override fun popularMangaFromElement(element: Element): SManga { + throw UnsupportedOperationException("Not used.") + } + + override fun popularMangaNextPageSelector(): String? { + throw UnsupportedOperationException("Not used.") + } + + override fun popularMangaSelector(): String { + throw UnsupportedOperationException("Not used.") + } + + override fun mangaDetailsParse(document: Document): SManga { + val profileManga = document.selectFirst(".grid.gtc-235fr") + return SManga.create().apply { + title = profileManga.selectFirst("h1.mt-0.mb-6.fs-20").text() + thumbnail_url = profileManga.selectFirst("img").attr("src") + description = profileManga.selectFirst("#synopsis").text() + status = SManga.UNKNOWN + } + } + + override fun pageListParse(document: Document): List { + val images = document.selectFirst(".check-box") + return images.select("img").mapIndexed { i, img -> + Page(i, "", img.attr("src")) + } + } + + override fun popularMangaParse(response: Response): MangasPage { + val jsonString = response.body?.string().orEmpty() + val result = json.decodeFromString(jsonString) + // Transform JSON response into List + val mangas = result.feed!!.entry?.map { it.toSManga(baseUrl) } + val mangalist = mangas!!.toMutableList() + if (mangas.size == maxResults + 1) { + mangalist.removeLast() + return MangasPage(mangalist, true) + } + return MangasPage(mangalist, false) + } + + override fun popularMangaRequest(page: Int): Request { + val startIndex = maxResults * (page - 1) + 1 + val url = apiUrl() + .addQueryParameter("orderby", "published") + .addQueryParameter("max-results", (maxResults + 1).toString()) + .addQueryParameter("start-index", startIndex.toString()) + .build() + + return GET(url.toString(), headers) + } + + override fun searchMangaSelector(): String = ".grid.gtc-f141a > div" + override fun searchMangaFromElement(element: Element): SManga { + return SManga.create().apply { + setUrlWithoutDomain(element.select(".block").attr("href")) + title = element.selectFirst(".clamp.toe.oh.block").text().trim() + thumbnail_url = element.selectFirst("img").attr("src") + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$baseUrl/search".toHttpUrl().newBuilder() + .addQueryParameter("q", query) + .build() + + return GET(url.toString(), headers) + } + + override fun searchMangaNextPageSelector(): String? = null + + private fun apiUrl(feed: String = "Series"): HttpUrl.Builder { + return "$baseUrl/feeds/posts/default/-/".toHttpUrl().newBuilder() + .addPathSegment(feed) + .addQueryParameter("alt", "json") + } + + companion object { + private const val maxResults = 20 + private val chapterFeedRegex = """clwd\.run\('([^']+)'""".toRegex() + } +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/zeistmanga/ZeistMangaDto.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/zeistmanga/ZeistMangaDto.kt new file mode 100644 index 000000000..25c66b9bb --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/zeistmanga/ZeistMangaDto.kt @@ -0,0 +1,79 @@ +package eu.kanade.tachiyomi.multisrc.zeistmanga + +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.jsoup.Jsoup +import java.text.SimpleDateFormat +import java.util.Locale + +private val DATE_FORMATTER by lazy { + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) +} + +private fun parseDate(dateStr: String): Long { + return runCatching { DATE_FORMATTER.parse(dateStr)?.time } + .getOrNull() ?: 0L +} + +@Serializable +data class ZeistMangaDto( + val feed: ZeistMangaFeedDto? = null, +) + +@Serializable +data class ZeistMangaFeedDto( + val entry: List? = emptyList() +) + +@Serializable +data class ZeistMangaEntryDto( + val title: ZeistMangaEntryTitleDto? = null, + val published: ZeistMangaEntryPublishedDto? = null, + @SerialName("link") val url: List? = emptyList(), + val content: ZeistMangaEntryContentDto? = null +) { + fun toSManga(baseurl: String): SManga = SManga.create().apply { + title = this@ZeistMangaEntryDto.title!!.t + url = getChapterLink(this@ZeistMangaEntryDto.url!!).substringAfter(baseurl) + thumbnail_url = getThumbnail(this@ZeistMangaEntryDto.content!!) + } + + fun toSChapter(baseurl: String): SChapter = SChapter.create().apply { + name = this@ZeistMangaEntryDto.title!!.t + url = getChapterLink(this@ZeistMangaEntryDto.url!!).substringAfter(baseurl) + val chapterDate = this@ZeistMangaEntryDto.published!!.t.trim() + date_upload = parseDate(chapterDate) + } + + private fun getChapterLink(list: List): String { + return list.first { it.rel == "alternate" }.href + } + + private fun getThumbnail(html: ZeistMangaEntryContentDto): String { + val document = Jsoup.parse(html.t) + return document.selectFirst("img").attr("src") + } +} + +@Serializable +data class ZeistMangaEntryTitleDto( + @SerialName("\$t") val t: String +) + +@Serializable +data class ZeistMangaEntryPublishedDto( + @SerialName("\$t") val t: String +) + +@Serializable +data class ZeistMangaEntryContentDto( + @SerialName("\$t") val t: String +) + +@Serializable +data class ZeistMangaEntryLink( + val rel: String, + val href: String +) diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/zeistmanga/ZeistMangaGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/zeistmanga/ZeistMangaGenerator.kt new file mode 100644 index 000000000..18dd162d7 --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/zeistmanga/ZeistMangaGenerator.kt @@ -0,0 +1,24 @@ +package eu.kanade.tachiyomi.multisrc.zeistmanga + +import generator.ThemeSourceData.SingleLang +import generator.ThemeSourceGenerator + +class ZeistMangaGenerator : ThemeSourceGenerator { + + override val themePkg = "zeistmanga" + + override val themeClass = "ZeistManga" + + override val baseVersionCode: Int = 1 + + override val sources = listOf( + SingleLang("DatGarScanlation", "https://datgarscanlation.blogspot.com", "es"), + ) + + companion object { + @JvmStatic + fun main(args: Array) { + ZeistMangaGenerator().createAll() + } + } +}