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