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 1 commit 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 android.util.Log
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.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,225 @@ class MangaTube : ParsedHttpSource() {

override val supportsLatest = true

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)
.build()

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

override fun getMangaUrl(manga: SManga): String {
if (!manga.url.startsWith(baseUrl)) {
return "$baseUrl${manga.url}"
}
return manga.url
}

override fun getChapterUrl(chapter: SChapter): String {
return chapter.url
}

override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/api/manga/search?page=$page&query=$query"

Log.d("MangaTube", "Search for: $query")
Copy link
Contributor

Choose a reason for hiding this comment

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

remove logs

Log.d("MangaTube", "Url -> $url")

private val json: Json by injectLazy()
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 searchMangaParse(response: Response): MangasPage {
val body = MangaTubeHelper.checkResponse(response)

// Popular
val res: BaseResponse<List<Manga>> = json.decodeFromString(body)

override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newCall(popularMangaRequest(page))
.asObservableSuccess()
.map { response ->
parseMangaFromJson(response, page < 96)
if(!res.success){
throw Exception("Something went wrong!")
Copy link
Contributor

Choose a reason for hiding this comment

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

can it be more specific?

}

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

return MangasPage(mangaList, res.pagination!!.lastPage())
}

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))
}
val url = "$baseUrl/api/home/top-manga"

// 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)
Log.d("MangaTube", "Request popular mangas")
Log.d("MangaTube", "Url -> $url")

return GET(url)
}

override fun popularMangaSelector() = throw UnsupportedOperationException()
override fun popularMangaParse(response: Response): MangasPage {
val body = MangaTubeHelper.checkResponse(response)

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

override fun popularMangaNextPageSelector() = throw UnsupportedOperationException()
if(!res.success){
throw Exception("Something went wrong!")
}


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 }
Comment on lines +122 to +123
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
genre = manga.genre.map { genre -> Genre.fromId(genre)!! }
.joinToString(", ") { genre -> genre.displayName }
genre = manga.genre.joinToString { genre -> Genre.fromId(genre)!!.displayName }

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

// Latest
return MangasPage(mangaList, false)
}

override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/?page=$page", headers)
val url = "$baseUrl/api/home/new-manga"

Log.d("MangaTube", "Request new mangas")
Log.d("MangaTube", "Url -> $url")

return GET(url)
}

override fun latestUpdatesSelector() = "div#series-updates div.series-update:not([style\$=none])"
override fun latestUpdatesParse(response: Response): MangasPage {
val body = MangaTubeHelper.checkResponse(response)
Copy link
Contributor

Choose a reason for hiding this comment

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

consider calling this method in an interceptor since you are checking response every time


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)

if(!res.success){
throw Exception("Something went wrong!")
}


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
}
thumbnail_url = element.select("div.cover img").attr("abs:data-original")
}

return MangasPage(mangaList, false)
}

override fun latestUpdatesNextPageSelector() = "button#load-more-updates"
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


// Search
Log.d("MangaTube", "Request manga details for: ${manga.title}")
Log.d("MangaTube", "Url -> $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))
return GET(url)
}

override fun searchMangaParse(response: Response): MangasPage {
return parseMangaFromJson(response, false)
}
override fun mangaDetailsParse(response: Response): SManga {
val body = MangaTubeHelper.checkResponse(response)

override fun searchMangaSelector() = throw UnsupportedOperationException()
val res: BaseResponse<MangaWrapper> = json.decodeFromString(body)

override fun searchMangaFromElement(element: Element): SManga = throw UnsupportedOperationException()
if(!res.success){
throw Exception("Something went wrong!")
}

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


Log.d("MangaTube", "Request chapters for manga: ${manga.title}")
Log.d("MangaTube", "Url -> $url")

return GET(url)
}

// Chapters
@SuppressLint("SimpleDateFormat")
override fun chapterListParse(response: Response): List<SChapter> {
val body = MangaTubeHelper.checkResponse(response)

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
}
if(!res.success){
throw Exception("Something went wrong!")
}

val chapterList = res.data.chapters.map { chapter ->
SChapter.create().apply {
url = "$baseUrl${chapter.readerURL}"
name = chapter.name.ifBlank { "Chapter ${chapter.number}" }
date_upload =
SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(chapter.publishedAt)!!.time
Copy link
Contributor

Choose a reason for hiding this comment

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

initialize `SimpleDateFormat in the class to avoide initializing it every time, and wrap the parsing in try catch to avoide breaking in case of bad dates

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 = MangaTubeHelper.checkResponse(response)

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

if(!res.success){
throw Exception("Something went wrong!")
}

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

return mangaList
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

consider creating one dto file with all the dtos in the package root (not in dto subpackage).

also use regular classes instead of data class unless needed

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(),
)
Loading