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:
AwkwardPeak7 2025-10-26 12:03:15 +05:00 committed by Draff
parent 775d39331e
commit 5a004d08f5
Signed by: Draff
GPG Key ID: E8A89F3211677653
5 changed files with 106 additions and 146 deletions

View File

@ -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" />

View File

@ -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)

View File

@ -1,7 +1,7 @@
ext {
extName = 'Cubari'
extClass = '.CubariFactory'
extVersionCode = 25
extVersionCode = 26
}
apply from: "$rootDir/common.gradle"

View File

@ -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,

View File

@ -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
}
}
}