Use MangaPlus JSON API instead of Protobuf. (#11326)

This commit is contained in:
Alessandro Jean 2022-04-04 08:43:15 -03:00 committed by GitHub
parent 29707fe4fd
commit 88fc45c2d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 108 additions and 104 deletions

View File

@ -22,6 +22,14 @@
android:host="www.mangaplus.shueisha.co.jp" android:host="www.mangaplus.shueisha.co.jp"
android:pathPattern="/titles/..*" android:pathPattern="/titles/..*"
android:scheme="https" /> android:scheme="https" />
<data
android:host="jumpg-webapi.tokyo-cdn.com"
android:path="/www/sns_share"
android:scheme="https" />
<data
android:host="www.jumpg-webapi.tokyo-cdn.com"
android:path="/www/sns_share"
android:scheme="https" />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>

View File

@ -6,7 +6,7 @@ ext {
extName = 'MANGA Plus by SHUEISHA' extName = 'MANGA Plus by SHUEISHA'
pkgNameSuffix = 'all.mangaplus' pkgNameSuffix = 'all.mangaplus'
extClass = '.MangaPlusFactory' extClass = '.MangaPlusFactory'
extVersionCode = 28 extVersionCode = 29
} }
dependencies { dependencies {

View File

@ -15,8 +15,8 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter 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 kotlinx.serialization.decodeFromByteArray import kotlinx.serialization.decodeFromString
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@ -29,6 +29,7 @@ import okhttp3.ResponseBody.Companion.toResponseBody
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.UUID import java.util.UUID
abstract class MangaPlus( abstract class MangaPlus(
@ -56,6 +57,8 @@ abstract class MangaPlus(
.addInterceptor(SpecificHostRateLimitInterceptor(baseUrl.toHttpUrl(), 2)) .addInterceptor(SpecificHostRateLimitInterceptor(baseUrl.toHttpUrl(), 2))
.build() .build()
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
@ -73,11 +76,11 @@ abstract class MangaPlus(
.set("Referer", "$baseUrl/manga_list/hot") .set("Referer", "$baseUrl/manga_list/hot")
.build() .build()
return GET("$API_URL/title_list/ranking", newHeaders) return GET("$API_URL/title_list/ranking?format=json", newHeaders)
} }
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val result = response.asProto() val result = response.asMangaPlusResponse()
if (result.success == null) if (result.success == null)
throw Exception(result.error!!.langPopup.body) throw Exception(result.error!!.langPopup.body)
@ -101,17 +104,18 @@ abstract class MangaPlus(
.set("Referer", "$baseUrl/updates") .set("Referer", "$baseUrl/updates")
.build() .build()
return GET("$API_URL/web/web_homeV3?lang=$internalLang", newHeaders) return GET("$API_URL/web/web_homeV3?lang=$internalLang&format=json", newHeaders)
} }
override fun latestUpdatesParse(response: Response): MangasPage { override fun latestUpdatesParse(response: Response): MangasPage {
val result = response.asProto() val result = response.asMangaPlusResponse()
if (result.success == null) if (result.success == null)
throw Exception(result.error!!.langPopup.body) throw Exception(result.error!!.langPopup.body)
// Fetch all titles to get newer thumbnail URLs in the interceptor. // Fetch all titles to get newer thumbnail URLs in the interceptor.
val popularResponse = client.newCall(popularMangaRequest(1)).execute().asProto() val popularResponse = client.newCall(popularMangaRequest(1)).execute()
.asMangaPlusResponse()
if (popularResponse.success != null) { if (popularResponse.success != null) {
titleList = popularResponse.success.titleRankingView!!.titles titleList = popularResponse.success.titleRankingView!!.titles
@ -143,7 +147,7 @@ abstract class MangaPlus(
} }
val filteredResult = it.mangas.filter { manga -> val filteredResult = it.mangas.filter { manga ->
manga.title.contains(query, true) manga.title.contains(query.trim(), true)
} }
MangasPage(filteredResult, it.hasNextPage) MangasPage(filteredResult, it.hasNextPage)
@ -159,11 +163,11 @@ abstract class MangaPlus(
.set("Referer", "$baseUrl/manga_list/all") .set("Referer", "$baseUrl/manga_list/all")
.build() .build()
return GET("$API_URL/title_list/allV2", newHeaders) return GET("$API_URL/title_list/allV2?format=json", newHeaders)
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
val result = response.asProto() val result = response.asMangaPlusResponse()
if (result.success == null) if (result.success == null)
throw Exception(result.error!!.langPopup.body) throw Exception(result.error!!.langPopup.body)
@ -206,7 +210,7 @@ abstract class MangaPlus(
.set("Referer", "$baseUrl/titles/$titleId") .set("Referer", "$baseUrl/titles/$titleId")
.build() .build()
return GET("$API_URL/title_detail?title_id=$titleId", newHeaders) return GET("$API_URL/title_detail?title_id=$titleId&format=json", newHeaders)
} }
// Workaround to allow "Open in browser" use the real URL. // Workaround to allow "Open in browser" use the real URL.
@ -224,7 +228,7 @@ abstract class MangaPlus(
} }
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val result = response.asProto() val result = response.asMangaPlusResponse()
if (result.success == null) if (result.success == null)
throw Exception(result.error!!.langPopup.body) throw Exception(result.error!!.langPopup.body)
@ -245,7 +249,7 @@ abstract class MangaPlus(
override fun chapterListRequest(manga: SManga): Request = titleDetailsRequest(manga.url) override fun chapterListRequest(manga: SManga): Request = titleDetailsRequest(manga.url)
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val result = response.asProto() val result = response.asMangaPlusResponse()
if (result.success == null) if (result.success == null)
throw Exception(result.error!!.langPopup.body) throw Exception(result.error!!.langPopup.body)
@ -278,13 +282,14 @@ abstract class MangaPlus(
.addQueryParameter("chapter_id", chapterId) .addQueryParameter("chapter_id", chapterId)
.addQueryParameter("split", if (splitImages) "yes" else "no") .addQueryParameter("split", if (splitImages) "yes" else "no")
.addQueryParameter("img_quality", imageQuality) .addQueryParameter("img_quality", imageQuality)
.addQueryParameter("format", "json")
.toString() .toString()
return GET(url, newHeaders) return GET(url, newHeaders)
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val result = response.asProto() val result = response.asMangaPlusResponse()
if (result.success == null) if (result.success == null)
throw Exception(result.error!!.langPopup.body) throw Exception(result.error!!.langPopup.body)
@ -292,7 +297,7 @@ abstract class MangaPlus(
val referer = response.request.header("Referer")!! val referer = response.request.header("Referer")!!
return result.success.mangaViewer!!.pages return result.success.mangaViewer!!.pages
.mapNotNull(MangaPlusPage::page) .mapNotNull(MangaPlusPage::mangaPage)
.mapIndexed { i, page -> .mapIndexed { i, page ->
val encryptionKey = if (page.encryptionKey == null) "" else val encryptionKey = if (page.encryptionKey == null) "" else
"&encryptionKey=${page.encryptionKey}" "&encryptionKey=${page.encryptionKey}"
@ -418,8 +423,8 @@ abstract class MangaPlus(
else -> englishPopup else -> englishPopup
} }
private fun Response.asProto(): MangaPlusResponse = use { private fun Response.asMangaPlusResponse(): MangaPlusResponse = use {
ProtoBuf.decodeFromByteArray(body!!.bytes()) json.decodeFromString(body!!.string())
} }
companion object { companion object {

View File

@ -1,66 +1,66 @@
package eu.kanade.tachiyomi.extension.all.mangaplus package eu.kanade.tachiyomi.extension.all.mangaplus
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable @Serializable
data class MangaPlusResponse( data class MangaPlusResponse(
@ProtoNumber(1) val success: SuccessResult? = null, val success: SuccessResult? = null,
@ProtoNumber(2) val error: ErrorResult? = null val error: ErrorResult? = null
) )
@Serializable @Serializable
data class ErrorResult( data class ErrorResult(
@ProtoNumber(2) val englishPopup: Popup, val englishPopup: Popup,
@ProtoNumber(3) val spanishPopup: Popup val spanishPopup: Popup
) )
@Serializable @Serializable
data class Popup( data class Popup(
@ProtoNumber(1) val subject: String, val subject: String,
@ProtoNumber(2) val body: String val body: String
) )
@Serializable @Serializable
data class SuccessResult( data class SuccessResult(
@ProtoNumber(1) val isFeaturedUpdated: Boolean? = false, val isFeaturedUpdated: Boolean? = false,
@ProtoNumber(6) val titleRankingView: TitleRankingView? = null, val titleRankingView: TitleRankingView? = null,
@ProtoNumber(8) val titleDetailView: TitleDetailView? = null, val titleDetailView: TitleDetailView? = null,
@ProtoNumber(10) val mangaViewer: MangaViewer? = null, val mangaViewer: MangaViewer? = null,
@ProtoNumber(25) val allTitlesViewV2: AllTitlesViewV2? = null, val allTitlesViewV2: AllTitlesViewV2? = null,
@ProtoNumber(31) val webHomeViewV3: WebHomeViewV3? = null val webHomeViewV3: WebHomeViewV3? = null
) )
@Serializable @Serializable
data class TitleRankingView(@ProtoNumber(1) val titles: List<Title> = emptyList()) data class TitleRankingView(val titles: List<Title> = emptyList())
@Serializable @Serializable
data class AllTitlesViewV2( data class AllTitlesViewV2(
@ProtoNumber(1) val allTitlesGroup: List<AllTitlesGroup> = emptyList() @SerialName("AllTitlesGroup") val allTitlesGroup: List<AllTitlesGroup> = emptyList()
) )
@Serializable @Serializable
data class AllTitlesGroup( data class AllTitlesGroup(
@ProtoNumber(1) val theTitle: String, val theTitle: String,
@ProtoNumber(2) val titles: List<Title> = emptyList() val titles: List<Title> = emptyList()
) )
@Serializable @Serializable
data class WebHomeViewV3(@ProtoNumber(2) val groups: List<UpdatedTitleV2Group> = emptyList()) data class WebHomeViewV3(val groups: List<UpdatedTitleV2Group> = emptyList())
@Serializable @Serializable
data class TitleDetailView( data class TitleDetailView(
@ProtoNumber(1) val title: Title, val title: Title,
@ProtoNumber(2) val titleImageUrl: String, val titleImageUrl: String,
@ProtoNumber(3) val overview: String, val overview: String,
@ProtoNumber(4) val backgroundImageUrl: String, val backgroundImageUrl: String,
@ProtoNumber(5) val nextTimeStamp: Int = 0, val nextTimeStamp: Int = 0,
@ProtoNumber(7) val viewingPeriodDescription: String = "", val viewingPeriodDescription: String = "",
@ProtoNumber(8) val nonAppearanceInfo: String = "", val nonAppearanceInfo: String = "",
@ProtoNumber(9) val firstChapterList: List<Chapter> = emptyList(), val firstChapterList: List<Chapter> = emptyList(),
@ProtoNumber(10) val lastChapterList: List<Chapter> = emptyList(), val lastChapterList: List<Chapter> = emptyList(),
@ProtoNumber(14) val isSimulReleased: Boolean = false, val isSimulReleased: Boolean = false,
@ProtoNumber(17) val chaptersDescending: Boolean = true val chaptersDescending: Boolean = true
) { ) {
private val isWebtoon: Boolean private val isWebtoon: Boolean
get() = firstChapterList.all(Chapter::isVerticalOnly) && get() = firstChapterList.all(Chapter::isVerticalOnly) &&
@ -89,76 +89,62 @@ data class TitleDetailView(
} }
@Serializable @Serializable
data class MangaViewer(@ProtoNumber(1) val pages: List<MangaPlusPage> = emptyList()) data class MangaViewer(val pages: List<MangaPlusPage> = emptyList())
@Serializable @Serializable
data class Title( data class Title(
@ProtoNumber(1) val titleId: Int, val titleId: Int,
@ProtoNumber(2) val name: String, val name: String,
@ProtoNumber(3) val author: String, val author: String,
@ProtoNumber(4) val portraitImageUrl: String, val portraitImageUrl: String,
@ProtoNumber(5) val landscapeImageUrl: String, val landscapeImageUrl: String,
@ProtoNumber(6) val viewCount: Int = 0, val viewCount: Int = 0,
@ProtoNumber(7) val language: Language? = Language.ENGLISH val language: Language? = Language.ENGLISH
) )
@Serializable enum class Language {
enum class Language(val id: Int) { ENGLISH,
@ProtoNumber(0) SPANISH,
ENGLISH(0), FRENCH,
INDONESIAN,
@ProtoNumber(1) PORTUGUESE_BR,
SPANISH(1), RUSSIAN,
THAI
@ProtoNumber(2)
FRENCH(2),
@ProtoNumber(3)
INDONESIAN(4),
@ProtoNumber(4)
PORTUGUESE_BR(4),
@ProtoNumber(5)
RUSSIAN(5),
@ProtoNumber(6)
THAI(6)
} }
@Serializable @Serializable
data class UpdatedTitleV2Group( data class UpdatedTitleV2Group(
@ProtoNumber(1) val groupName: String, val groupName: String,
@ProtoNumber(2) val titleGroups: List<OriginalTitleGroup> = emptyList() val titleGroups: List<OriginalTitleGroup> = emptyList()
) )
@Serializable @Serializable
data class OriginalTitleGroup( data class OriginalTitleGroup(
@ProtoNumber(1) val theTitle: String, val theTitle: String,
@ProtoNumber(3) val titles: List<UpdatedTitle> = emptyList() val titles: List<UpdatedTitle> = emptyList()
) )
@Serializable @Serializable
data class UpdatedTitle(@ProtoNumber(1) val title: Title) data class UpdatedTitle(val title: Title)
@Serializable @Serializable
data class Chapter( data class Chapter(
@ProtoNumber(1) val titleId: Int, val titleId: Int,
@ProtoNumber(2) val chapterId: Int, val chapterId: Int,
@ProtoNumber(3) val name: String, val name: String,
@ProtoNumber(4) val subTitle: String? = null, val subTitle: String? = null,
@ProtoNumber(6) val startTimeStamp: Int, val startTimeStamp: Int,
@ProtoNumber(7) val endTimeStamp: Int, val endTimeStamp: Int,
@ProtoNumber(9) val isVerticalOnly: Boolean = false val isVerticalOnly: Boolean = false
) )
@Serializable @Serializable
data class MangaPlusPage(@ProtoNumber(1) val page: MangaPage? = null) data class MangaPlusPage(val mangaPage: MangaPage? = null)
@Serializable @Serializable
data class MangaPage( data class MangaPage(
@ProtoNumber(1) val imageUrl: String, val imageUrl: String,
@ProtoNumber(2) val width: Int, val width: Int,
@ProtoNumber(3) val height: Int, val height: Int,
@ProtoNumber(5) val encryptionKey: String? = null val encryptionKey: String? = null
) )

View File

@ -14,18 +14,23 @@ class MangaPlusUrlActivity : Activity() {
val pathSegments = intent?.data?.pathSegments val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) { if (pathSegments != null && pathSegments.size > 1) {
val titleId = pathSegments[1] val titleId = if (!pathSegments[1].equals("sns_share")) pathSegments[1] else
intent?.data?.getQueryParameter("title_id")
val mainIntent = Intent().apply { if (titleId != null) {
action = "eu.kanade.tachiyomi.SEARCH" val mainIntent = Intent().apply {
putExtra("query", MangaPlus.PREFIX_ID_SEARCH + titleId) action = "eu.kanade.tachiyomi.SEARCH"
putExtra("filter", packageName) putExtra("query", MangaPlus.PREFIX_ID_SEARCH + titleId)
} putExtra("filter", packageName)
}
try { try {
startActivity(mainIntent) startActivity(mainIntent)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Log.e("MangaPlusUrlActivity", e.toString()) Log.e("MangaPlusUrlActivity", e.toString())
}
} else {
Log.e("MangaPlusUrlActivity", "Missing title ID from the URL")
} }
} else { } else {
Log.e("MangaPlusUrlActivity", "Could not parse URI from intent $intent") Log.e("MangaPlusUrlActivity", "Could not parse URI from intent $intent")