finalize twist

This commit is contained in:
jmir1
2021-07-15 22:07:40 +02:00
parent 5cd9f6f6e9
commit d2c1850e42
8 changed files with 110 additions and 64 deletions

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

View File

@ -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))
} }

View File

@ -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)
}
} }