finalize twist
This commit is contained in:
Binary file not shown.
Before Width: | Height: | Size: 4.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 6.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
Before Width: | Height: | Size: 18 KiB |
Binary file not shown.
Before Width: | Height: | Size: 84 KiB |
@ -1,21 +1,19 @@
|
|||||||
package eu.kanade.tachiyomi.animeextension.en.twistmoe
|
package eu.kanade.tachiyomi.animeextension.en.twistmoe
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.os.Build
|
import android.util.Base64
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.Base64
|
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.O)
|
|
||||||
class AESDecrypt {
|
class AESDecrypt {
|
||||||
private val decoder = Base64.getDecoder()
|
|
||||||
private val encoder = Base64.getEncoder()
|
|
||||||
fun aesEncrypt(v: String, secretKey: ByteArray, initializationVector: ByteArray) = encrypt(v, secretKey, initializationVector)
|
fun aesEncrypt(v: String, secretKey: ByteArray, initializationVector: ByteArray) = encrypt(v, secretKey, initializationVector)
|
||||||
|
|
||||||
fun aesDecrypt(v: ByteArray, secretKey: ByteArray, initializationVector: ByteArray) = decrypt(v, secretKey, initializationVector)
|
fun aesDecrypt(v: ByteArray, secretKey: ByteArray, initializationVector: ByteArray) = decrypt(v, secretKey, initializationVector)
|
||||||
|
|
||||||
fun getIvAndKey(v: String): ByteArray {
|
fun getIvAndKey(v: String): ByteArray {
|
||||||
// credits: https://github.com/anime-dl/anime-downloader/blob/c030fded0b7f79d5bb8a07f5cf6b2ae8fa3954a1/anime_downloader/sites/twistmoe.py
|
// credits: https://github.com/anime-dl/anime-downloader/blob/c030fded0b7f79d5bb8a07f5cf6b2ae8fa3954a1/anime_downloader/sites/twistmoe.py
|
||||||
val byteStr = decoder.decode(v.toByteArray(Charsets.UTF_8))
|
val byteStr = Base64.decode(v.toByteArray(Charsets.UTF_8), Base64.DEFAULT)
|
||||||
val md5 = MessageDigest.getInstance("MD5")
|
val md5 = MessageDigest.getInstance("MD5")
|
||||||
assert(byteStr.decodeToString(0, 8) == "Salted__")
|
assert(byteStr.decodeToString(0, 8) == "Salted__")
|
||||||
val salt = byteStr.sliceArray(8..15)
|
val salt = byteStr.sliceArray(8..15)
|
||||||
@ -33,14 +31,17 @@ class AESDecrypt {
|
|||||||
}
|
}
|
||||||
return finalKey.sliceArray(0..47)
|
return finalKey.sliceArray(0..47)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unpad(v: String): String {
|
fun unpad(v: String): String {
|
||||||
return v.substring(0..v.lastIndex - v.last().toInt())
|
return v.substring(0..v.lastIndex - v.last().toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getToDecode(v: String): ByteArray {
|
fun getToDecode(v: String): ByteArray {
|
||||||
val byteStr = decoder.decode(v.toByteArray(Charsets.UTF_8))
|
val byteStr = Base64.decode(v.toByteArray(Charsets.UTF_8), Base64.DEFAULT)
|
||||||
assert(byteStr.decodeToString(0, 8) == "Salted__")
|
assert(byteStr.decodeToString(0, 8) == "Salted__")
|
||||||
return byteStr.sliceArray(16..byteStr.lastIndex)
|
return byteStr.sliceArray(16..byteStr.lastIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cipher(opmode: Int, secretKey: ByteArray, initializationVector: ByteArray): Cipher {
|
private fun cipher(opmode: Int, secretKey: ByteArray, initializationVector: ByteArray): Cipher {
|
||||||
if (secretKey.lastIndex != 31) throw RuntimeException("SecretKey length is not 32 chars")
|
if (secretKey.lastIndex != 31) throw RuntimeException("SecretKey length is not 32 chars")
|
||||||
if (initializationVector.lastIndex != 15) throw RuntimeException("IV length is not 16 chars")
|
if (initializationVector.lastIndex != 15) throw RuntimeException("IV length is not 16 chars")
|
||||||
@ -50,10 +51,12 @@ class AESDecrypt {
|
|||||||
c.init(opmode, sk, iv)
|
c.init(opmode, sk, iv)
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun encrypt(str: String, secretKey: ByteArray, iv: ByteArray): String {
|
private fun encrypt(str: String, secretKey: ByteArray, iv: ByteArray): String {
|
||||||
val encrypted = cipher(Cipher.ENCRYPT_MODE, secretKey, iv).doFinal(str.toByteArray(Charsets.UTF_8))
|
val encrypted = cipher(Cipher.ENCRYPT_MODE, secretKey, iv).doFinal(str.toByteArray(Charsets.UTF_8))
|
||||||
return String(encoder.encode(encrypted))
|
return String(Base64.encode(encrypted, Base64.DEFAULT))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decrypt(str: ByteArray, secretKey: ByteArray, iv: ByteArray): String {
|
private fun decrypt(str: ByteArray, secretKey: ByteArray, iv: ByteArray): String {
|
||||||
return String(cipher(Cipher.DECRYPT_MODE, secretKey, iv).doFinal(str))
|
return String(cipher(Cipher.DECRYPT_MODE, secretKey, iv).doFinal(str))
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.animeextension.en.twistmoe
|
package eu.kanade.tachiyomi.animeextension.en.twistmoe
|
||||||
|
|
||||||
import android.util.Log
|
import android.annotation.SuppressLint
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import com.google.gson.JsonElement
|
import com.google.gson.JsonElement
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
@ -12,15 +12,15 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
|
|||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
import java.util.Date
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class TwistMoe : AnimeHttpSource() {
|
class TwistMoe : AnimeHttpSource() {
|
||||||
|
|
||||||
@ -32,20 +32,31 @@ class TwistMoe : AnimeHttpSource() {
|
|||||||
|
|
||||||
override val supportsLatest = false
|
override val supportsLatest = false
|
||||||
|
|
||||||
private val popularRequestHeaders =
|
override fun headersBuilder() = Headers.Builder().apply {
|
||||||
|
add("User-Agent", "Aniyomi")
|
||||||
|
add("Referer", "https://twist.moe/")
|
||||||
|
}
|
||||||
|
|
||||||
|
private val requestHeaders =
|
||||||
Headers.headersOf("x-access-token", "0df14814b9e590a1f26d3071a4ed7974", "referer", baseUrl)
|
Headers.headersOf("x-access-token", "0df14814b9e590a1f26d3071a4ed7974", "referer", baseUrl)
|
||||||
|
|
||||||
override fun popularAnimeRequest(page: Int): Request =
|
override fun popularAnimeRequest(page: Int): Request =
|
||||||
GET("https://api.twist.moe/api/anime", popularRequestHeaders)
|
GET("https://api.twist.moe/api/anime#$page", requestHeaders)
|
||||||
|
|
||||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||||
val responseString = response.body!!.string()
|
val responseString = response.body!!.string()
|
||||||
return parseSearchJson(responseString)
|
val jElement: JsonElement = JsonParser.parseString(responseString)
|
||||||
|
val array: JsonArray = jElement.asJsonArray
|
||||||
|
val list = mutableListOf<JsonElement>()
|
||||||
|
array.toCollection(list)
|
||||||
|
val page = response.request.url.fragment!!.toInt() - 1
|
||||||
|
val start = page * 10
|
||||||
|
val end = if (list.lastIndex > start + 9) start + 9 else list.lastIndex
|
||||||
|
val range = start..end
|
||||||
|
return AnimesPage(parseSearchJson(list.slice(range)), end != list.lastIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseSearchJson(jsonLine: String?): AnimesPage {
|
private fun parseSearchJson(array: List<JsonElement>): List<SAnime> {
|
||||||
val jElement: JsonElement = JsonParser.parseString(jsonLine)
|
|
||||||
val array: JsonArray = jElement.asJsonArray
|
|
||||||
val animeList = mutableListOf<SAnime>()
|
val animeList = mutableListOf<SAnime>()
|
||||||
for (item in array) {
|
for (item in array) {
|
||||||
val anime = SAnime.create()
|
val anime = SAnime.create()
|
||||||
@ -56,23 +67,43 @@ class TwistMoe : AnimeHttpSource() {
|
|||||||
1 -> SAnime.ONGOING
|
1 -> SAnime.ONGOING
|
||||||
else -> SAnime.UNKNOWN
|
else -> SAnime.UNKNOWN
|
||||||
}
|
}
|
||||||
anime.thumbnail_url = "https://homepages.cae.wisc.edu/~ece533/images/cat.png"
|
|
||||||
animeList.add(anime)
|
animeList.add(anime)
|
||||||
}
|
}
|
||||||
return AnimesPage(animeList, false)
|
return animeList
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request =
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request =
|
||||||
GET("https://api.twist.moe/api/anime", popularRequestHeaders)
|
GET("https://api.twist.moe/api/anime#query=$query;page=$page", requestHeaders)
|
||||||
|
|
||||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||||
val responseString = response.body!!.string()
|
val responseString = response.body!!.string()
|
||||||
return parseSearchJson(responseString)
|
val jElement: JsonElement = JsonParser.parseString(responseString)
|
||||||
|
val array: JsonArray = jElement.asJsonArray
|
||||||
|
val list = mutableListOf<JsonElement>()
|
||||||
|
array.toCollection(list)
|
||||||
|
val query = response.request.url.fragment!!
|
||||||
|
.substringAfter("query=")
|
||||||
|
.substringBeforeLast(";page=")
|
||||||
|
.toLowerCase(Locale.ROOT)
|
||||||
|
val toRemove = mutableListOf<JsonElement>()
|
||||||
|
for (entry in list) {
|
||||||
|
val title = entry.asJsonObject.get("title").asString.toLowerCase(Locale.ROOT)
|
||||||
|
val altTitle = try {
|
||||||
|
entry.asJsonObject.get("alt_title").asString.toLowerCase(Locale.ROOT)
|
||||||
|
} catch (e: Exception) { "" }
|
||||||
|
if (!(title.contains(query) || altTitle.contains(query))) toRemove.add(entry)
|
||||||
|
}
|
||||||
|
list.removeAll(toRemove)
|
||||||
|
val page = response.request.url.fragment!!.substringAfterLast(";page=").toInt() - 1
|
||||||
|
val start = page * 10
|
||||||
|
val end = if (list.lastIndex > start + 9) start + 9 else list.lastIndex
|
||||||
|
val range = start..end
|
||||||
|
return AnimesPage(parseSearchJson(list.slice(range)), end != list.lastIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun animeDetailsRequest(anime: SAnime): Request {
|
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||||
val slug = anime.url.substringAfter("/a/")
|
val slug = anime.url.substringAfter("/a/")
|
||||||
return GET("https://api.twist.moe/api/anime/$slug", popularRequestHeaders)
|
return GET("https://api.twist.moe/api/anime/$slug", requestHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun animeDetailsParse(response: Response): SAnime {
|
override fun animeDetailsParse(response: Response): SAnime {
|
||||||
@ -88,77 +119,89 @@ class TwistMoe : AnimeHttpSource() {
|
|||||||
1 -> SAnime.ONGOING
|
1 -> SAnime.ONGOING
|
||||||
else -> SAnime.UNKNOWN
|
else -> SAnime.UNKNOWN
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
val malID = jObject.get("mal_id").asNumber.toString()
|
||||||
|
if (malID.isNotEmpty()) {
|
||||||
|
val coverResponse = runBlocking {
|
||||||
|
client.newCall(GET("https://api.jikan.moe/v3/anime/$malID"))
|
||||||
|
.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageUrl = JsonParser.parseString(coverResponse.body!!.string()).asJsonObject.get("image_url").asString
|
||||||
|
if (!imageUrl.isNullOrEmpty()) anime.thumbnail_url = imageUrl
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {}
|
||||||
return anime
|
return anime
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun videoListRequest(episode: SEpisode): Request {
|
override fun videoListRequest(episode: SEpisode): Request {
|
||||||
return super.videoListRequest(episode)
|
return GET(episode.url, requestHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun videoListParse(response: Response): List<Video> {
|
override fun videoListParse(response: Response): List<Video> {
|
||||||
val responseString = response.body!!.string()
|
val responseString = response.body!!.string()
|
||||||
val jElement: JsonElement = JsonParser.parseString(responseString)
|
val array = JsonParser.parseString(responseString).asJsonArray
|
||||||
val jObject: JsonObject = jElement.asJsonObject
|
val list = mutableListOf<JsonElement>()
|
||||||
val server = jObject.get("videos_manifest").asJsonObject.get("servers").asJsonArray[0].asJsonObject
|
array.toCollection(list)
|
||||||
val streams = server.get("streams").asJsonArray
|
val episodeNumber = response.request.url.fragment!!.toFloat()
|
||||||
|
val videoList = mutableListOf<Video>()
|
||||||
val aes = AESDecrypt()
|
val aes = AESDecrypt()
|
||||||
val ivAndKey = aes.getIvAndKey("U2FsdGVkX19njUQXx448lKxE4wUQA8tH45sgjCYckbrdS15QHY3fW5ChD6UpcoackxmWn8/5Tk88yAAwSukKwKpfvI6rQ1ERxFcAspfBCj8U/IQYoE3gZy+Esgumt/Fz")
|
for (entry in list) {
|
||||||
val toDecode = aes.getToDecode("U2FsdGVkX19njUQXx448lKxE4wUQA8tH45sgjCYckbrdS15QHY3fW5ChD6UpcoackxmWn8/5Tk88yAAwSukKwKpfvI6rQ1ERxFcAspfBCj8U/IQYoE3gZy+Esgumt/Fz")
|
if (entry.asJsonObject.get("number").asNumber.toFloat() == episodeNumber) {
|
||||||
Log.i("lol", aes.aesDecrypt(toDecode, ivAndKey.sliceArray(0..31), ivAndKey.sliceArray(32..47)))
|
val source = entry.asJsonObject.get("source").asString
|
||||||
val linkList = mutableListOf<Video>()
|
val ivAndKey = aes.getIvAndKey(source)
|
||||||
for (stream in streams) {
|
val toDecode = aes.getToDecode(source)
|
||||||
if (stream.asJsonObject.get("kind").asString != "premium_alert") {
|
val url = "https://cdn.twist.moe" +
|
||||||
linkList.add(Video(stream.asJsonObject.get("url").asString, stream.asJsonObject.get("height").asString + "p", stream.asJsonObject.get("url").asString, null))
|
aes.unpad(aes.aesDecrypt(toDecode, ivAndKey.sliceArray(0..31), ivAndKey.sliceArray(32..47)))
|
||||||
|
videoList.add(Video(url, "1080p", url, null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return linkList
|
return videoList
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun episodeListRequest(anime: SAnime): Request {
|
override fun episodeListRequest(anime: SAnime): Request {
|
||||||
// aes.unpad(aes.aesDecrypt(toDecode, ivAndKey.sliceArray(0..31), ivAndKey.sliceArray(32..47)))
|
// aes.unpad(aes.aesDecrypt(toDecode, ivAndKey.sliceArray(0..31), ivAndKey.sliceArray(32..47)))
|
||||||
val slug = anime.url.substringAfter("/a/")
|
val slug = anime.url.substringAfter("/a/")
|
||||||
return GET("https://api.twist.moe/api/anime/$slug/sources", popularRequestHeaders)
|
return GET("https://api.twist.moe/api/anime/$slug/sources", requestHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
val responseString = response.body!!.string()
|
val responseString = response.body!!.string()
|
||||||
Log.i("lol_response", responseString)
|
|
||||||
val array = JsonParser.parseString(responseString).asJsonArray
|
val array = JsonParser.parseString(responseString).asJsonArray
|
||||||
|
val list = mutableListOf<JsonElement>()
|
||||||
|
array.toCollection(list)
|
||||||
val episodeList = mutableListOf<SEpisode>()
|
val episodeList = mutableListOf<SEpisode>()
|
||||||
for (entry in array) {
|
for (entry in list) {
|
||||||
try {
|
try {
|
||||||
Log.i("lol", entry.toString())
|
|
||||||
val episode = SEpisode.create()
|
val episode = SEpisode.create()
|
||||||
episode.date_upload = Date.parse(entry.asJsonObject.get("updated_at").asString)
|
episode.date_upload = parseDate(entry.asJsonObject.get("updated_at").asString)
|
||||||
episode.name = "Episode " + entry.asJsonObject.get("number").asNumber.toString()
|
episode.name = "Episode " + entry.asJsonObject.get("number").asNumber.toString()
|
||||||
episode.episode_number = entry.asJsonObject.get("number").asFloat
|
episode.episode_number = entry.asJsonObject.get("number").asNumber.toFloat()
|
||||||
episode.url = response.request.url.toString() + "#${episode.episode_number}"
|
episode.url = response.request.url.toString() + "#${episode.episode_number}"
|
||||||
episodeList.add(episode)
|
episodeList.add(episode)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.i("lol_e", e.message!!)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i("lol", episodeList.lastIndex.toString())
|
return episodeList.reversed()
|
||||||
return episodeList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun latestSearchRequestBody(page: Int): RequestBody {
|
@SuppressLint("SimpleDateFormat")
|
||||||
return """
|
private fun parseDate(date: String): Long {
|
||||||
{"search_text": "",
|
val knownPatterns: MutableList<SimpleDateFormat> = ArrayList()
|
||||||
"tags": [],
|
knownPatterns.add(SimpleDateFormat("yyyy-MM-dd hh:mm:ss"))
|
||||||
"tags_mode":"AND",
|
|
||||||
"brands": [],
|
for (pattern in knownPatterns) {
|
||||||
"blacklist": [],
|
try {
|
||||||
"order_by": "published_at_unix",
|
// Take a try
|
||||||
"ordering": "desc",
|
return pattern.parse(date)!!.time
|
||||||
"page": ${page - 1}}
|
} catch (e: Throwable) {
|
||||||
""".trimIndent().toRequestBody("application/json".toMediaType())
|
// Loop on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request = POST("https://search.htv-services.com/", popularRequestHeaders, latestSearchRequestBody(page))
|
override fun latestUpdatesRequest(page: Int): Request = throw Exception("not used")
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
override fun latestUpdatesParse(response: Response): AnimesPage = throw Exception("not used")
|
||||||
val responseString = response.body!!.string()
|
|
||||||
return parseSearchJson(responseString)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user