parent
							
								
									a599f54b8c
								
							
						
					
					
						commit
						7a2e2a62ff
					
				
							
								
								
									
										24
									
								
								src/all/akuma/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/all/akuma/AndroidManifest.xml
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										12
									
								
								src/all/akuma/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/all/akuma/build.gradle
									
									
									
									
									
										Normal 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" | ||||
							
								
								
									
										
											BIN
										
									
								
								src/all/akuma/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/all/akuma/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/all/akuma/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/all/akuma/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/all/akuma/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/all/akuma/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/all/akuma/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/all/akuma/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 6.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/all/akuma/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/all/akuma/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 9.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/all/akuma/res/web_hi_res_512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/all/akuma/res/web_hi_res_512.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 41 KiB | 
| @ -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() | ||||
| } | ||||
| @ -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) | ||||
|     } | ||||
| } | ||||
| @ -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") | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 AwkwardPeak7
						AwkwardPeak7