rework media parsing, parse secondary stream (sub/jap)
* use the secondary stream if no primary is present
This commit is contained in:
		| @ -23,11 +23,12 @@ | |||||||
| package org.mosad.teapod.parser | package org.mosad.teapod.parser | ||||||
|  |  | ||||||
| import android.util.Log | import android.util.Log | ||||||
| import com.google.gson.JsonParser | import com.google.gson.Gson | ||||||
| import kotlinx.coroutines.* | import kotlinx.coroutines.* | ||||||
| import org.jsoup.Connection | import org.jsoup.Connection | ||||||
| import org.jsoup.Jsoup | import org.jsoup.Jsoup | ||||||
| import org.mosad.teapod.preferences.EncryptedPreferences | import org.mosad.teapod.preferences.EncryptedPreferences | ||||||
|  | import org.mosad.teapod.util.AoDObject | ||||||
| import org.mosad.teapod.util.DataTypes.MediaType | import org.mosad.teapod.util.DataTypes.MediaType | ||||||
| import org.mosad.teapod.util.Episode | import org.mosad.teapod.util.Episode | ||||||
| import org.mosad.teapod.util.ItemMedia | import org.mosad.teapod.util.ItemMedia | ||||||
| @ -47,7 +48,7 @@ object AoDParser { | |||||||
|     private var csrfToken: String = "" |     private var csrfToken: String = "" | ||||||
|     private var loginSuccess = false |     private var loginSuccess = false | ||||||
|  |  | ||||||
|     val mediaList = arrayListOf<Media>() |     private val mediaList = arrayListOf<Media>() | ||||||
|     val itemMediaList = arrayListOf<ItemMedia>() |     val itemMediaList = arrayListOf<ItemMedia>() | ||||||
|     val newEpisodesList = arrayListOf<ItemMedia>() |     val newEpisodesList = arrayListOf<ItemMedia>() | ||||||
|  |  | ||||||
| @ -224,12 +225,69 @@ object AoDParser { | |||||||
|  |  | ||||||
|         withContext(Dispatchers.Default) { |         withContext(Dispatchers.Default) { | ||||||
|  |  | ||||||
|  |             // get the media page | ||||||
|             val res = Jsoup.connect(baseUrl + media.link) |             val res = Jsoup.connect(baseUrl + media.link) | ||||||
|                 .cookies(sessionCookies) |                 .cookies(sessionCookies) | ||||||
|                 .get() |                 .get() | ||||||
|  |  | ||||||
|             //println(res) |             //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 |             // parse additional info from the media page | ||||||
|             res.select("table.vertical-table").select("tr").forEach { row -> |             res.select("table.vertical-table").select("tr").forEach { row -> | ||||||
|                 when (row.select("th").text().toLowerCase(Locale.ROOT)) { |                 when (row.select("th").text().toLowerCase(Locale.ROOT)) { | ||||||
| @ -245,46 +303,31 @@ object AoDParser { | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             // parse additional information for tv shows |             // parse additional information for tv shows | ||||||
|             media.episodes = when (media.type) { |             if (media.type == MediaType.TVSHOW) { | ||||||
|                 MediaType.MOVIE -> listOf(Episode()) |                 res.select("div.three-box-container > div.episodebox").forEach { episodebox -> | ||||||
|                 MediaType.TVSHOW -> { |                     val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt() | ||||||
|                     res.select("div.three-box-container > div.episodebox").map { episodebox -> |                     val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text() | ||||||
|                         val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt() |                     val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange") | ||||||
|                         val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text() |                     val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first() | ||||||
|                         val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange") |  | ||||||
|                         val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first() |  | ||||||
|  |  | ||||||
|                         Episode( |                     media.episodes.firstOrNull { it.id == episodeId }?.apply { | ||||||
|                             id = episodeId, |                         shortDesc = episodeShortDesc | ||||||
|                             shortDesc = episodeShortDesc, |                         watched = episodeWatched | ||||||
|                             watched = episodeWatched, |                         watchedCallback = episodeWatchedCallback | ||||||
|                             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) |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     // TODO is this realy a save way, since we don't have any control over the "api" | ||||||
|      * load the playlist path and parse it, read the stream info from json |     private fun parsePlaylistAsync(playlistPath: String): Deferred<AoDObject> { | ||||||
|      * @param episodes is used as call ba reference |         if (playlistPath == "[]") { | ||||||
|      */ |             return CompletableDeferred(AoDObject(listOf())) | ||||||
|     private fun loadPlaylist(playlistPath: String, csrfToken: String, type: MediaType, episodes: List<Episode>) = runBlocking { |         } | ||||||
|         withContext(Dispatchers.Default) { |  | ||||||
|  |         return GlobalScope.async { | ||||||
|             val headers = mutableMapOf( |             val headers = mutableMapOf( | ||||||
|                 Pair("Accept", "application/json, text/javascript, */*; q=0.01"), |                 Pair("Accept", "application/json, text/javascript, */*; q=0.01"), | ||||||
|                 Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"), |                 Pair("Accept-Language", "de,en-US;q=0.7,en;q=0.3"), | ||||||
| @ -301,49 +344,9 @@ object AoDParser { | |||||||
|                 .headers(headers) |                 .headers(headers) | ||||||
|                 .execute() |                 .execute() | ||||||
|  |  | ||||||
|             //println(res.body()) |             Gson().fromJson(res.body(), AoDObject::class.java) | ||||||
|  |  | ||||||
|             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.") |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -30,10 +30,6 @@ class HomeFragment : Fragment() { | |||||||
|         super.onViewCreated(view, savedInstanceState) |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |  | ||||||
|         GlobalScope.launch { |         GlobalScope.launch { | ||||||
|             if (AoDParser.mediaList.isEmpty()) { |  | ||||||
|                 AoDParser().listAnimes() |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             withContext(Dispatchers.Main) { |             withContext(Dispatchers.Main) { | ||||||
|                 context?.let { |                 context?.let { | ||||||
|                     recycler_my_list.addItemDecoration(MediaItemDecoration(9)) |                     recycler_my_list.addItemDecoration(MediaItemDecoration(9)) | ||||||
|  | |||||||
| @ -26,10 +26,6 @@ class LibraryFragment : Fragment() { | |||||||
|  |  | ||||||
|         // init async |         // init async | ||||||
|         GlobalScope.launch { |         GlobalScope.launch { | ||||||
|             if (AoDParser.mediaList.isEmpty()) { |  | ||||||
|                 AoDParser().listAnimes() |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // create and set the adapter, needs context |             // create and set the adapter, needs context | ||||||
|             withContext(Dispatchers.Main) { |             withContext(Dispatchers.Main) { | ||||||
|                 context?.let { |                 context?.let { | ||||||
|  | |||||||
| @ -88,8 +88,8 @@ class MediaFragment(private val media: Media, private val tmdb: TMDBResponse) : | |||||||
|     private fun initActions() { |     private fun initActions() { | ||||||
|         button_play.setOnClickListener { |         button_play.setOnClickListener { | ||||||
|             when (media.type) { |             when (media.type) { | ||||||
|                 MediaType.MOVIE -> playStream(media.episodes.first().streamUrl) |                 MediaType.MOVIE -> playStream(media.episodes.first().priStreamUrl) | ||||||
|                 MediaType.TVSHOW -> playStream(media.episodes.first().streamUrl) |                 MediaType.TVSHOW -> playStream(media.episodes.first().priStreamUrl) | ||||||
|                 else -> Log.e(javaClass.name, "Wrong Type: $media.type") |                 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 |         // set onItemClick only in adapter is initialized | ||||||
|         if (this::adapterRecEpisodes.isInitialized) { |         if (this::adapterRecEpisodes.isInitialized) { | ||||||
|             adapterRecEpisodes.onImageClick = { _, position -> |             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 |                 // update watched state | ||||||
|                 AoDParser.sendCallback(media.episodes[position].watchedCallback) |                 AoDParser.sendCallback(media.episodes[position].watchedCallback) | ||||||
|  | |||||||
| @ -26,10 +26,6 @@ class SearchFragment : Fragment() { | |||||||
|         super.onViewCreated(view, savedInstanceState) |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |  | ||||||
|         GlobalScope.launch { |         GlobalScope.launch { | ||||||
|             if (AoDParser.mediaList.isEmpty()) { |  | ||||||
|                 AoDParser().listAnimes() |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // create and set the adapter, needs context |             // create and set the adapter, needs context | ||||||
|             withContext(Dispatchers.Main) { |             withContext(Dispatchers.Main) { | ||||||
|                 context?.let { |                 context?.let { | ||||||
|  | |||||||
| @ -27,7 +27,7 @@ data class Media( | |||||||
|     val link: String, |     val link: String, | ||||||
|     val type: DataTypes.MediaType, |     val type: DataTypes.MediaType, | ||||||
|     val info: Info = Info(), |     val info: Info = Info(), | ||||||
|     var episodes: List<Episode> = listOf() |     var episodes: ArrayList<Episode> = arrayListOf() | ||||||
| ) | ) | ||||||
|  |  | ||||||
| data class Info( | data class Info( | ||||||
| @ -40,10 +40,15 @@ data class Info( | |||||||
|     var episodesCount: Int = 0 |     var episodesCount: Int = 0 | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * if secStreamOmU == true, then a secondary stream is present | ||||||
|  |  */ | ||||||
| data class Episode( | data class Episode( | ||||||
|     val id: Int = 0, |     val id: Int = 0, | ||||||
|     var title: String = "", |     var title: String = "", | ||||||
|     var streamUrl: String = "", |     var priStreamUrl: String = "", | ||||||
|  |     var secStreamUrl: String = "", | ||||||
|  |     var secStreamOmU: Boolean = false, | ||||||
|     var posterUrl: String = "", |     var posterUrl: String = "", | ||||||
|     var description: String = "", |     var description: String = "", | ||||||
|     var shortDesc: String = "", |     var shortDesc: String = "", | ||||||
| @ -60,3 +65,17 @@ data class TMDBResponse( | |||||||
|     val backdropUrl: String = "", |     val backdropUrl: String = "", | ||||||
|     var runtime: Int = 0 |     var runtime: Int = 0 | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | data class AoDObject(val playlist: List<Playlist>) | ||||||
|  |  | ||||||
|  | data class Playlist( | ||||||
|  |     val sources: List<Source>, | ||||||
|  |     val image: String, | ||||||
|  |     val title: String, | ||||||
|  |     val description: String, | ||||||
|  |     val mediaid: Int | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | data class Source( | ||||||
|  |     val file: String = "" | ||||||
|  | ) | ||||||
|  | |||||||
| @ -25,21 +25,24 @@ class EpisodeItemAdapter(private val episodes: List<Episode>) : RecyclerView.Ada | |||||||
|  |  | ||||||
|     override fun onBindViewHolder(holder: MyViewHolder, position: Int) { |     override fun onBindViewHolder(holder: MyViewHolder, position: Int) { | ||||||
|         val context = holder.view.context |         val context = holder.view.context | ||||||
|  |         val ep = episodes[position] | ||||||
|  |  | ||||||
|         holder.view.text_episode_title.text = context.getString( |         val titleText = if (ep.priStreamUrl.isEmpty() && ep.secStreamOmU) { | ||||||
|             R.string.component_episode_title, |             context.getString(R.string.component_episode_title_sub, ep.number, ep.description) | ||||||
|             episodes[position].number, |         } else { | ||||||
|             episodes[position].description |             context.getString(R.string.component_episode_title, ep.number, ep.description) | ||||||
|         ) |         } | ||||||
|         holder.view.text_episode_desc.text = episodes[position].shortDesc |  | ||||||
|  |         holder.view.text_episode_title.text = titleText | ||||||
|  |         holder.view.text_episode_desc.text = ep.shortDesc | ||||||
|  |  | ||||||
|         if (episodes[position].posterUrl.isNotEmpty()) { |         if (episodes[position].posterUrl.isNotEmpty()) { | ||||||
|             Glide.with(context).load(episodes[position].posterUrl) |             Glide.with(context).load(ep.posterUrl) | ||||||
|                 .apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0))) |                 .apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0))) | ||||||
|                 .into(holder.view.image_episode) |                 .into(holder.view.image_episode) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (episodes[position].watched) { |         if (ep.watched) { | ||||||
|             holder.view.image_watched.setImageDrawable( |             holder.view.image_watched.setImageDrawable( | ||||||
|                 ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24) |                 ContextCompat.getDrawable(context, R.drawable.ic_baseline_check_circle_24) | ||||||
|             ) |             ) | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ | |||||||
|     <string name="text_episodes_count">%1$d Episoden</string> |     <string name="text_episodes_count">%1$d Episoden</string> | ||||||
|     <string name="text_runtime">%1$d Minuten</string> |     <string name="text_runtime">%1$d Minuten</string> | ||||||
|     <string name="component_episode_title">Episode %1$d %2$s</string> |     <string name="component_episode_title">Episode %1$d %2$s</string> | ||||||
|  |     <string name="component_episode_title_sub">Episode %1$d %2$s (OmU)</string> | ||||||
|  |  | ||||||
|     <!-- settings fragment --> |     <!-- settings fragment --> | ||||||
|     <string name="account">Account</string> |     <string name="account">Account</string> | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ | |||||||
|     <string name="text_episodes_count">%1$d episodes</string> |     <string name="text_episodes_count">%1$d episodes</string> | ||||||
|     <string name="text_runtime">%1$d Minutes</string> |     <string name="text_runtime">%1$d Minutes</string> | ||||||
|     <string name="component_episode_title">Episode %1$d %2$s</string> |     <string name="component_episode_title">Episode %1$d %2$s</string> | ||||||
|  |     <string name="component_episode_title_sub">Episode %1$d %2$s (Sub)</string> | ||||||
|     <string name="component_poster_desc" translatable="false">episode poster</string> |     <string name="component_poster_desc" translatable="false">episode poster</string> | ||||||
|     <string name="component_watched_desc" translatable="false">already watched</string> |     <string name="component_watched_desc" translatable="false">already watched</string> | ||||||
|  |  | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user