implement lazy loading for LibraryFragment & code cleanup

This commit is contained in:
Jannik 2021-12-27 21:14:35 +01:00
parent 6dac929550
commit 2d2c7b2580
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
16 changed files with 249 additions and 672 deletions

View File

@ -26,6 +26,34 @@
-keepattributes Signature -keepattributes Signature
-dontwarn sun.misc.** -dontwarn sun.misc.**
# kotlinx.serialization
# Keep `Companion` object fields of serializable classes.
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
-if @kotlinx.serialization.Serializable class **
-keepclassmembers class <1> {
static <1>$Companion Companion;
}
# Keep `serializer()` on companion objects (both default and named) of serializable classes.
-if @kotlinx.serialization.Serializable class ** {
static **$* *;
}
-keepclassmembers class <1>$<3> {
kotlinx.serialization.KSerializer serializer(...);
}
# Keep `INSTANCE.serializer()` of serializable objects.
-if @kotlinx.serialization.Serializable class ** {
public static ** INSTANCE;
}
-keepclassmembers class <1> {
public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...);
}
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
#misc #misc
-dontwarn java.lang.instrument.ClassFileTransformer -dontwarn java.lang.instrument.ClassFileTransformer
-dontwarn java.lang.ClassValue -dontwarn java.lang.ClassValue

View File

@ -1,472 +0,0 @@
/**
* Teapod
*
* Copyright 2020-2021 <seil0@mosad.xyz>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.teapod.parser
import android.util.Log
import com.google.gson.JsonParser
import kotlinx.coroutines.*
import org.jsoup.Connection
import org.jsoup.Jsoup
import org.mosad.teapod.preferences.EncryptedPreferences
import org.mosad.teapod.util.*
import org.mosad.teapod.util.DataTypes.MediaType
import java.io.IOException
import java.net.CookieStore
import java.util.*
import kotlin.random.Random
import kotlin.reflect.jvm.jvmName
object AoDParser {
private const val baseUrl = "https://www.anime-on-demand.de"
private const val loginPath = "/users/sign_in"
private const val libraryPath = "/animes"
private const val subscriptionPath = "/mypools"
private const val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0"
private lateinit var cookieStore: CookieStore
private var csrfToken: String = ""
private var loginSuccess = false
private val aodMediaList = arrayListOf<AoDMedia>() // actual media (data)
// gui media
val guiMediaList = arrayListOf<ItemMedia>()
val highlightsList = arrayListOf<ItemMedia>()
val newEpisodesList = arrayListOf<ItemMedia>()
val newSimulcastsList = arrayListOf<ItemMedia>()
val newTitlesList = arrayListOf<ItemMedia>()
val topTenList = arrayListOf<ItemMedia>()
fun login(): Boolean = runBlocking {
withContext(Dispatchers.IO) {
// get the authenticity token and cookies
val conAuth = Jsoup.connect(baseUrl + loginPath)
.header("User-Agent", userAgent)
cookieStore = conAuth.cookieStore()
csrfToken = conAuth.execute().parse().select("meta[name=csrf-token]").attr("content")
Log.d(AoDParser::class.jvmName, "Received authenticity token: $csrfToken")
Log.d(AoDParser::class.jvmName, "Received authenticity cookies: $cookieStore")
val data = mapOf(
Pair("user[login]", EncryptedPreferences.login),
Pair("user[password]", EncryptedPreferences.password),
Pair("user[remember_me]", "1"),
Pair("commit", "Einloggen"),
Pair("authenticity_token", csrfToken)
)
val resLogin = Jsoup.connect(baseUrl + loginPath)
.method(Connection.Method.POST)
.timeout(60000) // login can take some time default is 60000 (60 sec)
.data(data)
.postDataCharset("UTF-8")
.cookieStore(cookieStore)
.execute()
//println(resLogin.body())
loginSuccess = resLogin.body().contains("Hallo, du bist jetzt angemeldet.")
Log.i(AoDParser::class.jvmName, "Status: ${resLogin.statusCode()} (${resLogin.statusMessage()}), login successful: $loginSuccess")
loginSuccess
}
}
/**
* initially load all media and home screen data
*/
suspend fun initialLoading() {
coroutineScope {
launch { loadHome() }
launch { listAnimes() }
}
}
/**
* get a media by it's ID (int)
* @param aodId The AoD ID of the requested media
* @return returns a AoDMedia of type Movie or TVShow if found, else return AoDMediaNone
*/
suspend fun getMediaById(aodId: Int): AoDMedia {
return aodMediaList.firstOrNull { it.aodId == aodId } ?:
try {
loadMediaAsync(aodId).await().apply {
aodMediaList.add(this)
}
} catch (exn:NullPointerException) {
Log.e(AoDParser::class.jvmName, "Error while loading media $aodId", exn)
AoDMediaNone
}
}
/**
* get subscription info from aod website, remove "Anime-Abo" Prefix and trim
*/
suspend fun getSubscriptionInfoAsync(): Deferred<String> {
return coroutineScope {
async(Dispatchers.IO) {
val res = Jsoup.connect(baseUrl + subscriptionPath)
.cookieStore(cookieStore)
.get()
return@async res.select("a:contains(Anime-Abo)").text()
.removePrefix("Anime-Abo").trim()
}
}
}
fun getSubscriptionUrl(): String {
return baseUrl + subscriptionPath
}
suspend fun markAsWatched(aodId: Int, episodeId: Int) {
val episode = getMediaById(aodId).getEpisodeById(episodeId)
episode.watched = true
sendCallback(episode.watchedCallback)
Log.d(AoDParser::class.jvmName, "Marked episode ${episode.mediaId} as watched")
}
// TODO don't use jsoup here
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)
.cookieStore(cookieStore)
.headers(headers)
.execute()
} catch (ex: IOException) {
Log.e(AoDParser::class.jvmName, "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 suspend fun listAnimes() = withContext(Dispatchers.IO) {
launch(Dispatchers.IO) {
val resAnimes = Jsoup.connect(baseUrl + libraryPath).get()
//println(resAnimes)
guiMediaList.clear()
val animes = resAnimes.select("div.animebox")
guiMediaList.addAll(
animes.map {
ItemMedia(
id = it.select("p.animebox-link").select("a")
.attr("href").substringAfterLast("/").toInt(),
title = it.select("h3.animebox-title").text(),
posterUrl = it.select("p.animebox-image").select("img")
.attr("src")
)
}
)
Log.i(AoDParser::class.jvmName, "Total library size is: ${guiMediaList.size}")
}
}
/**
* load new episodes, titles and highlights
*/
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()}"
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()
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()
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()
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,"", ""))
}
}
Log.i(AoDParser::class.jvmName, "loaded home")
}
}
/**
* TODO catch SocketTimeoutException from loading to show a waring dialog
* Load media async. Every media has a playlist.
* @param aodId The AoD ID of the requested media
*/
private suspend fun loadMediaAsync(aodId: Int): Deferred<AoDMedia> = coroutineScope {
return@coroutineScope async (Dispatchers.IO) {
if (cookieStore.cookies.isEmpty()) login() // TODO is this needed?
// return none object, if login wasn't successful
if (!loginSuccess) {
Log.w(AoDParser::class.jvmName, "Login was not successful")
return@async AoDMediaNone
}
// get the media page
val res = Jsoup.connect("$baseUrl/anime/$aodId")
.cookieStore(cookieStore)
.get()
// println(res)
if (csrfToken.isEmpty()) {
csrfToken = res.select("meta[name=csrf-token]").attr("content")
Log.d(AoDParser::class.jvmName, "New csrf token is $csrfToken")
}
// playlist parsing TODO can this be async to the general info parsing?
val besides = res.select("div.besides").first()!!
val aodPlaylists = besides.select("input.streamstarter_html5").map { streamstarter ->
parsePlaylistAsync(
streamstarter.attr("data-playlist"),
streamstarter.attr("data-lang")
)
}
/**
* generic aod media data
*/
val title = res.select("h1[itemprop=name]").text()
val description = res.select("div[itemprop=description]").text()
val posterURL = res.select("img.fullwidth-image").attr("src")
val type = when {
posterURL.contains("films") -> MediaType.MOVIE
posterURL.contains("series") -> MediaType.TVSHOW
else -> MediaType.OTHER
}
var year = 0
var age = 0
res.select("table.vertical-table").select("tr").forEach { row ->
when (row.select("th").text().lowercase(Locale.ROOT)) {
"produktionsjahr" -> year = row.select("td").text().toInt()
"fsk" -> age = row.select("td").text().toInt()
}
}
// similar titles from media page
val 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 {
Log.i(AoDParser::class.jvmName, "MediaId for similar to $aodId was null")
null
}
}
/**
* additional information for episodes:
* description: a short description of the episode
* watched: indicates if the episodes has been watched
* watched callback: url to set watched in aod
*/
val episodesInfo: Map<Int, AoDEpisodeInfo> = if (type == MediaType.TVSHOW) {
res.select("div.three-box-container > div.episodebox").mapNotNull { episodeBox ->
// make sure the episode has a streaming link
if (episodeBox.select("input.streamstarter_html5").isNotEmpty()) {
val mediaId = 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()
AoDEpisodeInfo(mediaId, episodeShortDesc, episodeWatched, episodeWatchedCallback)
} else {
Log.i(AoDParser::class.jvmName, "Episode info for $aodId has empty streamstarter_html5 ")
null
}
}.associateBy { it.aodMediaId }
} else {
mapOf()
}
// map the aod api playlist to a teapod playlist
val playlist: List<AoDEpisode> = aodPlaylists.awaitAll().flatMap { aodPlaylist ->
aodPlaylist.list.mapIndexed { index, episode ->
AoDEpisode(
mediaId = episode.mediaid,
title = episode.title,
description = episode.description,
shortDesc = episodesInfo[episode.mediaid]?.shortDesc ?: "",
imageURL = episode.image,
numberStr = episode.title.substringAfter(", Ep. ", ""), // TODO move to parsePalylist
index = index,
watched = episodesInfo[episode.mediaid]?.watched ?: false,
watchedCallback = episodesInfo[episode.mediaid]?.watchedCallback ?: "",
streams = mutableListOf(Stream(episode.sources.first().file, aodPlaylist.language))
)
}
}.groupingBy { it.mediaId }.reduce{ _, accumulator, element ->
accumulator.copy().also {
it.streams.addAll(element.streams)
}
}.values.toList()
return@async AoDMedia(
aodId = aodId,
type = type,
title = title,
shortText = description,
posterURL = posterURL,
year = year,
age = age,
similar = similar,
playlist = playlist
)
}
}
/**
* don't use Gson().fromJson() as we don't have any control over the api and it may change
*/
private fun parsePlaylistAsync(playlistPath: String, language: String): Deferred<AoDPlaylist> {
if (playlistPath == "[]") {
return CompletableDeferred(AoDPlaylist(listOf(), Locale.ROOT))
}
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"),
Pair("Accept-Encoding", "gzip, deflate, br"),
Pair("X-CSRF-Token", csrfToken),
Pair("X-Requested-With", "XMLHttpRequest"),
)
//println("loading streaminfo with cstf: $csrfToken")
val res = Jsoup.connect(baseUrl + playlistPath)
.ignoreContentType(true)
.cookieStore(cookieStore)
.headers(headers)
.timeout(120000) // loading the playlist can take some time
.execute()
//Gson().fromJson(res.body(), AoDObject::class.java)
return@async AoDPlaylist(JsonParser.parseString(res.body()).asJsonObject
.get("playlist").asJsonArray.map {
Playlist(
sources = it.asJsonObject.get("sources").asJsonArray.map { source ->
Source(source.asJsonObject.get("file").asString)
},
image = it.asJsonObject.get("image").asString,
title = it.asJsonObject.get("title").asString,
description = it.asJsonObject.get("description").asString,
mediaid = it.asJsonObject.get("mediaid").asInt
)
},
// TODO improve language handling (via display language etc.)
language = when (language) {
"ger" -> Locale.GERMAN
"jap" -> Locale.JAPANESE
else -> Locale.ROOT
}
)
}
}
}

View File

@ -118,9 +118,9 @@ object Crunchyroll {
* *
* @return A **[BrowseResult]** object is returned. * @return A **[BrowseResult]** object is returned.
*/ */
suspend fun browse(sortBy: SortBy = SortBy.ALPHABETICAL, n: Int = 10): BrowseResult { suspend fun browse(sortBy: SortBy = SortBy.ALPHABETICAL, start: Int = 0, n: Int = 10): BrowseResult {
val browseEndpoint = "/content/v1/browse" val browseEndpoint = "/content/v1/browse"
val parameters = listOf("sort_by" to sortBy.str, "n" to n) val parameters = listOf("sort_by" to sortBy.str, "start" to start, "n" to n)
val result = request(browseEndpoint, parameters) val result = request(browseEndpoint, parameters)
val browseResult = result.component1()?.obj()?.let { val browseResult = result.component1()?.obj()?.let {

View File

@ -33,7 +33,6 @@ import com.google.android.material.navigation.NavigationBarView
import kotlinx.coroutines.* import kotlinx.coroutines.*
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.crunchyroll.Crunchyroll import org.mosad.teapod.parser.crunchyroll.Crunchyroll
import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.preferences.EncryptedPreferences
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
@ -47,7 +46,6 @@ import org.mosad.teapod.ui.components.LoginDialog
import org.mosad.teapod.util.DataTypes import org.mosad.teapod.util.DataTypes
import org.mosad.teapod.util.MetaDBController import org.mosad.teapod.util.MetaDBController
import org.mosad.teapod.util.StorageController import org.mosad.teapod.util.StorageController
import java.util.*
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener { class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener {
@ -152,7 +150,6 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
showOnboarding() showOnboarding()
} else { } else {
Crunchyroll.login(EncryptedPreferences.login, EncryptedPreferences.password) Crunchyroll.login(EncryptedPreferences.login, EncryptedPreferences.password)
runBlocking { Crunchyroll.browse() }
runBlocking { Crunchyroll.index() } runBlocking { Crunchyroll.index() }
} }
@ -188,10 +185,11 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
LoginDialog(this, false).positiveButton { LoginDialog(this, false).positiveButton {
EncryptedPreferences.saveCredentials(login, password, context) EncryptedPreferences.saveCredentials(login, password, context)
if (!AoDParser.login()) { // TODO
showLoginDialog() // if (!AoDParser.login()) {
Log.w(javaClass.name, "Login failed, please try again.") // showLoginDialog()
} // Log.w(javaClass.name, "Login failed, please try again.")
// }
}.negativeButton { }.negativeButton {
Log.i(javaClass.name, "Login canceled, exiting.") Log.i(javaClass.name, "Login canceled, exiting.")
finish() finish()

View File

@ -2,9 +2,7 @@ package org.mosad.teapod.ui.activity.main.fragments
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -19,7 +17,6 @@ import kotlinx.coroutines.launch
import org.mosad.teapod.BuildConfig import org.mosad.teapod.BuildConfig
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.FragmentAccountBinding import org.mosad.teapod.databinding.FragmentAccountBinding
import org.mosad.teapod.parser.AoDParser
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.activity.main.MainActivity import org.mosad.teapod.ui.activity.main.MainActivity
@ -62,12 +59,13 @@ class AccountFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// TODO reimplement for ct, if possible (maybe account status would be better? (premium))
// 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))
lifecycleScope.launch { lifecycleScope.launch {
binding.textAccountSubscription.text = getString( binding.textAccountSubscription.text = getString(
R.string.account_subscription, R.string.account_subscription,
AoDParser.getSubscriptionInfoAsync().await() "TODO"
) )
} }
@ -92,7 +90,8 @@ class AccountFragment : Fragment() {
} }
binding.linearAccountSubscription.setOnClickListener { binding.linearAccountSubscription.setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(AoDParser.getSubscriptionUrl()))) // TODO
//startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(AoDParser.getSubscriptionUrl())))
} }
binding.linearTheme.setOnClickListener { binding.linearTheme.setOnClickListener {
@ -133,10 +132,11 @@ class AccountFragment : Fragment() {
LoginDialog(requireContext(), firstTry).positiveButton { LoginDialog(requireContext(), firstTry).positiveButton {
EncryptedPreferences.saveCredentials(login, password, context) EncryptedPreferences.saveCredentials(login, password, context)
if (!AoDParser.login()) { // TODO
showLoginDialog(false) // if (!AoDParser.login()) {
Log.w(javaClass.name, "Login failed, please try again.") // showLoginDialog(false)
} // Log.w(javaClass.name, "Login failed, please try again.")
// }
}.show { }.show {
login = EncryptedPreferences.login login = EncryptedPreferences.login
password = "" password = ""

View File

@ -1,18 +1,14 @@
package org.mosad.teapod.ui.activity.main.fragments package org.mosad.teapod.ui.activity.main.fragments
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater 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 androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.FragmentHomeBinding 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.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
@ -49,19 +45,20 @@ class HomeFragment : Fragment() {
} }
private fun initHighlight() { private fun initHighlight() {
if (AoDParser.highlightsList.isNotEmpty()) { // TODO
highlightMedia = AoDParser.highlightsList[0] // if (AoDParser.highlightsList.isNotEmpty()) {
// highlightMedia = AoDParser.highlightsList[0]
binding.textHighlightTitle.text = highlightMedia.title //
Glide.with(requireContext()).load(highlightMedia.posterUrl) // binding.textHighlightTitle.text = highlightMedia.title
.into(binding.imageHighlight) // Glide.with(requireContext()).load(highlightMedia.posterUrl)
// .into(binding.imageHighlight)
if (StorageController.myList.contains(highlightMedia.id)) { //
binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24) // if (StorageController.myList.contains(0)) {
} else { // binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24)
binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24) // } else {
} // binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24)
} // }
// }
} }
private fun initRecyclerViews() { private fun initRecyclerViews() {
@ -75,40 +72,42 @@ class HomeFragment : Fragment() {
adapterMyList = MediaItemAdapter(mapMyListToItemMedia()) adapterMyList = MediaItemAdapter(mapMyListToItemMedia())
binding.recyclerMyList.adapter = adapterMyList binding.recyclerMyList.adapter = adapterMyList
// TODO
// new episodes // new episodes
adapterNewEpisodes = MediaItemAdapter(AoDParser.newEpisodesList) // adapterNewEpisodes = MediaItemAdapter(AoDParser.newEpisodesList)
binding.recyclerNewEpisodes.adapter = adapterNewEpisodes // binding.recyclerNewEpisodes.adapter = adapterNewEpisodes
// new simulcasts // new simulcasts
adapterNewSimulcasts = MediaItemAdapter(AoDParser.newSimulcastsList) // adapterNewSimulcasts = MediaItemAdapter(AoDParser.newSimulcastsList)
binding.recyclerNewSimulcasts.adapter = adapterNewSimulcasts // binding.recyclerNewSimulcasts.adapter = adapterNewSimulcasts
// new titles // new titles
adapterNewTitles = MediaItemAdapter(AoDParser.newTitlesList) // adapterNewTitles = MediaItemAdapter(AoDParser.newTitlesList)
binding.recyclerNewTitles.adapter = adapterNewTitles // binding.recyclerNewTitles.adapter = adapterNewTitles
// top ten // top ten
adapterTopTen = MediaItemAdapter(AoDParser.topTenList) // adapterTopTen = MediaItemAdapter(AoDParser.topTenList)
binding.recyclerTopTen.adapter = adapterTopTen // binding.recyclerTopTen.adapter = adapterTopTen
} }
private fun initActions() { private fun initActions() {
binding.buttonPlayHighlight.setOnClickListener { binding.buttonPlayHighlight.setOnClickListener {
// TODO get next episode // TODO get next episode
lifecycleScope.launch { lifecycleScope.launch {
val media = AoDParser.getMediaById(highlightMedia.id) // TODO
//val media = AoDParser.getMediaById(0)
Log.d(javaClass.name, "Starting Player with mediaId: ${media.aodId}") // Log.d(javaClass.name, "Starting Player with mediaId: ${media.aodId}")
//(activity as MainActivity).startPlayer(media.aodId, media.playlist.first().mediaId) // TODO //(activity as MainActivity).startPlayer(media.aodId, media.playlist.first().mediaId)
} }
} }
binding.textHighlightMyList.setOnClickListener { binding.textHighlightMyList.setOnClickListener {
if (StorageController.myList.contains(highlightMedia.id)) { if (StorageController.myList.contains(0)) {
StorageController.myList.remove(highlightMedia.id) StorageController.myList.remove(0)
binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24) binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24)
} else { } else {
StorageController.myList.add(highlightMedia.id) StorageController.myList.add(0)
binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24) binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24)
} }
StorageController.saveMyList(requireContext()) StorageController.saveMyList(requireContext())
@ -124,21 +123,21 @@ class HomeFragment : Fragment() {
activity?.showFragment(MediaFragment("")) //(mediaId)) activity?.showFragment(MediaFragment("")) //(mediaId))
} }
adapterNewEpisodes.onItemClick = { id, _ -> // adapterNewEpisodes.onItemClick = { id, _ ->
activity?.showFragment(MediaFragment("")) //(mediaId)) // activity?.showFragment(MediaFragment("")) //(mediaId))
} // }
//
adapterNewSimulcasts.onItemClick = { id, _ -> // adapterNewSimulcasts.onItemClick = { id, _ ->
activity?.showFragment(MediaFragment("")) //(mediaId)) // activity?.showFragment(MediaFragment("")) //(mediaId))
} // }
//
adapterNewTitles.onItemClick = { id, _ -> // adapterNewTitles.onItemClick = { id, _ ->
activity?.showFragment(MediaFragment("")) //(mediaId)) // activity?.showFragment(MediaFragment("")) //(mediaId))
} // }
//
adapterTopTen.onItemClick = { id, _ -> // adapterTopTen.onItemClick = { id, _ ->
activity?.showFragment(MediaFragment("")) //(mediaId)) // activity?.showFragment(MediaFragment("")) //(mediaId))
} // }
} }
/** /**
@ -153,14 +152,15 @@ class HomeFragment : Fragment() {
} }
private fun mapMyListToItemMedia(): List<ItemMedia> { private fun mapMyListToItemMedia(): List<ItemMedia> {
return StorageController.myList.mapNotNull { elementId -> return emptyList()
AoDParser.guiMediaList.firstOrNull { it.id == elementId }.also { // return StorageController.myList.mapNotNull { elementId ->
// it the my list entry wasn't found in itemMediaList Log it // AoDParser.guiMediaList.firstOrNull { it.id == elementId.toString() }.also {
if (it == null) { // // it the my list entry wasn't found in itemMediaList Log it
Log.w(javaClass.name, "The element with the id $elementId was not found.") // if (it == null) {
} // Log.w(javaClass.name, "The element with the id $elementId was not found.")
} // }
} // }
// }
} }
} }

View File

@ -6,9 +6,10 @@ 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 androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.mosad.teapod.databinding.FragmentLibraryBinding import org.mosad.teapod.databinding.FragmentLibraryBinding
import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.parser.crunchyroll.Crunchyroll import org.mosad.teapod.parser.crunchyroll.Crunchyroll
import org.mosad.teapod.util.ItemMedia import org.mosad.teapod.util.ItemMedia
import org.mosad.teapod.util.adapter.MediaItemAdapter import org.mosad.teapod.util.adapter.MediaItemAdapter
@ -20,6 +21,10 @@ class LibraryFragment : Fragment() {
private lateinit var binding: FragmentLibraryBinding private lateinit var binding: FragmentLibraryBinding
private lateinit var adapter: MediaItemAdapter private lateinit var adapter: MediaItemAdapter
private val itemList = arrayListOf<ItemMedia>()
private val pageSize = 30
private var nextItemIndex = 0
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentLibraryBinding.inflate(inflater, container, false) binding = FragmentLibraryBinding.inflate(inflater, container, false)
return binding.root return binding.root
@ -32,22 +37,55 @@ class LibraryFragment : Fragment() {
lifecycleScope.launch { lifecycleScope.launch {
// create and set the adapter, needs context // create and set the adapter, needs context
context?.let { context?.let {
// crunchy testing TODO implement lazy loading val initialResults = Crunchyroll.browse(n = pageSize)
val results = Crunchyroll.browse(n = 50) itemList.addAll(initialResults.items.map { item ->
val list = results.items.mapIndexed { index, item -> ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source)
ItemMedia(index, item.title, item.images.poster_wide[0][0].source, idStr = item.id) })
} nextItemIndex += pageSize
adapter = MediaItemAdapter(itemList)
adapter = MediaItemAdapter(list)
adapter.onItemClick = { mediaIdStr, _ -> adapter.onItemClick = { mediaIdStr, _ ->
activity?.showFragment(MediaFragment(mediaIdStr = mediaIdStr)) activity?.showFragment(MediaFragment(mediaIdStr = mediaIdStr))
} }
binding.recyclerMediaLibrary.adapter = adapter binding.recyclerMediaLibrary.adapter = adapter
binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9)) binding.recyclerMediaLibrary.addItemDecoration(MediaItemDecoration(9))
// TODO replace with pagination3
// https://medium.com/swlh/paging3-recyclerview-pagination-made-easy-333c7dfa8797
binding.recyclerMediaLibrary.addOnScrollListener(PaginationScrollListener())
} }
} }
} }
inner class PaginationScrollListener: RecyclerView.OnScrollListener() {
private var isLoading = false
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as GridLayoutManager?
if (!isLoading) layoutManager?.let {
// itemList.size - 5 to start loading a bit earlier than the actual end
if (layoutManager.findLastCompletelyVisibleItemPosition() >= (itemList.size - 5)) {
// load new browse results async
isLoading = true
lifecycleScope.launch {
val firstNewItemIndex = itemList.lastIndex + 1
val results = Crunchyroll.browse(start = nextItemIndex, n = pageSize)
itemList.addAll(results.items.map { item ->
ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source)
})
nextItemIndex += pageSize
adapter.updateMediaList(itemList)
adapter.notifyItemRangeInserted(firstNewItemIndex, pageSize)
isLoading = false
}
}
}
}
}
} }

View File

@ -9,7 +9,6 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch 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.util.decoration.MediaItemDecoration import org.mosad.teapod.util.decoration.MediaItemDecoration
import org.mosad.teapod.util.adapter.MediaItemAdapter import org.mosad.teapod.util.adapter.MediaItemAdapter
import org.mosad.teapod.util.showFragment import org.mosad.teapod.util.showFragment
@ -30,7 +29,7 @@ class SearchFragment : Fragment() {
lifecycleScope.launch { lifecycleScope.launch {
// create and set the adapter, needs context // create and set the adapter, needs context
context?.let { context?.let {
adapter = MediaItemAdapter(AoDParser.guiMediaList) adapter = MediaItemAdapter(emptyList()) // TODO
adapter!!.onItemClick = { mediaId, _ -> adapter!!.onItemClick = { mediaId, _ ->
binding.searchText.clearFocus() binding.searchText.clearFocus()
activity?.showFragment(MediaFragment("")) //(mediaId)) activity?.showFragment(MediaFragment("")) //(mediaId))

View File

@ -3,9 +3,7 @@ package org.mosad.teapod.ui.activity.main.viewmodel
import android.app.Application import android.app.Application
import android.util.Log import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.parser.crunchyroll.* import org.mosad.teapod.parser.crunchyroll.*
import org.mosad.teapod.ui.activity.main.MainActivity
import org.mosad.teapod.util.* import org.mosad.teapod.util.*
import org.mosad.teapod.util.DataTypes.MediaType import org.mosad.teapod.util.DataTypes.MediaType
import org.mosad.teapod.util.tmdb.TMDBApiController import org.mosad.teapod.util.tmdb.TMDBApiController
@ -50,11 +48,12 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
// load seasons // load seasons
seasonsCrunchy = Crunchyroll.seasons(crunchyId) seasonsCrunchy = Crunchyroll.seasons(crunchyId)
println("media: $seasonsCrunchy") println("seasons: $seasonsCrunchy")
// load first season // load first season
// TODO make sure to load the preferred season (language), language is set per season, not per stream
episodesCrunchy = Crunchyroll.episodes(seasonsCrunchy.items.first().id) episodesCrunchy = Crunchyroll.episodes(seasonsCrunchy.items.first().id)
println("media: $episodesCrunchy") println("episodes: $episodesCrunchy")
@ -75,47 +74,47 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
* set media, tmdb and nextEpisode * set media, tmdb and nextEpisode
* TODO run aod and tmdb load parallel * TODO run aod and tmdb load parallel
*/ */
suspend fun loadAoD(aodId: Int) { // suspend fun loadAoD(aodId: Int) {
val tmdbApiController = TMDBApiController() // val tmdbApiController = TMDBApiController()
media = AoDParser.getMediaById(aodId) // media = AoDParser.getMediaById(aodId)
//
// check if metaDB knows the title // // check if metaDB knows the title
val tmdbId: Int = if (MetaDBController.mediaList.media.contains(aodId)) { // val tmdbId: Int = if (MetaDBController.mediaList.media.contains(aodId)) {
// load media info from metaDB // // load media info from metaDB
val metaDB = MetaDBController() // val metaDB = MetaDBController()
mediaMeta = when (media.type) { // mediaMeta = when (media.type) {
MediaType.MOVIE -> metaDB.getMovieMetadata(media.aodId) // MediaType.MOVIE -> metaDB.getMovieMetadata(media.aodId)
MediaType.TVSHOW -> metaDB.getTVShowMetadata(media.aodId) // MediaType.TVSHOW -> metaDB.getTVShowMetadata(media.aodId)
else -> null // else -> null
} // }
//
mediaMeta?.tmdbId ?: -1 // mediaMeta?.tmdbId ?: -1
} else { // } else {
// use tmdb search to get media info // // use tmdb search to get media info
mediaMeta = null // set mediaMeta to null, if metaDB doesn't know the media // mediaMeta = null // set mediaMeta to null, if metaDB doesn't know the media
tmdbApiController.search(stripTitleInfo(media.title), media.type) // tmdbApiController.search(stripTitleInfo(media.title), media.type)
} // }
//
tmdbResult = when (media.type) { // tmdbResult = when (media.type) {
MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId) // MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId)
MediaType.TVSHOW -> tmdbApiController.getTVShowDetails(tmdbId) // MediaType.TVSHOW -> tmdbApiController.getTVShowDetails(tmdbId)
else -> null // else -> null
} // }
//
// get season info, if metaDB knows the tv show // // get season info, if metaDB knows the tv show
tmdbTVSeason = if (media.type == MediaType.TVSHOW && mediaMeta != null) { // tmdbTVSeason = if (media.type == MediaType.TVSHOW && mediaMeta != null) {
val tvShowMeta = mediaMeta as TVShowMeta // val tvShowMeta = mediaMeta as TVShowMeta
tmdbApiController.getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber) // tmdbApiController.getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber)
} else { // } else {
null // null
} // }
//
if (media.type == MediaType.TVSHOW) { // if (media.type == MediaType.TVSHOW) {
//nextEpisode = media.episodes.firstOrNull{ !it.watched } ?: media.episodes.first() // //nextEpisode = media.episodes.firstOrNull{ !it.watched } ?: media.episodes.first()
nextEpisodeId = media.playlist.firstOrNull { !it.watched }?.mediaId // nextEpisodeId = media.playlist.firstOrNull { !it.watched }?.mediaId
?: media.playlist.first().mediaId // ?: media.playlist.first().mediaId
} // }
} // }
/** /**
* get the next episode based on episodeId * get the next episode based on episodeId

View File

@ -7,9 +7,7 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.mosad.teapod.R
import org.mosad.teapod.databinding.FragmentOnLoginBinding import org.mosad.teapod.databinding.FragmentOnLoginBinding
import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.preferences.EncryptedPreferences
class OnLoginFragment: Fragment() { class OnLoginFragment: Fragment() {
@ -37,17 +35,18 @@ class OnLoginFragment: Fragment() {
binding.buttonLogin.isClickable = false binding.buttonLogin.isClickable = false
loginJob = lifecycleScope.launch { loginJob = lifecycleScope.launch {
if (AoDParser.login()) { // TODO
// if login was successful, switch to main // if (AoDParser.login()) {
if (activity is OnboardingActivity) { // // if login was successful, switch to main
(activity as OnboardingActivity).launchMainActivity() // if (activity is OnboardingActivity) {
} // (activity as OnboardingActivity).launchMainActivity()
} else { // }
withContext(Dispatchers.Main) { // } else {
binding.textLoginDesc.text = getString(R.string.on_login_failed) // withContext(Dispatchers.Main) {
binding.buttonLogin.isClickable = true // binding.textLoginDesc.text = getString(R.string.on_login_failed)
} // binding.buttonLogin.isClickable = true
} // }
// }
} }
} }
} }

View File

@ -172,7 +172,7 @@ class PlayerActivity : AppCompatActivity() {
} }
private fun initPlayer() { private fun initPlayer() {
if (model.currentEpisode.equals(NoneEpisode)) { if (model.currentEpisode == NoneEpisode) {
Log.e(javaClass.name, "No media was set.") Log.e(javaClass.name, "No media was set.")
this.finish() this.finish()
} }
@ -207,7 +207,7 @@ class PlayerActivity : AppCompatActivity() {
else -> View.VISIBLE else -> View.VISIBLE
} }
if (state == ExoPlayer.STATE_ENDED && model.currentEpisodeCr.nextEpisodeId != null && Preferences.autoplay) { if (state == ExoPlayer.STATE_ENDED && model.currentEpisode.nextEpisodeId != null && Preferences.autoplay) {
playNextEpisode() playNextEpisode()
} }
} }
@ -279,7 +279,7 @@ class PlayerActivity : AppCompatActivity() {
// if remaining time < 20 sec, a next ep is set, autoplay is enabled and not in pip: // if remaining time < 20 sec, a next ep is set, autoplay is enabled and not in pip:
// show next ep button // show next ep button
if (remainingTime in 1..20000) { if (remainingTime in 1..20000) {
if (!btnNextEpIsVisible && model.currentEpisodeCr.nextEpisodeId != null && Preferences.autoplay && !isInPiPMode()) { if (!btnNextEpIsVisible && model.currentEpisode.nextEpisodeId != null && Preferences.autoplay && !isInPiPMode()) {
showButtonNextEp() showButtonNextEp()
} }
} else if (btnNextEpIsVisible) { } else if (btnNextEpIsVisible) {
@ -337,7 +337,7 @@ class PlayerActivity : AppCompatActivity() {
exo_text_title.text = model.getMediaTitle() exo_text_title.text = model.getMediaTitle()
// hide the next ep button, if there is none // hide the next ep button, if there is none
button_next_ep_c.visibility = if (model.currentEpisodeCr.nextEpisodeId == null) { button_next_ep_c.visibility = if (model.currentEpisode.nextEpisodeId == null) {
View.GONE View.GONE
} else { } else {
View.VISIBLE View.VISIBLE

View File

@ -18,7 +18,6 @@ import org.mosad.teapod.parser.crunchyroll.NoneEpisode
import org.mosad.teapod.parser.crunchyroll.NoneEpisodes import org.mosad.teapod.parser.crunchyroll.NoneEpisodes
import org.mosad.teapod.parser.crunchyroll.NonePlayback import org.mosad.teapod.parser.crunchyroll.NonePlayback
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.util.AoDEpisodeNone
import org.mosad.teapod.util.EpisodeMeta import org.mosad.teapod.util.EpisodeMeta
import org.mosad.teapod.util.Meta import org.mosad.teapod.util.Meta
import org.mosad.teapod.util.TVShowMeta import org.mosad.teapod.util.TVShowMeta
@ -40,27 +39,25 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
val currentEpisodeChangedListener = ArrayList<() -> Unit>() val currentEpisodeChangedListener = ArrayList<() -> Unit>()
private val preferredLanguage = if (Preferences.preferSecondary) Locale.JAPANESE else Locale.GERMAN private val preferredLanguage = if (Preferences.preferSecondary) Locale.JAPANESE else Locale.GERMAN
// var media: AoDMedia = AoDMediaNone // tmdb/meta data TODO currently not implemented for cr
// internal set
var mediaMeta: Meta? = null var mediaMeta: Meta? = null
internal set internal set
var tmdbTVSeason: TMDBTVSeason? =null var tmdbTVSeason: TMDBTVSeason? =null
internal set internal set
var currentEpisode = AoDEpisodeNone
internal set
var currentEpisodeMeta: EpisodeMeta? = null var currentEpisodeMeta: EpisodeMeta? = null
internal set internal set
// var nextEpisodeId: Int? = null
// internal set // crunchyroll episodes/playback
var episodes = NoneEpisodes
internal set
var currentEpisode = NoneEpisode
internal set
private var currentPlayback = NonePlayback
// current playback settings
var currentLanguage: Locale = Locale.ROOT var currentLanguage: Locale = Locale.ROOT
internal set internal set
var episodesCrunchy = NoneEpisodes
internal set
var currentEpisodeCr = NoneEpisode
internal set
private var currentPlaybackCr = NonePlayback
init { init {
initMediaSession() initMediaSession()
} }
@ -87,16 +84,16 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
fun loadMedia(seasonId: String, episodeId: String) { fun loadMedia(seasonId: String, episodeId: String) {
runBlocking { runBlocking {
episodesCrunchy = Crunchyroll.episodes(seasonId) episodes = Crunchyroll.episodes(seasonId)
//mediaMeta = loadMediaMeta(media.aodId) // can be done blocking, since it should be cached //mediaMeta = loadMediaMeta(media.aodId) // can be done blocking, since it should be cached
// TODO replace this with setCurrentEpisode // TODO replace this with setCurrentEpisode
currentEpisodeCr = episodesCrunchy.items.find { episode -> currentEpisode = episodes.items.find { episode ->
episode.id == episodeId episode.id == episodeId
} ?: NoneEpisode } ?: NoneEpisode
println("loading playback ${currentEpisodeCr.playback}") println("loading playback ${currentEpisode.playback}")
currentPlaybackCr = Crunchyroll.playback(currentEpisodeCr.playback) currentPlayback = Crunchyroll.playback(currentEpisode.playback)
} }
// TODO reimplement for cr // TODO reimplement for cr
@ -108,9 +105,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
// tmdbTVSeason = TMDBApiController().getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber) // tmdbTVSeason = TMDBApiController().getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber)
// } // }
// } // }
//
currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisode.mediaId) // currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisodeAoD.mediaId)
currentLanguage = currentEpisode.getPreferredStream(preferredLanguage).language // currentLanguage = currentEpisodeAoD.getPreferredStream(preferredLanguage).language
} }
fun setLanguage(language: Locale) { fun setLanguage(language: Locale) {
@ -118,7 +115,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
playCurrentMedia(player.currentPosition) playCurrentMedia(player.currentPosition)
// val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource( // val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
// MediaItem.fromUri(Uri.parse(currentEpisode.getPreferredStream(language).url)) // MediaItem.fromUri(Uri.parse(currentEpisodeAoD.getPreferredStream(language).url))
// ) // )
// playMedia(mediaSource, seekTime) // playMedia(mediaSource, seekTime)
} }
@ -134,9 +131,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
} }
/** /**
* play the next episode, if nextEpisode is not null * play the next episode, if nextEpisodeId is not null
*/ */
fun playNextEpisode() = currentEpisodeCr.nextEpisodeId?.let { nextEpisodeId -> fun playNextEpisode() = currentEpisode.nextEpisodeId?.let { nextEpisodeId ->
setCurrentEpisode(nextEpisodeId, startPlayback = true) setCurrentEpisode(nextEpisodeId, startPlayback = true)
} }
@ -145,13 +142,13 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
* @param episodeId The ID of the episode you want to set currentEpisodeCr to * @param episodeId The ID of the episode you want to set currentEpisodeCr to
*/ */
fun setCurrentEpisode(episodeId: String, startPlayback: Boolean = false) { fun setCurrentEpisode(episodeId: String, startPlayback: Boolean = false) {
currentEpisodeCr = episodesCrunchy.items.find { episode -> currentEpisode = episodes.items.find { episode ->
episode.id == episodeId episode.id == episodeId
} ?: NoneEpisode } ?: NoneEpisode
// TODO don't run blocking // TODO don't run blocking
runBlocking { runBlocking {
currentPlaybackCr = Crunchyroll.playback(currentEpisodeCr.playback) currentPlayback = Crunchyroll.playback(currentEpisode.playback)
} }
// TODO update metadata and language (it should not be needed to update the language here!) // TODO update metadata and language (it should not be needed to update the language here!)
@ -171,7 +168,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
currentEpisodeChangedListener.forEach { it() } currentEpisodeChangedListener.forEach { it() }
// get preferred stream url TODO implement // get preferred stream url TODO implement
val url = currentPlaybackCr.streams.adaptive_hls["en-US"]?.url ?: "" val url = currentPlayback.streams.adaptive_hls["en-US"]?.url ?: ""
println("stream url: $url") println("stream url: $url")
// create the media source object // create the media source object
@ -194,12 +191,12 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
return if(isTVShow) { return if(isTVShow) {
getApplication<Application>().getString( getApplication<Application>().getString(
R.string.component_episode_title, R.string.component_episode_title,
currentEpisodeCr.episode, currentEpisode.episode,
currentEpisodeCr.title currentEpisode.title
) )
} else { } else {
// TODO movie // TODO movie
currentEpisodeCr.title currentEpisode.title
} }
} }
@ -223,17 +220,4 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
return null return null
} }
/**
* TODO reimplement for cr
* Based on the current episodes index, get the next episode.
* @return The next episode or null if there is none.
*/
private fun selectNextEpisode(): Int? {
// return media.playlist.firstOrNull {
// it.index > media.getEpisodeById(currentEpisode.mediaId).index
// }?.mediaId
return null
}
} }

View File

@ -28,16 +28,16 @@ class EpisodesListPlayer @JvmOverloads constructor(
} }
model?.let { model?.let {
adapterRecEpisodes = PlayerEpisodeItemAdapter(model.episodesCrunchy, model.tmdbTVSeason?.episodes) adapterRecEpisodes = PlayerEpisodeItemAdapter(model.episodes, model.tmdbTVSeason?.episodes)
adapterRecEpisodes.onImageClick = {_, episodeId -> adapterRecEpisodes.onImageClick = {_, episodeId ->
(this.parent as ViewGroup).removeView(this) (this.parent as ViewGroup).removeView(this)
model.setCurrentEpisode(episodeId, startPlayback = true) model.setCurrentEpisode(episodeId, startPlayback = true)
} }
// episodeNumber starts at 1, we need the episode index -> - 1 // episodeNumber starts at 1, we need the episode index -> - 1
adapterRecEpisodes.currentSelected = (model.currentEpisodeCr.episodeNumber - 1) adapterRecEpisodes.currentSelected = (model.currentEpisode.episodeNumber - 1)
binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes
binding.recyclerEpisodesPlayer.scrollToPosition(model.currentEpisode.index) binding.recyclerEpisodesPlayer.scrollToPosition(adapterRecEpisodes.currentSelected)
} }
} }

View File

@ -30,12 +30,13 @@ class LanguageSettingsPlayer @JvmOverloads constructor(
init { init {
model?.let { model?.let {
model.currentEpisode.streams.forEach { stream -> // TODO reimplement for cr
addLanguage(stream.language.displayName, stream.language == currentLanguage) { // it.currentEpisode.streams.forEach { stream ->
currentLanguage = stream.language // addLanguage(stream.language.displayName, stream.language == currentLanguage) {
updateSelectedLanguage(it as TextView) // currentLanguage = stream.language
} // updateSelectedLanguage(it as TextView)
} // }
// }
} }
binding.buttonCloseLanguageSettings.setOnClickListener { close() } binding.buttonCloseLanguageSettings.setOnClickListener { close() }

View File

@ -35,10 +35,9 @@ data class ThirdPartyComponent(
* it is uses in the ItemMediaAdapter (RecyclerView) * it is uses in the ItemMediaAdapter (RecyclerView)
*/ */
data class ItemMedia( data class ItemMedia(
val id: Int, // aod path id val id: String,
val title: String, val title: String,
val posterUrl: String, val posterUrl: String,
val idStr: String = "" // crunchyroll id
) )
// TODO replace playlist: List<AoDEpisode> with a map? // TODO replace playlist: List<AoDEpisode> with a map?

View File

@ -12,7 +12,7 @@ import java.util.*
class MediaItemAdapter(private val initMedia: List<ItemMedia>) : RecyclerView.Adapter<MediaItemAdapter.MediaViewHolder>(), Filterable { class MediaItemAdapter(private val initMedia: List<ItemMedia>) : RecyclerView.Adapter<MediaItemAdapter.MediaViewHolder>(), Filterable {
var onItemClick: ((String, Int) -> Unit)? = null var onItemClick: ((id: String, position: Int) -> Unit)? = null
private val filter = MediaFilter() private val filter = MediaFilter()
private var filteredMedia = initMedia.map { it.copy() } private var filteredMedia = initMedia.map { it.copy() }
@ -39,10 +39,14 @@ class MediaItemAdapter(private val initMedia: List<ItemMedia>) : RecyclerView.Ad
filteredMedia = mediaList filteredMedia = mediaList
} }
inner class MediaViewHolder(val binding: ItemMediaBinding) : RecyclerView.ViewHolder(binding.root) { inner class MediaViewHolder(val binding: ItemMediaBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
binding.root.setOnClickListener { binding.root.setOnClickListener {
onItemClick?.invoke(filteredMedia[adapterPosition].idStr, adapterPosition) onItemClick?.invoke(
filteredMedia[bindingAdapterPosition].id,
bindingAdapterPosition
)
} }
} }
} }