diff --git a/src/all/cubari/AndroidManifest.xml b/src/all/cubari/AndroidManifest.xml index 544187796..c03caeb49 100644 --- a/src/all/cubari/AndroidManifest.xml +++ b/src/all/cubari/AndroidManifest.xml @@ -32,19 +32,6 @@ - - - - - - - - - - - diff --git a/src/all/cubari/README.md b/src/all/cubari/README.md index d79b828ab..af45374ac 100644 --- a/src/all/cubari/README.md +++ b/src/all/cubari/README.md @@ -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) diff --git a/src/all/cubari/build.gradle b/src/all/cubari/build.gradle index d1593a0e3..225d80a12 100644 --- a/src/all/cubari/build.gradle +++ b/src/all/cubari/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Cubari' extClass = '.CubariFactory' - extVersionCode = 25 + extVersionCode = 26 } apply from: "$rootDir/common.gradle" diff --git a/src/all/cubari/src/eu/kanade/tachiyomi/extension/all/cubari/Cubari.kt b/src/all/cubari/src/eu/kanade/tachiyomi/extension/all/cubari/Cubari.kt index fb4f97b48..4b6add9d2 100644 --- a/src/all/cubari/src/eu/kanade/tachiyomi/extension/all/cubari/Cubari.kt +++ b/src/all/cubari/src/eu/kanade/tachiyomi/extension/all/cubari/Cubari.kt @@ -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 { @@ -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() 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 { @@ -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() return parseMangaList(result, SortType.PINNED) } override fun fetchMangaDetails(manga: SManga): Observable { - 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() 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 { - throw Exception("Unused") + throw UnsupportedOperationException() } // Called after the request private fun chapterListParse(response: Response, manga: SManga): List { - val res = response.body.string() - return parseChapterList(res, manga) + return parseChapterList(response, manga) } override fun fetchPageList(chapter: SChapter): Observable> { @@ -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 { - val res = response.body.string() - val pages = json.parseToJsonElement(res).jsonArray + val pages = response.parseAs() 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 { - val jsonObj = json.parseToJsonElement(response.body.string()).jsonObject + val jsonObj = response.parseAs() 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 { - throw Exception("Unused") + throw UnsupportedOperationException() } override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { 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() + 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 { + 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() 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 { - val jsonObj = json.parseToJsonElement(payload).jsonObject + private fun parseChapterList(response: Response, manga: SManga): List { + val jsonObj = response.parseAs() val groups = jsonObj["groups"]!!.jsonObject val chapters = jsonObj["chapters"]!!.jsonObject - val seriesSlug = jsonObj["slug"]!!.jsonPrimitive.content - - val seriesPrefs = Injekt.get().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/?" + const val SEARCH_FALLBACK_MSG = "Please enter a valid Cubari URL" enum class SortType { PINNED, diff --git a/src/all/cubari/src/eu/kanade/tachiyomi/extension/all/cubari/CubariUrlActivity.kt b/src/all/cubari/src/eu/kanade/tachiyomi/extension/all/cubari/CubariUrlActivity.kt index aafb34e65..aaae83920 100644 --- a/src/all/cubari/src/eu/kanade/tachiyomi/extension/all/cubari/CubariUrlActivity.kt +++ b/src/all/cubari/src/eu/kanade/tachiyomi/extension/all/cubari/CubariUrlActivity.kt @@ -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? { - if (pathSegments.size >= 2) { - val id = pathSegments[1] - - return "${Cubari.PROXY_PREFIX}$source/$id" - } - return null - } - - private fun fromCubari(pathSegments: MutableList): String? { - return if (pathSegments.size >= 3) { - val source = pathSegments[1] - val slug = pathSegments[2] - "${Cubari.PROXY_PREFIX}$source/$slug" - } else { - null - } - } }