Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/all/inkrush/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import io.github.keiyoushi.gradle.api.ContentWarning

plugins {
alias(kei.plugins.extension)
}

keiyoushi {
name = "INKRUSH"
versionCode = 1
contentWarning = ContentWarning.MIXED
libVersion = "1.6"

source {
lang = "en"
baseUrl = "https://inkrush.site"
}

source {
lang = "id"
baseUrl = "https://inkrush.site"
}

deeplink {
path("/comics/..*")
}
}
Binary file added src/all/inkrush/res/mipmap-hdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/all/inkrush/res/mipmap-mdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/all/inkrush/res/mipmap-xhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/all/inkrush/res/mipmap-xxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package eu.kanade.tachiyomi.extension.all.inkrush

import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.utils.tryParse
import kotlinx.serialization.Serializable
import kotlin.time.Instant

@Serializable
class CatalogDto(
val comics: List<ComicDto>,
val hasNextPage: Boolean = false,
)

@Serializable
class ComicDto(
private val id: String,
private val title: String,
private val cover: String? = null,
) {
fun toSManga(baseUrl: String) = SManga.create().apply {
url = id
title = this@ComicDto.title
thumbnail_url = cover?.let { baseUrl + it }
}
}

@Serializable
class DetailDto(
private val id: String,
private val title: String,
private val altTitles: List<String>? = null,
private val author: String? = null,
private val description: String? = null,
private val cover: String? = null,
private val format: String? = null,
private val status: String? = null,
private val genre: List<String> = emptyList(),
private val themes: List<String> = emptyList(),
val chapters: List<ChapterDto> = emptyList(),
) {
fun toSManga(baseUrl: String) = SManga.create().apply {
url = id
title = this@DetailDto.title
thumbnail_url = cover?.let { baseUrl + it }
author = this@DetailDto.author?.takeIf { it.isNotBlank() }
description = buildString {
this@DetailDto.description?.takeIf { it.isNotBlank() }?.let(::append)
if (!altTitles.isNullOrEmpty()) {
if (isNotEmpty()) append("\n\n")
append("Alternative titles:\n")
altTitles.joinTo(this, "\n") { "- $it" }
}
}
genre = (listOfNotNull(format) + this@DetailDto.genre + themes).distinct().joinToString()
status = when (this@DetailDto.status?.lowercase()) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
"hiatus" -> SManga.ON_HIATUS
"cancelled" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
}

@Serializable
class ChapterDto(
private val id: String,
private val chapterNumber: Float,
private val name: String,
private val releaseDate: String? = null,
) {
fun toSChapter(mangaId: String) = SChapter.create().apply {
url = "$mangaId/$id"
name = this@ChapterDto.name
chapter_number = chapterNumber
date_upload = Instant.tryParse(releaseDate)
}
}

@Serializable
class PagesDto(
val pages: List<PageDto>,
)

@Serializable
class PageDto(
val src: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package eu.kanade.tachiyomi.extension.all.inkrush

import eu.kanade.tachiyomi.source.model.Filter

open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) : Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}

class CategoryFilter :
UriPartFilter(
"Category",
arrayOf(
"Trending" to "trending",
"Latest updates" to "latest",
"New" to "new",
"Popular" to "popular",
),
)

class TypeFilter :
UriPartFilter(
"Type",
arrayOf(
"All" to "",
"Manga" to "ja",
"Manhwa" to "ko",
"Manhua" to "zh",
"Other / Webtoons" to "other",
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package eu.kanade.tachiyomi.extension.all.inkrush

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.model.SMangaUpdate
import keiyoushi.annotation.Source
import keiyoushi.network.get
import keiyoushi.source.KeiSource
import keiyoushi.utils.firstInstanceOrNull
import keiyoushi.utils.parseAs
import kotlinx.serialization.json.JsonElement
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl

@Source
abstract class Inkrush : KeiSource() {

override suspend fun getPopularManga(page: Int): MangasPage = getCatalog(page, "popular")

override suspend fun getLatestUpdates(page: Int): MangasPage = getCatalog(page, "latest")

override suspend fun getSearchMangaList(page: Int, query: String, filters: FilterList): MangasPage = getCatalog(
page,
filters.firstInstanceOrNull<CategoryFilter>()?.toUriPart() ?: "trending",
query,
filters.firstInstanceOrNull<TypeFilter>()?.toUriPart().orEmpty(),
)

private suspend fun getCatalog(page: Int, category: String, query: String = "", type: String = ""): MangasPage {
val url = apiUrl("catalog").apply {
addQueryParameter("q", query)
addQueryParameter("language", lang)
addQueryParameter("category", category)
addQueryParameter("page", page.toString())
if (type.isNotEmpty()) addQueryParameter("type", type)
}.build()
val catalog = client.get(url).parseAs<CatalogDto>()
return MangasPage(catalog.comics.map { it.toSManga(baseUrl) }, catalog.hasNextPage)
}

override suspend fun fetchMangaUpdate(
manga: SManga,
chapters: List<SChapter>,
fetchDetails: Boolean,
fetchChapters: Boolean,
): SMangaUpdate {
val detail = getDetail(manga.url)
return SMangaUpdate(
detail.toSManga(baseUrl),
detail.chapters.map { it.toSChapter(manga.url) }.reversed(),
)
}

private suspend fun getDetail(id: String): DetailDto {
val url = apiUrl("detail").apply {
addQueryParameter("id", id)
addQueryParameter("language", lang)
addQueryParameter("compact", "1")
}.build()
return client.get(url).parseAs()
}

// each chapter lists releases from several upstream groups; without `release`, the site
// serves its own default pick, which is what the web reader shows too.
override suspend fun getPageList(chapter: SChapter): List<Page> {
val url = apiUrl("pages")
.addQueryParameter("id", chapter.url.substringAfter('/'))
.build()
return client.get(url).parseAs<PagesDto>().pages.mapIndexed { index, page ->
Page(index, imageUrl = baseUrl + page.src)
}
}

override fun getFilterList(data: JsonElement?): FilterList = FilterList(
CategoryFilter(),
TypeFilter(),
)

override fun getMangaUrl(manga: SManga): String = "$baseUrl/#title/${manga.url}"

override fun getChapterUrl(chapter: SChapter): String = "$baseUrl/#read/${chapter.url}"

override suspend fun getMangaByUrl(url: HttpUrl): SManga? {
if (url.host != baseUrl.toHttpUrl().host) return null
val id = url.pathSegments.takeIf { it.firstOrNull() == "comics" }?.getOrNull(1)
?: url.fragment?.split('/')?.takeIf { it.first() == "title" || it.first() == "read" }?.getOrNull(1)
?: return null
return getDetail(id).toSManga(baseUrl)
}

private fun apiUrl(action: String) = "$baseUrl/api/library".toHttpUrl().newBuilder()
.addQueryParameter("action", action)
}
Loading