implement lazy loading for LibraryFragment & code cleanup
parent
7dc41da13c
commit
63ce910ec5
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|