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:
Jannik 2021-06-06 17:54:19 +02:00
parent 46e3d1f1b6
commit 5e48e724a7
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
15 changed files with 305 additions and 301 deletions

View File

@ -41,18 +41,20 @@ android {
dependencies { dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
implementation 'androidx.security:security-crypto:1.1.0-alpha03' implementation 'androidx.security:security-crypto:1.1.0-alpha03'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
implementation 'com.google.android.material:material:1.3.0' implementation 'com.google.android.material:material:1.3.0'
implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.google.code.gson:gson:2.8.7'
implementation 'com.google.android.exoplayer:exoplayer-core:2.13.3' implementation 'com.google.android.exoplayer:exoplayer-core:2.13.3'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.13.3' implementation 'com.google.android.exoplayer:exoplayer-hls:2.13.3'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.13.3' implementation 'com.google.android.exoplayer:exoplayer-dash:2.13.3'

View File

@ -99,10 +99,12 @@ object AoDParser {
/** /**
* initially load all media and home screen data * initially load all media and home screen data
*/ */
fun initialLoading() = listOf( suspend fun initialLoading() {
loadHome(), coroutineScope {
listAnimes() launch { loadHome() }
) launch { listAnimes() }
}
}
/** /**
* get a media by it's ID (int) * 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 * get subscription info from aod website, remove "Anime-Abo" Prefix and trim
*/ */
fun getSubscriptionInfoAsync(): Deferred<String> { suspend fun getSubscriptionInfoAsync(): Deferred<String> {
return GlobalScope.async(Dispatchers.IO) { return coroutineScope {
// get the subscription page async(Dispatchers.IO) {
val res = Jsoup.connect(baseUrl + subscriptionPath) val res = Jsoup.connect(baseUrl + subscriptionPath)
.cookies(sessionCookies) .cookies(sessionCookies)
.get() .get()
return@async res.select("a:contains(Anime-Abo)").text() return@async res.select("a:contains(Anime-Abo)").text()
.removePrefix("Anime-Abo").trim() .removePrefix("Anime-Abo").trim()
}
} }
} }
@ -137,7 +140,7 @@ object AoDParser {
return baseUrl + subscriptionPath return baseUrl + subscriptionPath
} }
fun markAsWatched(mediaId: Int, episodeId: Int) = GlobalScope.launch { suspend fun markAsWatched(mediaId: Int, episodeId: Int) {
val episode = getMediaById(mediaId).getEpisodeById(episodeId) val episode = getMediaById(mediaId).getEpisodeById(episodeId)
episode.watched = true episode.watched = true
sendCallback(episode.watchedCallback) sendCallback(episode.watchedCallback)
@ -146,137 +149,145 @@ object AoDParser {
} }
// TODO don't use jsoup here // TODO don't use jsoup here
private fun sendCallback(callbackPath: String) = GlobalScope.launch(Dispatchers.IO) { private suspend fun sendCallback(callbackPath: String) = coroutineScope {
val headers = mutableMapOf( launch(Dispatchers.IO) {
Pair("Accept", "application/json, text/javascript, */*; q=0.01"), val headers = mutableMapOf(
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"), Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
Pair("Accept-Encoding", "gzip, deflate, br"), Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),
Pair("X-CSRF-Token", csrfToken), Pair("Accept-Encoding", "gzip, deflate, br"),
Pair("X-Requested-With", "XMLHttpRequest"), Pair("X-CSRF-Token", csrfToken),
) Pair("X-Requested-With", "XMLHttpRequest"),
)
try { try {
Jsoup.connect(baseUrl + callbackPath) Jsoup.connect(baseUrl + callbackPath)
.ignoreContentType(true) .ignoreContentType(true)
.cookies(sessionCookies) .cookies(sessionCookies)
.headers(headers) .headers(headers)
.execute() .execute()
} catch (ex: IOException) { } catch (ex: IOException) {
Log.e(javaClass.name, "Callback for $callbackPath failed.", ex) Log.e(javaClass.name, "Callback for $callbackPath failed.", ex)
}
} }
} }
/** /**
* load all media from aod into itemMediaList and mediaList * 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) { private suspend fun listAnimes() = withContext(Dispatchers.IO) {
val resAnimes = Jsoup.connect(baseUrl + libraryPath).get() launch(Dispatchers.IO) {
//println(resAnimes) val resAnimes = Jsoup.connect(baseUrl + libraryPath).get()
//println(resAnimes)
itemMediaList.clear() itemMediaList.clear()
mediaList.clear() mediaList.clear()
resAnimes.select("div.animebox").forEach { resAnimes.select("div.animebox").forEach {
val type = if (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT) == "zur serie") { val type = if (it.select("p.animebox-link").select("a").text().lowercase(Locale.ROOT) == "zur serie") {
MediaType.TVSHOW MediaType.TVSHOW
} else { } else {
MediaType.MOVIE 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)) Log.i(javaClass.name, "Total library size is: ${mediaList.size}")
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}")
} }
/** /**
* load new episodes, titles and highlights * load new episodes, titles and highlights
*/ */
private fun loadHome() = GlobalScope.launch(Dispatchers.IO) { private suspend fun loadHome() = withContext(Dispatchers.IO) {
val resHome = Jsoup.connect(baseUrl).get() launch(Dispatchers.IO) {
val resHome = Jsoup.connect(baseUrl).get()
// get highlights from AoD // get highlights from AoD
highlightsList.clear() highlightsList.clear()
resHome.select("#aod-highlights").select("div.news-item").forEach { resHome.select("#aod-highlights").select("div.news-item").forEach {
val mediaId = it.select("div.news-item-text").select("a.serienlink") val mediaId = it.select("div.news-item-text").select("a.serienlink")
.attr("href").substringAfterLast("/").toIntOrNull() .attr("href").substringAfterLast("/").toIntOrNull()
val mediaTitle = it.select("div.news-title").select("h2").text() val mediaTitle = it.select("div.news-title").select("h2").text()
val mediaImage = it.select("img").attr("src") val mediaImage = it.select("img").attr("src")
if (mediaId != null) { if (mediaId != null) {
highlightsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) highlightsList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
}
} }
}
// get all new episodes from AoD // get all new episodes from AoD
newEpisodesList.clear() newEpisodesList.clear()
resHome.select("h2:contains(Neue Episoden)").next().select("li").forEach { resHome.select("h2:contains(Neue Episoden)").next().select("li").forEach {
val mediaId = it.select("a.thumbs").attr("href") val mediaId = it.select("a.thumbs").attr("href")
.substringAfterLast("/").toIntOrNull() .substringAfterLast("/").toIntOrNull()
val mediaImage = it.select("a.thumbs > img").attr("src") val mediaImage = it.select("a.thumbs > img").attr("src")
val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}" val mediaTitle = "${it.select("a").text()} - ${it.select("span.neweps").text()}"
if (mediaId != null) { if (mediaId != null) {
newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) newEpisodesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
}
} }
}
// get new simulcasts from AoD // get new simulcasts from AoD
newSimulcastsList.clear() newSimulcastsList.clear()
resHome.select("h2:contains(Neue Simulcasts)").next().select("li").forEach { resHome.select("h2:contains(Neue Simulcasts)").next().select("li").forEach {
val mediaId = it.select("a.thumbs").attr("href") val mediaId = it.select("a.thumbs").attr("href")
.substringAfterLast("/").toIntOrNull() .substringAfterLast("/").toIntOrNull()
val mediaImage = it.select("a.thumbs > img").attr("src") val mediaImage = it.select("a.thumbs > img").attr("src")
val mediaTitle = it.select("a").text() val mediaTitle = it.select("a").text()
if (mediaId != null) { if (mediaId != null) {
newSimulcastsList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) newSimulcastsList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
}
} }
}
// get new titles from AoD // get new titles from AoD
newTitlesList.clear() newTitlesList.clear()
resHome.select("h2:contains(Neue Anime-Titel)").next().select("li").forEach { resHome.select("h2:contains(Neue Anime-Titel)").next().select("li").forEach {
val mediaId = it.select("a.thumbs").attr("href") val mediaId = it.select("a.thumbs").attr("href")
.substringAfterLast("/").toIntOrNull() .substringAfterLast("/").toIntOrNull()
val mediaImage = it.select("a.thumbs > img").attr("src") val mediaImage = it.select("a.thumbs > img").attr("src")
val mediaTitle = it.select("a").text() val mediaTitle = it.select("a").text()
if (mediaId != null) { if (mediaId != null) {
newTitlesList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) newTitlesList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
}
} }
}
// get top ten from AoD // get top ten from AoD
topTenList.clear() topTenList.clear()
resHome.select("h2:contains(Anime Top 10)").next().select("li").forEach { resHome.select("h2:contains(Anime Top 10)").next().select("li").forEach {
val mediaId = it.select("a.thumbs").attr("href") val mediaId = it.select("a.thumbs").attr("href")
.substringAfterLast("/").toIntOrNull() .substringAfterLast("/").toIntOrNull()
val mediaImage = it.select("a.thumbs > img").attr("src") val mediaImage = it.select("a.thumbs > img").attr("src")
val mediaTitle = it.select("a").text() val mediaTitle = it.select("a").text()
if (mediaId != null) { if (mediaId != null) {
topTenList.add(ItemMedia(mediaId, mediaTitle, mediaImage)) topTenList.add(ItemMedia(mediaId, mediaTitle, mediaImage))
}
} }
}
// if highlights is empty, add a random new title // if highlights is empty, add a random new title
if (highlightsList.isEmpty()) { if (highlightsList.isEmpty()) {
if (newTitlesList.isNotEmpty()) { if (newTitlesList.isNotEmpty()) {
highlightsList.add(newTitlesList[Random.nextInt(0, newTitlesList.size)]) highlightsList.add(newTitlesList[Random.nextInt(0, newTitlesList.size)])
} else { } else {
highlightsList.add(ItemMedia(0,"", "")) 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 * load streams for the media path, movies have one episode
* @param media is used as call ba reference * @param media is used as call ba reference
*/ */
private fun loadStreams(media: Media) = GlobalScope.launch(Dispatchers.IO) { private suspend fun loadStreams(media: Media) = coroutineScope {
if (sessionCookies.isEmpty()) login() launch(Dispatchers.IO) {
if (sessionCookies.isEmpty()) login()
if (!loginSuccess) { if (!loginSuccess) {
Log.w(javaClass.name, "Login, was not successful.") Log.w(javaClass.name, "Login, was not successful.")
return@launch 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
} }
aod.playlist.forEach { ep -> // get the media page
try { val res = Jsoup.connect(baseUrl + media.link)
if (media.hasEpisode(ep.mediaid)) { .cookies(sessionCookies)
media.getEpisodeById(ep.mediaid).streams.add( .get()
Stream(ep.sources.first().file, locale)
) //println(res)
} else {
media.episodes.add(Episode( if (csrfToken.isEmpty()) {
id = ep.mediaid, csrfToken = res.select("meta[name=csrf-token]").attr("content")
streams = mutableListOf(Stream(ep.sources.first().file, locale)), //Log.i(javaClass.name, "New csrf token is $csrfToken")
posterUrl = ep.image, }
title = ep.title,
description = ep.description, val besides = res.select("div.besides").first()
number = getNumberFromTitle(ep.title, media.type) val playlists = besides.select("input.streamstarter_html5").map { streamstarter ->
)) parsePlaylistAsync(
} streamstarter.attr("data-playlist"),
} catch (ex: Exception) { streamstarter.attr("data-lang")
Log.w(javaClass.name, "Could not parse episode information.", ex) )
}.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 aod.playlist.forEach { ep ->
res.select("table.vertical-table").select("tr").forEach { row -> try {
when (row.select("th").text().lowercase(Locale.ROOT)) { if (media.hasEpisode(ep.mediaid)) {
"produktionsjahr" -> media.info.year = row.select("td").text().toInt() media.getEpisodeById(ep.mediaid).streams.add(
"fsk" -> media.info.age = row.select("td").text().toInt() Stream(ep.sources.first().file, locale)
"episodenanzahl" -> { )
media.info.episodesCount = row.select("td").text() } else {
.substringBefore("/") media.episodes.add(Episode(
.filter { it.isDigit() } id = ep.mediaid,
.toInt() streams = mutableListOf(Stream(ep.sources.first().file, locale)),
} posterUrl = ep.image,
} title = ep.title,
} description = ep.description,
number = getNumberFromTitle(ep.title, media.type)
// 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") } catch (ex: Exception) {
.substringAfterLast("/").toIntOrNull() Log.w(javaClass.name, "Could not parse episode information.", ex)
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, "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 CompletableDeferred(AoDObject(listOf(), language))
} }
return GlobalScope.async(Dispatchers.IO) { return CoroutineScope(Dispatchers.IO).async(Dispatchers.IO) {
val headers = mutableMapOf( val headers = mutableMapOf(
Pair("Accept", "application/json, text/javascript, */*; q=0.01"), Pair("Accept", "application/json, text/javascript, */*; q=0.01"),
Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"), Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"),

View File

@ -32,20 +32,19 @@ import androidx.fragment.app.commit
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.callbacks.onDismiss import com.afollestad.materialdialogs.callbacks.onDismiss
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.coroutines.joinAll import kotlinx.coroutines.*
import kotlinx.coroutines.runBlocking
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.ActivityMainBinding import org.mosad.teapod.databinding.ActivityMainBinding
import org.mosad.teapod.parser.AoDParser 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.EncryptedPreferences
import org.mosad.teapod.preferences.Preferences 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.AccountFragment
import org.mosad.teapod.ui.activity.main.fragments.HomeFragment 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.LibraryFragment
import org.mosad.teapod.ui.activity.main.fragments.SearchFragment import org.mosad.teapod.ui.activity.main.fragments.SearchFragment
import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity 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.DataTypes
import org.mosad.teapod.util.StorageController import org.mosad.teapod.util.StorageController
import org.mosad.teapod.util.exitAndRemoveTask import org.mosad.teapod.util.exitAndRemoveTask
@ -138,7 +137,8 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
*/ */
private fun load() { private fun load() {
val time = measureTimeMillis { 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 // load all saved stuff here
Preferences.load(this) 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") Log.i(javaClass.name, "loading and login in $time ms")

View File

@ -12,9 +12,9 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItemsSingleChoice import com.afollestad.materialdialogs.list.listItemsSingleChoice
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.mosad.teapod.BuildConfig import org.mosad.teapod.BuildConfig
import org.mosad.teapod.R import org.mosad.teapod.R
@ -64,7 +64,7 @@ class AccountFragment : Fragment() {
// load subscription (async) info before anything else // load subscription (async) info before anything else
binding.textAccountSubscription.text = getString(R.string.account_subscription, getString(R.string.loading)) binding.textAccountSubscription.text = getString(R.string.account_subscription, getString(R.string.loading))
GlobalScope.launch { lifecycleScope.launch {
binding.textAccountSubscription.text = getString( binding.textAccountSubscription.text = getString(
R.string.account_subscription, R.string.account_subscription,
AoDParser.getSubscriptionInfoAsync().await() AoDParser.getSubscriptionInfoAsync().await()

View File

@ -6,14 +6,13 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.ui.activity.main.MainActivity
import org.mosad.teapod.databinding.FragmentHomeBinding import org.mosad.teapod.databinding.FragmentHomeBinding
import org.mosad.teapod.parser.AoDParser 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.ItemMedia
import org.mosad.teapod.util.StorageController import org.mosad.teapod.util.StorageController
import org.mosad.teapod.util.adapter.MediaItemAdapter import org.mosad.teapod.util.adapter.MediaItemAdapter
@ -40,7 +39,7 @@ class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
GlobalScope.launch(Dispatchers.Main) { lifecycleScope.launch {
context?.let { context?.let {
initHighlight() initHighlight()
initRecyclerViews() initRecyclerViews()
@ -101,7 +100,7 @@ class HomeFragment : Fragment() {
private fun initActions() { private fun initActions() {
binding.buttonPlayHighlight.setOnClickListener { binding.buttonPlayHighlight.setOnClickListener {
// TODO get next episode // TODO get next episode
GlobalScope.launch { lifecycleScope.launch {
val media = AoDParser.getMediaById(highlightMedia.id) val media = AoDParser.getMediaById(highlightMedia.id)
Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}") Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}")

View File

@ -5,10 +5,8 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import kotlinx.coroutines.Dispatchers import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.mosad.teapod.databinding.FragmentLibraryBinding import org.mosad.teapod.databinding.FragmentLibraryBinding
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.util.adapter.MediaItemAdapter import org.mosad.teapod.util.adapter.MediaItemAdapter
@ -29,18 +27,16 @@ class LibraryFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// init async // init async
GlobalScope.launch { lifecycleScope.launch {
// create and set the adapter, needs context // create and set the adapter, needs context
withContext(Dispatchers.Main) { context?.let {
context?.let { adapter = MediaItemAdapter(AoDParser.itemMediaList)
adapter = MediaItemAdapter(AoDParser.itemMediaList) adapter.onItemClick = { mediaId, _ ->
adapter.onItemClick = { mediaId, _ -> activity?.showFragment(MediaFragment(mediaId))
activity?.showFragment(MediaFragment(mediaId))
}
binding.recyclerMediaLibrary.adapter = adapter
binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9))
} }
binding.recyclerMediaLibrary.adapter = adapter
binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9))
} }
} }

View File

@ -10,20 +10,21 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import jp.wasabeef.glide.transformations.BlurTransformation import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.coroutines.* import kotlinx.coroutines.launch
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.FragmentMediaBinding import org.mosad.teapod.databinding.FragmentMediaBinding
import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.ui.activity.main.MainActivity
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel 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.DataTypes.MediaType
import org.mosad.teapod.util.Episode
import org.mosad.teapod.util.StorageController
/** /**
* The media detail fragment. * The media detail fragment.
@ -61,13 +62,12 @@ class MediaFragment(private val mediaId: Int) : Fragment() {
} }
}.attach() }.attach()
GlobalScope.launch(Dispatchers.Main) {
lifecycleScope.launch {
model.load(mediaId) // load the streams and tmdb for the selected media model.load(mediaId) // load the streams and tmdb for the selected media
if (this@MediaFragment.isAdded) { updateGUI()
updateGUI() initActions()
initActions()
}
} }
} }

View File

@ -6,7 +6,8 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.SearchView import android.widget.SearchView
import androidx.fragment.app.Fragment 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.databinding.FragmentSearchBinding
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.util.decoration.MediaItemDecoration import org.mosad.teapod.util.decoration.MediaItemDecoration
@ -26,9 +27,8 @@ class SearchFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
GlobalScope.launch { lifecycleScope.launch {
// create and set the adapter, needs context // create and set the adapter, needs context
withContext(Dispatchers.Main) {
context?.let { context?.let {
adapter = MediaItemAdapter(AoDParser.itemMediaList) adapter = MediaItemAdapter(AoDParser.itemMediaList)
adapter!!.onItemClick = { mediaId, _ -> adapter!!.onItemClick = { mediaId, _ ->
@ -39,7 +39,6 @@ class SearchFragment : Fragment() {
binding.recyclerMediaSearch.adapter = adapter binding.recyclerMediaSearch.adapter = adapter
binding.recyclerMediaSearch.addItemDecoration(MediaItemDecoration(9)) binding.recyclerMediaSearch.addItemDecoration(MediaItemDecoration(9))
} }
}
} }
initActions() initActions()

View File

@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.FragmentOnLoginBinding import org.mosad.teapod.databinding.FragmentOnLoginBinding
@ -35,7 +36,7 @@ class OnLoginFragment: Fragment() {
EncryptedPreferences.saveCredentials(email, password, requireContext()) // save the credentials EncryptedPreferences.saveCredentials(email, password, requireContext()) // save the credentials
binding.buttonLogin.isClickable = false binding.buttonLogin.isClickable = false
loginJob = GlobalScope.launch { loginJob = lifecycleScope.launch {
if (AoDParser.login()) { if (AoDParser.login()) {
// if login was successful, switch to main // if login was successful, switch to main
if (activity is OnboardingActivity) { if (activity is OnboardingActivity) {

View File

@ -19,6 +19,7 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GestureDetectorCompat import androidx.core.view.GestureDetectorCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.StyledPlayerControlView 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.activity_player.*
import kotlinx.android.synthetic.main.player_controls.* import kotlinx.android.synthetic.main.player_controls.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.mosad.teapod.R import org.mosad.teapod.R
@ -255,7 +255,7 @@ class PlayerActivity : AppCompatActivity() {
} }
timerUpdates = Timer().scheduleAtFixedRate(0, 500) { timerUpdates = Timer().scheduleAtFixedRate(0, 500) {
GlobalScope.launch { lifecycleScope.launch {
var btnNextEpIsVisible: Boolean var btnNextEpIsVisible: Boolean
var controlsVisible: Boolean var controlsVisible: Boolean

View File

@ -4,6 +4,7 @@ import android.app.Application
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.exoplayer2.C import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.SimpleExoPlayer 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.source.hls.HlsMediaSource
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util import com.google.android.exoplayer2.util.Util
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.parser.AoDParser 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 episodes has not been watched, mark as watched
if (!episode.watched) { if (!episode.watched) {
AoDParser.markAsWatched(media.id, episode.id) viewModelScope.launch {
AoDParser.markAsWatched(media.id, episode.id)
}
} }
} }

View File

@ -3,16 +3,12 @@ package org.mosad.teapod.util
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import android.widget.Toast
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonParser import com.google.gson.JsonParser
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.mosad.teapod.R
import java.io.File import java.io.File
import java.io.FileReader import java.io.FileReader
import java.io.FileWriter import java.io.FileWriter
import java.lang.Exception
import java.net.URI
/** /**
* This controller contains the logic for permanently saved data. * This controller contains the logic for permanently saved data.
@ -45,7 +41,7 @@ object StorageController {
fun saveMyList(context: Context): Job { fun saveMyList(context: Context): Job {
val file = File(context.filesDir, fileNameMyList) val file = File(context.filesDir, fileNameMyList)
return GlobalScope.launch(Dispatchers.IO) { return CoroutineScope(Dispatchers.IO).launch {
file.writeText(Gson().toJson(myList.distinct())) file.writeText(Gson().toJson(myList.distinct()))
} }
} }

View File

@ -24,8 +24,8 @@ class TMDBApiController {
val searchTerm = title.replace("(Sub)", "").trim() val searchTerm = title.replace("(Sub)", "").trim()
return when (type) { return when (type) {
MediaType.MOVIE -> searchMovie(searchTerm).await() MediaType.MOVIE -> searchMovie(searchTerm)
MediaType.TVSHOW -> searchTVShow(searchTerm).await() MediaType.TVSHOW -> searchTVShow(searchTerm)
else -> { else -> {
Log.e(javaClass.name, "Wrong Type: $type") Log.e(javaClass.name, "Wrong Type: $type")
TMDBResponse() 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 url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
val response = JsonParser.parseString(url.readText()).asJsonObject
//println(response)
return GlobalScope.async { return@withContext if (response.get("total_results").asInt > 0) {
val response = JsonParser.parseString(url.readText()).asJsonObject response.get("results").asJsonArray.first().asJsonObject.let {
//println(response) 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) { TMDBResponse(id, "", overview, posterPath, backdropPath)
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()
} }
} 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 url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
val response = JsonParser.parseString(url.readText()).asJsonObject
//println(response)
return GlobalScope.async { return@withContext if (response.get("total_results").asInt > 0) {
val response = JsonParser.parseString(url.readText()).asJsonObject response.get("results").asJsonArray.first().asJsonObject.let {
//println(response) 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) { TMDBResponse(id, "", overview, posterPath, backdropPath, runtime)
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()
} }
} else {
TMDBResponse()
} }
} }
/** /**
* currently only used for runtime, need a rework * 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") val url = URL("$getMovieUrl/$id?api_key=$apiKey&language=$language")
GlobalScope.async { val response = JsonParser.parseString(url.readText()).asJsonObject
val response = JsonParser.parseString(url.readText()).asJsonObject return@withContext getStringNotNull(response,"runtime").toInt()
return@async getStringNotNull(response,"runtime").toInt()
}.await()
} }
/** /**

View File

@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = "1.5.0" ext.kotlin_version = "1.5.10"
repositories { repositories {
google() google()
mavenCentral() mavenCentral()

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists