From ce84cb57a8870dbe41d92810c63f6133618036d1 Mon Sep 17 00:00:00 2001 From: Jannik Date: Mon, 19 Oct 2020 21:57:02 +0200 Subject: [PATCH] rework media parsing, parse secondary stream (sub/jap) * use the secondary stream if no primary is present --- .../java/org/mosad/teapod/parser/AoDParser.kt | 155 +++++++++--------- .../mosad/teapod/ui/fragments/HomeFragment.kt | 4 - .../teapod/ui/fragments/LibraryFragment.kt | 4 - .../teapod/ui/fragments/MediaFragment.kt | 12 +- .../teapod/ui/fragments/SearchFragment.kt | 4 - .../java/org/mosad/teapod/util/DataTypes.kt | 23 ++- .../teapod/util/adapter/EpisodeItemAdapter.kt | 19 ++- app/src/main/res/values-de-rDE/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 9 files changed, 122 insertions(+), 101 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt index f9b5644..59e1626 100644 --- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt +++ b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt @@ -23,11 +23,12 @@ package org.mosad.teapod.parser import android.util.Log -import com.google.gson.JsonParser +import com.google.gson.Gson import kotlinx.coroutines.* import org.jsoup.Connection import org.jsoup.Jsoup import org.mosad.teapod.preferences.EncryptedPreferences +import org.mosad.teapod.util.AoDObject import org.mosad.teapod.util.DataTypes.MediaType import org.mosad.teapod.util.Episode import org.mosad.teapod.util.ItemMedia @@ -47,7 +48,7 @@ object AoDParser { private var csrfToken: String = "" private var loginSuccess = false - val mediaList = arrayListOf() + private val mediaList = arrayListOf() val itemMediaList = arrayListOf() val newEpisodesList = arrayListOf() @@ -224,12 +225,69 @@ object AoDParser { withContext(Dispatchers.Default) { + // get the media page val res = Jsoup.connect(baseUrl + media.link) .cookies(sessionCookies) .get() //println(res) + if (csrfToken.isEmpty()) { + csrfToken = res.select("meta[name=csrf-token]").attr("content") + //Log.i(javaClass.name, "New csrf token is $csrfToken") + } + + + val pl = res.select("input.streamstarter_html5").first() + val primary = pl.attr("data-playlist") + val secondary = pl.attr("data-otherplaylist") + val secondaryIsOmU = secondary.contains("OmU", true) + + println("primary: $primary") + println("secondary: $secondary") + println("secondaryIsOmU: $secondaryIsOmU") + + // load primary and secondary playlist + val primaryPlaylist = parsePlaylistAsync(primary) + val secondaryPlaylist = parsePlaylistAsync(secondary) + + primaryPlaylist.await().playlist.forEach { ep -> + media.episodes.add( + Episode( + id = ep.mediaid, + priStreamUrl = ep.sources.first().file, + posterUrl = ep.image, + title = ep.title, + description = ep.description, + number = ep.title.substringAfter(", Ep. ").toInt() + ) + ) + } + Log.i(javaClass.name, "Loading primary playlist finished") + + secondaryPlaylist.await().playlist.forEach { ep -> + val episode = media.episodes.firstOrNull { it.id == ep.mediaid } + + if (episode != null) { + episode.secStreamUrl = ep.sources.first().file + episode.secStreamOmU = secondaryIsOmU + println("adding secondary stream for ep: ${ep.title.substringAfter(", Ep. ").toInt()}") + } else { + media.episodes.add( + Episode( + id = ep.mediaid, + secStreamUrl = ep.sources.first().file, + secStreamOmU = secondaryIsOmU, + posterUrl = ep.image, + title = ep.title, + description = ep.description, + number = ep.title.substringAfter(", Ep. ").toInt() + ) + ) + } + } + Log.i(javaClass.name, "Loading secondary plalyist finished") + // parse additional info from the media page res.select("table.vertical-table").select("tr").forEach { row -> when (row.select("th").text().toLowerCase(Locale.ROOT)) { @@ -245,46 +303,31 @@ object AoDParser { } // parse additional information for tv shows - media.episodes = when (media.type) { - MediaType.MOVIE -> listOf(Episode()) - MediaType.TVSHOW -> { - res.select("div.three-box-container > div.episodebox").map { episodebox -> - val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt() - val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text() - val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange") - val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first() + if (media.type == MediaType.TVSHOW) { + res.select("div.three-box-container > div.episodebox").forEach { episodebox -> + val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt() + val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text() + val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange") + val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first() - Episode( - id = episodeId, - shortDesc = episodeShortDesc, - watched = episodeWatched, - watchedCallback = episodeWatchedCallback - ) + media.episodes.firstOrNull { it.id == episodeId }?.apply { + shortDesc = episodeShortDesc + watched = episodeWatched + watchedCallback = episodeWatchedCallback } } - MediaType.OTHER -> listOf() } - if (csrfToken.isEmpty()) { - csrfToken = res.select("meta[name=csrf-token]").attr("content") - //Log.i(javaClass.name, "New csrf token is $csrfToken") - } - - // TODO has attr data-lag (ger or jap) - val playlists = res.select("input.streamstarter_html5").eachAttr("data-playlist") - - if (playlists.size > 0) { - loadPlaylist(playlists.first(), csrfToken, media.type, media.episodes) - } } } - /** - * load the playlist path and parse it, read the stream info from json - * @param episodes is used as call ba reference - */ - private fun loadPlaylist(playlistPath: String, csrfToken: String, type: MediaType, episodes: List) = runBlocking { - withContext(Dispatchers.Default) { + // TODO is this realy a save way, since we don't have any control over the "api" + private fun parsePlaylistAsync(playlistPath: String): Deferred { + if (playlistPath == "[]") { + return CompletableDeferred(AoDObject(listOf())) + } + + return GlobalScope.async { 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"), @@ -301,49 +344,9 @@ object AoDParser { .headers(headers) .execute() - //println(res.body()) - - when (type) { - MediaType.MOVIE -> { - val movie = JsonParser.parseString(res.body()).asJsonObject - .get("playlist").asJsonArray - .first().asJsonObject - - movie.get("sources").asJsonArray.first().apply { - episodes.first().streamUrl = this.asJsonObject.get("file").asString - } - } - - MediaType.TVSHOW -> { - val episodesJson = JsonParser.parseString(res.body()).asJsonObject - .get("playlist").asJsonArray - - episodesJson.forEach { jsonElement -> - val episodeId = jsonElement.asJsonObject.get("mediaid") - val episodeStream = jsonElement.asJsonObject.get("sources").asJsonArray - .first().asJsonObject - .get("file").asString - val episodeTitle = jsonElement.asJsonObject.get("title").asString - val episodePoster = jsonElement.asJsonObject.get("image").asString - val episodeDescription = jsonElement.asJsonObject.get("description").asString - val episodeNumber = episodeTitle.substringAfter(", Ep. ").toInt() - - episodes.first { it.id == episodeId.asInt }.apply { - this.title = episodeTitle - this.posterUrl = episodePoster - this.streamUrl = episodeStream - this.description = episodeDescription - this.number = episodeNumber - } - } - - } - - else -> { - Log.e(javaClass.name, "Wrong Type, please report this issue.") - } - } + Gson().fromJson(res.body(), AoDObject::class.java) } + } } diff --git a/app/src/main/java/org/mosad/teapod/ui/fragments/HomeFragment.kt b/app/src/main/java/org/mosad/teapod/ui/fragments/HomeFragment.kt index 48ae6ab..25f0cf4 100644 --- a/app/src/main/java/org/mosad/teapod/ui/fragments/HomeFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/fragments/HomeFragment.kt @@ -30,10 +30,6 @@ class HomeFragment : Fragment() { super.onViewCreated(view, savedInstanceState) GlobalScope.launch { - if (AoDParser.mediaList.isEmpty()) { - AoDParser().listAnimes() - } - withContext(Dispatchers.Main) { context?.let { recycler_my_list.addItemDecoration(MediaItemDecoration(9)) diff --git a/app/src/main/java/org/mosad/teapod/ui/fragments/LibraryFragment.kt b/app/src/main/java/org/mosad/teapod/ui/fragments/LibraryFragment.kt index 0caeeff..76a4438 100644 --- a/app/src/main/java/org/mosad/teapod/ui/fragments/LibraryFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/fragments/LibraryFragment.kt @@ -26,10 +26,6 @@ class LibraryFragment : Fragment() { // init async GlobalScope.launch { - if (AoDParser.mediaList.isEmpty()) { - AoDParser().listAnimes() - } - // create and set the adapter, needs context withContext(Dispatchers.Main) { context?.let { diff --git a/app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt b/app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt index 26ee7bb..e121eac 100644 --- a/app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/fragments/MediaFragment.kt @@ -88,8 +88,8 @@ class MediaFragment(private val media: Media, private val tmdb: TMDBResponse) : private fun initActions() { button_play.setOnClickListener { when (media.type) { - MediaType.MOVIE -> playStream(media.episodes.first().streamUrl) - MediaType.TVSHOW -> playStream(media.episodes.first().streamUrl) + MediaType.MOVIE -> playStream(media.episodes.first().priStreamUrl) + MediaType.TVSHOW -> playStream(media.episodes.first().priStreamUrl) else -> Log.e(javaClass.name, "Wrong Type: $media.type") } } @@ -114,7 +114,13 @@ class MediaFragment(private val media: Media, private val tmdb: TMDBResponse) : // set onItemClick only in adapter is initialized if (this::adapterRecEpisodes.isInitialized) { adapterRecEpisodes.onImageClick = { _, position -> - playStream(media.episodes[position].streamUrl) + // TODO add option to prefer secondary stream + // try to use secondary stream if primary is missing + if (media.episodes[position].priStreamUrl.isNotEmpty()) { + playStream(media.episodes[position].priStreamUrl) + } else if (media.episodes[position].secStreamUrl.isNotEmpty()) { + playStream(media.episodes[position].secStreamUrl) + } // update watched state AoDParser.sendCallback(media.episodes[position].watchedCallback) diff --git a/app/src/main/java/org/mosad/teapod/ui/fragments/SearchFragment.kt b/app/src/main/java/org/mosad/teapod/ui/fragments/SearchFragment.kt index b6f87aa..4b0ca0d 100644 --- a/app/src/main/java/org/mosad/teapod/ui/fragments/SearchFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/fragments/SearchFragment.kt @@ -26,10 +26,6 @@ class SearchFragment : Fragment() { super.onViewCreated(view, savedInstanceState) GlobalScope.launch { - if (AoDParser.mediaList.isEmpty()) { - AoDParser().listAnimes() - } - // create and set the adapter, needs context withContext(Dispatchers.Main) { context?.let { diff --git a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt index ff51e1f..46fb2f1 100644 --- a/app/src/main/java/org/mosad/teapod/util/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/util/DataTypes.kt @@ -27,7 +27,7 @@ data class Media( val link: String, val type: DataTypes.MediaType, val info: Info = Info(), - var episodes: List = listOf() + var episodes: ArrayList = arrayListOf() ) data class Info( @@ -40,10 +40,15 @@ data class Info( var episodesCount: Int = 0 ) +/** + * if secStreamOmU == true, then a secondary stream is present + */ data class Episode( val id: Int = 0, var title: String = "", - var streamUrl: String = "", + var priStreamUrl: String = "", + var secStreamUrl: String = "", + var secStreamOmU: Boolean = false, var posterUrl: String = "", var description: String = "", var shortDesc: String = "", @@ -60,3 +65,17 @@ data class TMDBResponse( val backdropUrl: String = "", var runtime: Int = 0 ) + +data class AoDObject(val playlist: List) + +data class Playlist( + val sources: List, + val image: String, + val title: String, + val description: String, + val mediaid: Int +) + +data class Source( + val file: String = "" +) diff --git a/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt b/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt index d079bf8..2d9a639 100644 --- a/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt +++ b/app/src/main/java/org/mosad/teapod/util/adapter/EpisodeItemAdapter.kt @@ -25,21 +25,24 @@ class EpisodeItemAdapter(private val episodes: List) : RecyclerView.Ada override fun onBindViewHolder(holder: MyViewHolder, position: Int) { val context = holder.view.context + val ep = episodes[position] - holder.view.text_episode_title.text = context.getString( - R.string.component_episode_title, - episodes[position].number, - episodes[position].description - ) - holder.view.text_episode_desc.text = episodes[position].shortDesc + val titleText = if (ep.priStreamUrl.isEmpty() && ep.secStreamOmU) { + context.getString(R.string.component_episode_title_sub, ep.number, ep.description) + } else { + context.getString(R.string.component_episode_title, ep.number, ep.description) + } + + holder.view.text_episode_title.text = titleText + holder.view.text_episode_desc.text = ep.shortDesc if (episodes[position].posterUrl.isNotEmpty()) { - Glide.with(context).load(episodes[position].posterUrl) + Glide.with(context).load(ep.posterUrl) .apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0))) .into(holder.view.image_episode) } - if (episodes[position].watched) { + if (ep.watched) { holder.view.image_watched.setImageDrawable( ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24) ) diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index f11e429..4199f0e 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -17,6 +17,7 @@ %1$d Episoden %1$d Minuten Episode %1$d %2$s + Episode %1$d %2$s (OmU) Account diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e9b0839..d4e9f2c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,6 +22,7 @@ %1$d episodes %1$d Minutes Episode %1$d %2$s + Episode %1$d %2$s (Sub) episode poster already watched