diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 13a7bfd..73e6e44 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -26,6 +26,34 @@ -keepattributes Signature -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 -dontwarn java.lang.instrument.ClassFileTransformer -dontwarn java.lang.ClassValue diff --git a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt deleted file mode 100644 index e352731..0000000 --- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt +++ /dev/null @@ -1,472 +0,0 @@ -/** - * Teapod - * - * Copyright 2020-2021 - * - * 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() // actual media (data) - - // gui media - val guiMediaList = arrayListOf() - val highlightsList = arrayListOf() - val newEpisodesList = arrayListOf() - val newSimulcastsList = arrayListOf() - val newTitlesList = arrayListOf() - val topTenList = arrayListOf() - - 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 { - 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 = 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 = 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 = 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 { - 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 - } - ) - } - } - -} diff --git a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt index 91386cc..f1d734a 100644 --- a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt +++ b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt @@ -118,9 +118,9 @@ object Crunchyroll { * * @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 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 browseResult = result.component1()?.obj()?.let { diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt index 48326d6..7bb5f15 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/MainActivity.kt @@ -33,7 +33,6 @@ import com.google.android.material.navigation.NavigationBarView import kotlinx.coroutines.* import org.mosad.teapod.R import org.mosad.teapod.databinding.ActivityMainBinding -import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.crunchyroll.Crunchyroll import org.mosad.teapod.preferences.EncryptedPreferences 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.MetaDBController import org.mosad.teapod.util.StorageController -import java.util.* import kotlin.system.measureTimeMillis class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener { @@ -152,7 +150,6 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen showOnboarding() } else { Crunchyroll.login(EncryptedPreferences.login, EncryptedPreferences.password) - runBlocking { Crunchyroll.browse() } runBlocking { Crunchyroll.index() } } @@ -188,10 +185,11 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen LoginDialog(this, false).positiveButton { EncryptedPreferences.saveCredentials(login, password, context) - if (!AoDParser.login()) { - showLoginDialog() - Log.w(javaClass.name, "Login failed, please try again.") - } + // TODO +// if (!AoDParser.login()) { +// showLoginDialog() +// Log.w(javaClass.name, "Login failed, please try again.") +// } }.negativeButton { Log.i(javaClass.name, "Login canceled, exiting.") finish() diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt index 64c7b89..4829f3b 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/AccountFragment.kt @@ -2,9 +2,7 @@ package org.mosad.teapod.ui.activity.main.fragments import android.app.Activity import android.content.Intent -import android.net.Uri import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -19,7 +17,6 @@ import kotlinx.coroutines.launch import org.mosad.teapod.BuildConfig import org.mosad.teapod.R import org.mosad.teapod.databinding.FragmentAccountBinding -import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.ui.activity.main.MainActivity @@ -62,12 +59,13 @@ class AccountFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // TODO reimplement for ct, if possible (maybe account status would be better? (premium)) // load subscription (async) info before anything else binding.textAccountSubscription.text = getString(R.string.account_subscription, getString(R.string.loading)) lifecycleScope.launch { binding.textAccountSubscription.text = getString( R.string.account_subscription, - AoDParser.getSubscriptionInfoAsync().await() + "TODO" ) } @@ -92,7 +90,8 @@ class AccountFragment : Fragment() { } 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 { @@ -133,10 +132,11 @@ class AccountFragment : Fragment() { LoginDialog(requireContext(), firstTry).positiveButton { EncryptedPreferences.saveCredentials(login, password, context) - if (!AoDParser.login()) { - showLoginDialog(false) - Log.w(javaClass.name, "Login failed, please try again.") - } + // TODO +// if (!AoDParser.login()) { +// showLoginDialog(false) +// Log.w(javaClass.name, "Login failed, please try again.") +// } }.show { login = EncryptedPreferences.login password = "" diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt index 25f12f7..9592c9f 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/HomeFragment.kt @@ -1,18 +1,14 @@ package org.mosad.teapod.ui.activity.main.fragments import android.os.Bundle -import android.util.Log 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.launch import org.mosad.teapod.R 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 @@ -49,19 +45,20 @@ class HomeFragment : Fragment() { } private fun initHighlight() { - if (AoDParser.highlightsList.isNotEmpty()) { - highlightMedia = AoDParser.highlightsList[0] - - binding.textHighlightTitle.text = highlightMedia.title - Glide.with(requireContext()).load(highlightMedia.posterUrl) - .into(binding.imageHighlight) - - if (StorageController.myList.contains(highlightMedia.id)) { - binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24) - } else { - binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24) - } - } + // TODO +// if (AoDParser.highlightsList.isNotEmpty()) { +// highlightMedia = AoDParser.highlightsList[0] +// +// binding.textHighlightTitle.text = highlightMedia.title +// Glide.with(requireContext()).load(highlightMedia.posterUrl) +// .into(binding.imageHighlight) +// +// if (StorageController.myList.contains(0)) { +// binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24) +// } else { +// binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24) +// } +// } } private fun initRecyclerViews() { @@ -75,40 +72,42 @@ class HomeFragment : Fragment() { adapterMyList = MediaItemAdapter(mapMyListToItemMedia()) binding.recyclerMyList.adapter = adapterMyList + // TODO // new episodes - adapterNewEpisodes = MediaItemAdapter(AoDParser.newEpisodesList) - binding.recyclerNewEpisodes.adapter = adapterNewEpisodes +// adapterNewEpisodes = MediaItemAdapter(AoDParser.newEpisodesList) +// binding.recyclerNewEpisodes.adapter = adapterNewEpisodes // new simulcasts - adapterNewSimulcasts = MediaItemAdapter(AoDParser.newSimulcastsList) - binding.recyclerNewSimulcasts.adapter = adapterNewSimulcasts +// adapterNewSimulcasts = MediaItemAdapter(AoDParser.newSimulcastsList) +// binding.recyclerNewSimulcasts.adapter = adapterNewSimulcasts // new titles - adapterNewTitles = MediaItemAdapter(AoDParser.newTitlesList) - binding.recyclerNewTitles.adapter = adapterNewTitles +// adapterNewTitles = MediaItemAdapter(AoDParser.newTitlesList) +// binding.recyclerNewTitles.adapter = adapterNewTitles // top ten - adapterTopTen = MediaItemAdapter(AoDParser.topTenList) - binding.recyclerTopTen.adapter = adapterTopTen +// adapterTopTen = MediaItemAdapter(AoDParser.topTenList) +// binding.recyclerTopTen.adapter = adapterTopTen } private fun initActions() { binding.buttonPlayHighlight.setOnClickListener { // TODO get next episode lifecycleScope.launch { - val media = AoDParser.getMediaById(highlightMedia.id) + // TODO + //val media = AoDParser.getMediaById(0) - Log.d(javaClass.name, "Starting Player with mediaId: ${media.aodId}") - //(activity as MainActivity).startPlayer(media.aodId, media.playlist.first().mediaId) // TODO + // Log.d(javaClass.name, "Starting Player with mediaId: ${media.aodId}") + //(activity as MainActivity).startPlayer(media.aodId, media.playlist.first().mediaId) } } binding.textHighlightMyList.setOnClickListener { - if (StorageController.myList.contains(highlightMedia.id)) { - StorageController.myList.remove(highlightMedia.id) + if (StorageController.myList.contains(0)) { + StorageController.myList.remove(0) binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24) } else { - StorageController.myList.add(highlightMedia.id) + StorageController.myList.add(0) binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24) } StorageController.saveMyList(requireContext()) @@ -124,21 +123,21 @@ class HomeFragment : Fragment() { activity?.showFragment(MediaFragment("")) //(mediaId)) } - adapterNewEpisodes.onItemClick = { id, _ -> - activity?.showFragment(MediaFragment("")) //(mediaId)) - } - - adapterNewSimulcasts.onItemClick = { id, _ -> - activity?.showFragment(MediaFragment("")) //(mediaId)) - } - - adapterNewTitles.onItemClick = { id, _ -> - activity?.showFragment(MediaFragment("")) //(mediaId)) - } - - adapterTopTen.onItemClick = { id, _ -> - activity?.showFragment(MediaFragment("")) //(mediaId)) - } +// adapterNewEpisodes.onItemClick = { id, _ -> +// activity?.showFragment(MediaFragment("")) //(mediaId)) +// } +// +// adapterNewSimulcasts.onItemClick = { id, _ -> +// activity?.showFragment(MediaFragment("")) //(mediaId)) +// } +// +// adapterNewTitles.onItemClick = { id, _ -> +// activity?.showFragment(MediaFragment("")) //(mediaId)) +// } +// +// adapterTopTen.onItemClick = { id, _ -> +// activity?.showFragment(MediaFragment("")) //(mediaId)) +// } } /** @@ -153,14 +152,15 @@ class HomeFragment : Fragment() { } private fun mapMyListToItemMedia(): List { - return StorageController.myList.mapNotNull { elementId -> - AoDParser.guiMediaList.firstOrNull { it.id == elementId }.also { - // it the my list entry wasn't found in itemMediaList Log it - if (it == null) { - Log.w(javaClass.name, "The element with the id $elementId was not found.") - } - } - } + return emptyList() +// return StorageController.myList.mapNotNull { elementId -> +// AoDParser.guiMediaList.firstOrNull { it.id == elementId.toString() }.also { +// // it the my list entry wasn't found in itemMediaList Log it +// if (it == null) { +// Log.w(javaClass.name, "The element with the id $elementId was not found.") +// } +// } +// } } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt index cabe2b8..00f992c 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt @@ -6,9 +6,10 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.launch import org.mosad.teapod.databinding.FragmentLibraryBinding -import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.crunchyroll.Crunchyroll import org.mosad.teapod.util.ItemMedia import org.mosad.teapod.util.adapter.MediaItemAdapter @@ -20,6 +21,10 @@ class LibraryFragment : Fragment() { private lateinit var binding: FragmentLibraryBinding private lateinit var adapter: MediaItemAdapter + private val itemList = arrayListOf() + private val pageSize = 30 + private var nextItemIndex = 0 + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FragmentLibraryBinding.inflate(inflater, container, false) return binding.root @@ -32,22 +37,55 @@ class LibraryFragment : Fragment() { lifecycleScope.launch { // create and set the adapter, needs context context?.let { - // crunchy testing TODO implement lazy loading - val results = Crunchyroll.browse(n = 50) - val list = results.items.mapIndexed { index, item -> - ItemMedia(index, item.title, item.images.poster_wide[0][0].source, idStr = item.id) - } + val initialResults = Crunchyroll.browse(n = pageSize) + itemList.addAll(initialResults.items.map { item -> + ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source) + }) + nextItemIndex += pageSize - - adapter = MediaItemAdapter(list) + adapter = MediaItemAdapter(itemList) adapter.onItemClick = { mediaIdStr, _ -> activity?.showFragment(MediaFragment(mediaIdStr = mediaIdStr)) } binding.recyclerMediaLibrary.adapter = adapter 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 + } + } + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt index 08ea2ac..29d9bcd 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt @@ -9,7 +9,6 @@ import androidx.fragment.app.Fragment 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 import org.mosad.teapod.util.adapter.MediaItemAdapter import org.mosad.teapod.util.showFragment @@ -30,7 +29,7 @@ class SearchFragment : Fragment() { lifecycleScope.launch { // create and set the adapter, needs context context?.let { - adapter = MediaItemAdapter(AoDParser.guiMediaList) + adapter = MediaItemAdapter(emptyList()) // TODO adapter!!.onItemClick = { mediaId, _ -> binding.searchText.clearFocus() activity?.showFragment(MediaFragment("")) //(mediaId)) diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt index f6695b1..d422ca1 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt @@ -3,9 +3,7 @@ package org.mosad.teapod.ui.activity.main.viewmodel import android.app.Application import android.util.Log import androidx.lifecycle.AndroidViewModel -import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.crunchyroll.* -import org.mosad.teapod.ui.activity.main.MainActivity import org.mosad.teapod.util.* import org.mosad.teapod.util.DataTypes.MediaType import org.mosad.teapod.util.tmdb.TMDBApiController @@ -50,11 +48,12 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic // load seasons seasonsCrunchy = Crunchyroll.seasons(crunchyId) - println("media: $seasonsCrunchy") + println("seasons: $seasonsCrunchy") // 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) - println("media: $episodesCrunchy") + println("episodes: $episodesCrunchy") @@ -75,47 +74,47 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic * set media, tmdb and nextEpisode * TODO run aod and tmdb load parallel */ - suspend fun loadAoD(aodId: Int) { - val tmdbApiController = TMDBApiController() - media = AoDParser.getMediaById(aodId) - - // check if metaDB knows the title - val tmdbId: Int = if (MetaDBController.mediaList.media.contains(aodId)) { - // load media info from metaDB - val metaDB = MetaDBController() - mediaMeta = when (media.type) { - MediaType.MOVIE -> metaDB.getMovieMetadata(media.aodId) - MediaType.TVSHOW -> metaDB.getTVShowMetadata(media.aodId) - else -> null - } - - mediaMeta?.tmdbId ?: -1 - } else { - // use tmdb search to get media info - mediaMeta = null // set mediaMeta to null, if metaDB doesn't know the media - tmdbApiController.search(stripTitleInfo(media.title), media.type) - } - - tmdbResult = when (media.type) { - MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId) - MediaType.TVSHOW -> tmdbApiController.getTVShowDetails(tmdbId) - else -> null - } - - // get season info, if metaDB knows the tv show - tmdbTVSeason = if (media.type == MediaType.TVSHOW && mediaMeta != null) { - val tvShowMeta = mediaMeta as TVShowMeta - tmdbApiController.getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber) - } else { - null - } - - if (media.type == MediaType.TVSHOW) { - //nextEpisode = media.episodes.firstOrNull{ !it.watched } ?: media.episodes.first() - nextEpisodeId = media.playlist.firstOrNull { !it.watched }?.mediaId - ?: media.playlist.first().mediaId - } - } +// suspend fun loadAoD(aodId: Int) { +// val tmdbApiController = TMDBApiController() +// media = AoDParser.getMediaById(aodId) +// +// // check if metaDB knows the title +// val tmdbId: Int = if (MetaDBController.mediaList.media.contains(aodId)) { +// // load media info from metaDB +// val metaDB = MetaDBController() +// mediaMeta = when (media.type) { +// MediaType.MOVIE -> metaDB.getMovieMetadata(media.aodId) +// MediaType.TVSHOW -> metaDB.getTVShowMetadata(media.aodId) +// else -> null +// } +// +// mediaMeta?.tmdbId ?: -1 +// } else { +// // use tmdb search to get media info +// mediaMeta = null // set mediaMeta to null, if metaDB doesn't know the media +// tmdbApiController.search(stripTitleInfo(media.title), media.type) +// } +// +// tmdbResult = when (media.type) { +// MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId) +// MediaType.TVSHOW -> tmdbApiController.getTVShowDetails(tmdbId) +// else -> null +// } +// +// // get season info, if metaDB knows the tv show +// tmdbTVSeason = if (media.type == MediaType.TVSHOW && mediaMeta != null) { +// val tvShowMeta = mediaMeta as TVShowMeta +// tmdbApiController.getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber) +// } else { +// null +// } +// +// if (media.type == MediaType.TVSHOW) { +// //nextEpisode = media.episodes.firstOrNull{ !it.watched } ?: media.episodes.first() +// nextEpisodeId = media.playlist.firstOrNull { !it.watched }?.mediaId +// ?: media.playlist.first().mediaId +// } +// } /** * get the next episode based on episodeId diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt index 6a329be..9f7a060 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/onboarding/OnLoginFragment.kt @@ -7,9 +7,7 @@ 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 -import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.preferences.EncryptedPreferences class OnLoginFragment: Fragment() { @@ -37,17 +35,18 @@ class OnLoginFragment: Fragment() { binding.buttonLogin.isClickable = false loginJob = lifecycleScope.launch { - if (AoDParser.login()) { - // if login was successful, switch to main - if (activity is OnboardingActivity) { - (activity as OnboardingActivity).launchMainActivity() - } - } else { - withContext(Dispatchers.Main) { - binding.textLoginDesc.text = getString(R.string.on_login_failed) - binding.buttonLogin.isClickable = true - } - } + // TODO +// if (AoDParser.login()) { +// // if login was successful, switch to main +// if (activity is OnboardingActivity) { +// (activity as OnboardingActivity).launchMainActivity() +// } +// } else { +// withContext(Dispatchers.Main) { +// binding.textLoginDesc.text = getString(R.string.on_login_failed) +// binding.buttonLogin.isClickable = true +// } +// } } } } diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt index bcc63b2..cf6082d 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerActivity.kt @@ -172,7 +172,7 @@ class PlayerActivity : AppCompatActivity() { } private fun initPlayer() { - if (model.currentEpisode.equals(NoneEpisode)) { + if (model.currentEpisode == NoneEpisode) { Log.e(javaClass.name, "No media was set.") this.finish() } @@ -207,7 +207,7 @@ class PlayerActivity : AppCompatActivity() { 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() } } @@ -279,7 +279,7 @@ class PlayerActivity : AppCompatActivity() { // if remaining time < 20 sec, a next ep is set, autoplay is enabled and not in pip: // show next ep button if (remainingTime in 1..20000) { - if (!btnNextEpIsVisible && model.currentEpisodeCr.nextEpisodeId != null && Preferences.autoplay && !isInPiPMode()) { + if (!btnNextEpIsVisible && model.currentEpisode.nextEpisodeId != null && Preferences.autoplay && !isInPiPMode()) { showButtonNextEp() } } else if (btnNextEpIsVisible) { @@ -337,7 +337,7 @@ class PlayerActivity : AppCompatActivity() { exo_text_title.text = model.getMediaTitle() // 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 } else { View.VISIBLE diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt index 795707b..c812342 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/player/PlayerViewModel.kt @@ -18,7 +18,6 @@ import org.mosad.teapod.parser.crunchyroll.NoneEpisode import org.mosad.teapod.parser.crunchyroll.NoneEpisodes import org.mosad.teapod.parser.crunchyroll.NonePlayback import org.mosad.teapod.preferences.Preferences -import org.mosad.teapod.util.AoDEpisodeNone import org.mosad.teapod.util.EpisodeMeta import org.mosad.teapod.util.Meta import org.mosad.teapod.util.TVShowMeta @@ -40,27 +39,25 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) val currentEpisodeChangedListener = ArrayList<() -> Unit>() private val preferredLanguage = if (Preferences.preferSecondary) Locale.JAPANESE else Locale.GERMAN -// var media: AoDMedia = AoDMediaNone -// internal set + // tmdb/meta data TODO currently not implemented for cr var mediaMeta: Meta? = null internal set var tmdbTVSeason: TMDBTVSeason? =null internal set - var currentEpisode = AoDEpisodeNone - internal set var currentEpisodeMeta: EpisodeMeta? = null 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 internal set - var episodesCrunchy = NoneEpisodes - internal set - var currentEpisodeCr = NoneEpisode - internal set - private var currentPlaybackCr = NonePlayback - init { initMediaSession() } @@ -87,16 +84,16 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) fun loadMedia(seasonId: String, episodeId: String) { runBlocking { - episodesCrunchy = Crunchyroll.episodes(seasonId) + episodes = Crunchyroll.episodes(seasonId) //mediaMeta = loadMediaMeta(media.aodId) // can be done blocking, since it should be cached // TODO replace this with setCurrentEpisode - currentEpisodeCr = episodesCrunchy.items.find { episode -> + currentEpisode = episodes.items.find { episode -> episode.id == episodeId } ?: 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 @@ -108,9 +105,9 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) // tmdbTVSeason = TMDBApiController().getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber) // } // } - - currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisode.mediaId) - currentLanguage = currentEpisode.getPreferredStream(preferredLanguage).language +// +// currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisodeAoD.mediaId) +// currentLanguage = currentEpisodeAoD.getPreferredStream(preferredLanguage).language } fun setLanguage(language: Locale) { @@ -118,7 +115,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) playCurrentMedia(player.currentPosition) // 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) } @@ -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) } @@ -145,13 +142,13 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) * @param episodeId The ID of the episode you want to set currentEpisodeCr to */ fun setCurrentEpisode(episodeId: String, startPlayback: Boolean = false) { - currentEpisodeCr = episodesCrunchy.items.find { episode -> + currentEpisode = episodes.items.find { episode -> episode.id == episodeId } ?: NoneEpisode // TODO don't run blocking 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!) @@ -171,7 +168,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) currentEpisodeChangedListener.forEach { it() } // 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") // create the media source object @@ -194,12 +191,12 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) return if(isTVShow) { getApplication().getString( R.string.component_episode_title, - currentEpisodeCr.episode, - currentEpisodeCr.title + currentEpisode.episode, + currentEpisode.title ) } else { // TODO movie - currentEpisodeCr.title + currentEpisode.title } } @@ -223,17 +220,4 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) 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 - } - } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt index cb654e9..fd139aa 100644 --- a/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt +++ b/app/src/main/java/org/mosad/teapod/ui/components/EpisodesListPlayer.kt @@ -28,16 +28,16 @@ class EpisodesListPlayer @JvmOverloads constructor( } model?.let { - adapterRecEpisodes = PlayerEpisodeItemAdapter(model.episodesCrunchy, model.tmdbTVSeason?.episodes) + adapterRecEpisodes = PlayerEpisodeItemAdapter(model.episodes, model.tmdbTVSeason?.episodes) adapterRecEpisodes.onImageClick = {_, episodeId -> (this.parent as ViewGroup).removeView(this) model.setCurrentEpisode(episodeId, startPlayback = true) } // 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.scrollToPosition(model.currentEpisode.index) + binding.recyclerEpisodesPlayer.scrollToPosition(adapterRecEpisodes.currentSelected) } } diff --git a/app/src/main/java/org/mosad/teapod/ui/components/LanguageSettingsPlayer.kt b/app/src/main/java/org/mosad/teapod/ui/components/LanguageSettingsPlayer.kt index 404ba7e..8c90188 100644 --- a/app/src/main/java/org/mosad/teapod/ui/components/LanguageSettingsPlayer.kt +++ b/app/src/main/java/org/mosad/teapod/ui/components/LanguageSettingsPlayer.kt @@ -30,12 +30,13 @@ class LanguageSettingsPlayer @JvmOverloads constructor( init { model?.let { - model.currentEpisode.streams.forEach { stream -> - addLanguage(stream.language.displayName, stream.language == currentLanguage) { - currentLanguage = stream.language - updateSelectedLanguage(it as TextView) - } - } + // TODO reimplement for cr +// it.currentEpisode.streams.forEach { stream -> +// addLanguage(stream.language.displayName, stream.language == currentLanguage) { +// currentLanguage = stream.language +// updateSelectedLanguage(it as TextView) +// } +// } } binding.buttonCloseLanguageSettings.setOnClickListener { close() } diff --git a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt index 7e93be0..280cf1d 100644 --- a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt @@ -35,10 +35,9 @@ data class ThirdPartyComponent( * it is uses in the ItemMediaAdapter (RecyclerView) */ data class ItemMedia( - val id: Int, // aod path id + val id: String, val title: String, val posterUrl: String, - val idStr: String = "" // crunchyroll id ) // TODO replace playlist: List with a map? diff --git a/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt b/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt index f5b862c..63747d4 100644 --- a/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt +++ b/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt @@ -12,7 +12,7 @@ import java.util.* class MediaItemAdapter(private val initMedia: List) : RecyclerView.Adapter(), Filterable { - var onItemClick: ((String, Int) -> Unit)? = null + var onItemClick: ((id: String, position: Int) -> Unit)? = null private val filter = MediaFilter() private var filteredMedia = initMedia.map { it.copy() } @@ -39,10 +39,14 @@ class MediaItemAdapter(private val initMedia: List) : RecyclerView.Ad filteredMedia = mediaList } - inner class MediaViewHolder(val binding: ItemMediaBinding) : RecyclerView.ViewHolder(binding.root) { + inner class MediaViewHolder(val binding: ItemMediaBinding) : + RecyclerView.ViewHolder(binding.root) { init { binding.root.setOnClickListener { - onItemClick?.invoke(filteredMedia[adapterPosition].idStr, adapterPosition) + onItemClick?.invoke( + filteredMedia[bindingAdapterPosition].id, + bindingAdapterPosition + ) } } }