implement lazy loading for LibraryFragment & code cleanup
This commit is contained in:
		| @ -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 | ||||
|                 } | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -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 { | ||||
|  | ||||
| @ -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() | ||||
|  | ||||
| @ -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 = "" | ||||
|  | ||||
| @ -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<ItemMedia> { | ||||
|         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.") | ||||
| //                } | ||||
| //            } | ||||
| //        } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -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<ItemMedia>() | ||||
|     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 | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -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)) | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
| //                    } | ||||
| //                } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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<Application>().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 | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -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) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -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() } | ||||
|  | ||||
| @ -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<AoDEpisode> with a map? | ||||
|  | ||||
| @ -12,7 +12,7 @@ import java.util.* | ||||
|  | ||||
| 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 var filteredMedia = initMedia.map { it.copy() } | ||||
|  | ||||
| @ -39,10 +39,14 @@ class MediaItemAdapter(private val initMedia: List<ItemMedia>) : 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 | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user