Move MangaPlus Creators to new domain (#10337)
* start switching to new URLs for popular/manga/etc * fix popular * fix latest * minor change to manga parse * refactor popular, fix search * fix search/popular selector * fix chapters/pages * fixes from debugging on android emulator * increment ext version * support paginated chapter lists * break doesn't break well, that's not true, it did work once the extension was freshly installed. but I liked the alternative so I thought why not. can be removed if needed * cleanup * add TODOs * add intents to urls and search prefixes support both old and new domains (since it all redirects, bless them) * move around toSManga pro: we get setUrlWithoutDomain con: we lose this@<data-class-name>.title * add filter screen * debug search * fix pathPattern `..*` is the same as `.+`. however, the latter requires adding `advancedPathPattern` instead * what the intent: fix classdefnotfoundexception * categorise into sections * prefer helper functions from `utils` * Change inline import to explicit * inline baseUrl * inline apiUrl * remove superfluous header modifications * always pass headers on new requests * no need to convert HttpUrl to String * make helper functions private * use selectFirst instead of select, assert non-null * make sub classes defined under filters private * lint * prefer not data but class * Revert "break doesn't break" This reverts commit 23b2cfe46c0f57214443e138a06cadbef0cccb61. * lint * better chapterNumber fail case ( -1f instead of 1f ) * lint
This commit is contained in:
parent
0ec1a28ed7
commit
08b627c8e7
44
src/all/mangapluscreators/AndroidManifest.xml
Normal file
44
src/all/mangapluscreators/AndroidManifest.xml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<activity
|
||||||
|
android:name=".all.mangapluscreators.MPCUrlActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="mangaplus-creators.jp"
|
||||||
|
android:pathAdvancedPattern="/episodes/.+"
|
||||||
|
android:scheme="https" />
|
||||||
|
<data
|
||||||
|
android:host="mangaplus-creators.jp"
|
||||||
|
android:pathPattern="/titles/..*"
|
||||||
|
android:scheme="https" />
|
||||||
|
<data
|
||||||
|
android:host="mangaplus-creators.jp"
|
||||||
|
android:pathAdvancedPattern="/authors/.+"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="medibang.com"
|
||||||
|
android:pathAdvancedPattern="/mpc/episodes/.+"
|
||||||
|
android:scheme="https" />
|
||||||
|
<data
|
||||||
|
android:host="medibang.com"
|
||||||
|
android:pathAdvancedPattern="/mpc/titles/..+"
|
||||||
|
android:scheme="https" />
|
||||||
|
<data
|
||||||
|
android:host="medibang.com"
|
||||||
|
android:pathAdvancedPattern="/mpc/authors/[0-9]+"
|
||||||
|
android:scheme="https" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'MANGA Plus Creators by SHUEISHA'
|
extName = 'MANGA Plus Creators by SHUEISHA'
|
||||||
extClass = '.MangaPlusCreatorsFactory'
|
extClass = '.MangaPlusCreatorsFactory'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.mangapluscreators
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
class MPCUrlActivity : Activity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val pathSegments = intent?.data?.pathSegments
|
||||||
|
if (pathSegments != null && pathSegments.size > 1) {
|
||||||
|
// {medibang.com/mpc,mangaplus-creators.jp}/{episodes,titles,authors}
|
||||||
|
// TODO: val pathIndex = if (intent?.data?.host?.startsWith("medibang") == true) 1 else 0
|
||||||
|
val host = intent?.data?.host ?: ""
|
||||||
|
val pathIndex = with(host) {
|
||||||
|
when {
|
||||||
|
equals("medibang.com") -> 1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val idIndex = pathIndex + 1
|
||||||
|
val query = when {
|
||||||
|
pathSegments[pathIndex].equals("episodes") -> {
|
||||||
|
MangaPlusCreators.PREFIX_EPISODE_ID_SEARCH + pathSegments[idIndex]
|
||||||
|
}
|
||||||
|
pathSegments[pathIndex].equals("authors") -> {
|
||||||
|
MangaPlusCreators.PREFIX_AUTHOR_ID_SEARCH + pathSegments[idIndex]
|
||||||
|
}
|
||||||
|
pathSegments[pathIndex].equals("titles") -> {
|
||||||
|
MangaPlusCreators.PREFIX_TITLE_ID_SEARCH + pathSegments[idIndex]
|
||||||
|
}
|
||||||
|
else -> null // TODO: is this required?
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query != null) {
|
||||||
|
// TODO: val mainIntent = Intent().setAction("eu.kanade.tachiyomi.SEARCH").apply {
|
||||||
|
val mainIntent = Intent().apply {
|
||||||
|
setAction("eu.kanade.tachiyomi.SEARCH")
|
||||||
|
putExtra("query", query)
|
||||||
|
putExtra("filter", packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivity(mainIntent)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Log.e("MPCUrlActivity", e.toString())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e("MPCUrlActivity", "Missing alphanumeric ID from the URL")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e("MPCUrlActivity", "Could not parse URI from intent $intent")
|
||||||
|
}
|
||||||
|
|
||||||
|
finish()
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.mangapluscreators
|
package eu.kanade.tachiyomi.extension.all.mangapluscreators
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
@ -8,102 +10,199 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.decodeFromString
|
import keiyoushi.utils.parseAs
|
||||||
import kotlinx.serialization.json.Json
|
import keiyoushi.utils.tryParse
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class MangaPlusCreators(override val lang: String) : HttpSource() {
|
class MangaPlusCreators(override val lang: String) : HttpSource() {
|
||||||
|
|
||||||
override val name = "MANGA Plus Creators by SHUEISHA"
|
override val name = "MANGA Plus Creators by SHUEISHA"
|
||||||
|
|
||||||
override val baseUrl = "https://medibang.com/mpc"
|
override val baseUrl = "https://mangaplus-creators.jp"
|
||||||
|
|
||||||
|
private val apiUrl = "$baseUrl/api"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||||
.add("Origin", baseUrl.substringBeforeLast("/"))
|
|
||||||
.add("Referer", baseUrl)
|
.add("Referer", baseUrl)
|
||||||
.add("User-Agent", USER_AGENT)
|
.add("User-Agent", USER_AGENT)
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
// POPULAR Section
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
val newHeaders = headersBuilder()
|
val popularUrl = "$baseUrl/titles/popular/?p=m&l=$lang".toHttpUrl()
|
||||||
.set("Referer", "$baseUrl/titles/popular/?p=m")
|
return GET(popularUrl, headers)
|
||||||
.add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val apiUrl = "$API_URL/titles/popular/list".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("page", page.toString())
|
|
||||||
.addQueryParameter("pageSize", POPULAR_PAGE_SIZE)
|
|
||||||
.addQueryParameter("l", lang)
|
|
||||||
.addQueryParameter("p", "m")
|
|
||||||
.addQueryParameter("isWebview", "false")
|
|
||||||
.addQueryParameter("_", System.currentTimeMillis().toString())
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return GET(apiUrl, newHeaders)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response): MangasPage = parseMangasPageFromElement(
|
||||||
val result = response.asMpcResponse()
|
response,
|
||||||
|
"div.item-recent",
|
||||||
|
)
|
||||||
|
|
||||||
checkNotNull(result.titles) { EMPTY_RESPONSE_ERROR }
|
private fun parseMangasPageFromElement(response: Response, selector: String): MangasPage {
|
||||||
|
val result = response.asJsoup()
|
||||||
|
|
||||||
val titles = result.titles.titleList.orEmpty().map(MpcTitle::toSManga)
|
val mangas = result.select(selector).map { element ->
|
||||||
|
popularElementToSManga(element)
|
||||||
|
}
|
||||||
|
|
||||||
return MangasPage(titles, result.titles.pagination?.hasNextPage ?: false)
|
return MangasPage(mangas, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun popularElementToSManga(element: Element): SManga {
|
||||||
|
val titleThumbnailUrl = element.selectFirst(".image-area img")!!.attr("src")
|
||||||
|
val titleContentId = titleThumbnailUrl.toHttpUrl().pathSegments[2]
|
||||||
|
return SManga.create().apply {
|
||||||
|
title = element.selectFirst(".title-area .title")!!.text()
|
||||||
|
thumbnail_url = titleThumbnailUrl
|
||||||
|
setUrlWithoutDomain("/titles/$titleContentId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LATEST Section
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
val newHeaders = headersBuilder()
|
val apiUrl = "$apiUrl/titles/recent/".toHttpUrl().newBuilder()
|
||||||
.set("Referer", "$baseUrl/titles/recent/?t=episode")
|
.addQueryParameter("page", page.toString())
|
||||||
.add("X-Requested-With", "XMLHttpRequest")
|
.addQueryParameter("l", lang)
|
||||||
|
.addQueryParameter("t", "episode")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val apiUrl = "$API_URL/titles/recent/list".toHttpUrl().newBuilder()
|
return GET(apiUrl, headers)
|
||||||
.addQueryParameter("page", page.toString())
|
|
||||||
.addQueryParameter("pageSize", POPULAR_PAGE_SIZE)
|
|
||||||
.addQueryParameter("l", lang)
|
|
||||||
.addQueryParameter("c", "episode")
|
|
||||||
.addQueryParameter("isWebview", "false")
|
|
||||||
.addQueryParameter("_", System.currentTimeMillis().toString())
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return GET(apiUrl, newHeaders)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val result = response.parseAs<MpcResponse>()
|
||||||
|
|
||||||
|
val titles = result.titles.orEmpty().map { title -> title.toSManga() }
|
||||||
|
|
||||||
|
// TODO: handle last page of latest
|
||||||
|
return MangasPage(titles, result.status != "error")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MpcTitle.toSManga(): SManga {
|
||||||
|
val mTitle = this.title
|
||||||
|
val mAuthor = this.author.name // TODO: maybe not required
|
||||||
|
return SManga.create().apply {
|
||||||
|
title = mTitle
|
||||||
|
thumbnail_url = thumbnail
|
||||||
|
setUrlWithoutDomain("/titles/${latestEpisode.titleConnectId}")
|
||||||
|
author = mAuthor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SEARCH Section
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
// TODO: HTTPSource::fetchSearchManga is deprecated? super.getSearchManga
|
||||||
|
if (query.startsWith(PREFIX_TITLE_ID_SEARCH)) {
|
||||||
|
val titleContentId = query.removePrefix(PREFIX_TITLE_ID_SEARCH)
|
||||||
|
val titleUrl = "$baseUrl/titles/$titleContentId"
|
||||||
|
return client.newCall(GET(titleUrl, headers))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
val result = response.asJsoup()
|
||||||
|
val bookBox = result.selectFirst(".book-box")!!
|
||||||
|
val title = SManga.create().apply {
|
||||||
|
title = bookBox.selectFirst("div.title")!!.text()
|
||||||
|
thumbnail_url = bookBox.selectFirst("div.cover img")!!.attr("data-src")
|
||||||
|
setUrlWithoutDomain(titleUrl)
|
||||||
|
}
|
||||||
|
MangasPage(listOf(title), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (query.startsWith(PREFIX_EPISODE_ID_SEARCH)) {
|
||||||
|
val episodeId = query.removePrefix(PREFIX_EPISODE_ID_SEARCH)
|
||||||
|
return client.newCall(GET("$baseUrl/episodes/$episodeId", headers))
|
||||||
|
.asObservableSuccess().map { response ->
|
||||||
|
val result = response.asJsoup()
|
||||||
|
val readerElement = result.selectFirst("div[react=viewer]")!!
|
||||||
|
val dataTitle = readerElement.attr("data-title")
|
||||||
|
val dataTitleResult = dataTitle.parseAs<MpcReaderDataTitle>()
|
||||||
|
val episodeAsSManga = dataTitleResult.toSManga()
|
||||||
|
MangasPage(listOf(episodeAsSManga), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (query.startsWith(PREFIX_AUTHOR_ID_SEARCH)) {
|
||||||
|
val authorId = query.removePrefix(PREFIX_AUTHOR_ID_SEARCH)
|
||||||
|
return client.newCall(GET("$baseUrl/authors/$authorId", headers))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
val result = response.asJsoup()
|
||||||
|
val elements = result.select("#works .manga-list li .md\\:block")
|
||||||
|
val smangas = elements.map { element ->
|
||||||
|
val titleThumbnailUrl = element.selectFirst(".image-area img")!!.attr("src")
|
||||||
|
val titleContentId = titleThumbnailUrl.toHttpUrl().pathSegments[2]
|
||||||
|
SManga.create().apply {
|
||||||
|
title = element.selectFirst("p.text-white")!!.text().toString()
|
||||||
|
thumbnail_url = titleThumbnailUrl
|
||||||
|
setUrlWithoutDomain("/titles/$titleContentId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MangasPage(smangas, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (query.isNotBlank()) {
|
||||||
|
return super.fetchSearchManga(page, query, filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing to search, filters active -> browsing /genres instead
|
||||||
|
// TODO: check if there's a better way (filters is independent of search but part of it)
|
||||||
|
val genreUrl = baseUrl.toHttpUrl().newBuilder()
|
||||||
|
.apply {
|
||||||
|
addPathSegment("genres")
|
||||||
|
addQueryParameter("l", lang)
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is SortFilter -> {
|
||||||
|
if (filter.selected.isNotEmpty()) {
|
||||||
|
addQueryParameter("s", filter.selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is GenreFilter -> addPathSegment(filter.selected)
|
||||||
|
else -> { /* Nothing else is supported for now */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return client.newCall(GET(genreUrl, headers))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
popularMangaParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MpcReaderDataTitle.toSManga(): SManga {
|
||||||
|
val mTitle = title
|
||||||
|
return SManga.create().apply {
|
||||||
|
title = mTitle
|
||||||
|
thumbnail_url = thumbnail
|
||||||
|
setUrlWithoutDomain("/titles/$contentsId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val refererUrl = "$baseUrl/keywords".toHttpUrl().newBuilder()
|
// TODO: maybe this needn't be a new builder and just similar to `popularUrl` above?
|
||||||
|
val searchUrl = "$baseUrl/keywords".toHttpUrl().newBuilder()
|
||||||
.addQueryParameter("q", query)
|
.addQueryParameter("q", query)
|
||||||
.toString()
|
.addQueryParameter("s", "date")
|
||||||
|
.addQueryParameter("lang", lang)
|
||||||
val newHeaders = headersBuilder()
|
|
||||||
.set("Referer", refererUrl)
|
|
||||||
.add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val apiUrl = "$API_URL/search/titles".toHttpUrl().newBuilder()
|
return GET(searchUrl, headers)
|
||||||
.addQueryParameter("keyword", query)
|
|
||||||
.addQueryParameter("page", page.toString())
|
|
||||||
.addQueryParameter("pageSize", POPULAR_PAGE_SIZE)
|
|
||||||
.addQueryParameter("sort", "newly")
|
|
||||||
.addQueryParameter("lang", lang)
|
|
||||||
.addQueryParameter("_", System.currentTimeMillis().toString())
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return GET(apiUrl, newHeaders)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
|
override fun searchMangaParse(response: Response): MangasPage = parseMangasPageFromElement(
|
||||||
|
response,
|
||||||
|
"div.item-search",
|
||||||
|
)
|
||||||
|
|
||||||
|
// MANGA Section
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
val result = response.asJsoup()
|
val result = response.asJsoup()
|
||||||
val bookBox = result.selectFirst(".book-box")!!
|
val bookBox = result.selectFirst(".book-box")!!
|
||||||
@ -119,62 +218,82 @@ class MangaPlusCreators(override val lang: String) : HttpSource() {
|
|||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
genre = bookBox.select("div.genre-area div.tag-genre")
|
genre = bookBox.select("div.genre-area div.tag-genre")
|
||||||
.joinToString { it.text() }
|
.joinToString(", ") { it.text() }
|
||||||
thumbnail_url = bookBox.selectFirst("div.cover img")!!.attr("data-src")
|
thumbnail_url = bookBox.selectFirst("div.cover img")!!.attr("data-src")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CHAPTER Section
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
val titleId = manga.url.substringAfterLast("/")
|
val titleContentId = (baseUrl + manga.url).toHttpUrl().pathSegments[1]
|
||||||
|
return chapterListPageRequest(1, titleContentId)
|
||||||
|
}
|
||||||
|
|
||||||
val newHeaders = headersBuilder()
|
private fun chapterListPageRequest(page: Int, titleContentId: String): Request {
|
||||||
.set("Referer", baseUrl + manga.url)
|
return GET("$baseUrl/titles/$titleContentId/?page=$page", headers)
|
||||||
.add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val apiUrl = "$API_URL/titles/$titleId/episodes/".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("page", "1")
|
|
||||||
.addQueryParameter("pageSize", CHAPTER_PAGE_SIZE)
|
|
||||||
.addQueryParameter("_", System.currentTimeMillis().toString())
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return GET(apiUrl, newHeaders)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val result = response.asMpcResponse()
|
val chapterListResponse = chapterListPageParse(response)
|
||||||
|
val chapterListResult = chapterListResponse.chapters.toMutableList()
|
||||||
|
|
||||||
checkNotNull(result.episodes) { EMPTY_RESPONSE_ERROR }
|
var hasNextPage = chapterListResponse.hasNextPage
|
||||||
|
val titleContentId = response.request.url.pathSegments[1]
|
||||||
|
var page = 1
|
||||||
|
while (hasNextPage) {
|
||||||
|
page += 1
|
||||||
|
val nextPageRequest = chapterListPageRequest(page, titleContentId)
|
||||||
|
val nextPageResponse = client.newCall(nextPageRequest).execute()
|
||||||
|
val nextPageResult = chapterListPageParse(nextPageResponse)
|
||||||
|
if (nextPageResult.chapters.isEmpty()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
chapterListResult.addAll(nextPageResult.chapters)
|
||||||
|
hasNextPage = nextPageResult.hasNextPage
|
||||||
|
}
|
||||||
|
|
||||||
return result.episodes.episodeList.orEmpty()
|
return chapterListResult.asReversed()
|
||||||
.sortedByDescending(MpcEpisode::numbering)
|
|
||||||
.map(MpcEpisode::toSChapter)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
private fun chapterListPageParse(response: Response): ChaptersPage {
|
||||||
val chapterId = chapter.url.substringAfterLast("/")
|
val result = response.asJsoup()
|
||||||
|
val chapters = result.select(".mod-item-series").map {
|
||||||
val newHeaders = headersBuilder()
|
element ->
|
||||||
.set("Referer", baseUrl + chapter.url)
|
chapterElementToSChapter(element)
|
||||||
.add("X-Requested-With", "XMLHttpRequest")
|
}
|
||||||
.build()
|
val hasResult = result.select(".mod-pagination .next").isNotEmpty()
|
||||||
|
return ChaptersPage(
|
||||||
val apiUrl = "$API_URL/episodes/pageList/$chapterId/".toHttpUrl().newBuilder()
|
chapters,
|
||||||
.addQueryParameter("_", System.currentTimeMillis().toString())
|
hasResult,
|
||||||
.toString()
|
)
|
||||||
|
|
||||||
return GET(apiUrl, newHeaders)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun chapterElementToSChapter(element: Element): SChapter {
|
||||||
|
val episode = element.attr("href").substringAfterLast("/")
|
||||||
|
val latestUpdatedDate = element.selectFirst(".first-update")!!.text()
|
||||||
|
val chapterNumberElement = element.selectFirst(".number")!!.text()
|
||||||
|
val chapterNumber = chapterNumberElement.substringAfter("#").toFloatOrNull()
|
||||||
|
return SChapter.create().apply {
|
||||||
|
setUrlWithoutDomain("/episodes/$episode")
|
||||||
|
date_upload = CHAPTER_DATE_FORMAT.tryParse(latestUpdatedDate)
|
||||||
|
name = chapterNumberElement
|
||||||
|
chapter_number = if (chapterNumberElement == "One-shot") {
|
||||||
|
0F
|
||||||
|
} else {
|
||||||
|
chapterNumber ?: -1F
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PAGES & IMAGES Section
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val result = response.asMpcResponse()
|
val result = response.asJsoup()
|
||||||
|
val readerElement = result.selectFirst("div[react=viewer]")!!
|
||||||
checkNotNull(result.pageList) { EMPTY_RESPONSE_ERROR }
|
val dataPages = readerElement.attr("data-pages")
|
||||||
|
val refererUrl = response.request.url.toString()
|
||||||
val referer = response.request.header("Referer")!!
|
return dataPages.parseAs<MpcReaderDataPages>().pc.map {
|
||||||
|
page ->
|
||||||
return result.pageList.mapIndexed { i, page ->
|
Page(page.pageNo, refererUrl, page.imageUrl)
|
||||||
Page(i, referer, page.publicBgImage)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,18 +310,66 @@ class MangaPlusCreators(override val lang: String) : HttpSource() {
|
|||||||
return GET(page.imageUrl!!, newHeaders)
|
return GET(page.imageUrl!!, newHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Response.asMpcResponse(): MpcResponse = use {
|
|
||||||
json.decodeFromString(body.string())
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val API_URL = "https://medibang.com/api/mpc"
|
private val CHAPTER_DATE_FORMAT by lazy {
|
||||||
|
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
|
||||||
|
}
|
||||||
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
|
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
|
||||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
|
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
|
||||||
|
const val PREFIX_TITLE_ID_SEARCH = "title:"
|
||||||
private const val POPULAR_PAGE_SIZE = "30"
|
const val PREFIX_EPISODE_ID_SEARCH = "episode:"
|
||||||
private const val CHAPTER_PAGE_SIZE = "200"
|
const val PREFIX_AUTHOR_ID_SEARCH = "author:"
|
||||||
|
|
||||||
private const val EMPTY_RESPONSE_ERROR = "Empty response from the API. Try again later."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FILTERS Section
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
Filter.Separator(),
|
||||||
|
Filter.Header("NOTE: Ignored if using text search!"),
|
||||||
|
Filter.Separator(),
|
||||||
|
SortFilter(),
|
||||||
|
GenreFilter(),
|
||||||
|
Filter.Separator(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private class SortFilter() : SelectFilter(
|
||||||
|
"Sort",
|
||||||
|
listOf(
|
||||||
|
SelectFilterOption("Popularity", ""),
|
||||||
|
SelectFilterOption("Date", "latest_desc"),
|
||||||
|
SelectFilterOption("Likes", "like_desc"),
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
private class GenreFilter() : SelectFilter(
|
||||||
|
"Genres",
|
||||||
|
listOf(
|
||||||
|
SelectFilterOption("Fantasy", "fantasy"),
|
||||||
|
SelectFilterOption("Action", "action"),
|
||||||
|
SelectFilterOption("Romance", "romance"),
|
||||||
|
SelectFilterOption("Horror", "horror"),
|
||||||
|
SelectFilterOption("Slice of Life", "slice_of_life"),
|
||||||
|
SelectFilterOption("Comedy", "comedy"),
|
||||||
|
SelectFilterOption("Sports", "sports"),
|
||||||
|
SelectFilterOption("Sci-Fi", "sf"),
|
||||||
|
SelectFilterOption("Mystery", "mystery"),
|
||||||
|
SelectFilterOption("Others", "others"),
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
private abstract class SelectFilter(
|
||||||
|
name: String,
|
||||||
|
private val options: List<SelectFilterOption>,
|
||||||
|
default: Int = 0,
|
||||||
|
) : Filter.Select<String>(
|
||||||
|
name,
|
||||||
|
options.map { it.name }.toTypedArray(),
|
||||||
|
default,
|
||||||
|
) {
|
||||||
|
val selected: String
|
||||||
|
get() = options[state].value
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SelectFilterOption(val name: String, val value: String)
|
||||||
}
|
}
|
||||||
|
@ -1,68 +1,51 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.mangapluscreators
|
package eu.kanade.tachiyomi.extension.all.mangapluscreators
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MpcResponse(
|
class MpcResponse(
|
||||||
@SerialName("mpcEpisodesDto") val episodes: MpcEpisodesDto? = null,
|
val status: String,
|
||||||
@SerialName("mpcTitlesDto") val titles: MpcTitlesDto? = null,
|
val titles: List<MpcTitle>? = null,
|
||||||
val pageList: List<MpcPage>? = emptyList(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MpcEpisodesDto(
|
class MpcTitle(
|
||||||
val pagination: MpcPagination? = null,
|
|
||||||
val episodeList: List<MpcEpisode>? = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MpcTitlesDto(
|
|
||||||
val pagination: MpcPagination? = null,
|
|
||||||
val titleList: List<MpcTitle>? = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MpcPagination(
|
|
||||||
val page: Int,
|
|
||||||
val maxPage: Int,
|
|
||||||
) {
|
|
||||||
|
|
||||||
val hasNextPage: Boolean
|
|
||||||
get() = page < maxPage
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MpcTitle(
|
|
||||||
@SerialName("titleId") val id: String,
|
|
||||||
val title: String,
|
val title: String,
|
||||||
val thumbnailUrl: String,
|
val thumbnail: String,
|
||||||
) {
|
@SerialName("is_one_shot") val isOneShot: Boolean,
|
||||||
|
val author: MpcAuthorDto,
|
||||||
fun toSManga(): SManga = SManga.create().apply {
|
@SerialName("latest_episode") val latestEpisode: MpcLatestEpisode,
|
||||||
title = this@MpcTitle.title
|
)
|
||||||
thumbnail_url = thumbnailUrl
|
|
||||||
url = "/titles/$id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MpcEpisode(
|
class MpcAuthorDto(
|
||||||
@SerialName("episodeId") val id: String,
|
val name: String,
|
||||||
@SerialName("episodeTitle") val title: String,
|
)
|
||||||
val numbering: Int,
|
|
||||||
val oneshot: Boolean = false,
|
|
||||||
val publishDate: Long,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun toSChapter(): SChapter = SChapter.create().apply {
|
|
||||||
name = if (oneshot) "One-shot" else title
|
|
||||||
date_upload = publishDate
|
|
||||||
url = "/episodes/$id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MpcPage(val publicBgImage: String)
|
class MpcLatestEpisode(
|
||||||
|
@SerialName("title_connect_id") val titleConnectId: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MpcReaderDataPages(
|
||||||
|
val pc: List<MpcReaderPage>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MpcReaderPage(
|
||||||
|
@SerialName("page_no") val pageNo: Int,
|
||||||
|
@SerialName("image_url") val imageUrl: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MpcReaderDataTitle(
|
||||||
|
val title: String,
|
||||||
|
val thumbnail: String,
|
||||||
|
@SerialName("is_oneshot") val isOneShot: Boolean,
|
||||||
|
@SerialName("contents_id") val contentsId: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
class ChaptersPage(val chapters: List<SChapter>, val hasNextPage: Boolean)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user