diff --git a/src/en/twistmoe/res/mipmap-hdpi/ic_launcher.png b/src/en/twistmoe/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 1e0a83bc1..000000000 Binary files a/src/en/twistmoe/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/twistmoe/res/mipmap-mdpi/ic_launcher.png b/src/en/twistmoe/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index b9a3cd9b0..000000000 Binary files a/src/en/twistmoe/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/twistmoe/res/mipmap-xhdpi/ic_launcher.png b/src/en/twistmoe/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 0b0c111f8..000000000 Binary files a/src/en/twistmoe/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/twistmoe/res/mipmap-xxhdpi/ic_launcher.png b/src/en/twistmoe/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index b699a7a40..000000000 Binary files a/src/en/twistmoe/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/twistmoe/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/twistmoe/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index b27e7d5fe..000000000 Binary files a/src/en/twistmoe/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/twistmoe/res/web_hi_res_512.png b/src/en/twistmoe/res/web_hi_res_512.png deleted file mode 100644 index 2bbf17348..000000000 Binary files a/src/en/twistmoe/res/web_hi_res_512.png and /dev/null differ diff --git a/src/en/twistmoe/src/eu/kanade/tachiyomi/animeextension/en/twistmoe/AESDecrypt.kt b/src/en/twistmoe/src/eu/kanade/tachiyomi/animeextension/en/twistmoe/AESDecrypt.kt index 739c00c66..8ee11ff3d 100644 --- a/src/en/twistmoe/src/eu/kanade/tachiyomi/animeextension/en/twistmoe/AESDecrypt.kt +++ b/src/en/twistmoe/src/eu/kanade/tachiyomi/animeextension/en/twistmoe/AESDecrypt.kt @@ -1,21 +1,19 @@ 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.util.Base64 import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec -@TargetApi(Build.VERSION_CODES.O) 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 aesDecrypt(v: ByteArray, secretKey: ByteArray, initializationVector: ByteArray) = decrypt(v, secretKey, initializationVector) + fun getIvAndKey(v: String): ByteArray { // 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") assert(byteStr.decodeToString(0, 8) == "Salted__") val salt = byteStr.sliceArray(8..15) @@ -33,14 +31,17 @@ class AESDecrypt { } return finalKey.sliceArray(0..47) } + fun unpad(v: String): String { return v.substring(0..v.lastIndex - v.last().toInt()) } + 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__") return byteStr.sliceArray(16..byteStr.lastIndex) } + private fun cipher(opmode: Int, secretKey: ByteArray, initializationVector: ByteArray): Cipher { if (secretKey.lastIndex != 31) throw RuntimeException("SecretKey length is not 32 chars") if (initializationVector.lastIndex != 15) throw RuntimeException("IV length is not 16 chars") @@ -50,10 +51,12 @@ class AESDecrypt { c.init(opmode, sk, iv) return c } + private fun encrypt(str: String, secretKey: ByteArray, iv: ByteArray): String { 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 { return String(cipher(Cipher.DECRYPT_MODE, secretKey, iv).doFinal(str)) } diff --git a/src/en/twistmoe/src/eu/kanade/tachiyomi/animeextension/en/twistmoe/TwistMoe.kt b/src/en/twistmoe/src/eu/kanade/tachiyomi/animeextension/en/twistmoe/TwistMoe.kt index 3d47ffa8b..735dbcfd5 100644 --- a/src/en/twistmoe/src/eu/kanade/tachiyomi/animeextension/en/twistmoe/TwistMoe.kt +++ b/src/en/twistmoe/src/eu/kanade/tachiyomi/animeextension/en/twistmoe/TwistMoe.kt @@ -1,6 +1,6 @@ package eu.kanade.tachiyomi.animeextension.en.twistmoe -import android.util.Log +import android.annotation.SuppressLint import com.google.gson.JsonArray import com.google.gson.JsonElement 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.online.AnimeHttpSource 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.MediaType.Companion.toMediaType import okhttp3.Request -import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import java.lang.Exception -import java.util.Date +import java.text.SimpleDateFormat +import java.util.Locale +import kotlin.collections.ArrayList class TwistMoe : AnimeHttpSource() { @@ -32,20 +32,31 @@ class TwistMoe : AnimeHttpSource() { 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) 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 { val responseString = response.body!!.string() - return parseSearchJson(responseString) + val jElement: JsonElement = JsonParser.parseString(responseString) + val array: JsonArray = jElement.asJsonArray + val list = mutableListOf() + 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 { - val jElement: JsonElement = JsonParser.parseString(jsonLine) - val array: JsonArray = jElement.asJsonArray + private fun parseSearchJson(array: List): List { val animeList = mutableListOf() for (item in array) { val anime = SAnime.create() @@ -56,23 +67,43 @@ class TwistMoe : AnimeHttpSource() { 1 -> SAnime.ONGOING else -> SAnime.UNKNOWN } - anime.thumbnail_url = "https://homepages.cae.wisc.edu/~ece533/images/cat.png" animeList.add(anime) } - return AnimesPage(animeList, false) + return animeList } 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 { val responseString = response.body!!.string() - return parseSearchJson(responseString) + val jElement: JsonElement = JsonParser.parseString(responseString) + val array: JsonArray = jElement.asJsonArray + val list = mutableListOf() + array.toCollection(list) + val query = response.request.url.fragment!! + .substringAfter("query=") + .substringBeforeLast(";page=") + .toLowerCase(Locale.ROOT) + val toRemove = mutableListOf() + 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 { 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 { @@ -88,77 +119,89 @@ class TwistMoe : AnimeHttpSource() { 1 -> SAnime.ONGOING 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 } override fun videoListRequest(episode: SEpisode): Request { - return super.videoListRequest(episode) + return GET(episode.url, requestHeaders) } override fun videoListParse(response: Response): List