diff --git a/src/en/koushoku/AndroidManifest.xml b/src/en/koushoku/AndroidManifest.xml new file mode 100644 index 000000000..20f02414a --- /dev/null +++ b/src/en/koushoku/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/src/en/koushoku/build.gradle b/src/en/koushoku/build.gradle new file mode 100644 index 000000000..8ada26b84 --- /dev/null +++ b/src/en/koushoku/build.gradle @@ -0,0 +1,16 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Koushoku' + pkgNameSuffix = 'en.koushoku' + extClass = '.Koushoku' + extVersionCode = 1 + isNsfw = true +} + +dependencies { + implementation project(':lib-ratelimit') +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/koushoku/res/mipmap-hdpi/ic_launcher.png b/src/en/koushoku/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..0c9640875 Binary files /dev/null and b/src/en/koushoku/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/koushoku/res/mipmap-mdpi/ic_launcher.png b/src/en/koushoku/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..67771ee44 Binary files /dev/null and b/src/en/koushoku/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/koushoku/res/mipmap-xhdpi/ic_launcher.png b/src/en/koushoku/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..8ef3c0ba1 Binary files /dev/null and b/src/en/koushoku/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/koushoku/res/mipmap-xxhdpi/ic_launcher.png b/src/en/koushoku/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..ad2a381f2 Binary files /dev/null and b/src/en/koushoku/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/koushoku/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/koushoku/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..3fd854742 Binary files /dev/null and b/src/en/koushoku/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/koushoku/res/web_hi_res_512.png b/src/en/koushoku/res/web_hi_res_512.png new file mode 100644 index 000000000..39703f89f Binary files /dev/null and b/src/en/koushoku/res/web_hi_res_512.png differ diff --git a/src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/Koushoku.kt b/src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/Koushoku.kt new file mode 100644 index 000000000..3d86d41e8 --- /dev/null +++ b/src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/Koushoku.kt @@ -0,0 +1,202 @@ +package eu.kanade.tachiyomi.extension.en.koushoku + +import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +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.ParsedHttpSource +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable + +class Koushoku : ParsedHttpSource() { + companion object { + const val PREFIX_ID_SEARCH = "id:" + + val archiveRegex = "/archive/(\\d+)".toRegex() + const val thumbnailSelector = ".thumbnail img" + const val magazinesSelector = ".metadata .magazines a" + } + + override val baseUrl = "https://koushoku.org" + override val name = "Koushoku" + override val lang = "en" + override val supportsLatest = false + + private val rateLimitInterceptor = RateLimitInterceptor(5) + override val client: OkHttpClient = network.cloudflareClient.newBuilder() + .addNetworkInterceptor(rateLimitInterceptor) + .build() + + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/?page=$page", headers) + override fun latestUpdatesSelector() = "#archives.feed .entries > .entry" + override fun latestUpdatesNextPageSelector() = "#archives.feed .pagination .next" + + override fun latestUpdatesFromElement(element: Element) = SManga.create().apply { + setUrlWithoutDomain(element.select("a").attr("href")) + title = element.select(".title").text() + thumbnail_url = "$baseUrl${element.select(thumbnailSelector).attr("src")}" + } + + private fun searchMangaByIdRequest(id: String) = GET("$baseUrl/archive/$id", headers) + + // taken from Tsumino ext + private fun searchMangaByIdParse(response: Response, id: String): MangasPage { + val details = mangaDetailsParse(response) + details.url = "/archive/$id" + return MangasPage(listOf(details), false) + } + + // taken from Tsumino ext + override fun fetchSearchManga( + page: Int, + query: String, + filters: FilterList + ): Observable { + return if (query.startsWith(PREFIX_ID_SEARCH)) { + val id = query.removePrefix(PREFIX_ID_SEARCH) + client.newCall(searchMangaByIdRequest(id)).asObservableSuccess() + .map { response -> searchMangaByIdParse(response, id) } + } else { + super.fetchSearchManga(page, query, filters) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$baseUrl/search".toHttpUrlOrNull()!!.newBuilder() + .addQueryParameter("page", page.toString()) + .addQueryParameter("q", query) + + val filterList = if (filters.isEmpty()) getFilterList() else filters + filterList.findInstance()?.let { + url.addQueryParameter("sort", it.toUriPart()) + } + filterList.findInstance()?.let { + url.addQueryParameter("order", it.toUriPart()) + } + + return GET(url.toString(), headers) + } + + override fun searchMangaSelector() = latestUpdatesSelector() + override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector() + override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element) + + override fun popularMangaRequest(page: Int) = latestUpdatesRequest(page) + override fun popularMangaSelector() = latestUpdatesSelector() + override fun popularMangaNextPageSelector() = latestUpdatesNextPageSelector() + override fun popularMangaFromElement(element: Element) = latestUpdatesFromElement(element) + + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + title = document.select(".metadata .title").text() + thumbnail_url = "$baseUrl${document.select(thumbnailSelector).attr("src")}" + artist = document.select(".metadata .artists a, .metadata .circles a") + .joinToString { it.text() } + author = artist + genre = document.select(".metadata .tags a, $magazinesSelector") + .ifEmpty { null }?.joinToString { it.text() } + description = getDesc(document) + status = SManga.COMPLETED + } + + override fun chapterListRequest(manga: SManga) = GET("$baseUrl${manga.url}", headers) + + override fun chapterListParse(response: Response): List { + val document = response.asJsoup() + return listOf( + SChapter.create().apply { + setUrlWithoutDomain(response.request.url.encodedPath) + name = "Chapter" + date_upload = document.select(".metadata .published td:nth-child(2)") + .attr("data-unix").toLong() * 1000 + } + ) + } + + override fun chapterFromElement(element: Element) = + throw UnsupportedOperationException("Not used") + + override fun chapterListSelector() = throw UnsupportedOperationException("Not used") + + override fun pageListRequest(chapter: SChapter) = GET("$baseUrl${chapter.url}/1") + + override fun pageListParse(document: Document): List { + val totalPages = document.selectFirst(".total").text().toInt() + if (totalPages == 0) + throw UnsupportedOperationException("Error: Empty pages (try Webview)") + + val id = archiveRegex.find(document.location())?.groups?.get(1)?.value + if (id.isNullOrEmpty()) + throw UnsupportedOperationException("Error: Unknown archive id") + + return (1..totalPages).map { + Page(it, "", "$baseUrl/data/$id/$it.jpg") + } + } + + override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used") + + override fun getFilterList() = FilterList( + SortFilter(), + OrderFilter() + ) + + private class SortFilter : UriPartFilter( + "Sort", + arrayOf( + Pair("Created Date", "created_at"), + Pair("ID", "id"), + Pair("Title", "title"), + Pair("Published Date", "published_at") + ) + ) + + private class OrderFilter : UriPartFilter( + "Order", + arrayOf( + Pair("Descending", "desc"), + Pair("Ascending", "asc"), + ) + ) + + // Taken from nhentai ext + private open class UriPartFilter(displayName: String, val vals: Array>) : + Filter.Select(displayName, vals.map { it.first }.toTypedArray()) { + fun toUriPart() = vals[state].second + } + + // Taken from nhentai ext + private inline fun Iterable<*>.findInstance() = find { it is T } as? T + + private fun getDesc(document: Document) = buildString { + val magazines = document.select(magazinesSelector) + if (magazines.isNotEmpty()) { + append("Magazines: ") + append(magazines.joinToString { it.text() }) + append("\n") + } + + val parodies = document.select(".metadata .parodies a") + if (parodies.isNotEmpty()) { + append("Parodies: ") + append(parodies.joinToString { it.text() }) + append("\n") + } + + val pages = document.selectFirst(".metadata .pages td:nth-child(2)") + append("Pages: ").append(pages.text()).append("\n") + + val size = document.selectFirst(".metadata .size td:nth-child(2)") + append("Size: ").append(size.text()) + } +} diff --git a/src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/KoushokuUrlActivity.kt b/src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/KoushokuUrlActivity.kt new file mode 100644 index 000000000..b3795b90f --- /dev/null +++ b/src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/KoushokuUrlActivity.kt @@ -0,0 +1,38 @@ +package eu.kanade.tachiyomi.extension.en.koushoku + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +/** + * Springboard that accepts https://koushoku.org/archive/xxxxx intents and redirects them to + * the main Tachiyomi process. + */ +class KoushokuUrlActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size > 1) { + val id = pathSegments[1] + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${Koushoku.PREFIX_ID_SEARCH}$id") + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e("KoushokuUrlActivity", e.toString()) + } + } else { + Log.e("KoushokuUrlActivity", "could not parse uri from intent $intent") + } + + finish() + exitProcess(0) + } +}