Cubari: simplify url handling and search (#11216)
* migrate to keiyoushi.utils.parseAs * Refactor: Simplify URL handling - Pass URLs directly to search instead of pre-parsing in CubariUrlActivity - Remove custom `cubari:` prefix and parser - Handle direct URL searching in the main source file - Add support for Gist URLs - Remove redundant intent filters from AndroidManifest * bump and fix useragent * fix url * update useragent * trailing comma
This commit is contained in:
parent
775d39331e
commit
5a004d08f5
@ -32,19 +32,6 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="*.guya.moe" />
|
||||
<data android:host="guya.moe" />
|
||||
|
||||
<data
|
||||
android:pathPattern="/proxy/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<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="*.imgur.com" />
|
||||
<data android:host="imgur.com" />
|
||||
|
||||
|
||||
@ -20,6 +20,6 @@ If you've setup the Remote Storage via WebView the Recent tab shows your recent,
|
||||
You can visit the [Cubari](https://cubari.moe/) website for for more information.
|
||||
|
||||
### How do I add a gallery to Cubari?
|
||||
You can directly open a imgur or Cubari link in the extension.
|
||||
You can directly open a imgur or Cubari link in the extension or paste the url in cubari browse
|
||||
|
||||
[Uncomment this if needed]: <> (## Guides)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Cubari'
|
||||
extClass = '.CubariFactory'
|
||||
extVersionCode = 25
|
||||
extVersionCode = 26
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.extension.all.cubari
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.AppInfo
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservable
|
||||
@ -12,7 +12,7 @@ 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.json.Json
|
||||
import keiyoushi.utils.parseAs
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.boolean
|
||||
@ -20,23 +20,18 @@ import kotlinx.serialization.json.double
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
open class Cubari(override val lang: String) : HttpSource() {
|
||||
class Cubari(override val lang: String) : HttpSource() {
|
||||
|
||||
final override val name = "Cubari"
|
||||
override val name = "Cubari"
|
||||
|
||||
final override val baseUrl = "https://cubari.moe"
|
||||
override val baseUrl = "https://cubari.moe"
|
||||
|
||||
final override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor { chain ->
|
||||
@ -48,18 +43,17 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
}
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add(
|
||||
private val cubariHeaders = super.headersBuilder()
|
||||
.set(
|
||||
"User-Agent",
|
||||
"(Android ${Build.VERSION.RELEASE}; " +
|
||||
"${Build.MANUFACTURER} ${Build.MODEL}) " +
|
||||
"Tachiyomi/${AppInfo.getVersionName()} " +
|
||||
Build.ID,
|
||||
)
|
||||
}
|
||||
"Tachiyomi/${AppInfo.getVersionName()} ${Build.ID} " +
|
||||
"Keiyoushi",
|
||||
).build()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/", headers)
|
||||
return GET("$baseUrl/", cubariHeaders)
|
||||
}
|
||||
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
@ -72,12 +66,12 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val result = json.parseToJsonElement(response.body.string()).jsonArray
|
||||
val result = response.parseAs<JsonArray>()
|
||||
return parseMangaList(result, SortType.UNPINNED)
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/", headers)
|
||||
return GET("$baseUrl/", cubariHeaders)
|
||||
}
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
@ -90,19 +84,22 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val result = json.parseToJsonElement(response.body.string()).jsonArray
|
||||
val result = response.parseAs<JsonArray>()
|
||||
return parseMangaList(result, SortType.PINNED)
|
||||
}
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(chapterListRequest(manga))
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response -> mangaDetailsParse(response, manga) }
|
||||
}
|
||||
|
||||
// Called when the series is loaded, or when opening in browser
|
||||
override fun getMangaUrl(manga: SManga): String {
|
||||
return "$baseUrl${manga.url}"
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET("$baseUrl${manga.url}", headers)
|
||||
return chapterListRequest(manga)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
@ -110,7 +107,7 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
}
|
||||
|
||||
private fun mangaDetailsParse(response: Response, manga: SManga): SManga {
|
||||
val result = json.parseToJsonElement(response.body.string()).jsonObject
|
||||
val result = response.parseAs<JsonObject>()
|
||||
return parseManga(result, manga)
|
||||
}
|
||||
|
||||
@ -126,17 +123,16 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
val source = urlComponents[2]
|
||||
val slug = urlComponents[3]
|
||||
|
||||
return GET("$baseUrl/read/api/$source/series/$slug/", headers)
|
||||
return GET("$baseUrl/read/api/$source/series/$slug/", cubariHeaders)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
throw Exception("Unused")
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
// Called after the request
|
||||
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
|
||||
val res = response.body.string()
|
||||
return parseChapterList(res, manga)
|
||||
return parseChapterList(response, manga)
|
||||
}
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
@ -161,21 +157,20 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
return when {
|
||||
chapter.url.contains("/chapter/") -> {
|
||||
GET("$baseUrl${chapter.url}", headers)
|
||||
GET("$baseUrl${chapter.url}", cubariHeaders)
|
||||
}
|
||||
else -> {
|
||||
val url = chapter.url.split("/")
|
||||
val source = url[2]
|
||||
val slug = url[3]
|
||||
|
||||
GET("$baseUrl/read/api/$source/series/$slug/", headers)
|
||||
GET("$baseUrl/read/api/$source/series/$slug/", cubariHeaders)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun directPageListParse(response: Response): List<Page> {
|
||||
val res = response.body.string()
|
||||
val pages = json.parseToJsonElement(res).jsonArray
|
||||
val pages = response.parseAs<JsonArray>()
|
||||
|
||||
return pages.mapIndexed { i, jsonEl ->
|
||||
val page = if (jsonEl is JsonObject) {
|
||||
@ -189,7 +184,7 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
}
|
||||
|
||||
private fun seriesJsonPageListParse(response: Response, chapter: SChapter): List<Page> {
|
||||
val jsonObj = json.parseToJsonElement(response.body.string()).jsonObject
|
||||
val jsonObj = response.parseAs<JsonObject>()
|
||||
val groups = jsonObj["groups"]!!.jsonObject
|
||||
val groupMap = groups.entries.associateBy({ it.value.jsonPrimitive.content.ifEmpty { "default" } }, { it.key })
|
||||
val chapterScanlator = chapter.scanlator ?: "default" // workaround for "" as group causing NullPointerException (#13772)
|
||||
@ -222,23 +217,29 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
}
|
||||
}
|
||||
|
||||
// Stub
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
throw Exception("Unused")
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return when {
|
||||
query.startsWith(PROXY_PREFIX) -> {
|
||||
val trimmedQuery = query.removePrefix(PROXY_PREFIX)
|
||||
// handle direct links or old cubari:source/id format
|
||||
query.startsWith("https://") || query.startsWith("cubari:") -> {
|
||||
val (source, slug) = deepLinkHandler(query)
|
||||
// Only tag for recently read on search
|
||||
client.newBuilder()
|
||||
.addInterceptor(RemoteStorageUtils.TagInterceptor())
|
||||
.build()
|
||||
.newCall(proxySearchRequest(trimmedQuery))
|
||||
.newCall(GET("$baseUrl/read/api/$source/series/$slug/", cubariHeaders))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
proxySearchParse(response, trimmedQuery)
|
||||
val result = response.parseAs<JsonObject>()
|
||||
val manga = SManga.create().apply {
|
||||
url = "/read/$source/$slug"
|
||||
}
|
||||
val mangaList = listOf(parseManga(result, manga))
|
||||
|
||||
MangasPage(mangaList, false)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
@ -259,18 +260,57 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return GET("$baseUrl/", headers)
|
||||
return GET("$baseUrl/", cubariHeaders)
|
||||
}
|
||||
|
||||
private fun proxySearchRequest(query: String): Request {
|
||||
try {
|
||||
val queryFragments = query.split("/")
|
||||
val source = queryFragments[0]
|
||||
val slug = queryFragments[1]
|
||||
private fun deepLinkHandler(query: String): Pair<String, String> {
|
||||
return if (query.startsWith("cubari:")) { // legacy cubari:source/slug format
|
||||
val queryFragments = query.substringAfter("cubari:").split("/", limit = 2)
|
||||
queryFragments[0] to queryFragments[1]
|
||||
} else { // direct url searching
|
||||
val url = query.toHttpUrl()
|
||||
val host = url.host
|
||||
val pathSegments = url.pathSegments
|
||||
|
||||
return GET("$baseUrl/read/api/$source/series/$slug/", headers)
|
||||
} catch (e: Exception) {
|
||||
throw Exception(SEARCH_FALLBACK_MSG)
|
||||
if (
|
||||
host.endsWith("imgur.com") &&
|
||||
pathSegments.size >= 2 &&
|
||||
pathSegments[0] in listOf("a", "gallery")
|
||||
) {
|
||||
"imgur" to pathSegments[1]
|
||||
} else if (
|
||||
host.endsWith("reddit.com") &&
|
||||
pathSegments.size >= 2 &&
|
||||
pathSegments[0] == "gallery"
|
||||
) {
|
||||
"reddit" to pathSegments[1]
|
||||
} else if (
|
||||
host == "imgchest.com" &&
|
||||
pathSegments.size >= 2 &&
|
||||
pathSegments[0] == "p"
|
||||
) {
|
||||
"imgchest" to pathSegments[1]
|
||||
} else if (
|
||||
host.endsWith("catbox.moe") &&
|
||||
pathSegments.size >= 2 &&
|
||||
pathSegments[0] == "c"
|
||||
) {
|
||||
"catbox" to pathSegments[1]
|
||||
} else if (
|
||||
host.endsWith("cubari.moe") &&
|
||||
pathSegments.size >= 3
|
||||
) {
|
||||
pathSegments[1] to pathSegments[2]
|
||||
} else if (
|
||||
host.endsWith(".githubusercontent.com")
|
||||
) {
|
||||
val src = host.substringBefore(".")
|
||||
val path = url.encodedPath
|
||||
|
||||
"gist" to Base64.encodeToString("$src$path".toByteArray(), Base64.NO_PADDING)
|
||||
} else {
|
||||
throw Exception(SEARCH_FALLBACK_MSG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -279,7 +319,7 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
}
|
||||
|
||||
private fun searchMangaParse(response: Response, query: String): MangasPage {
|
||||
val result = json.parseToJsonElement(response.body.string()).jsonArray
|
||||
val result = response.parseAs<JsonArray>()
|
||||
|
||||
val filterList = result.asSequence()
|
||||
.map { it as JsonObject }
|
||||
@ -289,23 +329,14 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
return parseMangaList(JsonArray(filterList), SortType.ALL)
|
||||
}
|
||||
|
||||
private fun proxySearchParse(response: Response, query: String): MangasPage {
|
||||
val result = json.parseToJsonElement(response.body.string()).jsonObject
|
||||
return parseSearchList(result, query)
|
||||
}
|
||||
|
||||
// ------------- Helpers and whatnot ---------------
|
||||
|
||||
private val volumeNotSpecifiedTerms = setOf("Uncategorized", "null", "")
|
||||
|
||||
private fun parseChapterList(payload: String, manga: SManga): List<SChapter> {
|
||||
val jsonObj = json.parseToJsonElement(payload).jsonObject
|
||||
private fun parseChapterList(response: Response, manga: SManga): List<SChapter> {
|
||||
val jsonObj = response.parseAs<JsonObject>()
|
||||
val groups = jsonObj["groups"]!!.jsonObject
|
||||
val chapters = jsonObj["chapters"]!!.jsonObject
|
||||
val seriesSlug = jsonObj["slug"]!!.jsonPrimitive.content
|
||||
|
||||
val seriesPrefs = Injekt.get<Application>().getSharedPreferences("source_${id}_updateTime:$seriesSlug", 0)
|
||||
val seriesPrefsEditor = seriesPrefs.edit()
|
||||
|
||||
val chapterList = chapters.entries.flatMap { chapterEntry ->
|
||||
val chapterNum = chapterEntry.key
|
||||
@ -327,13 +358,7 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
date_upload = if (releaseDate != null) {
|
||||
releaseDate.jsonPrimitive.double.toLong() * 1000
|
||||
} else {
|
||||
val currentTimeMillis = System.currentTimeMillis()
|
||||
|
||||
if (!seriesPrefs.contains(chapterNum)) {
|
||||
seriesPrefsEditor.putLong(chapterNum, currentTimeMillis)
|
||||
}
|
||||
|
||||
seriesPrefs.getLong(chapterNum, currentTimeMillis)
|
||||
0L
|
||||
}
|
||||
|
||||
name = buildString {
|
||||
@ -351,8 +376,6 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
}
|
||||
}
|
||||
|
||||
seriesPrefsEditor.apply()
|
||||
|
||||
return chapterList.sortedByDescending { it.chapter_number }
|
||||
}
|
||||
|
||||
@ -375,16 +398,6 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
return MangasPage(mangaList, false)
|
||||
}
|
||||
|
||||
private fun parseSearchList(payload: JsonObject, query: String): MangasPage {
|
||||
val tempManga = SManga.create().apply {
|
||||
url = "/read/$query"
|
||||
}
|
||||
|
||||
val mangaList = listOf(parseManga(payload, tempManga))
|
||||
|
||||
return MangasPage(mangaList, false)
|
||||
}
|
||||
|
||||
private fun parseManga(jsonObj: JsonObject, mangaReference: SManga? = null): SManga =
|
||||
SManga.create().apply {
|
||||
title = jsonObj["title"]!!.jsonPrimitive.content
|
||||
@ -413,11 +426,10 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PROXY_PREFIX = "cubari:"
|
||||
const val AUTHOR_FALLBACK = "Unknown"
|
||||
const val ARTIST_FALLBACK = "Unknown"
|
||||
const val DESCRIPTION_FALLBACK = "No description."
|
||||
const val SEARCH_FALLBACK_MSG = "Unable to parse. Is your query in the format of $PROXY_PREFIX<source>/<slug>?"
|
||||
const val SEARCH_FALLBACK_MSG = "Please enter a valid Cubari URL"
|
||||
|
||||
enum class SortType {
|
||||
PINNED,
|
||||
|
||||
@ -11,59 +11,20 @@ class CubariUrlActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val host = intent?.data?.host
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
|
||||
if (host != null && pathSegments != null) {
|
||||
val query = with(host) {
|
||||
when {
|
||||
equals("m.imgur.com") || equals("imgur.com") -> fromSource("imgur", pathSegments)
|
||||
equals("m.reddit.com") || equals("reddit.com") || equals("www.reddit.com") -> fromSource("reddit", pathSegments)
|
||||
equals("imgchest.com") -> fromSource("imgchest", pathSegments)
|
||||
equals("catbox.moe") || equals("www.catbox.moe") -> fromSource("catbox", pathSegments)
|
||||
else -> fromCubari(pathSegments)
|
||||
}
|
||||
}
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", intent.data.toString())
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
if (query == null) {
|
||||
Log.e("CubariUrlActivity", "Unable to parse URI from intent $intent")
|
||||
finish()
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", query)
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("CubariUrlActivity", e.toString())
|
||||
}
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("CubariUrlActivity", "Unable to find activity", e)
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
private fun fromSource(source: String, pathSegments: List<String>): String? {
|
||||
if (pathSegments.size >= 2) {
|
||||
val id = pathSegments[1]
|
||||
|
||||
return "${Cubari.PROXY_PREFIX}$source/$id"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun fromCubari(pathSegments: MutableList<String>): String? {
|
||||
return if (pathSegments.size >= 3) {
|
||||
val source = pathSegments[1]
|
||||
val slug = pathSegments[2]
|
||||
"${Cubari.PROXY_PREFIX}$source/$slug"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user