-
Notifications
You must be signed in to change notification settings - Fork 562
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
base: main
Are you sure you want to change the base?
Changes from all commits
2072ede
e4578ec
fa66069
5b19920
4ff2780
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
|
||
|
@@ -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¶meter%5Bpage%5D=$page¶meter%5Bletter%5D=¶meter%5Bsortby%5D=popularity¶meter%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) | ||
} | ||
|
||
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}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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¶meter%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}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pass headers to set appropriate useragent and other headers to requests