Delegate mangaplus so that it doesnt crash the app, it is literally just a copy of the extension with no extra features, but at least the app doesnt crash when you use it now
This commit is contained in:
parent
f594962731
commit
9ecf83f842
@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
|||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||||
import eu.kanade.tachiyomi.source.online.all.Hitomi
|
import eu.kanade.tachiyomi.source.online.all.Hitomi
|
||||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||||
|
import eu.kanade.tachiyomi.source.online.all.MangaPlus
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.NHentai
|
import eu.kanade.tachiyomi.source.online.all.NHentai
|
||||||
import eu.kanade.tachiyomi.source.online.all.PervEden
|
import eu.kanade.tachiyomi.source.online.all.PervEden
|
||||||
@ -237,6 +238,13 @@ open class SourceManager(private val context: Context) {
|
|||||||
"eu.kanade.tachiyomi.extension.all.nhentai.NHentai",
|
"eu.kanade.tachiyomi.extension.all.nhentai.NHentai",
|
||||||
NHentai::class,
|
NHentai::class,
|
||||||
true
|
true
|
||||||
|
),
|
||||||
|
DelegatedSource(
|
||||||
|
"MANGA Plus",
|
||||||
|
fillInSourceId,
|
||||||
|
"eu.kanade.tachiyomi.extension.all.mangaplus.MangaPlus",
|
||||||
|
MangaPlus::class,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
).associateBy { it.originalSourceQualifiedClassName }
|
).associateBy { it.originalSourceQualifiedClassName }
|
||||||
|
|
||||||
|
@ -0,0 +1,372 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online.all
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.CheckBoxPreference
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
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.HttpSource
|
||||||
|
import exh.md.handlers.serializers.ErrorResult
|
||||||
|
import exh.md.handlers.serializers.Language
|
||||||
|
import exh.md.handlers.serializers.MangaPlusResponse
|
||||||
|
import exh.md.handlers.serializers.MangaPlusSerializer
|
||||||
|
import exh.md.handlers.serializers.Popup
|
||||||
|
import exh.md.handlers.serializers.Title
|
||||||
|
import exh.source.DelegatedHttpSource
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
class MangaPlus(delegate: HttpSource, val context: Context) :
|
||||||
|
DelegatedHttpSource(delegate),
|
||||||
|
ConfigurableSource {
|
||||||
|
override val lang = delegate.lang
|
||||||
|
|
||||||
|
private val internalLang: String
|
||||||
|
get() = when (lang) {
|
||||||
|
"es" -> "esp"
|
||||||
|
else -> "eng"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val langCode: Language
|
||||||
|
get() = when (lang) {
|
||||||
|
"es" -> Language.SPANISH
|
||||||
|
else -> Language.ENGLISH
|
||||||
|
}
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val imageResolution: String
|
||||||
|
get() = preferences.getString("${RESOLUTION_PREF_KEY}_$lang", RESOLUTION_PREF_DEFAULT_VALUE)!!
|
||||||
|
|
||||||
|
private val splitImages: String
|
||||||
|
get() = if (preferences.getBoolean("${SPLIT_PREF_KEY}_$lang", SPLIT_PREF_DEFAULT_VALUE)) "yes" else "no"
|
||||||
|
|
||||||
|
private var titleList: List<Title>? = null
|
||||||
|
|
||||||
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||||
|
return client.newCall(mpPopularMangaRequest())
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
mpPopularMangaParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mpPopularMangaRequest(): Request {
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.set("Referer", "$baseUrl/manga_list/hot")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET("$API_URL/title_list/ranking", newHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mpPopularMangaParse(response: Response): MangasPage {
|
||||||
|
val result = response.asProto()
|
||||||
|
|
||||||
|
if (result.success == null) {
|
||||||
|
throw Exception(result.error!!.langPopup.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
titleList = result.success.titleRankingView!!.titles
|
||||||
|
.filter { it.language == langCode }
|
||||||
|
|
||||||
|
val mangas = titleList!!.map {
|
||||||
|
SManga.create().apply {
|
||||||
|
title = it.name
|
||||||
|
thumbnail_url = it.portraitImageUrl
|
||||||
|
url = "#/titles/${it.titleId}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(mangas, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||||
|
return client.newCall(mpLatestUpdatesRequest())
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
mpLatestUpdatesParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mpLatestUpdatesRequest(): Request {
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.set("Referer", "$baseUrl/updates")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET("$API_URL/web/web_home?lang=$internalLang", newHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mpLatestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val result = response.asProto()
|
||||||
|
|
||||||
|
if (result.success == null) {
|
||||||
|
throw Exception(result.error!!.langPopup.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all titles to get newer thumbnail urls at the interceptor.
|
||||||
|
val popularResponse = client.newCall(mpPopularMangaRequest()).execute().asProto()
|
||||||
|
|
||||||
|
if (popularResponse.success != null) {
|
||||||
|
titleList = popularResponse.success.titleRankingView!!.titles
|
||||||
|
.filter { it.language == langCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangas = result.success.webHomeView!!.groups
|
||||||
|
.flatMap { it.titles }
|
||||||
|
.mapNotNull { it.title }
|
||||||
|
.filter { it.language == langCode }
|
||||||
|
.map {
|
||||||
|
SManga.create().apply {
|
||||||
|
title = it.name
|
||||||
|
thumbnail_url = it.portraitImageUrl
|
||||||
|
url = "#/titles/${it.titleId}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.distinctBy { it.title }
|
||||||
|
|
||||||
|
return MangasPage(mangas, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
return client.newCall(mpSearchMangaRequest())
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
mpSearchMangaParse(response)
|
||||||
|
}
|
||||||
|
.map { MangasPage(it.mangas.filter { m -> m.title.contains(query, true) }, it.hasNextPage) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mpSearchMangaRequest(): Request {
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.set("Referer", "$baseUrl/manga_list/all")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET("$API_URL/title_list/all", newHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mpSearchMangaParse(response: Response): MangasPage {
|
||||||
|
val result = response.asProto()
|
||||||
|
|
||||||
|
if (result.success == null) {
|
||||||
|
throw Exception(result.error!!.langPopup.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
titleList = result.success.allTitlesView!!.titles
|
||||||
|
.filter { it.language == langCode }
|
||||||
|
|
||||||
|
val mangas = titleList!!.map {
|
||||||
|
SManga.create().apply {
|
||||||
|
title = it.name
|
||||||
|
thumbnail_url = it.portraitImageUrl
|
||||||
|
url = "#/titles/${it.titleId}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(mangas, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun titleDetailsRequest(manga: SManga): Request {
|
||||||
|
val titleId = manga.url.substringAfterLast("/")
|
||||||
|
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.set("Referer", "$baseUrl/titles/$titleId")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET("$API_URL/title_detail?title_id=$titleId", newHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround to allow "Open in browser" use the real URL.
|
||||||
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
|
return client.newCall(titleDetailsRequest(manga))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
mpMangaDetailsParse(response).apply { initialized = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mpMangaDetailsParse(response: Response): SManga {
|
||||||
|
val result = response.asProto()
|
||||||
|
|
||||||
|
if (result.success == null) {
|
||||||
|
throw Exception(result.error!!.langPopup.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
val details = result.success.titleDetailView!!
|
||||||
|
val title = details.title
|
||||||
|
val isCompleted = details.nonAppearanceInfo.contains(COMPLETE_REGEX)
|
||||||
|
|
||||||
|
return SManga.create().apply {
|
||||||
|
author = title.author.replace(" / ", ", ")
|
||||||
|
artist = author
|
||||||
|
description = details.overview + "\n\n" + details.viewingPeriodDescription
|
||||||
|
status = if (isCompleted) SManga.COMPLETED else SManga.ONGOING
|
||||||
|
thumbnail_url = title.portraitImageUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
return if (manga.status != SManga.LICENSED) {
|
||||||
|
client.newCall(chapterListRequest(manga))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
mpChapterListParse(response)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Observable.error(Exception("Licensed - No chapters to show"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga): Request = titleDetailsRequest(manga)
|
||||||
|
|
||||||
|
private fun mpChapterListParse(response: Response): List<SChapter> {
|
||||||
|
val result = response.asProto()
|
||||||
|
|
||||||
|
if (result.success == null) {
|
||||||
|
throw Exception(result.error!!.langPopup.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
val titleDetailView = result.success.titleDetailView!!
|
||||||
|
|
||||||
|
val chapters = titleDetailView.firstChapterList + titleDetailView.lastChapterList
|
||||||
|
|
||||||
|
return chapters.reversed()
|
||||||
|
// If the subTitle is null, then the chapter time expired.
|
||||||
|
.filter { it.subTitle != null }
|
||||||
|
.map {
|
||||||
|
SChapter.create().apply {
|
||||||
|
name = "${it.name} - ${it.subTitle}"
|
||||||
|
scanlator = "Shueisha"
|
||||||
|
date_upload = 1000L * it.startTimeStamp
|
||||||
|
url = "#/viewer/${it.chapterId}"
|
||||||
|
chapter_number = it.name.substringAfter("#").toFloatOrNull() ?: 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
|
return client.newCall(pageListRequest(chapter))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
mpPageListParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
val chapterId = chapter.url.substringAfterLast("/")
|
||||||
|
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.set("Referer", "$baseUrl/viewer/$chapterId")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val url = "$API_URL/manga_viewer".toHttpUrlOrNull()!!.newBuilder()
|
||||||
|
.addQueryParameter("chapter_id", chapterId)
|
||||||
|
.addQueryParameter("split", splitImages)
|
||||||
|
.addQueryParameter("img_quality", imageResolution)
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
return GET(url, newHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mpPageListParse(response: Response): List<Page> {
|
||||||
|
val result = response.asProto()
|
||||||
|
|
||||||
|
if (result.success == null) {
|
||||||
|
throw Exception(result.error!!.langPopup.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
val referer = response.request.header("Referer")!!
|
||||||
|
|
||||||
|
return result.success.mangaViewer!!.pages
|
||||||
|
.mapNotNull { it.page }
|
||||||
|
.mapIndexed { i, page ->
|
||||||
|
val encryptionKey = if (page.encryptionKey == null) "" else "&encryptionKey=${page.encryptionKey}"
|
||||||
|
Page(i, referer, "${page.imageUrl}$encryptionKey")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageRequest(page: Page): Request {
|
||||||
|
val newHeaders = headersBuilder()
|
||||||
|
.removeAll("Origin")
|
||||||
|
.set("Referer", page.url)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET(page.imageUrl!!, newHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
val resolutionPref = ListPreference(screen.context).apply {
|
||||||
|
key = "${RESOLUTION_PREF_KEY}_$lang"
|
||||||
|
title = RESOLUTION_PREF_TITLE
|
||||||
|
entries = RESOLUTION_PREF_ENTRIES
|
||||||
|
entryValues = RESOLUTION_PREF_ENTRY_VALUES
|
||||||
|
setDefaultValue(RESOLUTION_PREF_DEFAULT_VALUE)
|
||||||
|
summary = "%s"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val selected = newValue as String
|
||||||
|
val index = findIndexOfValue(selected)
|
||||||
|
val entry = entryValues[index] as String
|
||||||
|
preferences.edit().putString("${RESOLUTION_PREF_KEY}_$lang", entry).commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val splitPref = CheckBoxPreference(screen.context).apply {
|
||||||
|
key = "${SPLIT_PREF_KEY}_$lang"
|
||||||
|
title = SPLIT_PREF_TITLE
|
||||||
|
summary = SPLIT_PREF_SUMMARY
|
||||||
|
setDefaultValue(SPLIT_PREF_DEFAULT_VALUE)
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val checkValue = newValue as Boolean
|
||||||
|
preferences.edit().putBoolean("${SPLIT_PREF_KEY}_$lang", checkValue).commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
screen.addPreference(resolutionPref)
|
||||||
|
screen.addPreference(splitPref)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val ErrorResult.langPopup: Popup
|
||||||
|
get() = when (lang) {
|
||||||
|
"es" -> spanishPopup
|
||||||
|
else -> englishPopup
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Response.asProto(): MangaPlusResponse {
|
||||||
|
return ProtoBuf.decodeFromByteArray(MangaPlusSerializer, body!!.bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val API_URL = "https://jumpg-webapi.tokyo-cdn.com/api"
|
||||||
|
|
||||||
|
private const val RESOLUTION_PREF_KEY = "imageResolution"
|
||||||
|
private const val RESOLUTION_PREF_TITLE = "Image resolution"
|
||||||
|
private val RESOLUTION_PREF_ENTRIES = arrayOf("Low resolution", "Medium resolution", "High resolution")
|
||||||
|
private val RESOLUTION_PREF_ENTRY_VALUES = arrayOf("low", "high", "super_high")
|
||||||
|
private val RESOLUTION_PREF_DEFAULT_VALUE = RESOLUTION_PREF_ENTRY_VALUES[2]
|
||||||
|
|
||||||
|
private const val SPLIT_PREF_KEY = "splitImage"
|
||||||
|
private const val SPLIT_PREF_TITLE = "Split double pages"
|
||||||
|
private const val SPLIT_PREF_SUMMARY = "Not all titles support disabling this."
|
||||||
|
private const val SPLIT_PREF_DEFAULT_VALUE = true
|
||||||
|
|
||||||
|
private val COMPLETE_REGEX = "completado|complete".toRegex()
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
package exh.md.handlers
|
package exh.md.handlers
|
||||||
|
|
||||||
import MangaPlusSerializer
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import exh.md.handlers.serializers.MangaPlusSerializer
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
package exh.md.handlers.serializers
|
||||||
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Serializer
|
import kotlinx.serialization.Serializer
|
||||||
@ -64,6 +66,7 @@ data class TitleDetailView(
|
|||||||
@ProtoNumber(5) val nextTimeStamp: Int = 0,
|
@ProtoNumber(5) val nextTimeStamp: Int = 0,
|
||||||
@ProtoNumber(6) val updateTiming: UpdateTiming? = UpdateTiming.DAY,
|
@ProtoNumber(6) val updateTiming: UpdateTiming? = UpdateTiming.DAY,
|
||||||
@ProtoNumber(7) val viewingPeriodDescription: String = "",
|
@ProtoNumber(7) val viewingPeriodDescription: String = "",
|
||||||
|
@ProtoNumber(8) val nonAppearanceInfo: String = "",
|
||||||
@ProtoNumber(9) val firstChapterList: List<Chapter> = emptyList(),
|
@ProtoNumber(9) val firstChapterList: List<Chapter> = emptyList(),
|
||||||
@ProtoNumber(10) val lastChapterList: List<Chapter> = emptyList(),
|
@ProtoNumber(10) val lastChapterList: List<Chapter> = emptyList(),
|
||||||
@ProtoNumber(14) val isSimulReleased: Boolean = true,
|
@ProtoNumber(14) val isSimulReleased: Boolean = true,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user