Add Manga UP! as a new source. (#12737)

This commit is contained in:
Alessandro Jean 2022-07-25 16:06:45 -03:00 committed by GitHub
parent 62351d69de
commit 871e8b7838
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 404 additions and 0 deletions

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.extension">
<application>
<activity
android:name=".all.mangaup.MangaUpUrlActivity"
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="global.manga-up.com"
android:pathPattern="/manga/..*"
android:scheme="https" />
<data
android:host="www.global.manga-up.com"
android:pathPattern="/manga/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

16
src/all/mangaup/README.md Normal file
View File

@ -0,0 +1,16 @@
# Manga UP!
Table of Content
- [FAQ](#FAQ)
- [Why chapters are missing in some titles?](#why-chapters-are-missing-in-some-titles)
## FAQ
### Why chapters are missing in some titles?
Manga UP! have series with paid chapters. These will be filtered out from the chapter
list by default, even if you have bought then before. To read these chapters, use their
official app to purchase with their coin system and read there.
To check if a chapter is paid, open the WebView and check if the chapter thumbnail is
grayed out with a "Exclusive on app" warning or has an "Advanced" badge.

View File

@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Manga UP!'
pkgNameSuffix = 'all.mangaup'
extClass = '.MangaUpFactory'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -0,0 +1,194 @@
package eu.kanade.tachiyomi.extension.all.mangaup
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.closeQuietly
import rx.Observable
import uy.kohesive.injekt.injectLazy
class MangaUp(override val lang: String) : HttpSource() {
override val name = "Manga UP!"
override val baseUrl = "https://global.manga-up.com"
override val supportsLatest = false
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Origin", baseUrl)
.add("Referer", baseUrl)
.add("User-Agent", USER_AGENT)
override val client: OkHttpClient = network.client.newBuilder()
.addInterceptor(::thumbnailIntercept)
.rateLimitHost(API_URL.toHttpUrl(), 1)
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
private val json: Json by injectLazy()
private var titleList: List<MangaUpTitle>? = null
override fun popularMangaRequest(page: Int): Request {
return GET("$API_URL/search?format=json", headers)
}
override fun popularMangaParse(response: Response): MangasPage {
titleList = response.parseAs<MangaUpSearch>().titles
val titles = titleList!!
.sortedByDescending { it.bookmarkCount ?: 0 }
.map(MangaUpTitle::toSManga)
return MangasPage(titles, hasNextPage = false)
}
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException("Not used")
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException("Not used")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.startsWith(PREFIX_ID_SEARCH) && query.matches(ID_SEARCH_PATTERN)) {
return titleDetailsRequest(query.removePrefix(PREFIX_ID_SEARCH))
}
val apiUrl = "$API_URL/manga/search".toHttpUrl().newBuilder()
.addQueryParameter("word", query)
.addQueryParameter("format", "json")
.toString()
return GET(apiUrl, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
if (response.request.url.toString().contains("manga/detail")) {
val titleId = response.request.url.queryParameter("title_id")!!
val title = response.parseAs<MangaUpTitle>().toSManga().apply {
url = "/manga/$titleId"
}
return MangasPage(listOf(title), hasNextPage = false)
}
val titles = response.parseAs<MangaUpSearch>().titles
val query = response.request.url.queryParameter("word")
if (query.isNullOrEmpty()) {
fetchAllTitles()
} else {
titleList = titles
}
return MangasPage(titles.map(MangaUpTitle::toSManga), hasNextPage = false)
}
private fun titleDetailsRequest(mangaUrl: String): Request {
val titleId = mangaUrl.substringAfterLast("/")
val apiUrl = "$API_URL/manga/detail".toHttpUrl().newBuilder()
.addQueryParameter("title_id", titleId)
.addQueryParameter("ui_lang", lang)
.addQueryParameter("format", "json")
.toString()
return GET(apiUrl, headers)
}
// Workaround to allow "Open in browser" use the real URL.
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(titleDetailsRequest(manga.url))
.asObservableSuccess()
.map { response ->
mangaDetailsParse(response).apply { initialized = true }
}
}
override fun mangaDetailsParse(response: Response): SManga {
return response.parseAs<MangaUpTitle>().toSManga()
}
override fun chapterListRequest(manga: SManga): Request = titleDetailsRequest(manga.url)
override fun chapterListParse(response: Response): List<SChapter> {
val titleId = response.request.url.queryParameter("title_id")!!.toInt()
return response.parseAs<MangaUpTitle>().readableChapters
.map { it.toSChapter(titleId) }
}
override fun pageListRequest(chapter: SChapter): Request {
val chapterId = chapter.url.substringAfterLast("/")
return GET("$API_URL/manga/viewer?chapter_id=$chapterId&format=json", headers)
}
override fun pageListParse(response: Response): List<Page> {
return response.parseAs<MangaUpViewer>().pages
.mapIndexed { i, page -> Page(i, "", page.imageUrl) }
}
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
override fun imageUrlParse(response: Response): String = ""
// Fetch all titles to get newer thumbnail URLs in the interceptor.
private fun fetchAllTitles() = runCatching {
val popularResponse = client.newCall(popularMangaRequest(1)).execute()
titleList = popularResponse.parseAs<MangaUpSearch>().titles
}
private fun thumbnailIntercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if (response.code == 401 && request.url.toString().contains(TITLE_THUMBNAIL_PATH)) {
val titleId = request.url.toString()
.substringAfter("/$TITLE_THUMBNAIL_PATH/")
.substringBefore(".webp")
.toInt()
val title = titleList?.find { it.id == titleId } ?: return response
val thumbnailUrl = title.mainThumbnailUrl
?: title.thumbnailUrl
?: return response
response.closeQuietly()
val thumbnailRequest = GET(thumbnailUrl, request.headers)
return chain.proceed(thumbnailRequest)
}
return response
}
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromString(body?.string().orEmpty())
}
companion object {
private const val API_URL = "https://global-web-api.manga-up.com/api"
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"
private const val TITLE_THUMBNAIL_PATH = "manga_list"
const val PREFIX_ID_SEARCH = "id:"
private val ID_SEARCH_PATTERN = "^id:(\\d+)$".toRegex()
}
}

View File

@ -0,0 +1,97 @@
package eu.kanade.tachiyomi.extension.all.mangaup
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.util.Date
@Serializable
data class MangaUpSearch(
val titles: List<MangaUpTitle> = emptyList()
)
@Serializable
data class MangaUpViewer(
val pages: List<MangaUpPage> = emptyList()
)
@Serializable
data class MangaUpTitle(
@SerialName("titleId") val id: Int? = null,
@SerialName("titleName") val name: String,
val authorName: String? = null,
val description: String? = null,
val copyright: String? = null,
val thumbnailUrl: String? = null,
val mainThumbnailUrl: String? = null,
val bookmarkCount: Int? = null,
val genres: List<MangaUpGenre> = emptyList(),
val chapters: List<MangaUpChapter> = emptyList()
) {
private val fullDescription: String
get() = buildString {
description?.let { append(it) }
copyright?.let { append("\n\n" + it.replace("(C)", "© ")) }
}
private val isFinished: Boolean
get() = chapters.any { it.mainName.contains("final chapter", ignoreCase = true) }
val readableChapters: List<MangaUpChapter>
get() = chapters.filter(MangaUpChapter::isReadable).reversed()
fun toSManga(): SManga = SManga.create().apply {
title = name
author = authorName
description = fullDescription.trim()
genre = genres.joinToString { it.name }
status = if (isFinished) SManga.COMPLETED else SManga.ONGOING
thumbnail_url = mainThumbnailUrl ?: thumbnailUrl
url = "/manga/$id"
}
}
@Serializable
data class MangaUpGenre(
val id: Int,
val name: String
)
@Serializable
data class MangaUpChapter(
val id: Int,
val mainName: String,
val subName: String? = null,
val price: Int? = null,
val published: Int,
val badge: MangaUpBadge = MangaUpBadge.FREE,
val available: Boolean = false
) {
val isReadable: Boolean
get() = badge == MangaUpBadge.FREE && available
fun toSChapter(titleId: Int): SChapter = SChapter.create().apply {
name = mainName.replace(WRONG_SPACING_REGEX, "-$1") +
if (!subName.isNullOrEmpty()) ": $subName" else ""
date_upload = (published * 1000L).takeIf { it <= Date().time } ?: 0L
url = "/manga/$titleId/$id"
}
companion object {
private val WRONG_SPACING_REGEX = "\\s+-(\\d+)$".toRegex()
}
}
enum class MangaUpBadge {
FREE,
ADVANCE,
UPDATE
}
@Serializable
data class MangaUpPage(
val imageUrl: String
)

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.extension.all.mangaup
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class MangaUpFactory : SourceFactory {
/**
* They only have English for now, but the website does have a
* language selector and the API also supports that.
*
* Probably it's something they will add in the future, so better
* to already make the extension a multilang to avoid users having
* to migrate to an All extension after.
*/
override fun createSources(): List<Source> = listOf(MangaUp("en"))
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.extension.all.mangaup
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 MangaUpUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val query = pathSegments[1]
if (query != null) {
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", MangaUp.PREFIX_ID_SEARCH + query)
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("MangaPlusUrlActivity", e.toString())
}
} else {
Log.e("MangaUpUrlActivity", "Missing the title ID from the URL")
}
} else {
Log.e("MangaUpUrlActivity", "Could not parse URI from intent $intent")
}
finish()
exitProcess(0)
}
}