feat(src/en): New source: Kiutaku (#19445)

* feat(src/en): Create Kiutaku base

* feat: Implement popular entries page

* feat: Implement latest updates page

* feat: Implement search

* feat: Implement entry details page

* feat: Implement chapter list page

* feat: Parse page list

* chore: Add source (fav)icon

The source doesn't seems to have a logo, so I'm considering the favicon used by
the site as their icon.
This commit is contained in:
Claudemirovsky 2023-12-28 13:19:03 -03:00 committed by GitHub
parent cab16d3f2b
commit 8b615b8d69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 202 additions and 0 deletions

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".en.kiutaku.KiutakuUrlActivity"
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="kiutaku.com"
android:pathPattern="/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Kiutaku'
pkgNameSuffix = 'en.kiutaku'
extClass = '.Kiutaku'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,127 @@
package eu.kanade.tachiyomi.extension.en.kiutaku
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
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.UpdateStrategy
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
class Kiutaku : ParsedHttpSource() {
override val name = "Kiutaku"
override val baseUrl = "https://kiutaku.com"
override val lang = "en"
override val supportsLatest = true
override val client by lazy {
network.client.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
}
// ============================== Popular ===============================
private fun getPage(page: Int) = (page - 1) * 20
override fun popularMangaRequest(page: Int) = GET("$baseUrl/hot?start=${getPage(page)}", headers)
override fun popularMangaSelector() = "div.blog > div.items-row"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a.item-link")!!.attr("href"))
thumbnail_url = element.selectFirst("img")?.attr("src")
title = element.selectFirst("h2")?.text() ?: "Cosplay"
}
override fun popularMangaNextPageSelector() = "nav > a.pagination-next:not([disabled])"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/?start=${getPage(page)}", headers)
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
// =============================== Search ===============================
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$id"))
.asObservableSuccess()
.map(::searchMangaByIdParse)
} else {
super.fetchSearchManga(page, query, filters)
}
}
private fun searchMangaByIdParse(response: Response): MangasPage {
val details = mangaDetailsParse(response.use { it.asJsoup() })
return MangasPage(listOf(details), false)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return GET("$baseUrl/?search=$query&start=${getPage(page)}", headers)
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
// =========================== Manga Details ============================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
status = SManga.COMPLETED
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
title = document.selectFirst("div.article-header")?.text() ?: "Cosplay"
genre = document.selectFirst("div.article-tags")
?.select("a.tag > span")
?.eachText()
?.joinToString { it.trimStart('#') }
}
// ============================== Chapters ==============================
// Fix chapter order
override fun chapterListParse(response: Response) =
super.chapterListParse(response).reversed()
override fun chapterListSelector() = "nav.pagination:first-of-type a"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.attr("href"))
val text = element.text()
name = "Page $text"
chapter_number = text.toFloatOrNull() ?: 1F
}
// =============================== Pages ================================
override fun pageListParse(document: Document): List<Page> {
return document.select("div.article-fulltext img[src]").mapIndexed { index, item ->
Page(index, "", item.attr("src"))
}
}
override fun imageUrlParse(document: Document): String {
throw UnsupportedOperationException("Not used.")
}
companion object {
const val PREFIX_SEARCH = "id:"
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.extension.en.kiutaku
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://kiutaku.com/<item> intents
* and redirects them to the main Tachiyomi process.
*/
class KiutakuUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.isNotEmpty()) {
val id = pathSegments.first()
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${Kiutaku.PREFIX_SEARCH}$id")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}