update some libraries & coroutines 1.5.0
* androidx.core 1.3.2 -> 1.5.0 * androidx.appcompat 1.2.0 -> 1.3.0 * gson 2.8.6 -> 2.8.7 * coroutines-android 1.4.3 -> 1.5.0 * don't use GlobalScope, use lifecycleScope and vieModelScope instead. This fixes a few issues when fragments where destroied befor the coroutine finished. * gradle wrapper 7.0 -> 7.9.2
This commit is contained in:
		| @ -99,10 +99,12 @@ object AoDParser { | ||||
|     /** | ||||
|      * initially load all media and home screen data | ||||
|      */ | ||||
|     fun initialLoading() = listOf( | ||||
|             loadHome(), | ||||
|             listAnimes() | ||||
|     ) | ||||
|     suspend fun initialLoading() { | ||||
|         coroutineScope { | ||||
|             launch { loadHome() } | ||||
|             launch { listAnimes() } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * get a media by it's ID (int) | ||||
| @ -121,15 +123,16 @@ object AoDParser { | ||||
|     /** | ||||
|      * get subscription info from aod website, remove "Anime-Abo" Prefix and trim | ||||
|      */ | ||||
|     fun getSubscriptionInfoAsync(): Deferred<String> { | ||||
|         return GlobalScope.async(Dispatchers.IO) { | ||||
|             // get the subscription page | ||||
|             val res = Jsoup.connect(baseUrl + subscriptionPath) | ||||
|                 .cookies(sessionCookies) | ||||
|                 .get() | ||||
|     suspend fun getSubscriptionInfoAsync(): Deferred<String> { | ||||
|         return coroutineScope { | ||||
|             async(Dispatchers.IO) { | ||||
|                 val res = Jsoup.connect(baseUrl + subscriptionPath) | ||||
|                     .cookies(sessionCookies) | ||||
|                     .get() | ||||
|  | ||||
|             return@async res.select("a:contains(Anime-Abo)").text() | ||||
|                 .removePrefix("Anime-Abo").trim() | ||||
|                 return@async res.select("a:contains(Anime-Abo)").text() | ||||
|                     .removePrefix("Anime-Abo").trim() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -137,7 +140,7 @@ object AoDParser { | ||||
|         return baseUrl + subscriptionPath | ||||
|     } | ||||
|  | ||||
|     fun markAsWatched(mediaId: Int, episodeId: Int) = GlobalScope.launch { | ||||
|     suspend fun markAsWatched(mediaId: Int, episodeId: Int) { | ||||
|         val episode = getMediaById(mediaId).getEpisodeById(episodeId) | ||||
|         episode.watched = true | ||||
|         sendCallback(episode.watchedCallback) | ||||
| @ -146,137 +149,145 @@ object AoDParser { | ||||
|     } | ||||
|  | ||||
|     // TODO don't use jsoup here | ||||
|     private fun sendCallback(callbackPath: String) = GlobalScope.launch(Dispatchers.IO) { | ||||
|         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"), | ||||
|         ) | ||||
|     private suspend fun sendCallback(callbackPath: String) = coroutineScope { | ||||
|         launch(Dispatchers.IO) { | ||||
|             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.", ex) | ||||
|             try { | ||||
|                 Jsoup.connect(baseUrl + callbackPath) | ||||
|                     .ignoreContentType(true) | ||||
|                     .cookies(sessionCookies) | ||||
|                     .headers(headers) | ||||
|                     .execute() | ||||
|             } catch (ex: IOException) { | ||||
|                 Log.e(javaClass.name, "Callback for $callbackPath failed.", ex) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * load all media from aod into itemMediaList and mediaList | ||||
|      * TODO private suspend fun listAnimes() = withContext(Dispatchers.IO) should also work, maybe a bug in android studio? | ||||
|      */ | ||||
|     private fun listAnimes() = GlobalScope.launch(Dispatchers.IO) { | ||||
|         val resAnimes = Jsoup.connect(baseUrl + libraryPath).get() | ||||
|         //println(resAnimes) | ||||
|     private suspend fun listAnimes() = withContext(Dispatchers.IO) { | ||||
|         launch(Dispatchers.IO) { | ||||
|             val resAnimes = Jsoup.connect(baseUrl + libraryPath).get() | ||||
|             //println(resAnimes) | ||||
|  | ||||
|         itemMediaList.clear() | ||||
|         mediaList.clear() | ||||
|         resAnimes.select("div.animebox").forEach { | ||||
|             val type = if (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT) == "zur serie") { | ||||
|                 MediaType.TVSHOW | ||||
|             } else { | ||||
|                 MediaType.MOVIE | ||||
|             itemMediaList.clear() | ||||
|             mediaList.clear() | ||||
|             resAnimes.select("div.animebox").forEach { | ||||
|                 val type = if (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT) == "zur serie") { | ||||
|                     MediaType.TVSHOW | ||||
|                 } else { | ||||
|                     MediaType.MOVIE | ||||
|                 } | ||||
|                 val mediaTitle = it.select("h3.animebox-title").text() | ||||
|                 val mediaLink = it.select("p.animebox-link").select("a").attr("href") | ||||
|                 val mediaImage = it.select("p.animebox-image").select("img").attr("src") | ||||
|                 val mediaShortText = it.select("p.animebox-shorttext").text() | ||||
|                 val mediaId = mediaLink.substringAfterLast("/").toInt() | ||||
|  | ||||
|                 itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) | ||||
|                 mediaList.add(Media(mediaId, mediaLink, type).apply { | ||||
|                     info.title = mediaTitle | ||||
|                     info.posterUrl = mediaImage | ||||
|                     info.shortDesc = mediaShortText | ||||
|                 }) | ||||
|             } | ||||
|             val mediaTitle = it.select("h3.animebox-title").text() | ||||
|             val mediaLink = it.select("p.animebox-link").select("a").attr("href") | ||||
|             val mediaImage = it.select("p.animebox-image").select("img").attr("src") | ||||
|             val mediaShortText = it.select("p.animebox-shorttext").text() | ||||
|             val mediaId = mediaLink.substringAfterLast("/").toInt() | ||||
|  | ||||
|             itemMediaList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) | ||||
|             mediaList.add(Media(mediaId, mediaLink, type).apply { | ||||
|                 info.title = mediaTitle | ||||
|                 info.posterUrl = mediaImage | ||||
|                 info.shortDesc = mediaShortText | ||||
|             }) | ||||
|             Log.i(javaClass.name, "Total library size is: ${mediaList.size}") | ||||
|         } | ||||
|  | ||||
|         Log.i(javaClass.name, "Total library size is: ${mediaList.size}") | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * load new episodes, titles and highlights | ||||
|      */ | ||||
|     private fun loadHome() = GlobalScope.launch(Dispatchers.IO) { | ||||
|          val resHome = Jsoup.connect(baseUrl).get() | ||||
|     private suspend fun loadHome() = withContext(Dispatchers.IO) { | ||||
|         launch(Dispatchers.IO) { | ||||
|             val resHome = Jsoup.connect(baseUrl).get() | ||||
|  | ||||
|         // get highlights from AoD | ||||
|         highlightsList.clear() | ||||
|         resHome.select("#aod-highlights").select("div.news-item").forEach { | ||||
|             val mediaId = it.select("div.news-item-text").select("a.serienlink") | ||||
|                 .attr("href").substringAfterLast("/").toIntOrNull() | ||||
|             val mediaTitle = it.select("div.news-title").select("h2").text() | ||||
|             val mediaImage = it.select("img").attr("src") | ||||
|             // get highlights from AoD | ||||
|             highlightsList.clear() | ||||
|             resHome.select("#aod-highlights").select("div.news-item").forEach { | ||||
|                 val mediaId = it.select("div.news-item-text").select("a.serienlink") | ||||
|                     .attr("href").substringAfterLast("/").toIntOrNull() | ||||
|                 val mediaTitle = it.select("div.news-title").select("h2").text() | ||||
|                 val mediaImage = it.select("img").attr("src") | ||||
|  | ||||
|             if (mediaId != null) { | ||||
|                 highlightsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) | ||||
|                 if (mediaId != null) { | ||||
|                     highlightsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // get all new episodes from AoD | ||||
|         newEpisodesList.clear() | ||||
|         resHome.select("h2:contains(Neue Episoden)").next().select("li").forEach { | ||||
|             val mediaId = it.select("a.thumbs").attr("href") | ||||
|                 .substringAfterLast("/").toIntOrNull() | ||||
|             val mediaImage = it.select("a.thumbs > img").attr("src") | ||||
|             val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}" | ||||
|             // get all new episodes from AoD | ||||
|             newEpisodesList.clear() | ||||
|             resHome.select("h2:contains(Neue Episoden)").next().select("li").forEach { | ||||
|                 val mediaId = it.select("a.thumbs").attr("href") | ||||
|                     .substringAfterLast("/").toIntOrNull() | ||||
|                 val mediaImage = it.select("a.thumbs > img").attr("src") | ||||
|                 val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}" | ||||
|  | ||||
|             if (mediaId != null) { | ||||
|                 newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) | ||||
|                 if (mediaId != null) { | ||||
|                     newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // get new simulcasts from AoD | ||||
|         newSimulcastsList.clear() | ||||
|         resHome.select("h2:contains(Neue Simulcasts)").next().select("li").forEach { | ||||
|             val mediaId = it.select("a.thumbs").attr("href") | ||||
|                 .substringAfterLast("/").toIntOrNull() | ||||
|             val mediaImage = it.select("a.thumbs > img").attr("src") | ||||
|             val mediaTitle = it.select("a").text() | ||||
|             // get new simulcasts from AoD | ||||
|             newSimulcastsList.clear() | ||||
|             resHome.select("h2:contains(Neue Simulcasts)").next().select("li").forEach { | ||||
|                 val mediaId = it.select("a.thumbs").attr("href") | ||||
|                     .substringAfterLast("/").toIntOrNull() | ||||
|                 val mediaImage = it.select("a.thumbs > img").attr("src") | ||||
|                 val mediaTitle = it.select("a").text() | ||||
|  | ||||
|             if (mediaId != null) { | ||||
|                 newSimulcastsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) | ||||
|                 if (mediaId != null) { | ||||
|                     newSimulcastsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // get new titles from AoD | ||||
|         newTitlesList.clear() | ||||
|         resHome.select("h2:contains(Neue Anime-Titel)").next().select("li").forEach { | ||||
|             val mediaId = it.select("a.thumbs").attr("href") | ||||
|                 .substringAfterLast("/").toIntOrNull() | ||||
|             val mediaImage = it.select("a.thumbs > img").attr("src") | ||||
|             val mediaTitle = it.select("a").text() | ||||
|             // get new titles from AoD | ||||
|             newTitlesList.clear() | ||||
|             resHome.select("h2:contains(Neue Anime-Titel)").next().select("li").forEach { | ||||
|                 val mediaId = it.select("a.thumbs").attr("href") | ||||
|                     .substringAfterLast("/").toIntOrNull() | ||||
|                 val mediaImage = it.select("a.thumbs > img").attr("src") | ||||
|                 val mediaTitle = it.select("a").text() | ||||
|  | ||||
|             if (mediaId != null) { | ||||
|                 newTitlesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) | ||||
|                 if (mediaId != null) { | ||||
|                     newTitlesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // get top ten from AoD | ||||
|         topTenList.clear() | ||||
|         resHome.select("h2:contains(Anime Top 10)").next().select("li").forEach { | ||||
|             val mediaId = it.select("a.thumbs").attr("href") | ||||
|                 .substringAfterLast("/").toIntOrNull() | ||||
|             val mediaImage = it.select("a.thumbs > img").attr("src") | ||||
|             val mediaTitle = it.select("a").text() | ||||
|             // get top ten from AoD | ||||
|             topTenList.clear() | ||||
|             resHome.select("h2:contains(Anime Top 10)").next().select("li").forEach { | ||||
|                 val mediaId = it.select("a.thumbs").attr("href") | ||||
|                     .substringAfterLast("/").toIntOrNull() | ||||
|                 val mediaImage = it.select("a.thumbs > img").attr("src") | ||||
|                 val mediaTitle = it.select("a").text() | ||||
|  | ||||
|             if (mediaId != null) { | ||||
|                 topTenList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) | ||||
|                 if (mediaId != null) { | ||||
|                     topTenList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // if highlights is empty, add a random new title | ||||
|         if (highlightsList.isEmpty()) { | ||||
|             if (newTitlesList.isNotEmpty()) { | ||||
|                 highlightsList.add(newTitlesList[Random.nextInt(0, newTitlesList.size)]) | ||||
|             } else { | ||||
|                 highlightsList.add(ItemMedia(0,"", "")) | ||||
|             // if highlights is empty, add a random new title | ||||
|             if (highlightsList.isEmpty()) { | ||||
|                 if (newTitlesList.isNotEmpty()) { | ||||
|                     highlightsList.add(newTitlesList[Random.nextInt(0, newTitlesList.size)]) | ||||
|                 } else { | ||||
|                     highlightsList.add(ItemMedia(0,"", "")) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             Log.i(javaClass.name, "loaded home") | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -286,112 +297,114 @@ object AoDParser { | ||||
|      * load streams for the media path, movies have one episode | ||||
|      * @param media is used as call ba reference | ||||
|      */ | ||||
|     private fun loadStreams(media: Media) = GlobalScope.launch(Dispatchers.IO) { | ||||
|         if (sessionCookies.isEmpty()) login() | ||||
|     private suspend fun loadStreams(media: Media) = coroutineScope { | ||||
|         launch(Dispatchers.IO) { | ||||
|             if (sessionCookies.isEmpty()) login() | ||||
|  | ||||
|         if (!loginSuccess) { | ||||
|             Log.w(javaClass.name, "Login, was not successful.") | ||||
|             return@launch | ||||
|         } | ||||
|  | ||||
|         // get the media page | ||||
|         val res = Jsoup.connect(baseUrl + media.link) | ||||
|             .cookies(sessionCookies) | ||||
|             .get() | ||||
|  | ||||
|         //println(res) | ||||
|  | ||||
|         if (csrfToken.isEmpty()) { | ||||
|             csrfToken = res.select("meta[name=csrf-token]").attr("content") | ||||
|             //Log.i(javaClass.name, "New csrf token is $csrfToken") | ||||
|         } | ||||
|  | ||||
|         val besides = res.select("div.besides").first() | ||||
|         val playlists = besides.select("input.streamstarter_html5").map { streamstarter -> | ||||
|             parsePlaylistAsync( | ||||
|                 streamstarter.attr("data-playlist"), | ||||
|                 streamstarter.attr("data-lang") | ||||
|             ) | ||||
|         }.awaitAll() | ||||
|  | ||||
|         playlists.forEach { aod -> | ||||
|             // TODO improve language handling | ||||
|             val locale = when (aod.extLanguage) { | ||||
|                 "ger" -> Locale.GERMAN | ||||
|                 "jap" -> Locale.JAPANESE | ||||
|                 else -> Locale.ROOT | ||||
|             if (!loginSuccess) { | ||||
|                 Log.w(javaClass.name, "Login, was not successful.") | ||||
|                 return@launch | ||||
|             } | ||||
|  | ||||
|             aod.playlist.forEach { ep -> | ||||
|                 try { | ||||
|                     if (media.hasEpisode(ep.mediaid)) { | ||||
|                         media.getEpisodeById(ep.mediaid).streams.add( | ||||
|                             Stream(ep.sources.first().file, locale) | ||||
|                         ) | ||||
|                     } else { | ||||
|                         media.episodes.add(Episode( | ||||
|                             id = ep.mediaid, | ||||
|                             streams = mutableListOf(Stream(ep.sources.first().file, locale)), | ||||
|                             posterUrl = ep.image, | ||||
|                             title = ep.title, | ||||
|                             description = ep.description, | ||||
|                             number = getNumberFromTitle(ep.title, media.type) | ||||
|                         )) | ||||
|                     } | ||||
|                 } catch (ex: Exception) { | ||||
|                     Log.w(javaClass.name, "Could not parse episode information.", ex) | ||||
|             // get the media page | ||||
|             val res = Jsoup.connect(baseUrl + media.link) | ||||
|                 .cookies(sessionCookies) | ||||
|                 .get() | ||||
|  | ||||
|             //println(res) | ||||
|  | ||||
|             if (csrfToken.isEmpty()) { | ||||
|                 csrfToken = res.select("meta[name=csrf-token]").attr("content") | ||||
|                 //Log.i(javaClass.name, "New csrf token is $csrfToken") | ||||
|             } | ||||
|  | ||||
|             val besides = res.select("div.besides").first() | ||||
|             val playlists = besides.select("input.streamstarter_html5").map { streamstarter -> | ||||
|                 parsePlaylistAsync( | ||||
|                     streamstarter.attr("data-playlist"), | ||||
|                     streamstarter.attr("data-lang") | ||||
|                 ) | ||||
|             }.awaitAll() | ||||
|  | ||||
|             playlists.forEach { aod -> | ||||
|                 // TODO improve language handling | ||||
|                 val locale = when (aod.extLanguage) { | ||||
|                     "ger" -> Locale.GERMAN | ||||
|                     "jap" -> Locale.JAPANESE | ||||
|                     else -> Locale.ROOT | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Log.i(javaClass.name, "Loaded playlists successfully") | ||||
|  | ||||
|         // additional info from the media page | ||||
|         res.select("table.vertical-table").select("tr").forEach { row -> | ||||
|             when (row.select("th").text().lowercase(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() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // similar titles from media page | ||||
|         media.info.similar = res.select("h2:contains(Ähnliche Animes)").next().select("li").mapNotNull { | ||||
|             val mediaId = it.select("a.thumbs").attr("href") | ||||
|                 .substringAfterLast("/").toIntOrNull() | ||||
|             val mediaImage = it.select("a.thumbs > img").attr("src") | ||||
|             val mediaTitle = it.select("a").text() | ||||
|  | ||||
|             if (mediaId != null) { | ||||
|                 ItemMedia(mediaId, mediaTitle, mediaImage) | ||||
|             } else { | ||||
|                 null | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // additional information for tv shows the episode title (description) is loaded from the "api" | ||||
|         if (media.type == MediaType.TVSHOW) { | ||||
|             res.select("div.three-box-container > div.episodebox").forEach { episodebox -> | ||||
|                 // make sure the episode has a streaming link | ||||
|                 if (episodebox.select("input.streamstarter_html5").isNotEmpty()) { | ||||
|                     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() | ||||
|  | ||||
|                     media.episodes.firstOrNull { it.id == episodeId }?.apply { | ||||
|                         shortDesc = episodeShortDesc | ||||
|                         watched = episodeWatched | ||||
|                         watchedCallback = episodeWatchedCallback | ||||
|                 aod.playlist.forEach { ep -> | ||||
|                     try { | ||||
|                         if (media.hasEpisode(ep.mediaid)) { | ||||
|                             media.getEpisodeById(ep.mediaid).streams.add( | ||||
|                                 Stream(ep.sources.first().file, locale) | ||||
|                             ) | ||||
|                         } else { | ||||
|                             media.episodes.add(Episode( | ||||
|                                 id = ep.mediaid, | ||||
|                                 streams = mutableListOf(Stream(ep.sources.first().file, locale)), | ||||
|                                 posterUrl = ep.image, | ||||
|                                 title = ep.title, | ||||
|                                 description = ep.description, | ||||
|                                 number = getNumberFromTitle(ep.title, media.type) | ||||
|                             )) | ||||
|                         } | ||||
|                     } catch (ex: Exception) { | ||||
|                         Log.w(javaClass.name, "Could not parse episode information.", ex) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             Log.i(javaClass.name, "Loaded playlists successfully") | ||||
|  | ||||
|             // additional info from the media page | ||||
|             res.select("table.vertical-table").select("tr").forEach { row -> | ||||
|                 when (row.select("th").text().lowercase(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() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // similar titles from media page | ||||
|             media.info.similar = res.select("h2:contains(Ähnliche Animes)").next().select("li").mapNotNull { | ||||
|                 val mediaId = it.select("a.thumbs").attr("href") | ||||
|                     .substringAfterLast("/").toIntOrNull() | ||||
|                 val mediaImage = it.select("a.thumbs > img").attr("src") | ||||
|                 val mediaTitle = it.select("a").text() | ||||
|  | ||||
|                 if (mediaId != null) { | ||||
|                     ItemMedia(mediaId, mediaTitle, mediaImage) | ||||
|                 } else { | ||||
|                     null | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // additional information for tv shows the episode title (description) is loaded from the "api" | ||||
|             if (media.type == MediaType.TVSHOW) { | ||||
|                 res.select("div.three-box-container > div.episodebox").forEach { episodebox -> | ||||
|                     // make sure the episode has a streaming link | ||||
|                     if (episodebox.select("input.streamstarter_html5").isNotEmpty()) { | ||||
|                         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() | ||||
|  | ||||
|                         media.episodes.firstOrNull { it.id == episodeId }?.apply { | ||||
|                             shortDesc = episodeShortDesc | ||||
|                             watched = episodeWatched | ||||
|                             watchedCallback = episodeWatchedCallback | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             Log.i(javaClass.name, "media loaded successfully") | ||||
|         } | ||||
|         Log.i(javaClass.name, "media loaded successfully") | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @ -402,7 +415,7 @@ object AoDParser { | ||||
|             return CompletableDeferred(AoDObject(listOf(), language)) | ||||
|         } | ||||
|  | ||||
|         return GlobalScope.async(Dispatchers.IO) { | ||||
|         return CoroutineScope(Dispatchers.IO).async(Dispatchers.IO) { | ||||
|             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"), | ||||
|  | ||||
| @ -32,20 +32,19 @@ import androidx.fragment.app.commit | ||||
| import com.afollestad.materialdialogs.MaterialDialog | ||||
| import com.afollestad.materialdialogs.callbacks.onDismiss | ||||
| import com.google.android.material.bottomnavigation.BottomNavigationView | ||||
| import kotlinx.coroutines.joinAll | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import kotlinx.coroutines.* | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.databinding.ActivityMainBinding | ||||
| import org.mosad.teapod.parser.AoDParser | ||||
| import org.mosad.teapod.ui.activity.player.PlayerActivity | ||||
| import org.mosad.teapod.preferences.EncryptedPreferences | ||||
| import org.mosad.teapod.preferences.Preferences | ||||
| import org.mosad.teapod.ui.components.LoginDialog | ||||
| import org.mosad.teapod.ui.activity.main.fragments.AccountFragment | ||||
| import org.mosad.teapod.ui.activity.main.fragments.HomeFragment | ||||
| import org.mosad.teapod.ui.activity.main.fragments.LibraryFragment | ||||
| import org.mosad.teapod.ui.activity.main.fragments.SearchFragment | ||||
| import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity | ||||
| import org.mosad.teapod.ui.activity.player.PlayerActivity | ||||
| import org.mosad.teapod.ui.components.LoginDialog | ||||
| import org.mosad.teapod.util.DataTypes | ||||
| import org.mosad.teapod.util.StorageController | ||||
| import org.mosad.teapod.util.exitAndRemoveTask | ||||
| @ -138,7 +137,8 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS | ||||
|      */ | ||||
|     private fun load() { | ||||
|         val time = measureTimeMillis { | ||||
|             val loadingJob = AoDParser.initialLoading() // start the initial loading | ||||
|             val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope")) | ||||
|                 .async { AoDParser.initialLoading() } // start the initial loading | ||||
|  | ||||
|             // load all saved stuff here | ||||
|             Preferences.load(this) | ||||
| @ -165,7 +165,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             runBlocking { loadingJob.joinAll() } // wait for initial loading to finish | ||||
|             runBlocking { loadingJob.await() } // wait for initial loading to finish | ||||
|         } | ||||
|         Log.i(javaClass.name, "loading and login in $time ms") | ||||
|  | ||||
|  | ||||
| @ -12,9 +12,9 @@ import android.widget.Toast | ||||
| import androidx.activity.result.contract.ActivityResultContracts | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import com.afollestad.materialdialogs.MaterialDialog | ||||
| import com.afollestad.materialdialogs.list.listItemsSingleChoice | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.launch | ||||
| import org.mosad.teapod.BuildConfig | ||||
| import org.mosad.teapod.R | ||||
| @ -64,7 +64,7 @@ class AccountFragment : Fragment() { | ||||
|  | ||||
|         // load subscription (async) info before anything else | ||||
|         binding.textAccountSubscription.text = getString(R.string.account_subscription, getString(R.string.loading)) | ||||
|         GlobalScope.launch { | ||||
|         lifecycleScope.launch { | ||||
|             binding.textAccountSubscription.text = getString( | ||||
|                 R.string.account_subscription, | ||||
|                 AoDParser.getSubscriptionInfoAsync().await() | ||||
|  | ||||
| @ -6,14 +6,13 @@ import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import com.bumptech.glide.Glide | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.launch | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.ui.activity.main.MainActivity | ||||
| import org.mosad.teapod.databinding.FragmentHomeBinding | ||||
| import org.mosad.teapod.parser.AoDParser | ||||
| import org.mosad.teapod.ui.activity.main.MainActivity | ||||
| import org.mosad.teapod.util.ItemMedia | ||||
| import org.mosad.teapod.util.StorageController | ||||
| import org.mosad.teapod.util.adapter.MediaItemAdapter | ||||
| @ -40,7 +39,7 @@ class HomeFragment : Fragment() { | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|  | ||||
|         GlobalScope.launch(Dispatchers.Main) { | ||||
|         lifecycleScope.launch { | ||||
|             context?.let { | ||||
|                 initHighlight() | ||||
|                 initRecyclerViews() | ||||
| @ -101,7 +100,7 @@ class HomeFragment : Fragment() { | ||||
|     private fun initActions() { | ||||
|         binding.buttonPlayHighlight.setOnClickListener { | ||||
|             // TODO get next episode | ||||
|             GlobalScope.launch { | ||||
|             lifecycleScope.launch { | ||||
|                 val media = AoDParser.getMediaById(highlightMedia.id) | ||||
|  | ||||
|                 Log.d(javaClass.name, "Starting Player with  mediaId: ${media.id}") | ||||
|  | ||||
| @ -5,10 +5,8 @@ import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.fragment.app.Fragment | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.withContext | ||||
| import org.mosad.teapod.databinding.FragmentLibraryBinding | ||||
| import org.mosad.teapod.parser.AoDParser | ||||
| import org.mosad.teapod.util.adapter.MediaItemAdapter | ||||
| @ -29,18 +27,16 @@ class LibraryFragment : Fragment() { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|  | ||||
|         // init async | ||||
|         GlobalScope.launch { | ||||
|         lifecycleScope.launch { | ||||
|             // create and set the adapter, needs context | ||||
|             withContext(Dispatchers.Main) { | ||||
|                 context?.let { | ||||
|                     adapter = MediaItemAdapter(AoDParser.itemMediaList) | ||||
|                     adapter.onItemClick = { mediaId, _ -> | ||||
|                         activity?.showFragment(MediaFragment(mediaId)) | ||||
|                     } | ||||
|  | ||||
|                     binding.recyclerMediaLibrary.adapter = adapter | ||||
|                     binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9)) | ||||
|             context?.let { | ||||
|                 adapter = MediaItemAdapter(AoDParser.itemMediaList) | ||||
|                 adapter.onItemClick = { mediaId, _ -> | ||||
|                     activity?.showFragment(MediaFragment(mediaId)) | ||||
|                 } | ||||
|  | ||||
|                 binding.recyclerMediaLibrary.adapter = adapter | ||||
|                 binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9)) | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
| @ -10,20 +10,21 @@ import android.view.ViewGroup | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import androidx.fragment.app.activityViewModels | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import androidx.viewpager2.adapter.FragmentStateAdapter | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.request.RequestOptions | ||||
| import com.google.android.material.appbar.AppBarLayout | ||||
| import com.google.android.material.tabs.TabLayoutMediator | ||||
| import jp.wasabeef.glide.transformations.BlurTransformation | ||||
| import kotlinx.coroutines.* | ||||
| import kotlinx.coroutines.launch | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.databinding.FragmentMediaBinding | ||||
| import org.mosad.teapod.ui.activity.main.MainActivity | ||||
| import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel | ||||
| import org.mosad.teapod.util.* | ||||
| import org.mosad.teapod.util.DataTypes.MediaType | ||||
|  | ||||
| import org.mosad.teapod.util.Episode | ||||
| import org.mosad.teapod.util.StorageController | ||||
|  | ||||
| /** | ||||
|  * The media detail fragment. | ||||
| @ -61,13 +62,12 @@ class MediaFragment(private val mediaId: Int) : Fragment() { | ||||
|             } | ||||
|         }.attach() | ||||
|  | ||||
|         GlobalScope.launch(Dispatchers.Main) { | ||||
|  | ||||
|         lifecycleScope.launch { | ||||
|             model.load(mediaId) // load the streams and tmdb for the selected media | ||||
|  | ||||
|             if (this@MediaFragment.isAdded) { | ||||
|                 updateGUI() | ||||
|                 initActions() | ||||
|             } | ||||
|             updateGUI() | ||||
|             initActions() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -6,7 +6,8 @@ import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.SearchView | ||||
| import androidx.fragment.app.Fragment | ||||
| import kotlinx.coroutines.* | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import kotlinx.coroutines.launch | ||||
| import org.mosad.teapod.databinding.FragmentSearchBinding | ||||
| import org.mosad.teapod.parser.AoDParser | ||||
| import org.mosad.teapod.util.decoration.MediaItemDecoration | ||||
| @ -26,9 +27,8 @@ class SearchFragment : Fragment() { | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|  | ||||
|         GlobalScope.launch { | ||||
|         lifecycleScope.launch { | ||||
|             // create and set the adapter, needs context | ||||
|             withContext(Dispatchers.Main) { | ||||
|                 context?.let { | ||||
|                     adapter = MediaItemAdapter(AoDParser.itemMediaList) | ||||
|                     adapter!!.onItemClick = { mediaId, _ -> | ||||
| @ -39,7 +39,6 @@ class SearchFragment : Fragment() { | ||||
|                     binding.recyclerMediaSearch.adapter = adapter | ||||
|                     binding.recyclerMediaSearch.addItemDecoration(MediaItemDecoration(9)) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         initActions() | ||||
|  | ||||
| @ -5,6 +5,7 @@ import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import kotlinx.coroutines.* | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.databinding.FragmentOnLoginBinding | ||||
| @ -35,7 +36,7 @@ class OnLoginFragment: Fragment() { | ||||
|             EncryptedPreferences.saveCredentials(email, password, requireContext()) // save the credentials | ||||
|  | ||||
|             binding.buttonLogin.isClickable = false | ||||
|             loginJob = GlobalScope.launch { | ||||
|             loginJob = lifecycleScope.launch { | ||||
|                 if (AoDParser.login()) { | ||||
|                     // if login was successful, switch to main | ||||
|                     if (activity is OnboardingActivity) { | ||||
|  | ||||
| @ -19,6 +19,7 @@ import androidx.annotation.RequiresApi | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.core.view.GestureDetectorCompat | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import com.google.android.exoplayer2.ExoPlayer | ||||
| import com.google.android.exoplayer2.Player | ||||
| import com.google.android.exoplayer2.ui.StyledPlayerControlView | ||||
| @ -26,7 +27,6 @@ import com.google.android.exoplayer2.util.Util | ||||
| import kotlinx.android.synthetic.main.activity_player.* | ||||
| import kotlinx.android.synthetic.main.player_controls.* | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.withContext | ||||
| import org.mosad.teapod.R | ||||
| @ -255,7 +255,7 @@ class PlayerActivity : AppCompatActivity() { | ||||
|         } | ||||
|  | ||||
|         timerUpdates = Timer().scheduleAtFixedRate(0, 500) { | ||||
|             GlobalScope.launch { | ||||
|             lifecycleScope.launch { | ||||
|                 var btnNextEpIsVisible: Boolean | ||||
|                 var controlsVisible: Boolean | ||||
|  | ||||
|  | ||||
| @ -4,6 +4,7 @@ import android.app.Application | ||||
| import android.net.Uri | ||||
| import android.util.Log | ||||
| import androidx.lifecycle.AndroidViewModel | ||||
| import androidx.lifecycle.viewModelScope | ||||
| import com.google.android.exoplayer2.C | ||||
| import com.google.android.exoplayer2.MediaItem | ||||
| import com.google.android.exoplayer2.SimpleExoPlayer | ||||
| @ -11,6 +12,7 @@ import com.google.android.exoplayer2.source.MediaSource | ||||
| import com.google.android.exoplayer2.source.hls.HlsMediaSource | ||||
| import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory | ||||
| import com.google.android.exoplayer2.util.Util | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.parser.AoDParser | ||||
| @ -107,7 +109,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) | ||||
|  | ||||
|         // if episodes has not been watched, mark as watched | ||||
|         if (!episode.watched) { | ||||
|             AoDParser.markAsWatched(media.id, episode.id) | ||||
|             viewModelScope.launch { | ||||
|                 AoDParser.markAsWatched(media.id, episode.id) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -3,16 +3,12 @@ package org.mosad.teapod.util | ||||
| import android.content.Context | ||||
| import android.net.Uri | ||||
| import android.util.Log | ||||
| import android.widget.Toast | ||||
| import com.google.gson.Gson | ||||
| import com.google.gson.JsonParser | ||||
| import kotlinx.coroutines.* | ||||
| import org.mosad.teapod.R | ||||
| import java.io.File | ||||
| import java.io.FileReader | ||||
| import java.io.FileWriter | ||||
| import java.lang.Exception | ||||
| import java.net.URI | ||||
|  | ||||
| /** | ||||
|  * This controller contains the logic for permanently saved data. | ||||
| @ -45,7 +41,7 @@ object StorageController { | ||||
|     fun saveMyList(context: Context): Job { | ||||
|         val file = File(context.filesDir, fileNameMyList) | ||||
|  | ||||
|         return GlobalScope.launch(Dispatchers.IO) { | ||||
|         return CoroutineScope(Dispatchers.IO).launch { | ||||
|             file.writeText(Gson().toJson(myList.distinct())) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -24,8 +24,8 @@ class TMDBApiController { | ||||
|         val searchTerm = title.replace("(Sub)", "").trim() | ||||
|  | ||||
|         return when (type) { | ||||
|             MediaType.MOVIE -> searchMovie(searchTerm).await() | ||||
|             MediaType.TVSHOW -> searchTVShow(searchTerm).await() | ||||
|             MediaType.MOVIE -> searchMovie(searchTerm) | ||||
|             MediaType.TVSHOW -> searchTVShow(searchTerm) | ||||
|             else -> { | ||||
|                 Log.e(javaClass.name, "Wrong Type: $type") | ||||
|                 TMDBResponse() | ||||
| @ -34,62 +34,56 @@ class TMDBApiController { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     fun searchTVShow(title: String): Deferred<TMDBResponse> { | ||||
|     @Suppress("BlockingMethodInNonBlockingContext") | ||||
|     private suspend fun searchTVShow(title: String): TMDBResponse = withContext(Dispatchers.IO) { | ||||
|         val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}") | ||||
|         val response = JsonParser.parseString(url.readText()).asJsonObject | ||||
|         //println(response) | ||||
|  | ||||
|         return GlobalScope.async { | ||||
|             val response = JsonParser.parseString(url.readText()).asJsonObject | ||||
|             //println(response) | ||||
|         return@withContext if (response.get("total_results").asInt > 0) { | ||||
|             response.get("results").asJsonArray.first().asJsonObject.let { | ||||
|                 val id = getStringNotNull(it, "id").toInt() | ||||
|                 val overview = getStringNotNull(it, "overview") | ||||
|                 val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) | ||||
|                 val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) | ||||
|  | ||||
|             if (response.get("total_results").asInt > 0) { | ||||
|                 response.get("results").asJsonArray.first().asJsonObject.let { | ||||
|                     val id = getStringNotNull(it, "id").toInt() | ||||
|                     val overview = getStringNotNull(it, "overview") | ||||
|                     val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) | ||||
|                     val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) | ||||
|  | ||||
|                     TMDBResponse(id, "", overview, posterPath, backdropPath) | ||||
|                 } | ||||
|             } else { | ||||
|                 TMDBResponse() | ||||
|                 TMDBResponse(id, "", overview, posterPath, backdropPath) | ||||
|             } | ||||
|         } else { | ||||
|             TMDBResponse() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun searchMovie(title: String): Deferred<TMDBResponse> { | ||||
|     @Suppress("BlockingMethodInNonBlockingContext") | ||||
|     private suspend fun searchMovie(title: String): TMDBResponse = withContext(Dispatchers.IO) { | ||||
|         val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}") | ||||
|         val response = JsonParser.parseString(url.readText()).asJsonObject | ||||
|         //println(response) | ||||
|  | ||||
|         return GlobalScope.async { | ||||
|             val response = JsonParser.parseString(url.readText()).asJsonObject | ||||
|             //println(response) | ||||
|         return@withContext if (response.get("total_results").asInt > 0) { | ||||
|             response.get("results").asJsonArray.first().asJsonObject.let { | ||||
|                 val id = getStringNotNull(it,"id").toInt() | ||||
|                 val overview = getStringNotNull(it,"overview") | ||||
|                 val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) | ||||
|                 val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) | ||||
|                 val runtime = getMovieRuntime(id) | ||||
|  | ||||
|             if (response.get("total_results").asInt > 0) { | ||||
|                 response.get("results").asJsonArray.first().asJsonObject.let { | ||||
|                     val id = getStringNotNull(it,"id").toInt() | ||||
|                     val overview = getStringNotNull(it,"overview") | ||||
|                     val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) | ||||
|                     val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) | ||||
|                     val runtime = getMovieRuntime(id) | ||||
|  | ||||
|                     TMDBResponse(id, "", overview, posterPath, backdropPath, runtime) | ||||
|                 } | ||||
|             } else { | ||||
|                 TMDBResponse() | ||||
|                 TMDBResponse(id, "", overview, posterPath, backdropPath, runtime) | ||||
|             } | ||||
|         } else { | ||||
|             TMDBResponse() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * currently only used for runtime, need a rework | ||||
|      */ | ||||
|     fun getMovieRuntime(id: Int): Int = runBlocking { | ||||
|     @Suppress("BlockingMethodInNonBlockingContext") | ||||
|     suspend fun getMovieRuntime(id: Int): Int = withContext(Dispatchers.IO) { | ||||
|         val url = URL("$getMovieUrl/$id?api_key=$apiKey&language=$language") | ||||
|  | ||||
|         GlobalScope.async { | ||||
|             val response = JsonParser.parseString(url.readText()).asJsonObject | ||||
|  | ||||
|             return@async getStringNotNull(response,"runtime").toInt() | ||||
|         }.await() | ||||
|         val response = JsonParser.parseString(url.readText()).asJsonObject | ||||
|         return@withContext getStringNotNull(response,"runtime").toInt() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|  | ||||
		Reference in New Issue
	
	Block a user