Initial commit

This commit is contained in:
FourTOne5
2024-01-09 04:12:39 +06:00
commit 600c345dfe
8593 changed files with 150590 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.akuma.AkumaUrlActivity"
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="akuma.moe"
android:pathPattern="/g/..*"
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 = 'Akuma'
pkgNameSuffix = 'all.akuma'
extClass = '.Akuma'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -0,0 +1,221 @@
package eu.kanade.tachiyomi.extension.all.akuma
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import java.io.IOException
class Akuma : ParsedHttpSource() {
override val name = "Akuma"
override val baseUrl = "https://akuma.moe"
override val lang = "all"
override val supportsLatest = false
private var nextHash: String? = null
private var storedToken: String? = null
private val ddosGuardIntercept = DDosGuardInterceptor(network.client)
override val client: OkHttpClient = network.client.newBuilder()
.addInterceptor(ddosGuardIntercept)
.addInterceptor(::tokenInterceptor)
.rateLimit(2)
.build()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private fun tokenInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.method == "POST" && request.header("X-CSRF-TOKEN") == null) {
val modifiedRequest = request.newBuilder()
.addHeader("X-Requested-With", "XMLHttpRequest")
val token = getToken()
val response = chain.proceed(
modifiedRequest
.addHeader("X-CSRF-TOKEN", token)
.build(),
)
if (!response.isSuccessful && response.code == 419) {
response.close()
storedToken = null // reset the token
val newToken = getToken()
return chain.proceed(
modifiedRequest
.addHeader("X-CSRF-TOKEN", newToken)
.build(),
)
}
return response
}
return chain.proceed(request)
}
private fun getToken(): String {
if (storedToken.isNullOrEmpty()) {
val request = GET(baseUrl, headers)
val response = client.newCall(request).execute()
val document = response.asJsoup()
val token = document.select("head meta[name*=csrf-token]")
.attr("content")
if (token.isEmpty()) {
throw IOException("Unable to find CSRF token")
}
storedToken = token
}
return storedToken!!
}
override fun popularMangaRequest(page: Int): Request {
val payload = FormBody.Builder()
.add("view", "3")
.build()
return if (page == 1) {
nextHash = null
POST(baseUrl, headers, payload)
} else {
POST("$baseUrl/?cursor=$nextHash", headers, payload)
}
}
override fun popularMangaSelector() = ".post-loop li"
override fun popularMangaNextPageSelector() = ".page-item a[rel*=next]"
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(popularMangaSelector()).map { element ->
popularMangaFromElement(element)
}
val nextUrl = document.select(popularMangaNextPageSelector()).first()?.attr("href")
nextHash = nextUrl?.toHttpUrlOrNull()?.queryParameter("cursor")
return MangasPage(mangas, !nextHash.isNullOrEmpty())
}
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
setUrlWithoutDomain(element.select("a").attr("href"))
title = element.select(".overlay-title").text()
thumbnail_url = element.select("img").attr("abs:src")
}
}
override fun fetchSearchManga(
page: Int,
query: String,
filters: FilterList,
): Observable<MangasPage> {
return if (query.startsWith(PREFIX_ID)) {
val url = "/g/${query.substringAfter(PREFIX_ID)}"
val manga = SManga.create().apply { this.url = url }
fetchMangaDetails(manga).map {
MangasPage(listOf(it.apply { this.url = url }), false)
}
} else {
super.fetchSearchManga(page, query, filters)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val request = popularMangaRequest(page)
val url = request.url.newBuilder()
.addQueryParameter("q", query.trim())
.build()
return request.newBuilder()
.url(url)
.build()
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.select(".entry-title").text()
thumbnail_url = document.select(".img-thumbnail").attr("abs:src")
author = document.select("li.meta-data > span.artist + span.value").text()
genre = document.select(".info-list a").joinToString { it.text() }
description = document.select(".pages span.value").text() + " Pages"
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
status = SManga.COMPLETED
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.just(
listOf(
SChapter.create().apply {
url = "${manga.url}/1"
name = "Chapter"
},
),
)
}
override fun pageListParse(document: Document): List<Page> {
val totalPages = document.select(".nav-select option").last()
?.attr("value")?.toIntOrNull() ?: return emptyList()
val url = document.location().substringBeforeLast("/")
val pageList = mutableListOf<Page>()
for (i in 1..totalPages) {
pageList.add(Page(i, "$url/$i"))
}
return pageList
}
override fun imageUrlParse(document: Document): String {
return document.select(".entry-content img").attr("abs:src")
}
companion object {
const val PREFIX_ID = "id:"
}
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
override fun chapterListSelector() = throw UnsupportedOperationException()
}

View File

@@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.extension.all.akuma
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 AkumaUrlActivity : 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", "${Akuma.PREFIX_ID}$id")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("AkumaUrlActivity", e.toString())
}
} else {
Log.e("AkumaUrlActivity", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@@ -0,0 +1,67 @@
package eu.kanade.tachiyomi.extension.all.akuma
import android.webkit.CookieManager
import eu.kanade.tachiyomi.network.GET
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
class DDosGuardInterceptor(private val client: OkHttpClient) : Interceptor {
private val cookieManager by lazy { CookieManager.getInstance() }
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val response = chain.proceed(originalRequest)
// Check if DDos-GUARD is on
if (response.code !in ERROR_CODES || response.header("Server") !in SERVER_CHECK) {
return response
}
val cookies = cookieManager.getCookie(originalRequest.url.toString())
val oldCookie = if (cookies != null && cookies.isNotEmpty()) {
cookies.split(";").mapNotNull { Cookie.parse(originalRequest.url, it) }
} else {
emptyList()
}
val ddg2Cookie = oldCookie.firstOrNull { it.name == "__ddg2_" }
if (!ddg2Cookie?.value.isNullOrEmpty()) {
return response
}
response.close()
val newCookie = getNewCookie(originalRequest.url)
?: return chain.proceed(originalRequest)
val newCookieHeader = (oldCookie + newCookie)
.joinToString("; ") { "${it.name}=${it.value}" }
val modifiedRequest = originalRequest.newBuilder()
.header("cookie", newCookieHeader)
.build()
return chain.proceed(modifiedRequest)
}
private fun getNewCookie(url: HttpUrl): Cookie? {
val wellKnown = client.newCall(GET(wellKnownUrl))
.execute().body.string()
.substringAfter("'", "")
.substringBefore("'", "")
val checkUrl = "${url.scheme}://${url.host + wellKnown}"
val response = client.newCall(GET(checkUrl)).execute()
return response.header("set-cookie")?.let {
Cookie.parse(url, it)
}
}
companion object {
private const val wellKnownUrl = "https://check.ddos-guard.net/check.js"
private val ERROR_CODES = listOf(403)
private val SERVER_CHECK = listOf("ddos-guard")
}
}

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.batoto.BatoToUrlActivity"
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="*.bato.to" />
<data android:host="bato.to" />
<data android:host="*.batocc.com" />
<data android:host="batocc.com" />
<data android:host="*.batotoo.com" />
<data android:host="batotoo.com" />
<data android:host="*.batotwo.com" />
<data android:host="batotwo.com" />
<data android:host="*.battwo.com" />
<data android:host="battwo.com" />
<data android:host="*.comiko.net" />
<data android:host="comiko.net" />
<data android:host="*.mangatoto.com" />
<data android:host="mangatoto.com" />
<data android:host="*.mangatoto.net" />
<data android:host="mangatoto.net" />
<data android:host="*.mangatoto.org" />
<data android:host="mangatoto.org" />
<data android:host="*.mycordant.co.uk" />
<data android:host="mycordant.co.uk" />
<data android:host="*.dto.to" />
<data android:host="dto.to" />
<data android:host="*.hto.to" />
<data android:host="hto.to" />
<data android:host="*.mto.to" />
<data android:host="mto.to" />
<data android:host="*.wto.to" />
<data android:host="wto.to" />
<data
android:pathPattern="/series/..*"
android:scheme="https" />
<data
android:pathPattern="/subject-overview/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

201
src/all/batoto/CHANGELOG.md Normal file
View File

@@ -0,0 +1,201 @@
## 1.3.30
### Refactor
* Replace CryptoJS with Native Kotlin Functions
* Remove QuickJS dependency
## 1.3.29
### Refactor
* Cleanup pageListParse function
* Replace Duktape with QuickJS
## 1.3.28
### Features
* Add mirror `batocc.com`
* Add mirror `batotwo.com`
* Add mirror `mangatoto.net`
* Add mirror `mangatoto.org`
* Add mirror `mycordant.co.uk`
* Add mirror `dto.to`
* Add mirror `hto.to`
* Add mirror `mto.to`
* Add mirror `wto.to`
* Remove mirror `mycdhands.com`
## 1.3.27
### Features
* Change default popular sort by `Most Views Totally`
## 1.3.26
### Fix
* Update author and artist parsing
## 1.3.25
### Fix
* Status parsing
* Artist name parsing
## 1.3.24
### Fix
* Bump versions for individual extension with URL handler activities
## 1.2.23
### Fix
* Update pageListParse logic to handle website changes
## 1.2.22
### Features
* Add `CHANGELOG.md` & `README.md`
## 1.2.21
### Fix
* Update lang codes
## 1.2.20
### Features
* Rework of search
## 1.2.19
### Features
* Support for alternative chapter list
* Personal lists filter
## 1.2.18
### Features
* Utils lists filter
* Letter matching filter
## 1.2.17
### Features
* Add mirror `mycdhands.com`
## 1.2.16
### Features
* Mirror support
* URL intent updates
## 1.2.15
### Fix
* Manga description
## 1.2.14
### Features
* Escape entities
## 1.2.13
### Refactor
* Replace Gson with kotlinx.serialization
## 1.2.12
### Fix
* Infinity search
## 1.2.11
### Fix
* No search result
## 1.2.10
### Features
* Support for URL intent
* Updated filters
## 1.2.9
### Fix
* Chapter parsing
## 1.2.8
### Features
* More chapter filtering
## 1.2.7
### Fix
* Language filtering in latest
* Parsing of seconds
## 1.2.6
### Features
* Scanlator support
### Fix
* Date parsing
## 1.2.5
### Features
* Update supported Language list
## 1.2.4
### Features
* Support for excluding genres
## 1.2.3
### Fix
* Typo in some genres
## 1.2.2
### Features
* Reworked filter option
## 1.2.1
### Features
* Conversion from Emerald to Bato.to
* First version

20
src/all/batoto/README.md Normal file
View File

@@ -0,0 +1,20 @@
# Bato.to
Table of Content
- [FAQ](#FAQ)
- [Why are there Manga of diffrent languge than the selected one in Personal & Utils lists?](#why-are-there-manga-of-diffrent-languge-than-the-selected-one-in-personal--utils-lists)
- [Bato.to is not loading anything?](#batoto-is-not-loading-anything)
[Uncomment this if needed; and replace &#40; and &#41; with ( and )]: <> (- [Guides]&#40;#Guides&#41;)
Don't find the question you are look for go check out our general FAQs and Guides over at [Extension FAQ](https://tachiyomi.org/help/faq/#extensions) or [Getting Started](https://tachiyomi.org/help/guides/getting-started/#installation)
## FAQ
### Why are there Manga of diffrent languge than the selected one in Personal & Utils lists?
Personol & Utils lists have no way to difritiate between langueges.
### Bato.to is not loading anything?
Bato.to get blocked by some ISPs, try using a diffrent mirror of Bato.to from the settings.
[Uncomment this if needed]: <> (## Guides)

View File

@@ -0,0 +1,17 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Bato.to'
pkgNameSuffix = 'all.batoto'
extClass = '.BatoToFactory'
extVersionCode = 33
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib-cryptoaes'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -0,0 +1,974 @@
package eu.kanade.tachiyomi.extension.all.batoto
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.CheckBoxPreference
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
import eu.kanade.tachiyomi.lib.cryptoaes.Deobfuscator
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.ConfigurableSource
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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
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.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.parser.Parser
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.concurrent.TimeUnit
open class BatoTo(
final override val lang: String,
private val siteLang: String,
) : ConfigurableSource, ParsedHttpSource() {
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val name: String = "Bato.to"
override val baseUrl: String = getMirrorPref()!!
override val id: Long = when (lang) {
"zh-Hans" -> 2818874445640189582
"zh-Hant" -> 38886079663327225
"ro-MD" -> 8871355786189601023
else -> super.id
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val mirrorPref = ListPreference(screen.context).apply {
key = "${MIRROR_PREF_KEY}_$lang"
title = MIRROR_PREF_TITLE
entries = MIRROR_PREF_ENTRIES
entryValues = MIRROR_PREF_ENTRY_VALUES
setDefaultValue(MIRROR_PREF_DEFAULT_VALUE)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString("${MIRROR_PREF_KEY}_$lang", entry).commit()
}
}
val altChapterListPref = CheckBoxPreference(screen.context).apply {
key = "${ALT_CHAPTER_LIST_PREF_KEY}_$lang"
title = ALT_CHAPTER_LIST_PREF_TITLE
summary = ALT_CHAPTER_LIST_PREF_SUMMARY
setDefaultValue(ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", checkValue).commit()
}
}
screen.addPreference(mirrorPref)
screen.addPreference(altChapterListPref)
}
private fun getMirrorPref(): String? = preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
private fun getAltChapterListPref(): Boolean = preferences.getBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE)
override val supportsLatest = true
private val json: Json by injectLazy()
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/browse?langs=$siteLang&sort=update&page=$page")
}
override fun latestUpdatesSelector(): String {
return when (siteLang) {
"" -> "div#series-list div.col"
"en" -> "div#series-list div.col.no-flag"
else -> "div#series-list div.col:has([data-lang=\"$siteLang\"])"
}
}
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
val item = element.select("a.item-cover")
val imgurl = item.select("img").attr("abs:src")
manga.setUrlWithoutDomain(item.attr("href"))
manga.title = element.select("a.item-title").text().removeEntities()
manga.thumbnail_url = imgurl
return manga
}
override fun latestUpdatesNextPageSelector() = "div#mainer nav.d-none .pagination .page-item:last-of-type:not(.disabled)"
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/browse?langs=$siteLang&sort=views_a&page=$page")
}
override fun popularMangaSelector() = latestUpdatesSelector()
override fun popularMangaFromElement(element: Element) = latestUpdatesFromElement(element)
override fun popularMangaNextPageSelector() = latestUpdatesNextPageSelector()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return when {
query.startsWith("ID:") -> {
val id = query.substringAfter("ID:")
client.newCall(GET("$baseUrl/series/$id", headers)).asObservableSuccess()
.map { response ->
queryIDParse(response)
}
}
query.isNotBlank() -> {
val url = "$baseUrl/search".toHttpUrl().newBuilder()
.addQueryParameter("word", query)
.addQueryParameter("page", page.toString())
filters.forEach { filter ->
when (filter) {
is LetterFilter -> {
if (filter.state == 1) {
url.addQueryParameter("mode", "letter")
}
}
else -> { /* Do Nothing */ }
}
}
client.newCall(GET(url.build().toString(), headers)).asObservableSuccess()
.map { response ->
queryParse(response)
}
}
else -> {
val url = "$baseUrl/browse".toHttpUrlOrNull()!!.newBuilder()
var min = ""
var max = ""
filters.forEach { filter ->
when (filter) {
is UtilsFilter -> {
if (filter.state != 0) {
val filterUrl = "$baseUrl/_utils/comic-list?type=${filter.selected}"
return client.newCall(GET(filterUrl, headers)).asObservableSuccess()
.map { response ->
queryUtilsParse(response)
}
}
}
is HistoryFilter -> {
if (filter.state != 0) {
val filterUrl = "$baseUrl/ajax.my.${filter.selected}.paging"
return client.newCall(POST(filterUrl, headers, formBuilder().build())).asObservableSuccess()
.map { response ->
queryHistoryParse(response)
}
}
}
is LangGroupFilter -> {
if (filter.selected.isEmpty()) {
url.addQueryParameter("langs", siteLang)
} else {
val selection = "${filter.selected.joinToString(",")},$siteLang"
url.addQueryParameter("langs", selection)
}
}
is GenreGroupFilter -> {
with(filter) {
url.addQueryParameter(
"genres",
included.joinToString(",") + "|" + excluded.joinToString(","),
)
}
}
is StatusFilter -> url.addQueryParameter("release", filter.selected)
is SortFilter -> {
if (filter.state != null) {
val sort = getSortFilter()[filter.state!!.index].value
val value = when (filter.state!!.ascending) {
true -> "az"
false -> "za"
}
url.addQueryParameter("sort", "$sort.$value")
}
}
is OriginGroupFilter -> {
if (filter.selected.isNotEmpty()) {
url.addQueryParameter("origs", filter.selected.joinToString(","))
}
}
is MinChapterTextFilter -> min = filter.state
is MaxChapterTextFilter -> max = filter.state
else -> { /* Do Nothing */ }
}
}
url.addQueryParameter("page", page.toString())
if (max.isNotEmpty() or min.isNotEmpty()) {
url.addQueryParameter("chapters", "$min-$max")
}
client.newCall(GET(url.build().toString(), headers)).asObservableSuccess()
.map { response ->
queryParse(response)
}
}
}
}
private fun queryIDParse(response: Response): MangasPage {
val document = response.asJsoup()
val infoElement = document.select("div#mainer div.container-fluid")
val manga = SManga.create()
manga.title = infoElement.select("h3").text().removeEntities()
manga.thumbnail_url = document.select("div.attr-cover img")
.attr("abs:src")
manga.url = infoElement.select("h3 a").attr("abs:href")
return MangasPage(listOf(manga), false)
}
private fun queryParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(latestUpdatesSelector())
.map { element -> latestUpdatesFromElement(element) }
val nextPage = document.select(latestUpdatesNextPageSelector()).first() != null
return MangasPage(mangas, nextPage)
}
private fun queryUtilsParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select("tbody > tr")
.map { element -> searchUtilsFromElement(element) }
return MangasPage(mangas, false)
}
private fun queryHistoryParse(response: Response): MangasPage {
val json = json.decodeFromString<JsonObject>(response.body.string())
val html = json.jsonObject["html"]!!.jsonPrimitive.content
val document = Jsoup.parse(html, response.request.url.toString())
val mangas = document.select(".my-history-item")
.map { element -> searchHistoryFromElement(element) }
return MangasPage(mangas, false)
}
private fun searchUtilsFromElement(element: Element): SManga {
val manga = SManga.create()
manga.setUrlWithoutDomain(element.select("td a").attr("href"))
manga.title = element.select("td a").text()
manga.thumbnail_url = element.select("img").attr("abs:src")
return manga
}
private fun searchHistoryFromElement(element: Element): SManga {
val manga = SManga.create()
manga.setUrlWithoutDomain(element.select(".position-relative a").attr("href"))
manga.title = element.select(".position-relative a").text()
manga.thumbnail_url = element.select("img").attr("abs:src")
return manga
}
open fun formBuilder() = FormBody.Builder().apply {
add("_where", "browse")
add("first", "0")
add("limit", "0")
add("prevPos", "null")
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException("Not used")
override fun searchMangaSelector() = throw UnsupportedOperationException("Not used")
override fun searchMangaFromElement(element: Element) = throw UnsupportedOperationException("Not used")
override fun searchMangaNextPageSelector() = throw UnsupportedOperationException("Not used")
override fun mangaDetailsRequest(manga: SManga): Request {
if (manga.url.startsWith("http")) {
return GET(manga.url, headers)
}
return super.mangaDetailsRequest(manga)
}
override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.select("div#mainer div.container-fluid")
val manga = SManga.create()
val workStatus = infoElement.select("div.attr-item:contains(original work) span").text()
val uploadStatus = infoElement.select("div.attr-item:contains(upload status) span").text()
manga.title = infoElement.select("h3").text().removeEntities()
manga.author = infoElement.select("div.attr-item:contains(author) span").text()
manga.artist = infoElement.select("div.attr-item:contains(artist) span").text()
manga.status = parseStatus(workStatus, uploadStatus)
manga.genre = infoElement.select(".attr-item b:contains(genres) + span ").joinToString { it.text() }
manga.description = infoElement.select("div.limit-html").text() + "\n" + infoElement.select(".episode-list > .alert-warning").text().trim()
manga.thumbnail_url = document.select("div.attr-cover img")
.attr("abs:src")
return manga
}
private fun parseStatus(workStatus: String?, uploadStatus: String?) = when {
workStatus == null -> SManga.UNKNOWN
workStatus.contains("Ongoing") -> SManga.ONGOING
workStatus.contains("Cancelled") -> SManga.CANCELLED
workStatus.contains("Hiatus") -> SManga.ON_HIATUS
workStatus.contains("Completed") -> when {
uploadStatus?.contains("Ongoing") == true -> SManga.PUBLISHING_FINISHED
else -> SManga.COMPLETED
}
else -> SManga.UNKNOWN
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val url = client.newCall(
GET(
when {
manga.url.startsWith("http") -> manga.url
else -> "$baseUrl${manga.url}"
},
),
).execute().asJsoup()
if (getAltChapterListPref() || checkChapterLists(url)) {
val id = manga.url.substringBeforeLast("/").substringAfterLast("/").trim()
return client.newCall(GET("$baseUrl/rss/series/$id.xml"))
.asObservableSuccess()
.map { altChapterParse(it, manga.title) }
}
return super.fetchChapterList(manga)
}
private fun altChapterParse(response: Response, title: String): List<SChapter> {
return Jsoup.parse(response.body.string(), response.request.url.toString(), Parser.xmlParser())
.select("channel > item").map { item ->
SChapter.create().apply {
url = item.selectFirst("guid")!!.text()
name = item.selectFirst("title")!!.text().substringAfter(title).trim()
date_upload = SimpleDateFormat("E, dd MMM yyyy H:m:s Z", Locale.US).parse(item.selectFirst("pubDate")!!.text())?.time ?: 0L
}
}
}
private fun checkChapterLists(document: Document): Boolean {
return document.select(".episode-list > .alert-warning").text().contains("This comic has been marked as deleted and the chapter list is not available.")
}
override fun chapterListRequest(manga: SManga): Request {
if (manga.url.startsWith("http")) {
return GET(manga.url, headers)
}
return super.chapterListRequest(manga)
}
override fun chapterListSelector() = "div.main div.p-2"
override fun chapterFromElement(element: Element): SChapter {
val chapter = SChapter.create()
val urlElement = element.select("a.chapt")
val group = element.select("div.extra > a:not(.ps-3)").text()
val time = element.select("div.extra > i.ps-3").text()
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text()
if (group != "") {
chapter.scanlator = group
}
if (time != "") {
chapter.date_upload = parseChapterDate(time)
}
return chapter
}
private fun parseChapterDate(date: String): Long {
val value = date.split(' ')[0].toInt()
return when {
"secs" in date -> Calendar.getInstance().apply {
add(Calendar.SECOND, value * -1)
}.timeInMillis
"mins" in date -> Calendar.getInstance().apply {
add(Calendar.MINUTE, value * -1)
}.timeInMillis
"hours" in date -> Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, value * -1)
}.timeInMillis
"days" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, value * -1)
}.timeInMillis
"weeks" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, value * 7 * -1)
}.timeInMillis
"months" in date -> Calendar.getInstance().apply {
add(Calendar.MONTH, value * -1)
}.timeInMillis
"years" in date -> Calendar.getInstance().apply {
add(Calendar.YEAR, value * -1)
}.timeInMillis
"sec" in date -> Calendar.getInstance().apply {
add(Calendar.SECOND, value * -1)
}.timeInMillis
"min" in date -> Calendar.getInstance().apply {
add(Calendar.MINUTE, value * -1)
}.timeInMillis
"hour" in date -> Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, value * -1)
}.timeInMillis
"day" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, value * -1)
}.timeInMillis
"week" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, value * 7 * -1)
}.timeInMillis
"month" in date -> Calendar.getInstance().apply {
add(Calendar.MONTH, value * -1)
}.timeInMillis
"year" in date -> Calendar.getInstance().apply {
add(Calendar.YEAR, value * -1)
}.timeInMillis
else -> {
return 0
}
}
}
override fun pageListRequest(chapter: SChapter): Request {
if (chapter.url.startsWith("http")) {
return GET(chapter.url, headers)
}
return super.pageListRequest(chapter)
}
override fun pageListParse(document: Document): List<Page> {
val script = document.selectFirst("script:containsData(imgHttps):containsData(batoWord):containsData(batoPass)")?.html()
?: throw RuntimeException("Couldn't find script with image data.")
val imgHttpLisString = script.substringAfter("const imgHttps =").substringBefore(";").trim()
val imgHttpLis = json.parseToJsonElement(imgHttpLisString).jsonArray.map { it.jsonPrimitive.content }
val batoWord = script.substringAfter("const batoWord =").substringBefore(";").trim()
val batoPass = script.substringAfter("const batoPass =").substringBefore(";").trim()
val evaluatedPass: String = Deobfuscator.deobfuscateJsPassword(batoPass)
val imgAccListString = CryptoAES.decrypt(batoWord.removeSurrounding("\""), evaluatedPass)
val imgAccList = json.parseToJsonElement(imgAccListString).jsonArray.map { it.jsonPrimitive.content }
return imgHttpLis.zip(imgAccList).mapIndexed { i, (imgUrl, imgAcc) ->
Page(i, imageUrl = "$imgUrl?$imgAcc")
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
private fun String.removeEntities(): String = Parser.unescapeEntities(this, true)
override fun getFilterList() = FilterList(
LetterFilter(getLetterFilter(), 0),
Filter.Separator(),
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
SortFilter(getSortFilter().map { it.name }.toTypedArray()),
StatusFilter(getStatusFilter(), 0),
GenreGroupFilter(getGenreFilter()),
OriginGroupFilter(getOrginFilter()),
LangGroupFilter(getLangFilter()),
MinChapterTextFilter(),
MaxChapterTextFilter(),
Filter.Separator(),
Filter.Header("NOTE: Filters below are incompatible with any other filters!"),
Filter.Header("NOTE: Login Required!"),
Filter.Separator(),
UtilsFilter(getUtilsFilter(), 0),
HistoryFilter(getHistoryFilter(), 0),
)
class SelectFilterOption(val name: String, val value: String)
class CheckboxFilterOption(val value: String, name: String, default: Boolean = false) : Filter.CheckBox(name, default)
class TriStateFilterOption(val value: String, name: String, default: Int = 0) : Filter.TriState(name, default)
abstract class SelectFilter(name: String, private val options: List<SelectFilterOption>, default: Int = 0) : Filter.Select<String>(name, options.map { it.name }.toTypedArray(), default) {
val selected: String
get() = options[state].value
}
abstract class CheckboxGroupFilter(name: String, options: List<CheckboxFilterOption>) : Filter.Group<CheckboxFilterOption>(name, options) {
val selected: List<String>
get() = state.filter { it.state }.map { it.value }
}
abstract class TriStateGroupFilter(name: String, options: List<TriStateFilterOption>) : Filter.Group<TriStateFilterOption>(name, options) {
val included: List<String>
get() = state.filter { it.isIncluded() }.map { it.value }
val excluded: List<String>
get() = state.filter { it.isExcluded() }.map { it.value }
}
abstract class TextFilter(name: String) : Filter.Text(name)
class SortFilter(sortables: Array<String>) : Filter.Sort("Sort", sortables, Selection(5, false))
class StatusFilter(options: List<SelectFilterOption>, default: Int) : SelectFilter("Status", options, default)
class OriginGroupFilter(options: List<CheckboxFilterOption>) : CheckboxGroupFilter("Origin", options)
class GenreGroupFilter(options: List<TriStateFilterOption>) : TriStateGroupFilter("Genre", options)
class MinChapterTextFilter : TextFilter("Min. Chapters")
class MaxChapterTextFilter : TextFilter("Max. Chapters")
class LangGroupFilter(options: List<CheckboxFilterOption>) : CheckboxGroupFilter("Languages", options)
class LetterFilter(options: List<SelectFilterOption>, default: Int) : SelectFilter("Letter matching mode (Slow)", options, default)
class UtilsFilter(options: List<SelectFilterOption>, default: Int) : SelectFilter("Utils comic list", options, default)
class HistoryFilter(options: List<SelectFilterOption>, default: Int) : SelectFilter("Personal list", options, default)
private fun getLetterFilter() = listOf(
SelectFilterOption("Disabled", "disabled"),
SelectFilterOption("Enabled", "enabled"),
)
private fun getSortFilter() = listOf(
SelectFilterOption("Z-A", "title"),
SelectFilterOption("Last Updated", "update"),
SelectFilterOption("Newest Added", "create"),
SelectFilterOption("Most Views Totally", "views_a"),
SelectFilterOption("Most Views 365 days", "views_y"),
SelectFilterOption("Most Views 30 days", "views_m"),
SelectFilterOption("Most Views 7 days", "views_w"),
SelectFilterOption("Most Views 24 hours", "views_d"),
SelectFilterOption("Most Views 60 minutes", "views_h"),
)
private fun getHistoryFilter() = listOf(
SelectFilterOption("None", ""),
SelectFilterOption("My History", "history"),
SelectFilterOption("My Updates", "updates"),
)
private fun getUtilsFilter() = listOf(
SelectFilterOption("None", ""),
SelectFilterOption("Comics: I Created", "i-created"),
SelectFilterOption("Comics: I Modified", "i-modified"),
SelectFilterOption("Comics: I Uploaded", "i-uploaded"),
SelectFilterOption("Comics: Authorized to me", "i-authorized"),
SelectFilterOption("Comics: Draft Status", "status-draft"),
SelectFilterOption("Comics: Hidden Status", "status-hidden"),
SelectFilterOption("Ongoing and Not updated in 30-60 days", "not-updated-30-60"),
SelectFilterOption("Ongoing and Not updated in 60-90 days", "not-updated-60-90"),
SelectFilterOption("Ongoing and Not updated in 90-180 days", "not-updated-90-180"),
SelectFilterOption("Ongoing and Not updated in 180-360 days", "not-updated-180-360"),
SelectFilterOption("Ongoing and Not updated in 360-1000 days", "not-updated-360-1000"),
SelectFilterOption("Ongoing and Not updated more than 1000 days", "not-updated-1000"),
)
private fun getStatusFilter() = listOf(
SelectFilterOption("All", ""),
SelectFilterOption("Pending", "pending"),
SelectFilterOption("Ongoing", "ongoing"),
SelectFilterOption("Completed", "completed"),
SelectFilterOption("Hiatus", "hiatus"),
SelectFilterOption("Cancelled", "cancelled"),
)
private fun getOrginFilter() = listOf(
// Values exported from publish.bato.to
CheckboxFilterOption("zh", "Chinese"),
CheckboxFilterOption("en", "English"),
CheckboxFilterOption("ja", "Japanese"),
CheckboxFilterOption("ko", "Korean"),
CheckboxFilterOption("af", "Afrikaans"),
CheckboxFilterOption("sq", "Albanian"),
CheckboxFilterOption("am", "Amharic"),
CheckboxFilterOption("ar", "Arabic"),
CheckboxFilterOption("hy", "Armenian"),
CheckboxFilterOption("az", "Azerbaijani"),
CheckboxFilterOption("be", "Belarusian"),
CheckboxFilterOption("bn", "Bengali"),
CheckboxFilterOption("bs", "Bosnian"),
CheckboxFilterOption("bg", "Bulgarian"),
CheckboxFilterOption("my", "Burmese"),
CheckboxFilterOption("km", "Cambodian"),
CheckboxFilterOption("ca", "Catalan"),
CheckboxFilterOption("ceb", "Cebuano"),
CheckboxFilterOption("zh_hk", "Chinese (Cantonese)"),
CheckboxFilterOption("zh_tw", "Chinese (Traditional)"),
CheckboxFilterOption("hr", "Croatian"),
CheckboxFilterOption("cs", "Czech"),
CheckboxFilterOption("da", "Danish"),
CheckboxFilterOption("nl", "Dutch"),
CheckboxFilterOption("en_us", "English (United States)"),
CheckboxFilterOption("eo", "Esperanto"),
CheckboxFilterOption("et", "Estonian"),
CheckboxFilterOption("fo", "Faroese"),
CheckboxFilterOption("fil", "Filipino"),
CheckboxFilterOption("fi", "Finnish"),
CheckboxFilterOption("fr", "French"),
CheckboxFilterOption("ka", "Georgian"),
CheckboxFilterOption("de", "German"),
CheckboxFilterOption("el", "Greek"),
CheckboxFilterOption("gn", "Guarani"),
CheckboxFilterOption("gu", "Gujarati"),
CheckboxFilterOption("ht", "Haitian Creole"),
CheckboxFilterOption("ha", "Hausa"),
CheckboxFilterOption("he", "Hebrew"),
CheckboxFilterOption("hi", "Hindi"),
CheckboxFilterOption("hu", "Hungarian"),
CheckboxFilterOption("is", "Icelandic"),
CheckboxFilterOption("ig", "Igbo"),
CheckboxFilterOption("id", "Indonesian"),
CheckboxFilterOption("ga", "Irish"),
CheckboxFilterOption("it", "Italian"),
CheckboxFilterOption("jv", "Javanese"),
CheckboxFilterOption("kn", "Kannada"),
CheckboxFilterOption("kk", "Kazakh"),
CheckboxFilterOption("ku", "Kurdish"),
CheckboxFilterOption("ky", "Kyrgyz"),
CheckboxFilterOption("lo", "Laothian"),
CheckboxFilterOption("lv", "Latvian"),
CheckboxFilterOption("lt", "Lithuanian"),
CheckboxFilterOption("lb", "Luxembourgish"),
CheckboxFilterOption("mk", "Macedonian"),
CheckboxFilterOption("mg", "Malagasy"),
CheckboxFilterOption("ms", "Malay"),
CheckboxFilterOption("ml", "Malayalam"),
CheckboxFilterOption("mt", "Maltese"),
CheckboxFilterOption("mi", "Maori"),
CheckboxFilterOption("mr", "Marathi"),
CheckboxFilterOption("mo", "Moldavian"),
CheckboxFilterOption("mn", "Mongolian"),
CheckboxFilterOption("ne", "Nepali"),
CheckboxFilterOption("no", "Norwegian"),
CheckboxFilterOption("ny", "Nyanja"),
CheckboxFilterOption("ps", "Pashto"),
CheckboxFilterOption("fa", "Persian"),
CheckboxFilterOption("pl", "Polish"),
CheckboxFilterOption("pt", "Portuguese"),
CheckboxFilterOption("pt_br", "Portuguese (Brazil)"),
CheckboxFilterOption("ro", "Romanian"),
CheckboxFilterOption("rm", "Romansh"),
CheckboxFilterOption("ru", "Russian"),
CheckboxFilterOption("sm", "Samoan"),
CheckboxFilterOption("sr", "Serbian"),
CheckboxFilterOption("sh", "Serbo-Croatian"),
CheckboxFilterOption("st", "Sesotho"),
CheckboxFilterOption("sn", "Shona"),
CheckboxFilterOption("sd", "Sindhi"),
CheckboxFilterOption("si", "Sinhalese"),
CheckboxFilterOption("sk", "Slovak"),
CheckboxFilterOption("sl", "Slovenian"),
CheckboxFilterOption("so", "Somali"),
CheckboxFilterOption("es", "Spanish"),
CheckboxFilterOption("es_419", "Spanish (Latin America)"),
CheckboxFilterOption("sw", "Swahili"),
CheckboxFilterOption("sv", "Swedish"),
CheckboxFilterOption("tg", "Tajik"),
CheckboxFilterOption("ta", "Tamil"),
CheckboxFilterOption("th", "Thai"),
CheckboxFilterOption("ti", "Tigrinya"),
CheckboxFilterOption("to", "Tonga"),
CheckboxFilterOption("tr", "Turkish"),
CheckboxFilterOption("tk", "Turkmen"),
CheckboxFilterOption("uk", "Ukrainian"),
CheckboxFilterOption("ur", "Urdu"),
CheckboxFilterOption("uz", "Uzbek"),
CheckboxFilterOption("vi", "Vietnamese"),
CheckboxFilterOption("yo", "Yoruba"),
CheckboxFilterOption("zu", "Zulu"),
CheckboxFilterOption("_t", "Other"),
)
private fun getGenreFilter() = listOf(
TriStateFilterOption("artbook", "Artbook"),
TriStateFilterOption("cartoon", "Cartoon"),
TriStateFilterOption("comic", "Comic"),
TriStateFilterOption("doujinshi", "Doujinshi"),
TriStateFilterOption("imageset", "Imageset"),
TriStateFilterOption("manga", "Manga"),
TriStateFilterOption("manhua", "Manhua"),
TriStateFilterOption("manhwa", "Manhwa"),
TriStateFilterOption("webtoon", "Webtoon"),
TriStateFilterOption("western", "Western"),
TriStateFilterOption("shoujo", "Shoujo(G)"),
TriStateFilterOption("shounen", "Shounen(B)"),
TriStateFilterOption("josei", "Josei(W)"),
TriStateFilterOption("seinen", "Seinen(M)"),
TriStateFilterOption("yuri", "Yuri(GL)"),
TriStateFilterOption("yaoi", "Yaoi(BL)"),
TriStateFilterOption("futa", "Futa(WL)"),
TriStateFilterOption("bara", "Bara(ML)"),
TriStateFilterOption("gore", "Gore"),
TriStateFilterOption("bloody", "Bloody"),
TriStateFilterOption("violence", "Violence"),
TriStateFilterOption("ecchi", "Ecchi"),
TriStateFilterOption("adult", "Adult"),
TriStateFilterOption("mature", "Mature"),
TriStateFilterOption("smut", "Smut"),
TriStateFilterOption("hentai", "Hentai"),
TriStateFilterOption("_4_koma", "4-Koma"),
TriStateFilterOption("action", "Action"),
TriStateFilterOption("adaptation", "Adaptation"),
TriStateFilterOption("adventure", "Adventure"),
TriStateFilterOption("age_gap", "Age Gap"),
TriStateFilterOption("aliens", "Aliens"),
TriStateFilterOption("animals", "Animals"),
TriStateFilterOption("anthology", "Anthology"),
TriStateFilterOption("beasts", "Beasts"),
TriStateFilterOption("bodyswap", "Bodyswap"),
TriStateFilterOption("cars", "cars"),
TriStateFilterOption("cheating_infidelity", "Cheating/Infidelity"),
TriStateFilterOption("childhood_friends", "Childhood Friends"),
TriStateFilterOption("college_life", "College Life"),
TriStateFilterOption("comedy", "Comedy"),
TriStateFilterOption("contest_winning", "Contest Winning"),
TriStateFilterOption("cooking", "Cooking"),
TriStateFilterOption("crime", "crime"),
TriStateFilterOption("crossdressing", "Crossdressing"),
TriStateFilterOption("delinquents", "Delinquents"),
TriStateFilterOption("dementia", "Dementia"),
TriStateFilterOption("demons", "Demons"),
TriStateFilterOption("drama", "Drama"),
TriStateFilterOption("dungeons", "Dungeons"),
TriStateFilterOption("emperor_daughte", "Emperor's Daughter"),
TriStateFilterOption("fantasy", "Fantasy"),
TriStateFilterOption("fan_colored", "Fan-Colored"),
TriStateFilterOption("fetish", "Fetish"),
TriStateFilterOption("full_color", "Full Color"),
TriStateFilterOption("game", "Game"),
TriStateFilterOption("gender_bender", "Gender Bender"),
TriStateFilterOption("genderswap", "Genderswap"),
TriStateFilterOption("ghosts", "Ghosts"),
TriStateFilterOption("gyaru", "Gyaru"),
TriStateFilterOption("harem", "Harem"),
TriStateFilterOption("harlequin", "Harlequin"),
TriStateFilterOption("historical", "Historical"),
TriStateFilterOption("horror", "Horror"),
TriStateFilterOption("incest", "Incest"),
TriStateFilterOption("isekai", "Isekai"),
TriStateFilterOption("kids", "Kids"),
TriStateFilterOption("loli", "Loli"),
TriStateFilterOption("magic", "Magic"),
TriStateFilterOption("magical_girls", "Magical Girls"),
TriStateFilterOption("martial_arts", "Martial Arts"),
TriStateFilterOption("mecha", "Mecha"),
TriStateFilterOption("medical", "Medical"),
TriStateFilterOption("military", "Military"),
TriStateFilterOption("monster_girls", "Monster Girls"),
TriStateFilterOption("monsters", "Monsters"),
TriStateFilterOption("music", "Music"),
TriStateFilterOption("mystery", "Mystery"),
TriStateFilterOption("netorare", "Netorare/NTR"),
TriStateFilterOption("ninja", "Ninja"),
TriStateFilterOption("office_workers", "Office Workers"),
TriStateFilterOption("omegaverse", "Omegaverse"),
TriStateFilterOption("oneshot", "Oneshot"),
TriStateFilterOption("parody", "parody"),
TriStateFilterOption("philosophical", "Philosophical"),
TriStateFilterOption("police", "Police"),
TriStateFilterOption("post_apocalyptic", "Post-Apocalyptic"),
TriStateFilterOption("psychological", "Psychological"),
TriStateFilterOption("regression", "Regression"),
TriStateFilterOption("reincarnation", "Reincarnation"),
TriStateFilterOption("reverse_harem", "Reverse Harem"),
TriStateFilterOption("reverse_isekai", "Reverse Isekai"),
TriStateFilterOption("romance", "Romance"),
TriStateFilterOption("royal_family", "Royal Family"),
TriStateFilterOption("royalty", "Royalty"),
TriStateFilterOption("samurai", "Samurai"),
TriStateFilterOption("school_life", "School Life"),
TriStateFilterOption("sci_fi", "Sci-Fi"),
TriStateFilterOption("shota", "Shota"),
TriStateFilterOption("shoujo_ai", "Shoujo Ai"),
TriStateFilterOption("shounen_ai", "Shounen Ai"),
TriStateFilterOption("showbiz", "Showbiz"),
TriStateFilterOption("slice_of_life", "Slice of Life"),
TriStateFilterOption("sm_bdsm", "SM/BDSM/SUB-DOM"),
TriStateFilterOption("space", "Space"),
TriStateFilterOption("sports", "Sports"),
TriStateFilterOption("super_power", "Super Power"),
TriStateFilterOption("superhero", "Superhero"),
TriStateFilterOption("supernatural", "Supernatural"),
TriStateFilterOption("survival", "Survival"),
TriStateFilterOption("thriller", "Thriller"),
TriStateFilterOption("time_travel", "Time Travel"),
TriStateFilterOption("tower_climbing", "Tower Climbing"),
TriStateFilterOption("traditional_games", "Traditional Games"),
TriStateFilterOption("tragedy", "Tragedy"),
TriStateFilterOption("transmigration", "Transmigration"),
TriStateFilterOption("vampires", "Vampires"),
TriStateFilterOption("villainess", "Villainess"),
TriStateFilterOption("video_games", "Video Games"),
TriStateFilterOption("virtual_reality", "Virtual Reality"),
TriStateFilterOption("wuxia", "Wuxia"),
TriStateFilterOption("xianxia", "Xianxia"),
TriStateFilterOption("xuanhuan", "Xuanhuan"),
TriStateFilterOption("zombies", "Zombies"),
// Hidden Genres
TriStateFilterOption("shotacon", "shotacon"),
TriStateFilterOption("lolicon", "lolicon"),
TriStateFilterOption("award_winning", "Award Winning"),
TriStateFilterOption("youkai", "Youkai"),
TriStateFilterOption("uncategorized", "Uncategorized"),
)
private fun getLangFilter() = listOf(
// Values exported from publish.bato.to
CheckboxFilterOption("en", "English"),
CheckboxFilterOption("ar", "Arabic"),
CheckboxFilterOption("bg", "Bulgarian"),
CheckboxFilterOption("zh", "Chinese"),
CheckboxFilterOption("cs", "Czech"),
CheckboxFilterOption("da", "Danish"),
CheckboxFilterOption("nl", "Dutch"),
CheckboxFilterOption("fil", "Filipino"),
CheckboxFilterOption("fi", "Finnish"),
CheckboxFilterOption("fr", "French"),
CheckboxFilterOption("de", "German"),
CheckboxFilterOption("el", "Greek"),
CheckboxFilterOption("he", "Hebrew"),
CheckboxFilterOption("hi", "Hindi"),
CheckboxFilterOption("hu", "Hungarian"),
CheckboxFilterOption("id", "Indonesian"),
CheckboxFilterOption("it", "Italian"),
CheckboxFilterOption("ja", "Japanese"),
CheckboxFilterOption("ko", "Korean"),
CheckboxFilterOption("ms", "Malay"),
CheckboxFilterOption("pl", "Polish"),
CheckboxFilterOption("pt", "Portuguese"),
CheckboxFilterOption("pt_br", "Portuguese (Brazil)"),
CheckboxFilterOption("ro", "Romanian"),
CheckboxFilterOption("ru", "Russian"),
CheckboxFilterOption("es", "Spanish"),
CheckboxFilterOption("es_419", "Spanish (Latin America)"),
CheckboxFilterOption("sv", "Swedish"),
CheckboxFilterOption("th", "Thai"),
CheckboxFilterOption("tr", "Turkish"),
CheckboxFilterOption("uk", "Ukrainian"),
CheckboxFilterOption("vi", "Vietnamese"),
CheckboxFilterOption("af", "Afrikaans"),
CheckboxFilterOption("sq", "Albanian"),
CheckboxFilterOption("am", "Amharic"),
CheckboxFilterOption("hy", "Armenian"),
CheckboxFilterOption("az", "Azerbaijani"),
CheckboxFilterOption("be", "Belarusian"),
CheckboxFilterOption("bn", "Bengali"),
CheckboxFilterOption("bs", "Bosnian"),
CheckboxFilterOption("my", "Burmese"),
CheckboxFilterOption("km", "Cambodian"),
CheckboxFilterOption("ca", "Catalan"),
CheckboxFilterOption("ceb", "Cebuano"),
CheckboxFilterOption("zh_hk", "Chinese (Cantonese)"),
CheckboxFilterOption("zh_tw", "Chinese (Traditional)"),
CheckboxFilterOption("hr", "Croatian"),
CheckboxFilterOption("en_us", "English (United States)"),
CheckboxFilterOption("eo", "Esperanto"),
CheckboxFilterOption("et", "Estonian"),
CheckboxFilterOption("fo", "Faroese"),
CheckboxFilterOption("ka", "Georgian"),
CheckboxFilterOption("gn", "Guarani"),
CheckboxFilterOption("gu", "Gujarati"),
CheckboxFilterOption("ht", "Haitian Creole"),
CheckboxFilterOption("ha", "Hausa"),
CheckboxFilterOption("is", "Icelandic"),
CheckboxFilterOption("ig", "Igbo"),
CheckboxFilterOption("ga", "Irish"),
CheckboxFilterOption("jv", "Javanese"),
CheckboxFilterOption("kn", "Kannada"),
CheckboxFilterOption("kk", "Kazakh"),
CheckboxFilterOption("ku", "Kurdish"),
CheckboxFilterOption("ky", "Kyrgyz"),
CheckboxFilterOption("lo", "Laothian"),
CheckboxFilterOption("lv", "Latvian"),
CheckboxFilterOption("lt", "Lithuanian"),
CheckboxFilterOption("lb", "Luxembourgish"),
CheckboxFilterOption("mk", "Macedonian"),
CheckboxFilterOption("mg", "Malagasy"),
CheckboxFilterOption("ml", "Malayalam"),
CheckboxFilterOption("mt", "Maltese"),
CheckboxFilterOption("mi", "Maori"),
CheckboxFilterOption("mr", "Marathi"),
CheckboxFilterOption("mo", "Moldavian"),
CheckboxFilterOption("mn", "Mongolian"),
CheckboxFilterOption("ne", "Nepali"),
CheckboxFilterOption("no", "Norwegian"),
CheckboxFilterOption("ny", "Nyanja"),
CheckboxFilterOption("ps", "Pashto"),
CheckboxFilterOption("fa", "Persian"),
CheckboxFilterOption("rm", "Romansh"),
CheckboxFilterOption("sm", "Samoan"),
CheckboxFilterOption("sr", "Serbian"),
CheckboxFilterOption("sh", "Serbo-Croatian"),
CheckboxFilterOption("st", "Sesotho"),
CheckboxFilterOption("sn", "Shona"),
CheckboxFilterOption("sd", "Sindhi"),
CheckboxFilterOption("si", "Sinhalese"),
CheckboxFilterOption("sk", "Slovak"),
CheckboxFilterOption("sl", "Slovenian"),
CheckboxFilterOption("so", "Somali"),
CheckboxFilterOption("sw", "Swahili"),
CheckboxFilterOption("tg", "Tajik"),
CheckboxFilterOption("ta", "Tamil"),
CheckboxFilterOption("ti", "Tigrinya"),
CheckboxFilterOption("to", "Tonga"),
CheckboxFilterOption("tk", "Turkmen"),
CheckboxFilterOption("ur", "Urdu"),
CheckboxFilterOption("uz", "Uzbek"),
CheckboxFilterOption("yo", "Yoruba"),
CheckboxFilterOption("zu", "Zulu"),
CheckboxFilterOption("_t", "Other"),
// Lang options from bato.to brows not in publish.bato.to
CheckboxFilterOption("eu", "Basque"),
CheckboxFilterOption("pt-PT", "Portuguese (Portugal)"),
).filterNot { it.value == siteLang }
companion object {
private const val MIRROR_PREF_KEY = "MIRROR"
private const val MIRROR_PREF_TITLE = "Mirror"
private val MIRROR_PREF_ENTRIES = arrayOf(
"bato.to",
"batocomic.com",
"batocomic.net",
"batocomic.org",
"batotoo.com",
"batotwo.com",
"battwo.com",
"comiko.net",
"comiko.org",
"mangatoto.com",
"mangatoto.net",
"mangatoto.org",
"readtoto.com",
"readtoto.net",
"readtoto.org",
"dto.to",
"hto.to",
"mto.to",
"wto.to",
"xbato.com",
"xbato.net",
"xbato.org",
"zbato.com",
"zbato.net",
"zbato.org",
)
private val MIRROR_PREF_ENTRY_VALUES = MIRROR_PREF_ENTRIES.map { "https://$it" }.toTypedArray()
private val MIRROR_PREF_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES[0]
private const val ALT_CHAPTER_LIST_PREF_KEY = "ALT_CHAPTER_LIST"
private const val ALT_CHAPTER_LIST_PREF_TITLE = "Alternative Chapter List"
private const val ALT_CHAPTER_LIST_PREF_SUMMARY = "If checked, uses an alternate chapter list"
private const val ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE = false
}
}

View File

@@ -0,0 +1,122 @@
package eu.kanade.tachiyomi.extension.all.batoto
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class BatoToFactory : SourceFactory {
override fun createSources(): List<Source> = languages.map { BatoTo(it.lang, it.siteLang) }
}
class LanguageOption(val lang: String, val siteLang: String = lang)
private val languages = listOf(
LanguageOption("all", ""),
// Lang options from publish.bato.to
LanguageOption("en"),
LanguageOption("ar"),
LanguageOption("bg"),
LanguageOption("zh"),
LanguageOption("cs"),
LanguageOption("da"),
LanguageOption("nl"),
LanguageOption("fil"),
LanguageOption("fi"),
LanguageOption("fr"),
LanguageOption("de"),
LanguageOption("el"),
LanguageOption("he"),
LanguageOption("hi"),
LanguageOption("hu"),
LanguageOption("id"),
LanguageOption("it"),
LanguageOption("ja"),
LanguageOption("ko"),
LanguageOption("ms"),
LanguageOption("pl"),
LanguageOption("pt"),
LanguageOption("pt-BR", "pt_br"),
LanguageOption("ro"),
LanguageOption("ru"),
LanguageOption("es"),
LanguageOption("es-419", "es_419"),
LanguageOption("sv"),
LanguageOption("th"),
LanguageOption("tr"),
LanguageOption("uk"),
LanguageOption("vi"),
LanguageOption("af"),
LanguageOption("sq"),
LanguageOption("am"),
LanguageOption("hy"),
LanguageOption("az"),
LanguageOption("be"),
LanguageOption("bn"),
LanguageOption("bs"),
LanguageOption("my"),
LanguageOption("km"),
LanguageOption("ca"),
LanguageOption("ceb"),
LanguageOption("zh-Hans", "zh_hk"),
LanguageOption("zh-Hant", "zh_tw"),
LanguageOption("hr"),
LanguageOption("en-US", "en_us"),
LanguageOption("eo"),
LanguageOption("et"),
LanguageOption("fo"),
LanguageOption("ka"),
LanguageOption("gn"),
LanguageOption("gu"),
LanguageOption("ht"),
LanguageOption("ha"),
LanguageOption("is"),
LanguageOption("ig"),
LanguageOption("ga"),
LanguageOption("jv"),
LanguageOption("kn"),
LanguageOption("kk"),
LanguageOption("ku"),
LanguageOption("ky"),
LanguageOption("lo"),
LanguageOption("lv"),
LanguageOption("lt"),
LanguageOption("lb"),
LanguageOption("mk"),
LanguageOption("mg"),
LanguageOption("ml"),
LanguageOption("mt"),
LanguageOption("mi"),
LanguageOption("mr"),
LanguageOption("mo", "ro-MD"),
LanguageOption("mn"),
LanguageOption("ne"),
LanguageOption("no"),
LanguageOption("ny"),
LanguageOption("ps"),
LanguageOption("fa"),
LanguageOption("rm"),
LanguageOption("sm"),
LanguageOption("sr"),
LanguageOption("sh"),
LanguageOption("st"),
LanguageOption("sn"),
LanguageOption("sd"),
LanguageOption("si"),
LanguageOption("sk"),
LanguageOption("sl"),
LanguageOption("so"),
LanguageOption("sw"),
LanguageOption("tg"),
LanguageOption("ta"),
LanguageOption("ti"),
LanguageOption("to"),
LanguageOption("tk"),
LanguageOption("ur"),
LanguageOption("uz"),
LanguageOption("yo"),
LanguageOption("zu"),
LanguageOption("other", "_t"),
// Lang options from bato.to brows not in publish.bato.to
LanguageOption("eu"),
LanguageOption("pt-PT", "pt_pt"),
// Lang options that got removed
// Pair("xh", "xh"),
)

View File

@@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.extension.all.batoto
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 BatoToUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val host = intent?.data?.host
val pathSegments = intent?.data?.pathSegments
if (host != null && pathSegments != null) {
val query = fromBatoTo(pathSegments)
if (query == null) {
Log.e("BatoToUrlActivity", "Unable to parse URI from intent $intent")
finish()
exitProcess(1)
}
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", query)
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("BatoToUrlActivity", e.toString())
}
}
finish()
exitProcess(0)
}
private fun fromBatoTo(pathSegments: MutableList<String>): String? {
return if (pathSegments.size >= 2) {
val id = pathSegments[1]
"ID:$id"
} else {
null
}
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Buon Dua'
pkgNameSuffix = 'all.buondua'
extClass = '.BuonDua'
extVersionCode = 2
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

@@ -0,0 +1,117 @@
package eu.kanade.tachiyomi.extension.all.buondua
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
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.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
class BuonDua() : ParsedHttpSource() {
override val baseUrl = "https://buondua.com"
override val lang = "all"
override val name = "Buon Dua"
override val supportsLatest = true
// Latest
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
manga.thumbnail_url = element.select("img").attr("abs:src")
manga.title = element.select(".item-content .item-link").text()
manga.setUrlWithoutDomain(element.select(".item-content .item-link").attr("abs:href"))
return manga
}
override fun latestUpdatesNextPageSelector() = ".pagination-next:not([disabled])"
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/?start=${20 * (page - 1)}")
}
override fun latestUpdatesSelector() = ".blog > div"
// Popular
override fun popularMangaFromElement(element: Element) = latestUpdatesFromElement(element)
override fun popularMangaNextPageSelector() = latestUpdatesNextPageSelector()
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/hot?start=${20 * (page - 1)}")
}
override fun popularMangaSelector() = latestUpdatesSelector()
// Search
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val tagFilter = filters.findInstance<TagFilter>()!!
return when {
query.isNotEmpty() -> GET("$baseUrl/?search=$query&start=${20 * (page - 1)}")
tagFilter.state.isNotEmpty() -> GET("$baseUrl/tag/${tagFilter.state}&start=${20 * (page - 1)}")
else -> popularMangaRequest(page)
}
}
override fun searchMangaSelector() = latestUpdatesSelector()
// Details
override fun mangaDetailsParse(document: Document): SManga {
val manga = SManga.create()
manga.title = document.select(".article-header").text()
manga.description = document.select(".article-info > strong").text().trim()
val genres = mutableListOf<String>()
document.select(".article-tags").first()!!.select(".tags > .tag").forEach {
genres.add(it.text().substringAfter("#"))
}
manga.genre = genres.joinToString(", ")
return manga
}
override fun chapterFromElement(element: Element): SChapter {
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(element.select(".is-current").first()!!.attr("abs:href"))
chapter.chapter_number = 0F
chapter.name = element.select(".article-header").text()
chapter.date_upload = SimpleDateFormat("H:m DD-MM-yyyy", Locale.US).parse(element.select(".article-info > small").text())?.time ?: 0L
return chapter
}
override fun chapterListSelector() = "html"
// Pages
override fun pageListParse(document: Document): List<Page> {
val numpages = document.selectFirst(".pagination-list")!!.select(".pagination-link")
val pages = mutableListOf<Page>()
numpages.forEachIndexed { index, page ->
val doc = when (index) {
0 -> document
else -> client.newCall(GET(page.attr("abs:href"))).execute().asJsoup()
}
doc.select(".article-fulltext img").forEach {
val itUrl = it.attr("abs:src")
pages.add(Page(pages.size, "", itUrl))
}
}
return pages
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
// Filters
override fun getFilterList(): FilterList = FilterList(
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
TagFilter(),
)
class TagFilter : Filter.Text("Tag ID")
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@@ -0,0 +1,17 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Comic Fury'
pkgNameSuffix = 'all.comicfury'
extClass = '.ComicFuryFactory'
extVersionCode = 2
isNsfw = true
}
dependencies {
implementation(project(':lib-textinterceptor'))
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -0,0 +1,291 @@
package eu.kanade.tachiyomi.extension.all.comicfury
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptor
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptorHelper
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
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 okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
class ComicFury(
override val lang: String,
private val siteLang: String = lang, // override lang string used in MangaSearch
private val extraName: String = "",
) : HttpSource(), ConfigurableSource {
override val baseUrl: String = "https://comicfury.com"
override val name: String = "Comic Fury$extraName" //Used for No Text
override val supportsLatest: Boolean = true
private val dateFormat = SimpleDateFormat("dd MMM yyyy hh:mm aa", Locale.US)
private val dateFormatSlim = SimpleDateFormat("dd MMM yyyy", Locale.US)
override val client = super.client.newBuilder().addInterceptor(TextInterceptor()).build()
/**
* Archive is on a separate page from manga info
*/
override fun chapterListRequest(manga: SManga): Request =
GET("$baseUrl/read/${manga.url.substringAfter("?url=")}/archive")
/**
* Open Archive Url instead of the details page
* Helps with getting past the nfsw pages
*/
override fun getMangaUrl(manga: SManga): String {
return "$baseUrl/read/" + manga.url.substringAfter("?url=") + "/archive"
}
/**
* There are two different ways chapters are setup
* First Way if (true)
* Manga -> Chapter -> Comic -> Pages
* The Second Way if (false)
* Manga -> Comic -> Pages
*
* Importantly the Chapter And Comic Pages can be easy distinguished
* by the name of the list elements in this case archive-chapter/archive-comic
*
* For Manga that doesn't have "chapters" skip the loop. Including All Sub-Comics of Chapters
*
* Put the chapter name into scanlator so read can know what chapter it is.
*
* Chapter Number is handled as Chapter dot Comic. Ex. Chapter 6, Comic 4: chapter_number = 6.4
*
*/
override fun chapterListParse(response: Response): List<SChapter> {
val jsp = response.asJsoup()
if (jsp.selectFirst("div.archive-chapter") != null) {
val chapters: MutableList<SChapter> = arrayListOf()
for (chapter in jsp.select("div.archive-chapter").parents().reversed()) {
val name = chapter.text()
chapters.addAll(
client.newCall(
GET("$baseUrl${chapter.attr("href")}"),
).execute()
.use { chapterListParse(it) }
.mapIndexed { i, it ->
it.apply {
scanlator = name
chapter_number += i
}
},
)
}
return chapters
} else {
return jsp.select("div.archive-comic").mapIndexed { i, it ->
SChapter.create().apply {
url = it.parent()!!.attr("href")
name = it.child(0).ownText()
date_upload = it.child(1).ownText().toDate()
chapter_number = "0.$i".toFloat()
}
}.toList().reversed()
}
}
override fun pageListParse(response: Response): List<Page> {
val jsp = response.asJsoup()
val pages: MutableList<Page> = arrayListOf()
val comic = jsp.selectFirst("div.is--comic-page")
for (child in comic!!.select("div.is--image-segment div img")) {
pages.add(
Page(
pages.size,
response.request.url.toString(),
child.attr("src"),
),
)
}
if (showAuthorsNotesPref()) {
for (child in comic.select("div.is--author-notes div.is--comment-box").withIndex()) {
pages.add(
Page(
pages.size,
response.request.url.toString(),
TextInterceptorHelper.createUrl(
jsp.selectFirst("a.is--comment-author")?.ownText()
?: "Error No Author For Comment Found",
jsp.selectFirst("div.is--comment-content")?.html()
?: "Error No Comment Content Found",
),
),
)
}
}
return pages
}
/**
* Author name joining maybe redundant.
*
* Manga Status is available but not currently implemented.
*/
override fun mangaDetailsParse(response: Response): SManga {
val jsp = response.asJsoup()
val desDiv = jsp.selectFirst("div.description-tags")
return SManga.create().apply {
setUrlWithoutDomain(response.request.url.toString())
description = desDiv?.parent()?.ownText()
genre = desDiv?.children()?.eachText()?.joinToString(", ")
author = jsp.select("a.authorname").eachText().joinToString(", ")
initialized = true
}
}
override fun searchMangaParse(response: Response): MangasPage {
val jsp = response.asJsoup()
val list: MutableList<SManga> = arrayListOf()
for (result in jsp.select("div.webcomic-result")) {
list.add(
SManga.create().apply {
url = result.selectFirst("div.webcomic-result-avatar a")!!.attr("href")
title = result.selectFirst("div.webcomic-result-title")!!.attr("title")
thumbnail_url = result.selectFirst("div.webcomic-result-avatar a img")!!.absUrl("src")
},
)
}
return MangasPage(list, (jsp.selectFirst("div.search-next-page") != null))
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val req: HttpUrl.Builder = "$baseUrl/search.php".toHttpUrl().newBuilder()
req.addQueryParameter("query", query)
req.addQueryParameter("page", page.toString())
req.addQueryParameter("language", siteLang)
filters.forEach {
when (it) {
is TagsFilter -> req.addEncodedQueryParameter(
"tags",
it.state.replace(", ", ","),
)
is SortFilter -> req.addQueryParameter("sort", it.state.toString())
is CompletedComicFilter -> req.addQueryParameter(
"completed",
it.state.toInt().toString(),
)
is LastUpdatedFilter -> req.addQueryParameter(
"lastupdate",
it.state.toString(),
)
is ViolenceFilter -> req.addQueryParameter("fv", it.state.toString())
is NudityFilter -> req.addQueryParameter("fn", it.state.toString())
is StrongLangFilter -> req.addQueryParameter("fl", it.state.toString())
is SexualFilter -> req.addQueryParameter("fs", it.state.toString())
else -> {}
}
}
return Request.Builder().url(req.build()).build()
}
// START OF AUTHOR NOTES //
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
companion object {
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
}
private fun showAuthorsNotesPref() =
preferences.getBoolean(SHOW_AUTHORS_NOTES_KEY, false)
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val authorsNotesPref = SwitchPreferenceCompat(screen.context).apply {
key = SHOW_AUTHORS_NOTES_KEY; title = "Show author's notes"
summary = "Enable to see the author's notes at the end of chapters (if they're there)."
setDefaultValue(false)
}
screen.addPreference(authorsNotesPref)
}
// END OF AUTHOR NOTES //
// START OF FILTERS //
override fun getFilterList(): FilterList = getFilterList(0)
private fun getFilterList(sortIndex: Int): FilterList = FilterList(
TagsFilter(),
Filter.Separator(),
SortFilter(sortIndex),
Filter.Separator(),
LastUpdatedFilter(),
CompletedComicFilter(),
Filter.Separator(),
Filter.Header("Flags"),
ViolenceFilter(),
NudityFilter(),
StrongLangFilter(),
SexualFilter(),
)
internal class SortFilter(index: Int) : Filter.Select<String>(
"Sort By",
arrayOf("Relevance", "Popularity", "Last Update"),
index,
)
internal class CompletedComicFilter : Filter.CheckBox("Comic Completed", false)
internal class LastUpdatedFilter : Filter.Select<String>(
"Last Updated",
arrayOf("All Time", "This Week", "This Month", "This Year", "Completed Only"),
0,
)
internal class ViolenceFilter : Filter.Select<String>(
"Violence",
arrayOf("None / Minimal", "Violent Content", "Gore / Graphic"),
2,
)
internal class NudityFilter : Filter.Select<String>(
"Frontal Nudity",
arrayOf("None", "Occasional", "Frequent"),
2,
)
internal class StrongLangFilter : Filter.Select<String>(
"Strong Language",
arrayOf("None", "Occasional", "Frequent"),
2,
)
internal class SexualFilter : Filter.Select<String>(
"Sexual Content",
arrayOf("No Sexual Content", "Sexual Situations", "Strong Sexual Themes"),
2,
)
internal class TagsFilter : Filter.Text("Tags")
// END OF FILTERS //
override fun popularMangaRequest(page: Int): Request =
searchMangaRequest(page, "", getFilterList(1))
override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
override fun latestUpdatesRequest(page: Int): Request =
searchMangaRequest(page, "", getFilterList(2))
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
override fun imageUrlParse(response: Response): String =
throw UnsupportedOperationException("Not Used")
private fun String.toDate(): Long {
val ret = this.replace("st", "")
.replace("nd", "")
.replace("rd", "")
.replace("th", "")
.replace(",", "")
return dateFormat.parse(ret)?.time ?: dateFormatSlim.parse(ret)!!.time
}
private fun Boolean.toInt(): Int = if (this) { 0 } else { 1 }
}

View File

@@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.extension.all.comicfury
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class ComicFuryFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
ComicFury("all"),
ComicFury("en"),
ComicFury("es"),
ComicFury("pt-BR", "pt"),
ComicFury("de"),
ComicFury("fr"),
ComicFury("it"),
ComicFury("pl"),
ComicFury("ja"),
ComicFury("zh"),
ComicFury("ru"),
ComicFury("fi"),
ComicFury("other"),
ComicFury("other", "notext", " (No Text)"),
)
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.comickfun.ComickFunUrlActivity"
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:scheme="https" />
<data android:host="comick.cc" />
<data android:host="comick.ink" />
<data android:host="comick.app" />
<data android:host="comick.fun" />
<data android:pathPattern="/comic/.*/..*" />
<data android:pathPattern="/comic/..*" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,13 @@
ignored_groups_title=Ignored Groups
ignored_groups_summary=Chapters from these groups won't be shown.\nOne group name per line (case-insensitive)
include_tags_title=Include Tags
include_tags_on=More specific, but might contain spoilers!
include_tags_off=Only the broader genres
update_cover_title=Update Covers
update_cover_on=Keep cover updated
update_cover_off=Prefer first cover
score_position_title=Score Position in the Description
score_position_top=Top
score_position_middle=Middle
score_position_bottom=Bottom
score_position_none=Hide Score

View File

@@ -0,0 +1,13 @@
ignored_groups_title=Grupos Ignorados
ignored_groups_summary=Capítulos desses grupos não aparecerão.\nUm grupo por linha
include_tags_title=Incluir Tags
include_tags_on=Mais detalhadas, mas podem conter spoilers
include_tags_off=Apenas os gêneros básicos
update_cover_title=Atualizar Capas
update_cover_on=Manter capas atualizadas
update_cover_off=Usar apenas a primeira capa
score_position_title=Posição da Nota na Descrição
score_position_top=Topo
score_position_middle=Meio
score_position_bottom=Final
score_position_none=Sem Nota

View File

@@ -0,0 +1,17 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Comick'
pkgNameSuffix = 'all.comickfun'
extClass = '.ComickFunFactory'
extVersionCode = 41
isNsfw = true
}
dependencies {
implementation(project(":lib-i18n"))
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -0,0 +1,497 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import kotlin.math.min
abstract class ComickFun(
override val lang: String,
private val comickFunLang: String,
) : ConfigurableSource, HttpSource() {
override val name = "Comick"
override val baseUrl = "https://comick.cc"
private val apiUrl = "https://api.comick.fun"
override val supportsLatest = true
private val json = Json {
ignoreUnknownKeys = true
isLenient = true
coerceInputValues = true
explicitNulls = true
}
private lateinit var searchResponse: List<SearchManga>
private val intl by lazy {
Intl(
language = lang,
baseLanguage = "en",
availableLanguages = setOf("en", "pt-BR"),
classLoader = this::class.java.classLoader!!,
)
}
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
EditTextPreference(screen.context).apply {
key = IGNORED_GROUPS_PREF
title = intl["ignored_groups_title"]
summary = intl["ignored_groups_summary"]
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putString(IGNORED_GROUPS_PREF, newValue.toString())
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = INCLUDE_MU_TAGS_PREF
title = intl["include_tags_title"]
summaryOn = intl["include_tags_on"]
summaryOff = intl["include_tags_off"]
setDefaultValue(INCLUDE_MU_TAGS_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(INCLUDE_MU_TAGS_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = FIRST_COVER_PREF
title = intl["update_cover_title"]
summaryOff = intl["update_cover_off"]
summaryOn = intl["update_cover_on"]
setDefaultValue(FIRST_COVER_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(FIRST_COVER_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = SCORE_POSITION_PREF
title = intl["score_position_title"]
summary = "%s"
entries = arrayOf(
intl["score_position_top"],
intl["score_position_middle"],
intl["score_position_bottom"],
intl["score_position_none"],
)
entryValues = arrayOf(SCORE_POSITION_DEFAULT, "middle", "bottom", "none")
setDefaultValue(SCORE_POSITION_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit()
.putString(SCORE_POSITION_PREF, entry)
.commit()
}
}.also(screen::addPreference)
}
private val SharedPreferences.ignoredGroups: Set<String>
get() = getString(IGNORED_GROUPS_PREF, "")
?.lowercase()
?.split("\n")
?.map(String::trim)
?.filter(String::isNotEmpty)
?.sorted()
.orEmpty()
.toSet()
private val SharedPreferences.includeMuTags: Boolean
get() = getBoolean(INCLUDE_MU_TAGS_PREF, INCLUDE_MU_TAGS_DEFAULT)
private val SharedPreferences.updateCover: Boolean
get() = getBoolean(FIRST_COVER_PREF, FIRST_COVER_DEFAULT)
private val SharedPreferences.scorePosition: String
get() = getString(SCORE_POSITION_PREF, SCORE_POSITION_DEFAULT) ?: SCORE_POSITION_DEFAULT
init {
preferences.newLineIgnoredGroups()
}
override fun headersBuilder() = Headers.Builder().apply {
add("Referer", "$baseUrl/")
add("User-Agent", "Tachiyomi ${System.getProperty("http.agent")}")
}
override val client = network.client.newBuilder()
.addInterceptor(::thumbnailIntercept)
.rateLimit(3, 1)
.build()
/** Popular Manga **/
override fun popularMangaRequest(page: Int): Request {
val url = "$apiUrl/v1.0/search?sort=follow&limit=$limit&page=$page&tachiyomi=true"
return GET(url, headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val result = response.parseAs<List<SearchManga>>()
return MangasPage(
result.map(SearchManga::toSManga),
hasNextPage = result.size >= limit,
)
}
/** Latest Manga **/
override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/v1.0/search?sort=uploaded&limit=$limit&page=$page&tachiyomi=true"
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
/** Manga Search **/
override fun fetchSearchManga(
page: Int,
query: String,
filters: FilterList,
): Observable<MangasPage> {
return if (query.startsWith(SLUG_SEARCH_PREFIX)) {
// url deep link
val slugOrHid = query.substringAfter(SLUG_SEARCH_PREFIX)
val manga = SManga.create().apply { this.url = "/comic/$slugOrHid#" }
fetchMangaDetails(manga).map {
MangasPage(listOf(it), false)
}
} else if (query.isEmpty()) {
// regular filtering without text search
client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map(::searchMangaParse)
} else {
// text search, no pagination in api
if (page == 1) {
client.newCall(querySearchRequest(query))
.asObservableSuccess()
.map(::querySearchParse)
} else {
Observable.just(paginatedSearchPage(page))
}
}
}
private fun querySearchRequest(query: String): Request {
val url = "$apiUrl/v1.0/search?limit=300&page=1&tachiyomi=true"
.toHttpUrl().newBuilder()
.addQueryParameter("q", query.trim())
.build()
return GET(url, headers)
}
private fun querySearchParse(response: Response): MangasPage {
searchResponse = response.parseAs()
return paginatedSearchPage(1)
}
private fun paginatedSearchPage(page: Int): MangasPage {
val end = min(page * limit, searchResponse.size)
val entries = searchResponse.subList((page - 1) * limit, end)
.map(SearchManga::toSManga)
return MangasPage(entries, end < searchResponse.size)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/v1.0/search".toHttpUrl().newBuilder().apply {
filters.forEach { it ->
when (it) {
is CompletedFilter -> {
if (it.state) {
addQueryParameter("completed", "true")
}
}
is GenreFilter -> {
it.state.filter { it.isIncluded() }.forEach {
addQueryParameter("genres", it.value)
}
it.state.filter { it.isExcluded() }.forEach {
addQueryParameter("excludes", it.value)
}
}
is DemographicFilter -> {
it.state.filter { it.isIncluded() }.forEach {
addQueryParameter("demographic", it.value)
}
}
is TypeFilter -> {
it.state.filter { it.state }.forEach {
addQueryParameter("country", it.value)
}
}
is SortFilter -> {
addQueryParameter("sort", it.getValue())
}
is StatusFilter -> {
if (it.state > 0) {
addQueryParameter("status", it.getValue())
}
}
is CreatedAtFilter -> {
if (it.state > 0) {
addQueryParameter("time", it.getValue())
}
}
is MinimumFilter -> {
if (it.state.isNotEmpty()) {
addQueryParameter("minimum", it.state)
}
}
is FromYearFilter -> {
if (it.state.isNotEmpty()) {
addQueryParameter("from", it.state)
}
}
is ToYearFilter -> {
if (it.state.isNotEmpty()) {
addQueryParameter("to", it.state)
}
}
is TagFilter -> {
if (it.state.isNotEmpty()) {
it.state.split(",").forEach {
addQueryParameter("tags", it.trim())
}
}
}
else -> {}
}
}
addQueryParameter("tachiyomi", "true")
addQueryParameter("limit", "$limit")
addQueryParameter("page", "$page")
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
/** Manga Details **/
override fun mangaDetailsRequest(manga: SManga): Request {
// Migration from slug based urls to hid based ones
if (!manga.url.endsWith("#")) {
throw Exception("Migrate from Comick to Comick")
}
val mangaUrl = manga.url.removeSuffix("#")
return GET("$apiUrl$mangaUrl?tachiyomi=true", headers)
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.map { response ->
mangaDetailsParse(response, manga).apply { initialized = true }
}
}
override fun mangaDetailsParse(response: Response): SManga =
mangaDetailsParse(response, SManga.create())
private fun mangaDetailsParse(response: Response, manga: SManga): SManga {
val mangaData = response.parseAs<Manga>()
if (!preferences.updateCover && manga.thumbnail_url != mangaData.comic.cover) {
if (manga.thumbnail_url.toString().endsWith("#1")) {
return mangaData.toSManga(
includeMuTags = preferences.includeMuTags,
scorePosition = preferences.scorePosition,
covers = listOf(
MDcovers(
b2key = manga.thumbnail_url?.substringBeforeLast("#")
?.substringAfterLast("/"),
vol = "1",
),
),
)
}
val coversUrl =
"$apiUrl/comic/${mangaData.comic.slug ?: mangaData.comic.hid}/covers?tachiyomi=true"
val covers = client.newCall(GET(coversUrl)).execute()
.parseAs<Covers>().md_covers.reversed()
return mangaData.toSManga(
includeMuTags = preferences.includeMuTags,
covers = if (covers.any { it.vol == "1" }) covers.filter { it.vol == "1" } else covers,
)
}
return mangaData.toSManga(includeMuTags = preferences.includeMuTags)
}
override fun getMangaUrl(manga: SManga): String {
return "$baseUrl${manga.url.removeSuffix("#")}"
}
/** Manga Chapter List **/
override fun chapterListRequest(manga: SManga): Request {
// Migration from slug based urls to hid based ones
if (!manga.url.endsWith("#")) {
throw Exception("Migrate from Comick to Comick")
}
return paginatedChapterListRequest(manga.url.removeSuffix("#"), 1)
}
private fun paginatedChapterListRequest(mangaUrl: String, page: Int): Request {
return GET(
"$apiUrl$mangaUrl".toHttpUrl().newBuilder().apply {
addPathSegment("chapters")
if (comickFunLang != "all") addQueryParameter("lang", comickFunLang)
addQueryParameter("tachiyomi", "true")
addQueryParameter("page", "$page")
}.build(),
headers,
)
}
override fun chapterListParse(response: Response): List<SChapter> {
val chapterListResponse = response.parseAs<ChapterList>()
val mangaUrl = response.request.url.toString()
.substringBefore("/chapters")
.substringAfter(apiUrl)
var resultSize = chapterListResponse.chapters.size
var page = 2
while (chapterListResponse.total > resultSize) {
val newRequest = paginatedChapterListRequest(mangaUrl, page)
val newResponse = client.newCall(newRequest).execute()
val newChapterListResponse = newResponse.parseAs<ChapterList>()
chapterListResponse.chapters += newChapterListResponse.chapters
resultSize += newChapterListResponse.chapters.size
page += 1
}
return chapterListResponse.chapters
.filter {
it.groups.map { g -> g.lowercase() }.intersect(preferences.ignoredGroups).isEmpty()
}
.map { it.toSChapter(mangaUrl) }
}
override fun getChapterUrl(chapter: SChapter): String {
return "$baseUrl${chapter.url}"
}
/** Chapter Pages **/
override fun pageListRequest(chapter: SChapter): Request {
val chapterHid = chapter.url.substringAfterLast("/").substringBefore("-")
return GET("$apiUrl/chapter/$chapterHid?tachiyomi=true", headers)
}
override fun pageListParse(response: Response): List<Page> {
val result = response.parseAs<PageList>()
return result.chapter.images.mapIndexedNotNull { index, data ->
if (data.url == null) null else Page(index = index, imageUrl = data.url)
}
}
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException("Not used")
}
override fun getFilterList() = getFilters()
private fun SharedPreferences.newLineIgnoredGroups() {
if (getBoolean(MIGRATED_IGNORED_GROUPS, false)) return
val ignoredGroups = getString(IGNORED_GROUPS_PREF, "").orEmpty()
edit()
.putString(
IGNORED_GROUPS_PREF,
ignoredGroups
.split(",")
.map(String::trim)
.filter(String::isNotEmpty)
.joinToString("\n"),
)
.putBoolean(MIGRATED_IGNORED_GROUPS, true)
.apply()
}
companion object {
const val SLUG_SEARCH_PREFIX = "id:"
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
private const val INCLUDE_MU_TAGS_DEFAULT = false
private const val MIGRATED_IGNORED_GROUPS = "MigratedIgnoredGroups"
private const val FIRST_COVER_PREF = "DefaultCover"
private const val FIRST_COVER_DEFAULT = true
private const val SCORE_POSITION_PREF = "ScorePosition"
private const val SCORE_POSITION_DEFAULT = "top"
private const val limit = 20
val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}
val markdownLinksRegex = "\\[([^]]+)]\\(([^)]+)\\)".toRegex()
val markdownItalicBoldRegex = "\\*+\\s*([^*]*)\\s*\\*+".toRegex()
val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex()
}
}

View File

@@ -0,0 +1,200 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.math.BigDecimal
import java.math.RoundingMode
@Serializable
data class SearchManga(
val hid: String,
val title: String,
@SerialName("md_covers") val mdCovers: List<MDcovers> = emptyList(),
@SerialName("cover_url") val cover: String? = null,
) {
fun toSManga() = SManga.create().apply {
// appending # at end as part of migration from slug to hid
url = "/comic/$hid#"
title = this@SearchManga.title
thumbnail_url = parseCover(cover, mdCovers)
}
}
@Serializable
data class Manga(
val comic: Comic,
val artists: List<Name> = emptyList(),
val authors: List<Name> = emptyList(),
val genres: List<Name> = emptyList(),
val demographic: String? = null,
) {
fun toSManga(
includeMuTags: Boolean = false,
scorePosition: String = "",
covers: List<MDcovers>? = null,
) =
SManga.create().apply {
// appennding # at end as part of migration from slug to hid
url = "/comic/${comic.hid}#"
title = comic.title
description = buildString {
if (scorePosition == "top") append(comic.fancyScore)
val desc = comic.desc?.beautifyDescription()
if (!desc.isNullOrEmpty()) {
if (this.isNotEmpty()) append("\n\n")
append(desc)
}
if (scorePosition == "middle") {
if (this.isNotEmpty()) append("\n\n")
append(comic.fancyScore)
}
if (comic.altTitles.isNotEmpty()) {
if (this.isNotEmpty()) append("\n\n")
append("Alternative Titles:\n")
append(
comic.altTitles.mapNotNull { title ->
title.title?.let { "$it" }
}.joinToString("\n"),
)
}
if (scorePosition == "bottom") {
if (this.isNotEmpty()) append("\n\n")
append(comic.fancyScore)
}
}
status = comic.status.parseStatus(comic.translationComplete)
thumbnail_url = parseCover(
comic.cover,
covers ?: comic.mdCovers,
)
artist = artists.joinToString { it.name.trim() }
author = authors.joinToString { it.name.trim() }
genre = buildList {
comic.origination?.let(::add)
demographic?.let { add(Name(it)) }
addAll(genres)
addAll(comic.mdGenres.mapNotNull { it.name })
if (includeMuTags) {
comic.muGenres.categories.forEach { category ->
category?.category?.title?.let { add(Name(it)) }
}
}
}
.distinctBy { it.name }
.filter { it.name.isNotBlank() }
.joinToString { it.name.trim() }
}
}
@Serializable
data class Comic(
val hid: String,
val title: String,
val country: String? = null,
val slug: String? = null,
@SerialName("md_titles") val altTitles: List<Title> = emptyList(),
val desc: String? = null,
val status: Int? = 0,
@SerialName("translation_completed") val translationComplete: Boolean? = true,
@SerialName("md_covers") val mdCovers: List<MDcovers> = emptyList(),
@SerialName("cover_url") val cover: String? = null,
@SerialName("md_comic_md_genres") val mdGenres: List<MdGenres>,
@SerialName("mu_comics") val muGenres: MuComicCategories = MuComicCategories(emptyList()),
@SerialName("bayesian_rating") val score: String? = null,
) {
val origination = when (country) {
"jp" -> Name("Manga")
"kr" -> Name("Manhwa")
"cn" -> Name("Manhua")
else -> null
}
val fancyScore: String = if (score.isNullOrEmpty()) {
""
} else {
val stars = score.toBigDecimal().div(BigDecimal(2))
.setScale(0, RoundingMode.HALF_UP).toInt()
buildString {
append("".repeat(stars))
if (stars < 5) append("".repeat(5 - stars))
append(" $score")
}
}
}
@Serializable
data class MdGenres(
@SerialName("md_genres") val name: Name? = null,
)
@Serializable
data class MuComicCategories(
@SerialName("mu_comic_categories") val categories: List<MuCategories?> = emptyList(),
)
@Serializable
data class MuCategories(
@SerialName("mu_categories") val category: Title? = null,
)
@Serializable
data class Covers(
val md_covers: List<MDcovers> = emptyList(),
)
@Serializable
data class MDcovers(
val b2key: String?,
val vol: String? = null,
)
@Serializable
data class Title(
val title: String?,
)
@Serializable
data class Name(
val name: String,
)
@Serializable
data class ChapterList(
val chapters: MutableList<Chapter>,
val total: Int,
)
@Serializable
data class Chapter(
val hid: String,
val lang: String = "",
val title: String = "",
@SerialName("created_at") val createdAt: String = "",
val chap: String = "",
val vol: String = "",
@SerialName("group_name") val groups: List<String> = emptyList(),
) {
fun toSChapter(mangaUrl: String) = SChapter.create().apply {
url = "$mangaUrl/$hid-chapter-$chap-$lang"
name = beautifyChapterName(vol, chap, title)
date_upload = createdAt.parseDate()
scanlator = groups.joinToString().takeUnless { it.isBlank() } ?: "Unknown"
}
}
@Serializable
data class PageList(
val chapter: ChapterPageData,
)
@Serializable
data class ChapterPageData(
val images: List<Page>,
)
@Serializable
data class Page(
val url: String? = null,
)

View File

@@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
// A legacy mapping of language codes to ensure that source IDs don't change
val legacyLanguageMappings = mapOf(
"pt-br" to "pt-BR", // Brazilian Portuguese
"zh-hk" to "zh-Hant", // Traditional Chinese,
"zh" to "zh-Hans", // Simplified Chinese
).withDefault { it } // country code matches language code
class ComickFunFactory : SourceFactory {
private val idMap = listOf(
"all" to 982606170401027267,
"en" to 2971557565147974499,
"pt-br" to 8729626158695297897,
"ru" to 5846182885417171581,
"fr" to 9126078936214680667,
"es-419" to 3182432228546767958,
"pl" to 7005108854993254607,
"tr" to 7186425300860782365,
"it" to 8807318985460553537,
"es" to 9052019484488287695,
"id" to 5506707690027487154,
"hu" to 7838940669485160901,
"vi" to 9191587139933034493,
"zh-hk" to 3140511316190656180,
"ar" to 8266599095155001097,
"de" to 7552236568334706863,
"zh" to 1071494508319622063,
"ca" to 2159382907508433047,
"bg" to 8981320463367739957,
"th" to 4246541831082737053,
"fa" to 3146252372540608964,
"uk" to 3505068018066717349,
"mn" to 2147260678391898600,
"ro" to 6676949771764486043,
"he" to 5354540502202034685,
"ms" to 4731643595200952045,
"tl" to 8549617092958820123,
"ja" to 8288710818308434509,
"hi" to 5176570178081213805,
"my" to 9199495862098963317,
"ko" to 3493720175703105662,
"cs" to 2651978322082769022,
"pt" to 4153491877797434408,
"nl" to 6104206360977276112,
"sv" to 979314012722687145,
"bn" to 3598159956413889411,
"no" to 5932005504194733317,
"lt" to 1792260331167396074,
"el" to 6190162673651111756,
"sr" to 571668187470919545,
"da" to 7137437402245830147,
).toMap()
override fun createSources(): List<Source> = idMap.keys.map {
object : ComickFun(legacyLanguageMappings.getValue(it), it) {
override val id: Long = idMap[it]!!
}
}
}

View File

@@ -0,0 +1,192 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
Filter.Header(name = "The filter is ignored when using text search."),
GenreFilter("Genre", getGenresList),
DemographicFilter("Demographic", getDemographicList),
TypeFilter("Type", getTypeList),
SortFilter("Sort", getSortsList),
StatusFilter("Status", getStatusList),
CompletedFilter("Completely Scanlated?"),
CreatedAtFilter("Created at", getCreatedAtList),
MinimumFilter("Minimum Chapters"),
Filter.Header("From Year, ex: 2010"),
FromYearFilter("From"),
Filter.Header("To Year, ex: 2021"),
ToYearFilter("To"),
Filter.Header("Separate tags with commas"),
TagFilter("Tags"),
)
}
/** Filters **/
internal class GenreFilter(name: String, genreList: List<Pair<String, String>>) :
Filter.Group<TriFilter>(name, genreList.map { TriFilter(it.first, it.second) })
internal class TagFilter(name: String) : TextFilter(name)
internal class DemographicFilter(name: String, demographicList: List<Pair<String, String>>) :
Filter.Group<TriFilter>(name, demographicList.map { TriFilter(it.first, it.second) })
internal class TypeFilter(name: String, typeList: List<Pair<String, String>>) :
Filter.Group<CheckBoxFilter>(name, typeList.map { CheckBoxFilter(it.first, it.second) })
internal class CompletedFilter(name: String) : CheckBoxFilter(name)
internal class CreatedAtFilter(name: String, createdAtList: List<Pair<String, String>>) :
SelectFilter(name, createdAtList)
internal class MinimumFilter(name: String) : TextFilter(name)
internal class FromYearFilter(name: String) : TextFilter(name)
internal class ToYearFilter(name: String) : TextFilter(name)
internal class SortFilter(name: String, sortList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, sortList, state)
internal class StatusFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, statusList, state)
/** Generics **/
internal open class TriFilter(name: String, val value: String) : Filter.TriState(name)
internal open class TextFilter(name: String) : Filter.Text(name)
internal open class CheckBoxFilter(name: String, val value: String = "") : Filter.CheckBox(name)
internal open class SelectFilter(name: String, private val vals: List<Pair<String, String>>, state: Int = 0) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
fun getValue() = vals[state].second
}
/** Filters Data **/
private val getGenresList: List<Pair<String, String>> = listOf(
Pair("4-Koma", "4-koma"),
Pair("Action", "action"),
Pair("Adaptation", "adaptation"),
Pair("Adult", "adult"),
Pair("Adventure", "adventure"),
Pair("Aliens", "aliens"),
Pair("Animals", "animals"),
Pair("Anthology", "anthology"),
Pair("Award Winning", "award-winning"),
Pair("Comedy", "comedy"),
Pair("Cooking", "cooking"),
Pair("Crime", "crime"),
Pair("Crossdressing", "crossdressing"),
Pair("Delinquents", "delinquents"),
Pair("Demons", "demons"),
Pair("Doujinshi", "doujinshi"),
Pair("Drama", "drama"),
Pair("Ecchi", "ecchi"),
Pair("Fan Colored", "fan-colored"),
Pair("Fantasy", "fantasy"),
Pair("Full Color", "full-color"),
Pair("Gender Bender", "gender-bender"),
Pair("Genderswap", "genderswap"),
Pair("Ghosts", "ghosts"),
Pair("Gore", "gore"),
Pair("Gyaru", "gyaru"),
Pair("Harem", "harem"),
Pair("Historical", "historical"),
Pair("Horror", "horror"),
Pair("Incest", "incest"),
Pair("Isekai", "isekai"),
Pair("Loli", "loli"),
Pair("Long Strip", "long-strip"),
Pair("Mafia", "mafia"),
Pair("Magic", "magic"),
Pair("Magical Girls", "magical-girls"),
Pair("Martial Arts", "martial-arts"),
Pair("Mature", "mature"),
Pair("Mecha", "mecha"),
Pair("Medical", "medical"),
Pair("Military", "military"),
Pair("Monster Girls", "monster-girls"),
Pair("Monsters", "monsters"),
Pair("Music", "music"),
Pair("Mystery", "mystery"),
Pair("Ninja", "ninja"),
Pair("Office Workers", "office-workers"),
Pair("Official Colored", "official-colored"),
Pair("Oneshot", "oneshot"),
Pair("Philosophical", "philosophical"),
Pair("Police", "police"),
Pair("Post-Apocalyptic", "post-apocalyptic"),
Pair("Psychological", "psychological"),
Pair("Reincarnation", "reincarnation"),
Pair("Reverse Harem", "reverse-harem"),
Pair("Romance", "romance"),
Pair("Samurai", "samurai"),
Pair("School Life", "school-life"),
Pair("Sci-Fi", "sci-fi"),
Pair("Sexual Violence", "sexual-violence"),
Pair("Shota", "shota"),
Pair("Shoujo Ai", "shoujo-ai"),
Pair("Shounen Ai", "shounen-ai"),
Pair("Slice of Life", "slice-of-life"),
Pair("Smut", "smut"),
Pair("Sports", "sports"),
Pair("Superhero", "superhero"),
Pair("Supernatural", "supernatural"),
Pair("Survival", "survival"),
Pair("Thriller", "thriller"),
Pair("Time Travel", "time-travel"),
Pair("Traditional Games", "traditional-games"),
Pair("Tragedy", "tragedy"),
Pair("User Created", "user-created"),
Pair("Vampires", "vampires"),
Pair("Video Games", "video-games"),
Pair("Villainess", "villainess"),
Pair("Virtual Reality", "virtual-reality"),
Pair("Web Comic", "web-comic"),
Pair("Wuxia", "wuxia"),
Pair("Yaoi", "yaoi"),
Pair("Yuri", "yuri"),
Pair("Zombies", "zombies"),
)
private val getDemographicList: List<Pair<String, String>> = listOf(
Pair("Shounen", "1"),
Pair("Shoujo", "2"),
Pair("Seinen", "3"),
Pair("Josei", "4"),
)
private val getTypeList: List<Pair<String, String>> = listOf(
Pair("Manga", "jp"),
Pair("Manhwa", "kr"),
Pair("Manhua", "cn"),
)
private val getCreatedAtList: List<Pair<String, String>> = listOf(
Pair("", ""),
Pair("3 days", "3"),
Pair("7 days", "7"),
Pair("30 days", "30"),
Pair("3 months", "90"),
Pair("6 months", "180"),
Pair("1 year", "365"),
)
private val getSortsList: List<Pair<String, String>> = listOf(
Pair("Most popular", "follow"),
Pair("Most follows", "user_follow_count"),
Pair("Most views", "view"),
Pair("High rating", "rating"),
Pair("Last updated", "uploaded"),
Pair("Newest", "created_at"),
)
private val getStatusList: List<Pair<String, String>> = listOf(
Pair("All", "0"),
Pair("Ongoing", "1"),
Pair("Completed", "2"),
Pair("Cancelled", "3"),
Pair("Hiatus", "4"),
)

View File

@@ -0,0 +1,81 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.extension.all.comickfun.ComickFun.Companion.dateFormat
import eu.kanade.tachiyomi.extension.all.comickfun.ComickFun.Companion.markdownItalicBoldRegex
import eu.kanade.tachiyomi.extension.all.comickfun.ComickFun.Companion.markdownItalicRegex
import eu.kanade.tachiyomi.extension.all.comickfun.ComickFun.Companion.markdownLinksRegex
import eu.kanade.tachiyomi.source.model.SManga
import okhttp3.Interceptor
import okhttp3.Response
import org.jsoup.parser.Parser
internal fun String.beautifyDescription(): String {
return Parser.unescapeEntities(this, false)
.substringBefore("---")
.replace(markdownLinksRegex, "")
.replace(markdownItalicBoldRegex, "")
.replace(markdownItalicRegex, "")
.trim()
}
internal fun Int?.parseStatus(translationComplete: Boolean?): Int {
return when (this) {
1 -> SManga.ONGOING
2 -> {
if (translationComplete == true) {
SManga.COMPLETED
} else {
SManga.PUBLISHING_FINISHED
}
}
3 -> SManga.CANCELLED
4 -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
internal fun parseCover(thumbnailUrl: String?, mdCovers: List<MDcovers>): String? {
val b2key = mdCovers.firstOrNull()?.b2key
?: return thumbnailUrl
val vol = mdCovers.firstOrNull()?.vol.orEmpty()
return thumbnailUrl?.replaceAfterLast("/", "$b2key#$vol")
}
internal fun thumbnailIntercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val frag = request.url.fragment
if (frag.isNullOrEmpty()) return chain.proceed(request)
val response = chain.proceed(request)
if (!response.isSuccessful && response.code == 404) {
response.close()
val url = request.url.toString()
.replaceAfterLast("/", frag)
return chain.proceed(
request.newBuilder()
.url(url)
.build(),
)
}
return response
}
internal fun beautifyChapterName(vol: String, chap: String, title: String): String {
return buildString {
if (vol.isNotEmpty()) {
if (chap.isEmpty()) append("Volume $vol") else append("Vol. $vol")
}
if (chap.isNotEmpty()) {
if (vol.isEmpty()) append("Chapter $chap") else append(", Ch. $chap")
}
if (title.isNotEmpty()) {
if (chap.isEmpty()) append(title) else append(": $title")
}
}
}
internal fun String.parseDate(): Long {
return runCatching { dateFormat.parse(this)?.time }
.getOrNull() ?: 0L
}

View File

@@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.extension.all.comickfun
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 ComickFunUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val slug = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${ComickFun.SLUG_SEARCH_PREFIX}$slug")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("ComickFunUrlActivity", e.toString())
}
} else {
Log.e("ComickFunUrlActivity", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,17 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Comico'
pkgNameSuffix = 'all.comico'
extClass = '.ComicoFactory'
extVersionCode = 5
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib-cryptoaes'))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,258 @@
package eu.kanade.tachiyomi.extension.all.comico
import android.webkit.CookieManager
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
open class Comico(
final override val baseUrl: String,
final override val name: String,
private val langCode: String,
) : HttpSource() {
final override val supportsLatest = true
override val lang = langCode.substring(0, 2)
protected open val apiUrl = baseUrl.replace("www", "api")
private val json by injectLazy<Json>()
private val cookieManager by lazy { CookieManager.getInstance() }
private val imgHeaders by lazy {
headersBuilder().set("Accept", ACCEPT_IMAGE).build()
}
private val apiHeaders: Headers
get() = headersBuilder().apply {
val time = System.currentTimeMillis() / 1000L
this["X-comico-request-time"] = time.toString()
this["X-comico-check-sum"] = sha256(time)
this["X-comico-client-immutable-uid"] = ANON_IP
this["X-comico-client-accept-mature"] = "Y"
this["X-comico-client-platform"] = "web"
this["X-comico-client-store"] = "other"
this["X-comico-client-os"] = "aos"
this["Origin"] = baseUrl
}.build()
override val client = network.client.newBuilder()
.cookieJar(
object : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) =
cookies.filter { it.matches(url) }.forEach {
cookieManager.setCookie(url.toString(), it.toString())
}
override fun loadForRequest(url: HttpUrl) =
cookieManager.getCookie(url.toString())?.split("; ")
?.mapNotNull { Cookie.parse(url, it) } ?: emptyList()
},
).build()
override fun headersBuilder() = Headers.Builder()
.set("Accept-Language", langCode)
.set("User-Agent", userAgent)
.set("Referer", "$baseUrl/")
override fun latestUpdatesRequest(page: Int) =
paginate("all_comic/daily/$day", page)
override fun popularMangaRequest(page: Int) =
paginate("all_comic/ranking/trending", page)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
if (query.isEmpty()) {
paginate("all_comic/read_for_free", page)
} else {
POST("$apiUrl/search", apiHeaders, search(query, page))
}
override fun chapterListRequest(manga: SManga) =
GET(apiUrl + manga.url + "/episode", apiHeaders)
override fun pageListRequest(chapter: SChapter) =
GET(apiUrl + chapter.url, apiHeaders)
override fun imageRequest(page: Page) =
GET(page.imageUrl!!, imgHeaders)
override fun latestUpdatesParse(response: Response) =
popularMangaParse(response)
override fun popularMangaParse(response: Response): MangasPage {
val data = response.data
val hasNext = data["page"]["hasNext"]
val mangas = data.map<ContentInfo, SManga>("contents") {
SManga.create().apply {
title = it.name
url = "/comic/${it.id}"
thumbnail_url = it.cover
description = it.description
status = when (it.status) {
"completed" -> SManga.COMPLETED
else -> SManga.ONGOING
}
author = it.authors?.filter { it.isAuthor }?.joinToString()
artist = it.authors?.filter { it.isArtist }?.joinToString()
genre = buildString {
it.genres?.joinTo(this)
if (it.mature) append(", Mature")
if (it.original) append(", Original")
if (it.exclusive) append(", Exclusive")
}
}
}
return MangasPage(mangas, hasNext.jsonPrimitive.boolean)
}
override fun searchMangaParse(response: Response) =
popularMangaParse(response)
override fun chapterListParse(response: Response): List<SChapter> {
val content = response.data["episode"]["content"]
val id = content["id"].jsonPrimitive.int
return content.map<Chapter, SChapter>("chapters") {
SChapter.create().apply {
chapter_number = it.id.toFloat()
url = "/comic/$id/chapter/${it.id}/product"
name = it.name + if (it.isAvailable) "" else LOCK
date_upload = dateFormat.parse(it.publishedAt)?.time ?: 0L
}
}.reversed()
}
override fun pageListParse(response: Response) =
response.data["chapter"].map<ChapterImage, Page>("images") {
Page(it.sort, "", it.url.decrypt() + "?" + it.parameter)
}
override fun fetchMangaDetails(manga: SManga) =
rx.Observable.just(manga.apply { initialized = true })!!
override fun fetchPageList(chapter: SChapter) =
if (!chapter.name.endsWith(LOCK)) {
super.fetchPageList(chapter)
} else {
throw Error("You are not authorized to view this!")
}
private fun search(query: String, page: Int) =
FormBody.Builder().add("query", query)
.add("pageNo", (page - 1).toString())
.add("pageSize", "25").build()
private fun paginate(route: String, page: Int) =
GET("$apiUrl/$route?pageNo=${page - 1}&pageSize=25", apiHeaders)
private fun String.decrypt() =
CryptoAES.decrypt(this, keyBytes, ivBytes)
private val Response.data: JsonElement?
get() = json.parseToJsonElement(body.string()).jsonObject.also {
val code = it["result"]["code"].jsonPrimitive.int
if (code != 200) throw Error(status(code))
}["data"]
private operator fun JsonElement?.get(key: String) =
this!!.jsonObject[key]!!
private inline fun <reified T, R> JsonElement?.map(
key: String,
transform: (T) -> R,
) = json.decodeFromJsonElement<List<T>>(this[key]).map(transform)
override fun mangaDetailsParse(response: Response) =
throw UnsupportedOperationException("Not used")
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException("Not used")
companion object {
private const val ANON_IP = "0.0.0.0"
private const val LOCK = " \uD83D\uDD12"
private const val ISO_DATE = "yyyy-MM-dd'T'HH:mm:ss'Z'"
private const val WEB_KEY = "9241d2f090d01716feac20ae08ba791a"
private const val AES_KEY = "a7fc9dc89f2c873d79397f8a0028a4cd"
private val keyBytes = AES_KEY.toByteArray(Charsets.UTF_8)
private val ivBytes = ByteArray(16) // Zero filled array as IV
private const val ACCEPT_IMAGE =
"image/avif,image/jxl,image/webp,image/*,*/*"
private val userAgent = System.getProperty("http.agent")!!
private val dateFormat = SimpleDateFormat(ISO_DATE, Locale.ROOT)
private val SHA256 = MessageDigest.getInstance("SHA-256")
private val day by lazy {
when (Calendar.getInstance()[Calendar.DAY_OF_WEEK]) {
Calendar.SUNDAY -> "sunday"
Calendar.MONDAY -> "monday"
Calendar.TUESDAY -> "tuesday"
Calendar.WEDNESDAY -> "wednesday"
Calendar.THURSDAY -> "thursday"
Calendar.FRIDAY -> "friday"
Calendar.SATURDAY -> "saturday"
else -> "completed"
}
}
fun sha256(timestamp: Long) = buildString(64) {
SHA256.digest((WEB_KEY + ANON_IP + timestamp).toByteArray())
.joinTo(this, "") { "%02x".format(it) }
SHA256.reset()
}
private fun status(code: Int) = when (code) {
400 -> "Bad Request"
401 -> "Unauthorized"
402 -> "Payment Required"
403 -> "Forbidden"
404 -> "Not Found"
408 -> "Request Timeout"
409 -> "Conflict"
410 -> "DormantAccount"
417 -> "Expectation Failed"
426 -> "Upgrade Required"
428 -> "성인 on/off 권한"
429 -> "Too Many Requests"
500 -> "Internal Server Error"
503 -> "Service Unavailable"
451 -> "성인 인증"
else -> "Error $code"
}
}
}

View File

@@ -0,0 +1,19 @@
package eu.kanade.tachiyomi.extension.all.comico
import eu.kanade.tachiyomi.source.SourceFactory
class ComicoFactory : SourceFactory {
open class PocketComics(langCode: String) :
Comico("https://www.pocketcomics.com", "POCKET COMICS", langCode)
class ComicoJP : Comico("https://www.comico.jp", "コミコ", "ja-JP")
class ComicoKR : Comico("https://www.comico.kr", "코미코", "ko-KR")
override fun createSources() = listOf(
PocketComics("en-US"),
PocketComics("zh-TW"),
ComicoJP(),
ComicoKR(),
)
}

View File

@@ -0,0 +1,75 @@
package eu.kanade.tachiyomi.extension.all.comico
import kotlinx.serialization.Serializable
@Serializable
data class ContentInfo(
val id: Int,
val name: String,
val description: String,
val original: Boolean,
val exclusive: Boolean,
val mature: Boolean,
val status: String? = null,
val genres: List<Genre>? = null,
val authors: List<Author>? = null,
private val thumbnails: List<Thumbnail>,
) {
val cover: String
get() = thumbnails[0].toString()
}
@Serializable
data class Thumbnail(private val url: String) {
override fun toString() = url
}
@Serializable
data class Author(private val name: String, private val role: String) {
val isAuthor: Boolean
get() = role == "creator" ||
role == "writer" ||
role == "original_creator"
val isArtist: Boolean
get() = role == "creator" ||
role == "artist" ||
role == "studio" ||
role == "assistant"
override fun toString() = name
}
@Serializable
data class Genre(private val name: String) {
override fun toString() = name
}
@Serializable
data class Chapter(
val id: Int,
val name: String,
val publishedAt: String,
private val salesConfig: SalesConfig,
private val hasTrial: Boolean,
private val activity: Activity,
) {
val isAvailable: Boolean
get() = salesConfig.free || hasTrial || activity.owned
}
@Serializable
data class SalesConfig(val free: Boolean)
@Serializable
data class Activity(val rented: Boolean, val unlocked: Boolean) {
inline val owned: Boolean
get() = rented || unlocked
}
@Serializable
data class ChapterImage(
val sort: Int,
val url: String,
val parameter: String,
)

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Commit Strip'
pkgNameSuffix = 'all.commitstrip'
extClass = '.CommitStripFactory'
extVersionCode = 3
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

View File

@@ -0,0 +1,192 @@
package eu.kanade.tachiyomi.extension.all.commitstrip
import eu.kanade.tachiyomi.network.GET
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.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
abstract class CommitStrip(
override val lang: String,
private val siteLang: String,
) : ParsedHttpSource() {
override val name = "Commit Strip"
override val baseUrl = "https://www.commitstrip.com"
override val supportsLatest = false
// Helper
private fun createManga(year: Int): SManga = SManga.create().apply {
url = "$baseUrl/$siteLang/$year"
title = "$name ($year)"
thumbnail_url = when (lang) {
"en" -> LOGO_EN
"fr" -> LOGO_FR
else -> LOGO_EN
}
author = when (lang) {
"en" -> AUTHOR_EN
"fr" -> AUTHOR_FR
else -> AUTHOR_EN
}
artist = ARTIST
status = if (year != currentYear) SManga.COMPLETED else SManga.ONGOING
description = when (lang) {
"en" -> "$SUMMARY_EN $NOTE $year"
"fr" -> "$SUMMARY_FR $NOTE $year"
else -> "$SUMMARY_EN $NOTE $year"
}
}
// Popular
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
// have one manga entry for each year
return (currentYear downTo 2012)
.map { createManga(it) }
.let { Observable.just(MangasPage(it, false))!! }
}
// Search
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
fetchPopularManga(1).map { mangaList ->
mangaList.copy(mangaList.mangas.filter { it.title.contains(query) })
}
// Details
override fun fetchMangaDetails(manga: SManga) = Observable.just(
manga.apply {
initialized = true
},
)!!
// Open in WebView
override fun mangaDetailsRequest(manga: SManga): Request {
return GET("${manga.url}/?", headers)
}
// Chapters
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
// create a new call to parse the no of pages in the site
// example responseString - Page 1 of 11
val responseString = client.newCall(GET(manga.url, headers)).execute().run {
asJsoup().selectFirst(".wp-pagenavi .pages")?.text() ?: "1"
}
// use regex to get the last number (i.e. 11 above)
val pages = Regex("\\d+").findAll(responseString).last().value.toInt()
return (1..pages).map {
val response = chapterListRequest(manga, it)
chapterListParse(response)
}.let { Observable.just(it.flatten()) }
}
private fun chapterListRequest(manga: SManga, page: Int): Response =
client.newCall(GET("${manga.url}/page/$page", headers)).execute().run {
if (!isSuccessful) {
close()
throw Exception("HTTP error $code")
}
this
}
override fun chapterListParse(response: Response): List<SChapter> {
return super.chapterListParse(response).reversed().distinct().mapIndexed { index, chapter ->
chapter.apply { chapter_number = index.toFloat() }
}
}
override fun chapterListSelector() = ".excerpt a"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
url = "$baseUrl/$siteLang" + element.attr("href").substringAfter(baseUrl)
// get the chapter date from the url
val date = Regex("\\d{4}\\/\\d{2}\\/\\d{2}").find(url)?.value
val parsedDate = date?.let { SimpleDateFormat("yyyy/MM/dd", Locale.US).parse(it) }
date_upload = parsedDate?.time ?: 0L
name = element.select("span").text()
}
// Page
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return client.newCall(GET(chapter.url, headers)).execute().run {
asJsoup().select(".entry-content p img").attr("src")
}.let {
Observable.just(listOf(Page(0, "", it)))
}
}
// Unsupported
override fun pageListParse(document: Document): List<Page> = throw Exception("Not Used")
override fun imageUrlParse(document: Document) = throw Exception("Not used")
override fun popularMangaSelector() = throw Exception("Not used")
override fun searchMangaFromElement(element: Element) = throw Exception("Not used")
override fun searchMangaNextPageSelector() = throw Exception("Not used")
override fun searchMangaSelector() = throw Exception("Not used")
override fun popularMangaRequest(page: Int) = throw Exception("Not used")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw Exception("Not used")
override fun popularMangaNextPageSelector() = throw Exception("Not used")
override fun popularMangaFromElement(element: Element) = throw Exception("Not used")
override fun mangaDetailsParse(document: Document) = throw Exception("Not used")
override fun latestUpdatesNextPageSelector() = throw Exception("Not used")
override fun latestUpdatesFromElement(element: Element) = throw Exception("Not used")
override fun latestUpdatesRequest(page: Int) = throw Exception("Not used")
override fun latestUpdatesSelector() = throw Exception("Not used")
companion object {
private const val LOGO_EN = "https://i.imgur.com/HODJlt9.jpg"
private const val LOGO_FR = "https://i.imgur.com/I7ps9zS.jpg"
private const val AUTHOR_EN = "Mark Nightingale"
private const val AUTHOR_FR = "Thomas Gx"
private const val ARTIST = "Etienne Issartial"
private const val SUMMARY_EN = "The blog relating the daily life of web agency developers."
private const val SUMMARY_FR = "Le blog qui raconte la vie des codeurs"
private const val NOTE = "\n\nNote: This entry includes all the chapters published in"
private val currentYear by lazy {
Calendar.getInstance()[Calendar.YEAR]
}
}
}

View File

@@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.extension.all.commitstrip
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class CommitStripFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
CommitStripEnglish(),
CommitStripFrench(),
)
}
class CommitStripEnglish() : CommitStrip("en", "en")
class CommitStripFrench() : CommitStrip("fr", "fr")

View File

@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.cubari.CubariUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<!-- We need another intent filter so the /a/..* shortcut -->
<!-- doesn't pollute the cubari one, since they work in any combination -->
<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="*.cubari.moe" />
<data android:host="cubari.moe" />
<data
android:pathPattern="/read/..*"
android:scheme="https" />
<data
android:pathPattern="/proxy/..*"
android:scheme="https" />
</intent-filter>
<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="*.guya.moe" />
<data android:host="guya.moe" />
<data
android:pathPattern="/proxy/..*"
android:scheme="https" />
</intent-filter>
<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="*.imgur.com" />
<data android:host="imgur.com" />
<data
android:pathPattern="/a/..*"
android:scheme="https" />
<data
android:pathPattern="/gallery/..*"
android:scheme="https" />
</intent-filter>
<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="*.reddit.com" />
<data android:host="reddit.com" />
<data
android:pathPattern="/gallery/..*"
android:scheme="https" />
</intent-filter>
<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="*.imgchest.com" />
<data android:host="imgchest.com" />
<data
android:pathPattern="/p/..*"
android:scheme="https" />
</intent-filter>
<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="*.catbox.moe" />
<data android:host="catbox.moe" />
<data
android:pathPattern="/c/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

25
src/all/cubari/README.md Normal file
View File

@@ -0,0 +1,25 @@
# Cubari
Table of Content
- [FAQ](#FAQ)
- [Why do I see no manga?](#why-do-i-see-no-manga)
- [Where can I get more information about Cubari?](#where-can-i-get-more-information-about-cubari)
- [How do I add a gallery to Cubari?](#how-do-i-add-a-gallery-to-cubari)
[Uncomment this if needed; and replace &#40; and &#41; with ( and )]: <> (- [Guides]&#40;#Guides&#41;)
Don't find the question you are look for go check out our general FAQs and Guides over at [Extension FAQ](https://tachiyomi.org/help/faq/#extensions) or [Getting Started](https://tachiyomi.org/help/guides/getting-started/#installation)
## FAQ
### Why do I see no manga?
Cubari is a proxy for image galleries.
If you've setup the Remote Storage via WebView the Recent tab shows your recent, unpinned entries, conversely the Popular tab shows your pinned entries.
### Where can I get more information about Cubari?
You can visit the [Cubari](https://cubari.moe/) website for for more information.
### How do I add a gallery to Cubari?
You can directly open a imgur or Cubari link in the extension.
[Uncomment this if needed]: <> (## Guides)

View File

@@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Cubari'
pkgNameSuffix = "all.cubari"
extClass = '.CubariFactory'
extVersionCode = 23
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1,418 @@
package eu.kanade.tachiyomi.extension.all.cubari
import android.app.Application
import android.os.Build
import eu.kanade.tachiyomi.AppInfo
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.network.asObservableSuccess
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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.double
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
open class Cubari(override val lang: String) : HttpSource() {
final override val name = "Cubari"
final override val baseUrl = "https://cubari.moe"
final override val supportsLatest = true
private val json: Json by injectLazy()
override fun headersBuilder() = Headers.Builder().apply {
add(
"User-Agent",
"(Android ${Build.VERSION.RELEASE}; " +
"${Build.MANUFACTURER} ${Build.MODEL}) " +
"Tachiyomi/${AppInfo.getVersionName()} " +
Build.ID,
)
}
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/", headers)
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return client.newBuilder()
.addInterceptor(RemoteStorageUtils.HomeInterceptor())
.build()
.newCall(latestUpdatesRequest(page))
.asObservableSuccess()
.map { response -> latestUpdatesParse(response) }
}
override fun latestUpdatesParse(response: Response): MangasPage {
val result = json.parseToJsonElement(response.body.string()).jsonArray
return parseMangaList(result, SortType.UNPINNED)
}
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/", headers)
}
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newBuilder()
.addInterceptor(RemoteStorageUtils.HomeInterceptor())
.build()
.newCall(popularMangaRequest(page))
.asObservableSuccess()
.map { response -> popularMangaParse(response) }
}
override fun popularMangaParse(response: Response): MangasPage {
val result = json.parseToJsonElement(response.body.string()).jsonArray
return parseMangaList(result, SortType.PINNED)
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(chapterListRequest(manga))
.asObservableSuccess()
.map { response -> mangaDetailsParse(response, manga) }
}
// Called when the series is loaded, or when opening in browser
override fun mangaDetailsRequest(manga: SManga): Request {
return GET("$baseUrl${manga.url}", headers)
}
override fun mangaDetailsParse(response: Response): SManga {
throw Exception("Unused")
}
private fun mangaDetailsParse(response: Response, manga: SManga): SManga {
val result = json.parseToJsonElement(response.body.string()).jsonObject
return parseManga(result, manga)
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return client.newCall(chapterListRequest(manga))
.asObservable()
.map { response -> chapterListParse(response, manga) }
}
// Gets the chapter list based on the series being viewed
override fun chapterListRequest(manga: SManga): Request {
val urlComponents = manga.url.split("/")
val source = urlComponents[2]
val slug = urlComponents[3]
return GET("$baseUrl/read/api/$source/series/$slug/", headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
throw Exception("Unused")
}
// Called after the request
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
val res = response.body.string()
return parseChapterList(res, manga)
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return when {
chapter.url.contains("/chapter/") -> {
client.newCall(pageListRequest(chapter))
.asObservableSuccess()
.map { response ->
directPageListParse(response)
}
}
else -> {
client.newCall(pageListRequest(chapter))
.asObservableSuccess()
.map { response ->
seriesJsonPageListParse(response, chapter)
}
}
}
}
override fun pageListRequest(chapter: SChapter): Request {
return when {
chapter.url.contains("/chapter/") -> {
GET("$baseUrl${chapter.url}", headers)
}
else -> {
val url = chapter.url.split("/")
val source = url[2]
val slug = url[3]
GET("$baseUrl/read/api/$source/series/$slug/", headers)
}
}
}
private fun directPageListParse(response: Response): List<Page> {
val res = response.body.string()
val pages = json.parseToJsonElement(res).jsonArray
return pages.mapIndexed { i, jsonEl ->
val page = if (jsonEl is JsonObject) {
jsonEl.jsonObject["src"]!!.jsonPrimitive.content
} else {
jsonEl.jsonPrimitive.content
}
Page(i, "", page)
}
}
private fun seriesJsonPageListParse(response: Response, chapter: SChapter): List<Page> {
val jsonObj = json.parseToJsonElement(response.body.string()).jsonObject
val groups = jsonObj["groups"]!!.jsonObject
val groupMap = groups.entries.associateBy({ it.value.jsonPrimitive.content.ifEmpty { "default" } }, { it.key })
val chapterScanlator = chapter.scanlator ?: "default" // workaround for "" as group causing NullPointerException (#13772)
// prevent NullPointerException when chapters.key is 084 and chapter.chapter_number is 84
val chapters = jsonObj["chapters"]!!.jsonObject.mapKeys {
it.key.replace(Regex("^0+(?!$)"), "")
}
val pages = if (chapters[chapter.chapter_number.toString()] != null) {
chapters[chapter.chapter_number.toString()]!!
.jsonObject["groups"]!!
.jsonObject[groupMap[chapterScanlator]]!!
.jsonArray
} else {
chapters[chapter.chapter_number.toInt().toString()]!!
.jsonObject["groups"]!!
.jsonObject[groupMap[chapterScanlator]]!!
.jsonArray
}
return pages.mapIndexed { i, jsonEl ->
val page = if (jsonEl is JsonObject) {
jsonEl.jsonObject["src"]!!.jsonPrimitive.content
} else {
jsonEl.jsonPrimitive.content
}
Page(i, "", page)
}
}
// Stub
override fun pageListParse(response: Response): List<Page> {
throw Exception("Unused")
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return when {
query.startsWith(PROXY_PREFIX) -> {
val trimmedQuery = query.removePrefix(PROXY_PREFIX)
// Only tag for recently read on search
client.newBuilder()
.addInterceptor(RemoteStorageUtils.TagInterceptor())
.build()
.newCall(proxySearchRequest(trimmedQuery))
.asObservableSuccess()
.map { response ->
proxySearchParse(response, trimmedQuery)
}
}
else -> {
client.newBuilder()
.addInterceptor(RemoteStorageUtils.HomeInterceptor())
.build()
.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response, query)
}
.map { mangasPage ->
require(mangasPage.mangas.isNotEmpty()) { SEARCH_FALLBACK_MSG }
mangasPage
}
}
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return GET("$baseUrl/", headers)
}
private fun proxySearchRequest(query: String): Request {
try {
val queryFragments = query.split("/")
val source = queryFragments[0]
val slug = queryFragments[1]
return GET("$baseUrl/read/api/$source/series/$slug/", headers)
} catch (e: Exception) {
throw Exception(SEARCH_FALLBACK_MSG)
}
}
override fun searchMangaParse(response: Response): MangasPage {
throw Exception("Unused")
}
private fun searchMangaParse(response: Response, query: String): MangasPage {
val result = json.parseToJsonElement(response.body.string()).jsonArray
val filterList = result.asSequence()
.map { it as JsonObject }
.filter { it["title"].toString().contains(query.trim(), true) }
.toList()
return parseMangaList(JsonArray(filterList), SortType.ALL)
}
private fun proxySearchParse(response: Response, query: String): MangasPage {
val result = json.parseToJsonElement(response.body.string()).jsonObject
return parseSearchList(result, query)
}
// ------------- Helpers and whatnot ---------------
private val volumeNotSpecifiedTerms = setOf("Uncategorized", "null", "")
private fun parseChapterList(payload: String, manga: SManga): List<SChapter> {
val jsonObj = json.parseToJsonElement(payload).jsonObject
val groups = jsonObj["groups"]!!.jsonObject
val chapters = jsonObj["chapters"]!!.jsonObject
val seriesSlug = jsonObj["slug"]!!.jsonPrimitive.content
val seriesPrefs = Injekt.get<Application>().getSharedPreferences("source_${id}_updateTime:$seriesSlug", 0)
val seriesPrefsEditor = seriesPrefs.edit()
val chapterList = chapters.entries.flatMap { chapterEntry ->
val chapterNum = chapterEntry.key
val chapterObj = chapterEntry.value.jsonObject
val chapterGroups = chapterObj["groups"]!!.jsonObject
val volume = chapterObj["volume"]!!.jsonPrimitive.content.let {
if (volumeNotSpecifiedTerms.contains(it)) null else it
}
val title = chapterObj["title"]!!.jsonPrimitive.content
chapterGroups.entries.map { groupEntry ->
val groupNum = groupEntry.key
val releaseDate = chapterObj["release_date"]?.jsonObject?.get(groupNum)
SChapter.create().apply {
scanlator = groups[groupNum]!!.jsonPrimitive.content
chapter_number = chapterNum.toFloatOrNull() ?: -1f
date_upload = if (releaseDate != null) {
releaseDate.jsonPrimitive.double.toLong() * 1000
} else {
val currentTimeMillis = System.currentTimeMillis()
if (!seriesPrefs.contains(chapterNum)) {
seriesPrefsEditor.putLong(chapterNum, currentTimeMillis)
}
seriesPrefs.getLong(chapterNum, currentTimeMillis)
}
name = buildString {
if (!volume.isNullOrBlank()) append("Vol.$volume ")
append("Ch.$chapterNum")
if (title.isNotBlank()) append(" - $title")
}
url = if (chapterGroups[groupNum] is JsonArray) {
"${manga.url}/$chapterNum/$groupNum"
} else {
chapterGroups[groupNum]!!.jsonPrimitive.content
}
}
}
}
seriesPrefsEditor.apply()
return chapterList.sortedByDescending { it.chapter_number }
}
private fun parseMangaList(payload: JsonArray, sortType: SortType): MangasPage {
val mangaList = payload.mapNotNull { jsonEl ->
val jsonObj = jsonEl.jsonObject
val pinned = jsonObj["pinned"]!!.jsonPrimitive.boolean
if (sortType == SortType.PINNED && pinned) {
parseManga(jsonObj)
} else if (sortType == SortType.UNPINNED && !pinned) {
parseManga(jsonObj)
} else if (sortType == SortType.ALL) {
parseManga(jsonObj)
} else {
null
}
}
return MangasPage(mangaList, false)
}
private fun parseSearchList(payload: JsonObject, query: String): MangasPage {
val tempManga = SManga.create().apply {
url = "/read/$query"
}
val mangaList = listOf(parseManga(payload, tempManga))
return MangasPage(mangaList, false)
}
private fun parseManga(jsonObj: JsonObject, mangaReference: SManga? = null): SManga =
SManga.create().apply {
title = jsonObj["title"]!!.jsonPrimitive.content
artist = jsonObj["artist"]?.jsonPrimitive?.content ?: ARTIST_FALLBACK
author = jsonObj["author"]?.jsonPrimitive?.content ?: AUTHOR_FALLBACK
val descriptionFull = jsonObj["description"]?.jsonPrimitive?.content
description = descriptionFull?.substringBefore("Tags: ") ?: DESCRIPTION_FALLBACK
genre = descriptionFull?.let {
if (it.contains("Tags: ")) {
it.substringAfter("Tags: ")
} else {
""
}
} ?: ""
url = mangaReference?.url ?: jsonObj["url"]!!.jsonPrimitive.content
thumbnail_url = jsonObj["coverUrl"]?.jsonPrimitive?.content
?: jsonObj["cover"]?.jsonPrimitive?.content ?: ""
}
// ----------------- Things we aren't supporting -----------------
override fun imageUrlParse(response: Response): String {
throw Exception("imageUrlParse not supported.")
}
companion object {
const val PROXY_PREFIX = "cubari:"
const val AUTHOR_FALLBACK = "Unknown"
const val ARTIST_FALLBACK = "Unknown"
const val DESCRIPTION_FALLBACK = "No description."
const val SEARCH_FALLBACK_MSG = "Unable to parse. Is your query in the format of $PROXY_PREFIX<source>/<slug>?"
enum class SortType {
PINNED,
UNPINNED,
ALL,
}
}
}

View File

@@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.extension.all.cubari
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class CubariFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
Cubari("en"),
Cubari("all"),
Cubari("other"),
)
}

View File

@@ -0,0 +1,69 @@
package eu.kanade.tachiyomi.extension.all.cubari
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 CubariUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val host = intent?.data?.host
val pathSegments = intent?.data?.pathSegments
if (host != null && pathSegments != null) {
val query = with(host) {
when {
equals("m.imgur.com") || equals("imgur.com") -> fromSource("imgur", pathSegments)
equals("m.reddit.com") || equals("reddit.com") || equals("www.reddit.com") -> fromSource("reddit", pathSegments)
equals("imgchest.com") -> fromSource("imgchest", pathSegments)
equals("catbox.moe") || equals("www.catbox.moe") -> fromSource("catbox", pathSegments)
else -> fromCubari(pathSegments)
}
}
if (query == null) {
Log.e("CubariUrlActivity", "Unable to parse URI from intent $intent")
finish()
exitProcess(1)
}
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", query)
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("CubariUrlActivity", e.toString())
}
}
finish()
exitProcess(0)
}
private fun fromSource(source: String, pathSegments: List<String>): String? {
if (pathSegments.size >= 2) {
val id = pathSegments[1]
return "${Cubari.PROXY_PREFIX}$source/$id"
}
return null
}
private fun fromCubari(pathSegments: MutableList<String>): String? {
return if (pathSegments.size >= 3) {
val source = pathSegments[1]
val slug = pathSegments[2]
"${Cubari.PROXY_PREFIX}$source/$slug"
} else {
null
}
}
}

View File

@@ -0,0 +1,145 @@
package eu.kanade.tachiyomi.extension.all.cubari
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class RemoteStorageUtils {
abstract class GenericInterceptor(private val transparent: Boolean) : Interceptor {
private val handler = Handler(Looper.getMainLooper())
abstract val jsScript: String
abstract fun urlModifier(originalUrl: String): String
internal class JsInterface(private val latch: CountDownLatch, var payload: String = "") {
@JavascriptInterface
fun passPayload(passedPayload: String) {
payload = passedPayload
latch.countDown()
}
}
@Synchronized
override fun intercept(chain: Interceptor.Chain): Response {
try {
val originalRequest = chain.request()
val originalResponse = chain.proceed(originalRequest)
return proceedWithWebView(originalRequest, originalResponse)
} catch (e: Exception) {
throw IOException(e)
}
}
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
private fun proceedWithWebView(request: Request, response: Response): Response {
val latch = CountDownLatch(1)
var webView: WebView? = null
val origRequestUrl = request.url.toString()
val headers = request.headers.toMultimap().mapValues {
it.value.getOrNull(0) ?: ""
}.toMutableMap()
val jsInterface = JsInterface(latch)
handler.post {
val webview = WebView(Injekt.get<Application>())
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = request.header("User-Agent")
}
webview.addJavascriptInterface(jsInterface, "android")
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
view.evaluateJavascript(jsScript) {}
if (transparent) {
latch.countDown()
}
}
}
webview.loadUrl(urlModifier(origRequestUrl), headers)
}
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
handler.postDelayed(
{ webView?.destroy() },
DELAY_MILLIS * (if (transparent) 2 else 1),
)
return if (transparent) {
response
} else {
response.newBuilder().body(jsInterface.payload.toResponseBody(response.body.contentType())).build()
}
}
}
class TagInterceptor : GenericInterceptor(true) {
override val jsScript: String = """
let dispatched = false;
window.addEventListener('history-ready', function () {
if (!dispatched) {
dispatched = true;
Promise.all(
[globalHistoryHandler.getAllPinnedSeries(), globalHistoryHandler.getAllUnpinnedSeries()]
).then(e => {
window.android.passPayload(JSON.stringify(e.flatMap(e => e)))
});
}
});
tag();
"""
override fun urlModifier(originalUrl: String): String {
return originalUrl.replace("/api/", "/").replace("/series/", "/")
}
}
class HomeInterceptor : GenericInterceptor(false) {
override val jsScript: String = """
let dispatched = false;
(function () {
if (!dispatched) {
dispatched = true;
Promise.all(
[globalHistoryHandler.getAllPinnedSeries(), globalHistoryHandler.getAllUnpinnedSeries()]
).then(e => {
window.android.passPayload(JSON.stringify(e.flatMap(e => e) ) )
});
}
})();
"""
override fun urlModifier(originalUrl: String): String {
return originalUrl
}
}
companion object {
const val TIMEOUT_SEC: Long = 10
const val DELAY_MILLIS: Long = 10000
}
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,13 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Danbooru'
pkgNameSuffix = 'all.danbooru'
extClass = '.Danbooru'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Some files were not shown because too many files have changed in this diff Show More