From a7408115ed69a2d34a5818214204e2a21537d095 Mon Sep 17 00:00:00 2001 From: manti <133025162+manti-X@users.noreply.github.com> Date: Fri, 19 Sep 2025 08:51:03 +0200 Subject: [PATCH] Add K Manga (#10498) * Add K Manga * apply fixes, different and consistent thumbnail, better search * simplify * manifest, deeplink * exception message --- src/en/kmanga/AndroidManifest.xml | 22 + src/en/kmanga/build.gradle | 8 + src/en/kmanga/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4208 bytes src/en/kmanga/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2429 bytes .../kmanga/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 5689 bytes .../kmanga/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 10677 bytes .../kmanga/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 17353 bytes .../tachiyomi/extension/en/kmanga/Dto.kt | 64 +++ .../extension/en/kmanga/ImageInterceptor.kt | 95 +++++ .../tachiyomi/extension/en/kmanga/KManga.kt | 394 ++++++++++++++++++ .../extension/en/kmanga/KMangaUrlActivity.kt | 34 ++ 11 files changed, 617 insertions(+) create mode 100644 src/en/kmanga/AndroidManifest.xml create mode 100644 src/en/kmanga/build.gradle create mode 100644 src/en/kmanga/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/en/kmanga/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/en/kmanga/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/en/kmanga/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/en/kmanga/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/en/kmanga/src/eu/kanade/tachiyomi/extension/en/kmanga/Dto.kt create mode 100644 src/en/kmanga/src/eu/kanade/tachiyomi/extension/en/kmanga/ImageInterceptor.kt create mode 100644 src/en/kmanga/src/eu/kanade/tachiyomi/extension/en/kmanga/KManga.kt create mode 100644 src/en/kmanga/src/eu/kanade/tachiyomi/extension/en/kmanga/KMangaUrlActivity.kt diff --git a/src/en/kmanga/AndroidManifest.xml b/src/en/kmanga/AndroidManifest.xml new file mode 100644 index 000000000..add22611a --- /dev/null +++ b/src/en/kmanga/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/en/kmanga/build.gradle b/src/en/kmanga/build.gradle new file mode 100644 index 000000000..db9216258 --- /dev/null +++ b/src/en/kmanga/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'K Manga' + extClass = '.KManga' + extVersionCode = 1 + isNsfw = false +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/kmanga/res/mipmap-hdpi/ic_launcher.png b/src/en/kmanga/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4e7de12f2da3f2a3638369ec588aa3ba6c24701d GIT binary patch literal 4208 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 literal 0 HcmV?d00001 diff --git a/src/en/kmanga/res/mipmap-xhdpi/ic_launcher.png b/src/en/kmanga/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a7fdeca4c9d20d58da98e251acdda1f56deaf0a0 GIT binary patch literal 5689 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 literal 0 HcmV?d00001 diff --git a/src/en/kmanga/res/mipmap-xxhdpi/ic_launcher.png b/src/en/kmanga/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4f1c1d421b2fd38d15dec77931d7ba034c9952eb GIT binary patch literal 10677 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

Jk`zd@ef!Aw2s8v-P|76`J@+*XAldx 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, +) + +@Serializable +class RankingTitleId( + val id: Int, +) + +@Serializable +class TitleListResponse( + @SerialName("title_list") val titleList: List, +) + +@Serializable +class TitleDetail( + @SerialName("title_id") val titleId: Int, + @SerialName("title_name") val titleName: String, + @SerialName("thumbnail_image_url") val thumbnailImageUrl: String? = null, + @SerialName("banner_image_url") val bannerImageUrl: String? = null, +) + +@Serializable +class BirthdayCookie(val value: String, val expires: Long) + +@Serializable +class EpisodeListResponse( + @SerialName("episode_list") val episodeList: List, +) + +@Serializable +class Episode( + @SerialName("episode_id") val episodeId: Int, + @SerialName("episode_name") val episodeName: String, + @SerialName("start_time") val startTime: String, + val point: Int, + @SerialName("title_id") val titleId: Int, + val badge: Int, + @SerialName("rental_finish_time") val rentalFinishTime: String? = null, +) + +@Serializable +class ViewerApiResponse( + @SerialName("page_list") val pageList: List, + @SerialName("scramble_seed") val scrambleSeed: Long, +) + +@Serializable +class SearchApiResponse( + @SerialName("title_list") val titleList: List, +) + +@Serializable +class SearchTitleDetail( + @SerialName("title_id") val titleId: Int, + @SerialName("title_name") val titleName: String, + @SerialName("thumbnail_image_url") val thumbnailImageUrl: String? = null, +) diff --git a/src/en/kmanga/src/eu/kanade/tachiyomi/extension/en/kmanga/ImageInterceptor.kt b/src/en/kmanga/src/eu/kanade/tachiyomi/extension/en/kmanga/ImageInterceptor.kt new file mode 100644 index 000000000..ab4257950 --- /dev/null +++ b/src/en/kmanga/src/eu/kanade/tachiyomi/extension/en/kmanga/ImageInterceptor.kt @@ -0,0 +1,95 @@ +package eu.kanade.tachiyomi.extension.en.kmanga + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Rect +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Response +import okhttp3.ResponseBody +import okhttp3.ResponseBody.Companion.toResponseBody +import java.io.ByteArrayOutputStream + +// https://greasyfork.org/en/scripts/467901-k-manga-ripper +class ImageInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val fragment = request.url.fragment + if (fragment.isNullOrEmpty() || !fragment.startsWith("scramble_seed=")) { + return chain.proceed(request) + } + + val seed = fragment.substringAfter("scramble_seed=").toLong() + val response = chain.proceed(request) + val descrambledBody = descrambleImage(response.body, seed) + + return response.newBuilder().body(descrambledBody).build() + } + + private class Coord(val x: Int, val y: Int) + private class CoordPair(val source: Coord, val dest: Coord) + + private fun xorshift32(seed: UInt): UInt { + var n = seed + n = n xor (n shl 13) + n = n xor (n shr 17) + n = n xor (n shl 5) + return n + } + + private fun getUnscrambledCoords(seed: Long): List { + var seed32 = seed.toUInt() + val pairs = mutableListOf>() + + for (i in 0 until 16) { + seed32 = xorshift32(seed32) + pairs.add(seed32 to i) + } + + pairs.sortBy { it.first } + val sortedVal = pairs.map { it.second } + + return sortedVal.mapIndexed { i, e -> + CoordPair( + source = Coord(x = e % 4, y = e / 4), + dest = Coord(x = i % 4, y = i / 4), + ) + } + } + + private fun descrambleImage(responseBody: ResponseBody, seed: Long): ResponseBody { + val unscrambledCoords = getUnscrambledCoords(seed) + val originalBitmap = BitmapFactory.decodeStream(responseBody.byteStream()) + ?: throw Exception("Failed to decode image stream") + + val originalWidth = originalBitmap.width + val originalHeight = originalBitmap.height + + val descrambledBitmap = Bitmap.createBitmap(originalWidth, originalHeight, originalBitmap.config) + val canvas = Canvas(descrambledBitmap) + + val getTileDimension = { size: Int -> (size / 8 * 8) / 4 } + val tileWidth = getTileDimension(originalWidth) + val tileHeight = getTileDimension(originalHeight) + + unscrambledCoords.forEach { coord -> + val sx = coord.source.x * tileWidth + val sy = coord.source.y * tileHeight + val dx = coord.dest.x * tileWidth + val dy = coord.dest.y * tileHeight + + val srcRect = Rect(sx, sy, sx + tileWidth, sy + tileHeight) + val destRect = Rect(dx, dy, dx + tileWidth, dy + tileHeight) + + canvas.drawBitmap(originalBitmap, srcRect, destRect, null) + } + originalBitmap.recycle() + + val outputStream = ByteArrayOutputStream() + descrambledBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream) + descrambledBitmap.recycle() + + return outputStream.toByteArray().toResponseBody("image/jpeg".toMediaType()) + } +} diff --git a/src/en/kmanga/src/eu/kanade/tachiyomi/extension/en/kmanga/KManga.kt b/src/en/kmanga/src/eu/kanade/tachiyomi/extension/en/kmanga/KManga.kt new file mode 100644 index 000000000..5446fb077 --- /dev/null +++ b/src/en/kmanga/src/eu/kanade/tachiyomi/extension/en/kmanga/KManga.kt @@ -0,0 +1,394 @@ +package eu.kanade.tachiyomi.extension.en.kmanga + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.asObservable +import eu.kanade.tachiyomi.network.interceptor.rateLimitHost +import eu.kanade.tachiyomi.source.model.Filter +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 eu.kanade.tachiyomi.util.asJsoup +import keiyoushi.utils.firstInstance +import keiyoushi.utils.parseAs +import keiyoushi.utils.tryParse +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.FormBody +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okio.ByteString.Companion.encodeUtf8 +import rx.Observable +import java.net.URLDecoder +import java.text.SimpleDateFormat +import java.util.Locale + +class KManga : HttpSource() { + + override val name = "K Manga" + override val baseUrl = "https://kmanga.kodansha.com" + override val lang = "en" + override val supportsLatest = false + + private val apiUrl = "https://api.kmanga.kodansha.com" + private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) + private val pageLimit = 25 + private val searchLimit = 25 + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + .add("X-Requested-With", "XMLHttpRequest") + .add("x-kmanga-platform", "3") + + override val client: OkHttpClient = network.cloudflareClient.newBuilder() + .addInterceptor(ImageInterceptor()) + .rateLimitHost(apiUrl.toHttpUrl(), 1) + .build() + + // Popular + override fun popularMangaRequest(page: Int): Request { + val offset = (page - 1) * pageLimit + val url = apiUrl.toHttpUrl().newBuilder() + .addPathSegments("ranking/all") + .addQueryParameter("ranking_id", "12") + .addQueryParameter("offset", offset.toString()) + .addQueryParameter("limit", (pageLimit + 1).toString()) + .build() + + return hashedGet(url) + } + + override fun popularMangaParse(response: Response): MangasPage { + val rankingResult = response.parseAs() + val titleIds = rankingResult.rankingTitleList.map { it.id.toString() } + + if (titleIds.isEmpty()) { + return MangasPage(emptyList(), false) + } + + val hasNextPage = titleIds.size > pageLimit + val mangaIdsToFetch = if (hasNextPage) titleIds.dropLast(1) else titleIds + + val detailsUrl = apiUrl.toHttpUrl().newBuilder() + .addPathSegments("title/list") + .addQueryParameter("title_id_list", mangaIdsToFetch.joinToString(",")) + .build() + + val detailsRequest = hashedGet(detailsUrl) + val detailsResponse = client.newCall(detailsRequest).execute() + + if (!detailsResponse.isSuccessful) { + throw Exception("Failed to fetch title details: ${detailsResponse.code} - ${detailsResponse.body.string()}") + } + + val detailsResult = detailsResponse.parseAs() + val mangas = detailsResult.titleList.map { manga -> + SManga.create().apply { + url = "/title/${manga.titleId}" + title = manga.titleName + thumbnail_url = manga.thumbnailImageUrl ?: manga.bannerImageUrl + } + } + return MangasPage(mangas, hasNextPage) + } + + // Search + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return if (query.startsWith(PREFIX_SEARCH)) { + val titleId = query.removePrefix(PREFIX_SEARCH) + fetchMangaDetails( + SManga.create().apply { url = "/title/$titleId" }, + ).map { manga -> + MangasPage(listOf(manga.apply { url = "/title/$titleId" }), false) + } + } else { + super.fetchSearchManga(page, query, filters) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val genreFilter = filters.firstInstance() + + if (query.isNotBlank()) { + val offset = (page - 1) * searchLimit + val url = apiUrl.toHttpUrl().newBuilder() + .addPathSegments("search/title") + .addQueryParameter("keyword", query) + .addQueryParameter("offset", offset.toString()) + .addQueryParameter("limit", searchLimit.toString()) + .build() + return hashedGet(url) + } + + if (genreFilter.state != 0) { + val url = baseUrl.toHttpUrl().newBuilder() + .addPathSegments(genreFilter.toUriPart().removePrefix("/")) + .build() + return GET(url, headers) + } + return popularMangaRequest(page) + } + + override fun searchMangaParse(response: Response): MangasPage { + if (response.request.url.host.contains("api.")) { + val result = response.parseAs() + val mangas = result.titleList.map { manga -> + SManga.create().apply { + url = "/title/${manga.titleId}" + title = manga.titleName + thumbnail_url = manga.thumbnailImageUrl + } + } + return MangasPage(mangas, mangas.size >= searchLimit) + } + + if (response.request.url.toString().contains("/search/genre/")) { + val document = response.asJsoup() + val nuxtData = document.selectFirst("script#__NUXT_DATA__")?.data() + ?: return MangasPage(emptyList(), false) + + val rootArray = nuxtData.parseAs() + fun resolve(ref: JsonElement): JsonElement = rootArray[ref.jsonPrimitive.content.toInt()] + + val genreResultObject = rootArray.firstOrNull { it is JsonObject && "title_list" in it.jsonObject } + ?: return MangasPage(emptyList(), false) + + val mangaRefs = resolve(genreResultObject.jsonObject["title_list"]!!).jsonArray + + val mangas = mangaRefs.map { ref -> + val mangaObject = resolve(ref).jsonObject + SManga.create().apply { + url = "/title/${resolve(mangaObject["title_id"]!!).jsonPrimitive.content}" + title = resolve(mangaObject["title_name"]!!).jsonPrimitive.content + thumbnail_url = mangaObject["thumbnail_image_url"]?.let { resolve(it).jsonPrimitive.content } + } + } + return MangasPage(mangas, false) + } + return popularMangaParse(response) + } + + // Details + override fun mangaDetailsParse(response: Response): SManga { + val document = response.asJsoup() + val nuxtData = document.selectFirst("script#__NUXT_DATA__")?.data() + ?: throw Exception("Could not find Nuxt data") + + val rootArray = nuxtData.parseAs() + fun resolve(ref: JsonElement): JsonElement = rootArray[ref.jsonPrimitive.content.toInt()] + + val titleDetailsObject = rootArray.first { it is JsonObject && it.jsonObject.containsKey("title_in_japanese") }.jsonObject + + val genreMap = buildMap { + rootArray.forEach { element -> + if (element is JsonObject && element.jsonObject.containsKey("genre_id")) { + val genreObject = element.jsonObject + val id = resolve(genreObject["genre_id"]!!).jsonPrimitive.content.toInt() + val name = resolve(genreObject["genre_name"]!!).jsonPrimitive.content + put(id, name) + } + } + } + + return SManga.create().apply { + title = resolve(titleDetailsObject["title_name"]!!).jsonPrimitive.content + author = resolve(titleDetailsObject["author_text"]!!).jsonPrimitive.content + description = resolve(titleDetailsObject["introduction_text"]!!).jsonPrimitive.content + thumbnail_url = titleDetailsObject["thumbnail_image_url"]?.let { resolve(it).jsonPrimitive.content } + val genreIdRefs = resolve(titleDetailsObject["genre_id_list"]!!).jsonArray + val genreIds = genreIdRefs.map { resolve(it).jsonPrimitive.content.toInt() } + genre = genreIds.mapNotNull { genreMap[it] }.joinToString() + } + } + + // Chapters + override fun chapterListRequest(manga: SManga): Request { + return GET(baseUrl + manga.url, headers) + } + + override fun chapterListParse(response: Response): List { + val document = response.asJsoup() + val nuxtData = document.selectFirst("script#__NUXT_DATA__")?.data() + ?: throw Exception("Could not find Nuxt data") + + val rootArray = nuxtData.parseAs() + fun resolve(ref: JsonElement): JsonElement = rootArray[ref.jsonPrimitive.content.toInt()] + + val titleDetailsObject = rootArray.first { it is JsonObject && it.jsonObject.containsKey("episode_id_list") }.jsonObject + val episodeIdRefs = resolve(titleDetailsObject["episode_id_list"]!!).jsonArray + val episodeIds = episodeIdRefs.map { resolve(it).jsonPrimitive.content } + + val (birthday, expires) = getBirthdayCookie(response.request.url) + + val formBody = FormBody.Builder() + .add("episode_id_list", episodeIds.joinToString(",")) + .build() + + val params = (0 until formBody.size).associate { formBody.name(it) to formBody.value(it) } + val hash = generateHash(params, birthday, expires) + + val postHeaders = headersBuilder() + .add("Accept", "application/json") + .add("Content-Type", "application/x-www-form-urlencoded") + .add("Origin", baseUrl) + .add("x-kmanga-is-crawler", "false") + .add("x-kmanga-hash", hash) + .build() + + val apiRequest = POST("$apiUrl/episode/list", postHeaders, formBody) + val apiResponse = client.newCall(apiRequest).execute() + + if (!apiResponse.isSuccessful) { + throw Exception("API request failed with code ${apiResponse.code}: ${apiResponse.body.string()}") + } + + val result = apiResponse.parseAs() + + return result.episodeList.map { chapter -> + SChapter.create().apply { + url = "/title/${chapter.titleId}/episode/${chapter.episodeId}" + name = if (chapter.point > 0 && chapter.badge != 3 && chapter.rentalFinishTime == null) { + "🔒 ${chapter.episodeName}" + } else { + chapter.episodeName + } + date_upload = dateFormat.tryParse(chapter.startTime) + } + }.reversed() + } + + private fun getBirthdayCookie(url: HttpUrl): Pair { + val cookies = client.cookieJar.loadForRequest(url) + val birthdayCookie = cookies.firstOrNull { it.name == "birthday" }?.value + + return if (birthdayCookie != null) { + try { + val decoded = URLDecoder.decode(birthdayCookie, "UTF-8") + val cookieData = decoded.parseAs() + cookieData.value to cookieData.expires.toString() + } catch (e: Exception) { + // Fallback to default if cookie is malformed + "2000-01" to (System.currentTimeMillis() / 1000 + 315360000).toString() + } + } else { + // Default for logged out users or users without the cookie to bypass age restrictions + "2000-01" to (System.currentTimeMillis() / 1000 + 315360000).toString() + } + } + + // https://kmanga.kodansha.com/_nuxt/vl9so/entry-CSwIbMdW.js + private fun generateHash(params: Map, birthday: String, expires: String): String { + val paramStrings = params.toSortedMap().map { (key, value) -> + getHashedParam(key, value) + } + + val joinedParams = paramStrings.joinToString(",") + val hash1 = joinedParams.encodeUtf8().sha256().hex() + + val cookieHash = getHashedParam(birthday, expires) + + val finalString = "$hash1$cookieHash" + return finalString.encodeUtf8().sha512().hex() + } + + private fun getHashedParam(key: String, value: String): String { + val keyHash = key.encodeUtf8().sha256().hex() + val valueHash = value.encodeUtf8().sha512().hex() + return "${keyHash}_$valueHash" + } + + // Pages + override fun fetchPageList(chapter: SChapter): Observable> { + return client.newCall(pageListRequest(chapter)) + .asObservable() + .map { response -> + if (!response.isSuccessful) { + if (response.code == 400) { + throw Exception("This chapter is locked. Log in via WebView and rent or purchase the chapter to read.") + } + throw Exception("HTTP error ${response.code}") + } + pageListParse(response) + } + } + + override fun pageListParse(response: Response): List { + val apiResponse = response.parseAs() + val seed = apiResponse.scrambleSeed + return apiResponse.pageList.mapIndexed { index, imageUrl -> + Page(index = index, imageUrl = "$imageUrl#scramble_seed=$seed") + } + } + + override fun pageListRequest(chapter: SChapter): Request { + val episodeId = chapter.url.substringAfter("episode/") + val url = "$apiUrl/web/episode/viewer".toHttpUrl().newBuilder() + .addQueryParameter("episode_id", episodeId) + .build() + + return hashedGet(url) + } + + private fun hashedGet(url: HttpUrl): Request { + val (birthday, expires) = getBirthdayCookie(url) + val queryParams = url.queryParameterNames.associateWith { url.queryParameter(it)!! } + val hash = generateHash(queryParams, birthday, expires) + val newHeaders = headersBuilder() + .add("x-kmanga-hash", hash) + .build() + return GET(url, newHeaders) + } + + // Filters + override fun getFilterList() = FilterList( + Filter.Header("NOTE: Search query will ignore genre filter"), + GenreFilter(), + ) + + private class GenreFilter : UriPartFilter( + "Genre", + arrayOf( + Pair("All", "/ranking"), + Pair("Romance・Romcom", "/search/genre/1"), + Pair("Horror・Mystery・Suspense", "/search/genre/2"), + Pair("Gag・Comedy・Slice-of-Life", "/search/genre/3"), + Pair("SF・Fantasy", "/search/genre/4"), + Pair("Sports", "/search/genre/5"), + Pair("Drama", "/search/genre/6"), + Pair("Outlaws・Underworld・Punks", "/search/genre/7"), + Pair("Action・Battle", "/search/genre/8"), + Pair("Isekai・Super Powers", "/search/genre/9"), + Pair("One-off Books", "/search/genre/10"), + Pair("Shojo/josei", "/search/genre/11"), + Pair("Yaoi/BL", "/search/genre/12"), + Pair("LGBTQ", "/search/genre/13"), + Pair("Yuri/GL", "/search/genre/14"), + Pair("Anime", "/search/genre/15"), + Pair("Award Winner", "/search/genre/16"), + ), + ) + + private open class UriPartFilter(displayName: String, val vals: Array>) : + Filter.Select(displayName, vals.map { it.first }.toTypedArray()) { + fun toUriPart() = vals[state].second + } + + companion object { + const val PREFIX_SEARCH = "id:" + } + + // Unsupported + override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() + override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException() + override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() +} diff --git a/src/en/kmanga/src/eu/kanade/tachiyomi/extension/en/kmanga/KMangaUrlActivity.kt b/src/en/kmanga/src/eu/kanade/tachiyomi/extension/en/kmanga/KMangaUrlActivity.kt new file mode 100644 index 000000000..755021a6a --- /dev/null +++ b/src/en/kmanga/src/eu/kanade/tachiyomi/extension/en/kmanga/KMangaUrlActivity.kt @@ -0,0 +1,34 @@ +package eu.kanade.tachiyomi.extension.en.kmanga + +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 KMangaUrlActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size > 1) { + val titleId = pathSegments[1] + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${KManga.PREFIX_SEARCH}$titleId") + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e("KMangaUrlActivity", e.toString()) + } + } else { + Log.e("KMangaUrlActivity", "Could not parse URI from intent $intent") + } + + finish() + exitProcess(0) + } +}