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 {
|
||||
extName = 'MANGA Plus Creators by SHUEISHA'
|
||||
extClass = '.MangaPlusCreatorsFactory'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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.MangasPage
|
||||
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.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import keiyoushi.utils.parseAs
|
||||
import keiyoushi.utils.tryParse
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class MangaPlusCreators(override val lang: String) : HttpSource() {
|
||||
|
||||
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 fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("Origin", baseUrl.substringBeforeLast("/"))
|
||||
.add("Referer", baseUrl)
|
||||
.add("User-Agent", USER_AGENT)
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
// POPULAR Section
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Referer", "$baseUrl/titles/popular/?p=m")
|
||||
.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)
|
||||
val popularUrl = "$baseUrl/titles/popular/?p=m&l=$lang".toHttpUrl()
|
||||
return GET(popularUrl, headers)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val result = response.asMpcResponse()
|
||||
override fun popularMangaParse(response: Response): MangasPage = parseMangasPageFromElement(
|
||||
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 {
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Referer", "$baseUrl/titles/recent/?t=episode")
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
val apiUrl = "$apiUrl/titles/recent/".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("page", page.toString())
|
||||
.addQueryParameter("l", lang)
|
||||
.addQueryParameter("t", "episode")
|
||||
.build()
|
||||
|
||||
val apiUrl = "$API_URL/titles/recent/list".toHttpUrl().newBuilder()
|
||||
.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)
|
||||
return GET(apiUrl, headers)
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
.toString()
|
||||
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Referer", refererUrl)
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.addQueryParameter("s", "date")
|
||||
.addQueryParameter("lang", lang)
|
||||
.build()
|
||||
|
||||
val apiUrl = "$API_URL/search/titles".toHttpUrl().newBuilder()
|
||||
.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)
|
||||
return GET(searchUrl, headers)
|
||||
}
|
||||
|
||||
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 {
|
||||
val result = response.asJsoup()
|
||||
val bookBox = result.selectFirst(".book-box")!!
|
||||
@ -119,62 +218,82 @@ class MangaPlusCreators(override val lang: String) : HttpSource() {
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// CHAPTER Section
|
||||
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()
|
||||
.set("Referer", baseUrl + manga.url)
|
||||
.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)
|
||||
private fun chapterListPageRequest(page: Int, titleContentId: String): Request {
|
||||
return GET("$baseUrl/titles/$titleContentId/?page=$page", headers)
|
||||
}
|
||||
|
||||
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()
|
||||
.sortedByDescending(MpcEpisode::numbering)
|
||||
.map(MpcEpisode::toSChapter)
|
||||
return chapterListResult.asReversed()
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val chapterId = chapter.url.substringAfterLast("/")
|
||||
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Referer", baseUrl + chapter.url)
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.build()
|
||||
|
||||
val apiUrl = "$API_URL/episodes/pageList/$chapterId/".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("_", System.currentTimeMillis().toString())
|
||||
.toString()
|
||||
|
||||
return GET(apiUrl, newHeaders)
|
||||
private fun chapterListPageParse(response: Response): ChaptersPage {
|
||||
val result = response.asJsoup()
|
||||
val chapters = result.select(".mod-item-series").map {
|
||||
element ->
|
||||
chapterElementToSChapter(element)
|
||||
}
|
||||
val hasResult = result.select(".mod-pagination .next").isNotEmpty()
|
||||
return ChaptersPage(
|
||||
chapters,
|
||||
hasResult,
|
||||
)
|
||||
}
|
||||
|
||||
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> {
|
||||
val result = response.asMpcResponse()
|
||||
|
||||
checkNotNull(result.pageList) { EMPTY_RESPONSE_ERROR }
|
||||
|
||||
val referer = response.request.header("Referer")!!
|
||||
|
||||
return result.pageList.mapIndexed { i, page ->
|
||||
Page(i, referer, page.publicBgImage)
|
||||
val result = response.asJsoup()
|
||||
val readerElement = result.selectFirst("div[react=viewer]")!!
|
||||
val dataPages = readerElement.attr("data-pages")
|
||||
val refererUrl = response.request.url.toString()
|
||||
return dataPages.parseAs<MpcReaderDataPages>().pc.map {
|
||||
page ->
|
||||
Page(page.pageNo, refererUrl, page.imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,18 +310,66 @@ class MangaPlusCreators(override val lang: String) : HttpSource() {
|
||||
return GET(page.imageUrl!!, newHeaders)
|
||||
}
|
||||
|
||||
private fun Response.asMpcResponse(): MpcResponse = use {
|
||||
json.decodeFromString(body.string())
|
||||
}
|
||||
|
||||
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) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
|
||||
|
||||
private const val POPULAR_PAGE_SIZE = "30"
|
||||
private const val CHAPTER_PAGE_SIZE = "200"
|
||||
|
||||
private const val EMPTY_RESPONSE_ERROR = "Empty response from the API. Try again later."
|
||||
const val PREFIX_TITLE_ID_SEARCH = "title:"
|
||||
const val PREFIX_EPISODE_ID_SEARCH = "episode:"
|
||||
const val PREFIX_AUTHOR_ID_SEARCH = "author:"
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MpcResponse(
|
||||
@SerialName("mpcEpisodesDto") val episodes: MpcEpisodesDto? = null,
|
||||
@SerialName("mpcTitlesDto") val titles: MpcTitlesDto? = null,
|
||||
val pageList: List<MpcPage>? = emptyList(),
|
||||
class MpcResponse(
|
||||
val status: String,
|
||||
val titles: List<MpcTitle>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MpcEpisodesDto(
|
||||
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,
|
||||
class MpcTitle(
|
||||
val title: String,
|
||||
val thumbnailUrl: String,
|
||||
) {
|
||||
|
||||
fun toSManga(): SManga = SManga.create().apply {
|
||||
title = this@MpcTitle.title
|
||||
thumbnail_url = thumbnailUrl
|
||||
url = "/titles/$id"
|
||||
}
|
||||
}
|
||||
val thumbnail: String,
|
||||
@SerialName("is_one_shot") val isOneShot: Boolean,
|
||||
val author: MpcAuthorDto,
|
||||
@SerialName("latest_episode") val latestEpisode: MpcLatestEpisode,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MpcEpisode(
|
||||
@SerialName("episodeId") val id: 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"
|
||||
}
|
||||
}
|
||||
class MpcAuthorDto(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@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