rework media parsing, parse secondary stream (sub/jap)
* use the secondary stream if no primary is present
This commit is contained in:
parent
4c274eb062
commit
ce84cb57a8
|
@ -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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue