package org.mosad.teapod.parser import android.util.Log import com.google.gson.JsonParser import kotlinx.coroutines.* import org.jsoup.Connection import org.jsoup.Jsoup import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.util.DataTypes.MediaType import org.mosad.teapod.util.Episode import org.mosad.teapod.util.Media import java.io.IOException import java.util.* import kotlin.collections.ArrayList /** * maybe AoDParser as object would be useful */ class AoDParser { private val baseUrl = "https://www.anime-on-demand.de" private val loginPath = "/users/sign_in" private val libraryPath = "/animes" private val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0" companion object { private var csrfToken: String = "" private var sessionCookies = mutableMapOf() private var loginSuccess = false val mediaList = arrayListOf() } fun login(): Boolean = runBlocking { withContext(Dispatchers.Default) { // get the authenticity token val resAuth = Jsoup.connect(baseUrl + loginPath) .header("User-Agent", userAgent) .execute() val authenticityToken = resAuth.parse().select("meta[name=csrf-token]").attr("content") val authCookies = resAuth.cookies() Log.i(javaClass.name, "Received authenticity token: $authenticityToken") Log.i(javaClass.name, "Received authenticity cookies: $authCookies") val data = mapOf( Pair("user[login]", EncryptedPreferences.login), Pair("user[password]", EncryptedPreferences.password), Pair("user[remember_me]", "1"), Pair("commit", "Einloggen"), Pair("authenticity_token", authenticityToken) ) val resLogin = Jsoup.connect(baseUrl + loginPath) .method(Connection.Method.POST) .data(data) .postDataCharset("UTF-8") .cookies(authCookies) .execute() //println(resLogin.body()) sessionCookies = resLogin.cookies() loginSuccess = resLogin.body().contains("Hallo, du bist jetzt angemeldet.") Log.i(javaClass.name, "Status: ${resLogin.statusCode()} (${resLogin.statusMessage()}), login successful: $loginSuccess") loginSuccess } } /** * list all animes from the website */ fun listAnimes(): ArrayList = runBlocking { if (sessionCookies.isEmpty()) login() withContext(Dispatchers.Default) { val resAnimes = Jsoup.connect(baseUrl + libraryPath) .cookies(sessionCookies) .get() //println(resAnimes) mediaList.clear() resAnimes.select("div.animebox").forEach { val type = if (it.select("p.animebox-link").select("a").text().toLowerCase(Locale.ROOT) == "zur serie") { MediaType.TVSHOW } else { MediaType.MOVIE } val media = Media( it.select("h3.animebox-title").text(), it.select("p.animebox-link").select("a").attr("href"), type ) media.info.posterLink = it.select("p.animebox-image").select("img").attr("src") media.info.shortDesc = it.select("p.animebox-shorttext").text() mediaList.add(media) } Log.i(javaClass.name, "Total library size is: ${mediaList.size}") return@withContext mediaList } } /** * load streams for the media path */ fun loadStreams(media: Media): List = runBlocking { if (sessionCookies.isEmpty()) login() if (!loginSuccess) { Log.w(javaClass.name, "Login, was not successful.") return@runBlocking listOf() } // if the episodes list is not empty it was loaded before if (media.episodes.isNotEmpty()) { return@runBlocking media.episodes } withContext(Dispatchers.Default) { val res = Jsoup.connect(baseUrl + media.link) .cookies(sessionCookies) .get() //println(res) // parse additional info from the media page res.select("table.vertical-table").select("tr").forEach { row -> when (row.select("th").text().toLowerCase(Locale.ROOT)) { "produktionsjahr" -> media.info.year = row.select("td").text().toInt() "fsk" -> media.info.age = row.select("td").text().toInt() "episodenanzahl" -> { media.info.episodesCount = row.select("td").text() .substringBefore("/") .filter{ it.isDigit() } .toInt() } } } // parse additional information for tv shows val episodes = if (media.type == MediaType.TVSHOW) { res.select("div.three-box-container > div.episodebox").map { episodebox -> val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt() val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text() val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange") val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first() Episode( id = episodeId, shortDesc = episodeShortDesc, watched = episodeWatched, watchedCallback = episodeWatchedCallback ) } } else { listOf(Episode()) } if (csrfToken.isEmpty()) { csrfToken = res.select("meta[name=csrf-token]").attr("content") } // has attr data-lag (ger or jap) val playlists = res.select("input.streamstarter_html5").eachAttr("data-playlist") //println("first entry: ${playlists.first()}") //println("csrf token is: $csrfToken") return@withContext if (playlists.size > 0) { loadStreamInfo(playlists.first(), csrfToken, media.type, episodes) } else { listOf() } } } /** * load the playlist path and parse it, read the stream info from json * @param episodes is used as call ba reference, additionally it is passed a return value */ private fun loadStreamInfo(playlistPath: String, csrfToken: String, type: MediaType, episodes: List): List = runBlocking { withContext(Dispatchers.Default) { val headers = mutableMapOf( Pair("Accept", "application/json, text/javascript, */*; q=0.01"), Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"), Pair("Accept-Encoding", "gzip, deflate, br"), Pair("X-CSRF-Token", csrfToken), Pair("X-Requested-With", "XMLHttpRequest"), ) println("loading streaminfo with cstf: $csrfToken") val res = Jsoup.connect(baseUrl + playlistPath) .ignoreContentType(true) .cookies(sessionCookies) .headers(headers) .execute() //println(res.body()) when (type) { MediaType.MOVIE -> { val movie = JsonParser.parseString(res.body()).asJsonObject .get("playlist").asJsonArray .first().asJsonObject movie.get("sources").asJsonArray.first().apply { episodes.first().streamUrl = this.asJsonObject.get("file").asString } } MediaType.TVSHOW -> { val episodesJson = JsonParser.parseString(res.body()).asJsonObject .get("playlist").asJsonArray episodesJson.forEach { jsonElement -> val episodeId = jsonElement.asJsonObject.get("mediaid") val episodeStream = jsonElement.asJsonObject.get("sources").asJsonArray .first().asJsonObject .get("file").asString val episodeTitle = jsonElement.asJsonObject.get("title").asString val episodePoster = jsonElement.asJsonObject.get("image").asString val episodeDescription = jsonElement.asJsonObject.get("description").asString val episodeNumber = episodeTitle.substringAfter(", Ep. ").toInt() episodes.first { it.id == episodeId.asInt }.apply { this.title = episodeTitle this.posterLink = episodePoster this.streamUrl = episodeStream this.description = episodeDescription this.number = episodeNumber } } } else -> { Log.e(javaClass.name, "Wrong Type, please report this issue.") } } return@withContext episodes } } fun sendCallback(callbackPath: String) = GlobalScope.launch { val headers = mutableMapOf( Pair("Accept", "application/json, text/javascript, */*; q=0.01"), Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"), Pair("Accept-Encoding", "gzip, deflate, br"), Pair("X-CSRF-Token", csrfToken), Pair("X-Requested-With", "XMLHttpRequest"), ) try { Jsoup.connect(baseUrl + callbackPath) .ignoreContentType(true) .cookies(sessionCookies) .headers(headers) .execute() } catch (ex: IOException) { Log.e(javaClass.name, "Callback for $callbackPath failed.") } } }