Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite MangaTube extension #7241

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/de/mangatube/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ext {
extName = 'Manga Tube'
extClass = '.MangaTube'
extVersionCode = 2
extVersionCode = 3
}

apply from: "$rootDir/common.gradle"
Original file line number Diff line number Diff line change
@@ -1,36 +1,31 @@
package eu.kanade.tachiyomi.extension.de.mangatube

import Manga
import android.annotation.SuppressLint
import eu.kanade.tachiyomi.extension.de.mangatube.dio.wrapper.ChapterWrapper
import eu.kanade.tachiyomi.extension.de.mangatube.dio.wrapper.ChaptersWrapper
import eu.kanade.tachiyomi.extension.de.mangatube.dio.wrapper.MangaWrapper
import eu.kanade.tachiyomi.extension.de.mangatube.dio.wrapper.MangasWrapper
import eu.kanade.tachiyomi.extension.de.mangatube.util.BaseResponse
import eu.kanade.tachiyomi.extension.de.mangatube.util.Genre
import eu.kanade.tachiyomi.extension.de.mangatube.util.MangaTubeHelper
import eu.kanade.tachiyomi.extension.de.mangatube.util.ResponseInterceptor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
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.source.online.HttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit

class MangaTube : ParsedHttpSource() {
class MangaTube : HttpSource() {

override val name = "Manga Tube"

Expand All @@ -40,146 +35,186 @@ class MangaTube : ParsedHttpSource() {

override val supportsLatest = true

@SuppressLint("SimpleDateFormat")
private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")

private val mangas: LinkedHashMap<String, Manga> = LinkedHashMap()

private val json = Json {
isLenient = true
ignoreUnknownKeys = true
allowSpecialFloatingPointValues = true
prettyPrint = true
}

override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(1, TimeUnit.MINUTES)
.writeTimeout(1, TimeUnit.MINUTES)
.addInterceptor(ResponseInterceptor())
.build()

private val xhrHeaders: Headers = headersBuilder().add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8").build()

private val json: Json by injectLazy()
override fun imageUrlParse(response: Response): String = ""

// Popular

override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newCall(popularMangaRequest(page))
.asObservableSuccess()
.map { response ->
parseMangaFromJson(response, page < 96)
}
override fun getMangaUrl(manga: SManga): String {
if (!manga.url.startsWith(baseUrl)) {
return "$baseUrl${manga.url}"
}
return manga.url
}

override fun popularMangaRequest(page: Int): Request {
val rbodyContent = "action=load_series_list_entries&parameter%5Bpage%5D=$page&parameter%5Bletter%5D=&parameter%5Bsortby%5D=popularity&parameter%5Border%5D=asc"
return POST("$baseUrl/ajax", xhrHeaders, rbodyContent.toRequestBody(null))
override fun getChapterUrl(chapter: SChapter): String {
return chapter.url
}

// popular uses "success" as a key, search uses "suggestions"
// for future reference: if adding filters, advanced search might use a different key
private fun parseMangaFromJson(response: Response, hasNextPage: Boolean): MangasPage {
var titleKey = "manga_title"
val mangas = json.decodeFromString<JsonObject>(response.body.string())
.let { it["success"] ?: it["suggestions"].also { titleKey = "value" } }!!
.jsonArray
.map { json ->
SManga.create().apply {
title = json.jsonObject[titleKey]!!.jsonPrimitive.content
url = "/series/${json.jsonObject["manga_slug"]!!.jsonPrimitive.content}"
thumbnail_url = json.jsonObject["covers"]!!.jsonArray[0].jsonObject["img_name"]!!.jsonPrimitive.content
}
}
return MangasPage(mangas, hasNextPage)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/api/manga/search?page=$page&query=$query"

return GET(url)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return GET(url)
return GET(url, headers)

pass headers to set appropriate useragent and other headers to requests

}

override fun popularMangaSelector() = throw UnsupportedOperationException()
override fun searchMangaParse(response: Response): MangasPage {
val body = response.body.string()

override fun popularMangaFromElement(element: Element): SManga = throw UnsupportedOperationException()
val res: BaseResponse<List<Manga>> = json.decodeFromString(body)

val mangaList = res.data.map { manga ->
mangas[manga.title] = manga
SManga.create().apply {
title = manga.title
url = "$baseUrl${manga.url}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set relative urls or slug/id

thumbnail_url = manga.cover
status = MangaTubeHelper.mangaStatus(manga.status)
}
}

override fun popularMangaNextPageSelector() = throw UnsupportedOperationException()
return MangasPage(mangaList, res.pagination!!.lastPage())
}

// Latest
override fun popularMangaRequest(page: Int): Request {
val url = "$baseUrl/api/home/top-manga"

override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/?page=$page", headers)
return GET(url)
}

override fun latestUpdatesSelector() = "div#series-updates div.series-update:not([style\$=none])"
override fun popularMangaParse(response: Response): MangasPage {
val body = response.body.string()

override fun latestUpdatesFromElement(element: Element): SManga {
return SManga.create().apply {
element.select("a.series-name").let {
title = it.text()
setUrlWithoutDomain(it.attr("href"))
val res: BaseResponse<MangasWrapper> = json.decodeFromString(body)

val mangaList = res.data.manga.map { manga ->
mangas[manga.title] = manga
SManga.create().apply {
title = manga.title
url = "$baseUrl${manga.url}"
thumbnail_url = manga.cover
genre = manga.genre.map { genre -> Genre.fromId(genre)!! }
.joinToString(", ") { genre -> genre.displayName }
Ayley marked this conversation as resolved.
Show resolved Hide resolved
status = MangaTubeHelper.mangaStatus(manga.status)
}
thumbnail_url = element.select("div.cover img").attr("abs:data-original")
}

return MangasPage(mangaList, false)
}

override fun latestUpdatesNextPageSelector() = "button#load-more-updates"
override fun latestUpdatesRequest(page: Int): Request {
val url = "$baseUrl/api/home/new-manga"

// Search
return GET(url)
}

override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val rbodyContent = "action=search_query&parameter%5Bquery%5D=$query"
return POST("$baseUrl/ajax", xhrHeaders, rbodyContent.toRequestBody(null))
override fun latestUpdatesParse(response: Response): MangasPage {
val body = response.body.string()

val res: BaseResponse<MangasWrapper> = json.decodeFromString(body)

val mangaList = res.data.manga.map { manga ->
mangas[manga.title] = manga
SManga.create().apply {
title = manga.title
url = "$baseUrl${manga.url}"
thumbnail_url = manga.cover
genre = manga.genre.map { genre -> Genre.fromId(genre)!! }
.joinToString(", ") { genre -> genre.displayName }
status = MangaTubeHelper.mangaStatus(manga.status)
description = manga.description
}
}

return MangasPage(mangaList, false)
}

override fun searchMangaParse(response: Response): MangasPage {
return parseMangaFromJson(response, false)
override fun mangaDetailsRequest(manga: SManga): Request {
val url = "$baseUrl/api/manga/${mangas[manga.title]!!.id}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

save the id in the actual SManga url when returning, the hashmap above will not be initialized on app restart


return GET(url)
}

override fun searchMangaSelector() = throw UnsupportedOperationException()
override fun mangaDetailsParse(response: Response): SManga {
val body = response.body.string()

override fun searchMangaFromElement(element: Element): SManga = throw UnsupportedOperationException()
val res: BaseResponse<MangaWrapper> = json.decodeFromString(body)

override fun searchMangaNextPageSelector() = throw UnsupportedOperationException()
val manga: Manga = res.data.manga

// Details
mangas[manga.title] = manga

override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
document.select("div.series-detailed div.row").first()!!.let { info ->
author = info.select("li:contains(Autor:) a").joinToString { it.text() }
artist = info.select("li:contains(Artist:) a").joinToString { it.text() }
status = info.select("li:contains(Offiziel)").firstOrNull()?.ownText().toStatus()
genre = info.select(".genre-list a").joinToString { it.text() }
thumbnail_url = info.select("img").attr("abs:data-original")
}
description = document.select("div.series-footer h4 ~ p").joinToString("\n\n") { it.text() }
title = manga.title
author = manga.author.joinToString(", ") { author -> author.name }
url = "$baseUrl${manga.url}"
artist = manga.artist.joinToString(", ") { artist -> artist.name }
description = manga.description
thumbnail_url = manga.cover
genre = manga.genre.map { genre -> Genre.fromId(genre)!! }
.joinToString(", ") { genre -> genre.displayName }
status = MangaTubeHelper.mangaStatus(manga.status)
}
}

private fun String?.toStatus() = when {
this == null -> SManga.UNKNOWN
this.contains("laufend", ignoreCase = true) -> SManga.ONGOING
this.contains("abgeschlossen", ignoreCase = true) -> SManga.COMPLETED
else -> SManga.UNKNOWN
override fun chapterListRequest(manga: SManga): Request {
val url = "$baseUrl/api/manga/${mangas[manga.title]!!.slug}/chapters"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here


return GET(url)
}

// Chapters
override fun chapterListParse(response: Response): List<SChapter> {
val body = response.body.string()

override fun chapterListSelector() = "ul.chapter-list li"
val res: BaseResponse<ChaptersWrapper> = json.decodeFromString(body)

override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
element.select("a[title]").let {
name = "${it.select("b").text()} ${it.select("span:not(.btn)").joinToString(" ") { span -> span.text() }}"
setUrlWithoutDomain(it.attr("href"))
}
date_upload = element.select("p.chapter-date").text().let {
try {
SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()).parse(it.substringAfter(" "))?.time ?: 0L
} catch (_: ParseException) {
0L
}
val chapterList = res.data.chapters.map { chapter ->
SChapter.create().apply {
url = "$baseUrl${chapter.readerURL}"
name = chapter.name.ifBlank { "Chapter ${chapter.number}" }
date_upload = dateFormat.parse(chapter.publishedAt)!!.time
chapter_number = chapter.number.toFloat()
scanlator = chapter.volume.toString()
}
}

return chapterList
}

// Pages
override fun pageListRequest(chapter: SChapter): Request {
val split = chapter.url.split("/")
val slug = split[split.size - 4]
val id = split[split.size - 2]

override fun pageListParse(document: Document): List<Page> {
val script = document.select("script:containsData(current_chapter:)").first()!!.data()
val imagePath = Regex("""img_path: '(.*)'""").find(script)?.groupValues?.get(1)
?: throw Exception("Couldn't find image path")
val jsonArray = Regex("""pages: (\[.*]),""").find(script)?.groupValues?.get(1)
?: throw Exception("Couldn't find JSON array")
val url = "$baseUrl/api/manga/$slug/chapter/$id"

return json.decodeFromString<JsonArray>(jsonArray).mapIndexed { i, json ->
Page(i, "", imagePath + json.jsonObject["file_name"]!!.jsonPrimitive.content)
}
return GET(url)
}

override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
override fun pageListParse(response: Response): List<Page> {
val body = response.body.string()

val res: BaseResponse<ChapterWrapper> = json.decodeFromString(body)

val mangaList = res.data.chapter.pages.map { page ->
Page(page.index, page.url, page.altSource)
}

return mangaList
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.extension.de.mangatube.dio

import kotlinx.serialization.Serializable

@Serializable
data class Chapter(
val id: Int,
val number: Int,
val volume: Int,
val name: String,
val publishedAt: String,
val readerURL: String,
val pages: List<Page> = emptyList(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames

@Serializable
data class Manga(
val id: Int,
val title: String,
val url: String,
val slug: String,
val cover: String,
@JsonNames("status", "statusScanlation")val status: Int = -1,
val description: String = "",
val genre: List<Int> = emptyList(),
val artist: List<Person> = emptyList(),
val author: List<Person> = emptyList(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.extension.de.mangatube.dio

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class Page(
val url: String,
@SerialName("page") val index: Int,
@SerialName("alt_source") val altSource: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.extension.de.mangatube.dio

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class Pagination(
@SerialName("current_page") val currentPage: Int,
@SerialName("last_page") val lastPage: Int,
) {

fun lastPage(): Boolean {
return currentPage == lastPage
}
}
Loading
Loading