Browse Source

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
pull/41/head
Jannik 1 year ago
parent
commit
5e48e724a7
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
  1. 10
      app/build.gradle
  2. 425
      app/src/main/java/org/mosad/teapod/parser/AoDParser.kt
  3. 12
      app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt
  4. 4
      app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt
  5. 9
      app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt
  6. 22
      app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt
  7. 16
      app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt
  8. 7
      app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt
  9. 3
      app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt
  10. 4
      app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt
  11. 6
      app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt
  12. 6
      app/src/main/java/org/mosad/teapod/util/StorageController.kt
  13. 76
      app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt
  14. 2
      build.gradle
  15. 2
      gradle/wrapper/gradle-wrapper.properties

10
app/build.gradle

@ -41,18 +41,20 @@ android {
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
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.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
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.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-hls:2.13.3'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.13.3'

425
app/src/main/java/org/mosad/teapod/parser/AoDParser.kt

@ -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()
return@async res.select("a:contains(Anime-Abo)").text()
.removePrefix("Anime-Abo").trim()
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()
}
}
}
@ -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"),
)
try {
Jsoup.connect(baseUrl + callbackPath)
.ignoreContentType(true)
.cookies(sessionCookies)
.headers(headers)
.execute()
} catch (ex: IOException) {
Log.e(javaClass.name, "Callback for $callbackPath failed.", ex)
}
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)
}
}
}
/**
* 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)
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
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
}
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()
// 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))
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")
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()
if (!loginSuccess) {
Log.w(javaClass.name, "Login, was not successful.")
return@launch
}
private suspend fun loadStreams(media: Media) = coroutineScope {
launch(Dispatchers.IO) {
if (sessionCookies.isEmpty()) login()
// get the media page
val res = Jsoup.connect(baseUrl + media.link)
.cookies(sessionCookies)
.get()
if (!loginSuccess) {
Log.w(javaClass.name, "Login, was not successful.")
return@launch
}
//println(res)
// get the media page
val res = Jsoup.connect(baseUrl + media.link)
.cookies(sessionCookies)
.get()
if (csrfToken.isEmpty()) {
csrfToken = res.select("meta[name=csrf-token]").attr("content")
//Log.i(javaClass.name, "New csrf token is $csrfToken")
}
//println(res)
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 (csrfToken.isEmpty()) {
csrfToken = res.select("meta[name=csrf-token]").attr("content")
//Log.i(javaClass.name, "New csrf token is $csrfToken")
}
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)
))
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 ->
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)
}
} 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()
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
// 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
// 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"),

12
app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt

@ -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")

4
app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt

@ -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()

9
app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt

@ -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}")

22
app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt

@ -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))
}
}

16
app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/MediaFragment.kt

@ -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()
}
}

7
app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt

@ -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()

3
app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt

@ -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) {

4
app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt

@ -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

6
app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt

@ -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)
}
}
}

6
app/src/main/java/org/mosad/teapod/util/StorageController.kt

@ -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()))
}
}

76
app/src/main/java/org/mosad/teapod/util/TMDBApiController.kt

@ -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")}")
return GlobalScope.async {
val response = JsonParser.parseString(url.readText()).asJsonObject
//println(response)
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()
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)
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()
}
/**

2
build.gradle

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

2
gradle/wrapper/gradle-wrapper.properties vendored

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists

Loading…
Cancel
Save

Du besuchst diese Seite mit einem veralteten IPv4-Internetzugang. Möglicherweise treten in Zukunft Probleme mit der Erreichbarkeit und Performance auf. Bitte frage deinen Internetanbieter oder Netzwerkadministrator nach IPv6-Unterstützung.
You are visiting this site with an outdated IPv4 internet access. You may experience problems with accessibility and performance in the future. Please ask your ISP or network administrator for IPv6 support.
Weitere Infos | More Information
Klicke zum schließen | Click to close