new source: Akuma.moe
This commit is contained in:
AwkwardPeak7 2023-06-13 00:00:23 +05:00 committed by GitHub
parent a599f54b8c
commit 7a2e2a62ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 358 additions and 0 deletions

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.extension">
<application>
<activity
android:name=".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")
}
}