add koushoku (#11053)
* add koushoku * rewrite * fix URL intent * Update src/en/koushoku/src/eu/kanade/tachiyomi/extension/en/koushoku/Koushoku.kt Co-authored-by: ObserverOfTime <chronobserver@disroot.org> Co-authored-by: ObserverOfTime <chronobserver@disroot.org>
This commit is contained in:
parent
407f6029b4
commit
955f89ff76
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="eu.kanade.tachiyomi.extension">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".en.koushoku.KoushokuUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<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="koushoku.org"
|
||||
android:pathPattern="/archive/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
|
@ -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"
|
Binary file not shown.
After Width: | Height: | Size: 8.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
Binary file not shown.
After Width: | Height: | Size: 194 KiB |
|
@ -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<MangasPage> {
|
||||
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<SortFilter>()?.let {
|
||||
url.addQueryParameter("sort", it.toUriPart())
|
||||
}
|
||||
filterList.findInstance<OrderFilter>()?.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<SChapter> {
|
||||
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<Page> {
|
||||
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<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
// Taken from nhentai ext
|
||||
private inline fun <reified T> 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())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue