Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
77e657d37c
|
|||
20407d9cac
|
|||
dbd4b26a65
|
|||
ac5aee20de
|
|||
32844223fc
|
|||
d01e87bf14
|
@ -10,8 +10,8 @@ android {
|
|||||||
applicationId "org.mosad.teapod"
|
applicationId "org.mosad.teapod"
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 2000 //00.02.000
|
versionCode 2100 //00.02.100
|
||||||
versionName "0.2.0"
|
versionName "0.2.1"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resValue "string", "build_time", buildTime()
|
resValue "string", "build_time", buildTime()
|
||||||
@ -51,7 +51,7 @@ dependencies {
|
|||||||
implementation 'androidx.security:security-crypto:1.1.0-alpha02'
|
implementation 'androidx.security:security-crypto:1.1.0-alpha02'
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.3.0-alpha03'
|
implementation 'com.google.android.material:material:1.3.0-alpha04'
|
||||||
implementation 'com.google.code.gson:gson:2.8.6'
|
implementation 'com.google.code.gson:gson:2.8.6'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.1'
|
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.1'
|
implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.1'
|
||||||
|
@ -110,18 +110,18 @@ object AoDParser {
|
|||||||
* get a media by it's ID (int)
|
* get a media by it's ID (int)
|
||||||
* @return Media
|
* @return Media
|
||||||
*/
|
*/
|
||||||
fun getMediaById(mediaId: Int): Media {
|
suspend fun getMediaById(mediaId: Int): Media {
|
||||||
val media = mediaList.first { it.id == mediaId }
|
val media = mediaList.first { it.id == mediaId }
|
||||||
|
|
||||||
if (media.episodes.isEmpty()) {
|
if (media.episodes.isEmpty()) {
|
||||||
loadStreams(media)
|
loadStreams(media).join()
|
||||||
}
|
}
|
||||||
|
|
||||||
return media
|
return media
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO don't use jsoup here
|
// TODO don't use jsoup here
|
||||||
fun sendCallback(callbackPath: String) = GlobalScope.launch {
|
fun sendCallback(callbackPath: String) = GlobalScope.launch(Dispatchers.IO) {
|
||||||
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"),
|
||||||
@ -131,13 +131,11 @@ object AoDParser {
|
|||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
withContext(Dispatchers.IO) {
|
Jsoup.connect(baseUrl + callbackPath)
|
||||||
Jsoup.connect(baseUrl + callbackPath)
|
.ignoreContentType(true)
|
||||||
.ignoreContentType(true)
|
.cookies(sessionCookies)
|
||||||
.cookies(sessionCookies)
|
.headers(headers)
|
||||||
.headers(headers)
|
.execute()
|
||||||
.execute()
|
|
||||||
}
|
|
||||||
} catch (ex: IOException) {
|
} catch (ex: IOException) {
|
||||||
Log.e(javaClass.name, "Callback for $callbackPath failed.", ex)
|
Log.e(javaClass.name, "Callback for $callbackPath failed.", ex)
|
||||||
}
|
}
|
||||||
@ -213,38 +211,62 @@ object AoDParser {
|
|||||||
* load streams for the media path, movies have one episode
|
* load streams for the media path, movies have one episode
|
||||||
* @param media is used as call ba reference
|
* @param media is used as call ba reference
|
||||||
*/
|
*/
|
||||||
private fun loadStreams(media: Media) = runBlocking {
|
private suspend fun loadStreams(media: Media) = GlobalScope.launch(Dispatchers.IO) {
|
||||||
if (sessionCookies.isEmpty()) login()
|
if (sessionCookies.isEmpty()) login()
|
||||||
|
|
||||||
if (!loginSuccess) {
|
if (!loginSuccess) {
|
||||||
Log.w(javaClass.name, "Login, was not successful.")
|
Log.w(javaClass.name, "Login, was not successful.")
|
||||||
return@runBlocking
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
// get the media page
|
||||||
|
val res = Jsoup.connect(baseUrl + media.link)
|
||||||
|
.cookies(sessionCookies)
|
||||||
|
.get()
|
||||||
|
|
||||||
// get the media page
|
//println(res)
|
||||||
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")
|
||||||
|
}
|
||||||
|
|
||||||
if (csrfToken.isEmpty()) {
|
val pl = res.select("input.streamstarter_html5").first()
|
||||||
csrfToken = res.select("meta[name=csrf-token]").attr("content")
|
val primary = pl.attr("data-playlist")
|
||||||
//Log.i(javaClass.name, "New csrf token is $csrfToken")
|
val secondary = pl.attr("data-otherplaylist")
|
||||||
|
val secondaryIsOmU = secondary.contains("OmU", true)
|
||||||
|
|
||||||
|
// load primary and secondary playlist
|
||||||
|
val primaryPlaylist = parsePlaylistAsync(primary)
|
||||||
|
val secondaryPlaylist = parsePlaylistAsync(secondary)
|
||||||
|
|
||||||
|
primaryPlaylist.await().playlist.forEach { ep ->
|
||||||
|
val epNumber = if (media.type == MediaType.TVSHOW) {
|
||||||
|
ep.title.substringAfter(", Ep. ").toInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
val pl = res.select("input.streamstarter_html5").first()
|
media.episodes.add(
|
||||||
val primary = pl.attr("data-playlist")
|
Episode(
|
||||||
val secondary = pl.attr("data-otherplaylist")
|
id = ep.mediaid,
|
||||||
val secondaryIsOmU = secondary.contains("OmU", true)
|
priStreamUrl = ep.sources.first().file,
|
||||||
|
posterUrl = ep.image,
|
||||||
|
title = ep.title,
|
||||||
|
description = ep.description,
|
||||||
|
number = epNumber
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.i(javaClass.name, "Loading primary playlist finished")
|
||||||
|
|
||||||
// load primary and secondary playlist
|
secondaryPlaylist.await().playlist.forEach { ep ->
|
||||||
val primaryPlaylist = parsePlaylistAsync(primary)
|
val episode = media.episodes.firstOrNull { it.id == ep.mediaid }
|
||||||
val secondaryPlaylist = parsePlaylistAsync(secondary)
|
|
||||||
|
|
||||||
primaryPlaylist.await().playlist.forEach { ep ->
|
if (episode != null) {
|
||||||
|
episode.secStreamUrl = ep.sources.first().file
|
||||||
|
episode.secStreamOmU = secondaryIsOmU
|
||||||
|
} else {
|
||||||
val epNumber = if (media.type == MediaType.TVSHOW) {
|
val epNumber = if (media.type == MediaType.TVSHOW) {
|
||||||
ep.title.substringAfter(", Ep. ").toInt()
|
ep.title.substringAfter(", Ep. ").toInt()
|
||||||
} else {
|
} else {
|
||||||
@ -254,7 +276,8 @@ object AoDParser {
|
|||||||
media.episodes.add(
|
media.episodes.add(
|
||||||
Episode(
|
Episode(
|
||||||
id = ep.mediaid,
|
id = ep.mediaid,
|
||||||
priStreamUrl = ep.sources.first().file,
|
secStreamUrl = ep.sources.first().file,
|
||||||
|
secStreamOmU = secondaryIsOmU,
|
||||||
posterUrl = ep.image,
|
posterUrl = ep.image,
|
||||||
title = ep.title,
|
title = ep.title,
|
||||||
description = ep.description,
|
description = ep.description,
|
||||||
@ -262,69 +285,40 @@ object AoDParser {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Log.i(javaClass.name, "Loading primary playlist finished")
|
}
|
||||||
|
Log.i(javaClass.name, "Loading secondary playlist finished")
|
||||||
|
|
||||||
secondaryPlaylist.await().playlist.forEach { ep ->
|
// parse additional info from the media page
|
||||||
val episode = media.episodes.firstOrNull { it.id == ep.mediaid }
|
res.select("table.vertical-table").select("tr").forEach { row ->
|
||||||
|
when (row.select("th").text().toLowerCase(Locale.ROOT)) {
|
||||||
if (episode != null) {
|
"produktionsjahr" -> media.info.year = row.select("td").text().toInt()
|
||||||
episode.secStreamUrl = ep.sources.first().file
|
"fsk" -> media.info.age = row.select("td").text().toInt()
|
||||||
episode.secStreamOmU = secondaryIsOmU
|
"episodenanzahl" -> {
|
||||||
} else {
|
media.info.episodesCount = row.select("td").text()
|
||||||
val epNumber = if (media.type == MediaType.TVSHOW) {
|
.substringBefore("/")
|
||||||
ep.title.substringAfter(", Ep. ").toInt()
|
.filter { it.isDigit() }
|
||||||
} else {
|
.toInt()
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
media.episodes.add(
|
|
||||||
Episode(
|
|
||||||
id = ep.mediaid,
|
|
||||||
secStreamUrl = ep.sources.first().file,
|
|
||||||
secStreamOmU = secondaryIsOmU,
|
|
||||||
posterUrl = ep.image,
|
|
||||||
title = ep.title,
|
|
||||||
description = ep.description,
|
|
||||||
number = epNumber
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i(javaClass.name, "Loading secondary playlist finished")
|
}
|
||||||
|
|
||||||
// parse additional info from the media page
|
// parse additional information for tv shows the episode title (description) is loaded from the "api"
|
||||||
res.select("table.vertical-table").select("tr").forEach { row ->
|
if (media.type == MediaType.TVSHOW) {
|
||||||
when (row.select("th").text().toLowerCase(Locale.ROOT)) {
|
res.select("div.three-box-container > div.episodebox").forEach { episodebox ->
|
||||||
"produktionsjahr" -> media.info.year = row.select("td").text().toInt()
|
// make sure the episode has a streaming link
|
||||||
"fsk" -> media.info.age = row.select("td").text().toInt()
|
if (episodebox.select("input.streamstarter_html5").isNotEmpty()) {
|
||||||
"episodenanzahl" -> {
|
val episodeId = episodebox.select("div.flip-front").attr("id").substringAfter("-").toInt()
|
||||||
media.info.episodesCount = row.select("td").text()
|
val episodeShortDesc = episodebox.select("p.episodebox-shorttext").text()
|
||||||
.substringBefore("/")
|
val episodeWatched = episodebox.select("div.episodebox-icons > div").hasClass("status-icon-orange")
|
||||||
.filter{ it.isDigit() }
|
val episodeWatchedCallback = episodebox.select("input.streamstarter_html5").eachAttr("data-playlist").first()
|
||||||
.toInt()
|
|
||||||
|
media.episodes.firstOrNull { it.id == episodeId }?.apply {
|
||||||
|
shortDesc = episodeShortDesc
|
||||||
|
watched = episodeWatched
|
||||||
|
watchedCallback = episodeWatchedCallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse additional information for tv shows
|
|
||||||
if (media.type == MediaType.TVSHOW) {
|
|
||||||
res.select("div.three-box-container > div.episodebox").forEach { episodebox ->
|
|
||||||
// make sure the episode has a streaming link
|
|
||||||
if (episodebox.select("input.streamstarter_html5").isNotEmpty()) {
|
|
||||||
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()
|
|
||||||
|
|
||||||
media.episodes.firstOrNull { it.id == episodeId }?.apply {
|
|
||||||
shortDesc = episodeShortDesc
|
|
||||||
watched = episodeWatched
|
|
||||||
watchedCallback = episodeWatchedCallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,7 +330,7 @@ object AoDParser {
|
|||||||
return CompletableDeferred(AoDObject(listOf()))
|
return CompletableDeferred(AoDObject(listOf()))
|
||||||
}
|
}
|
||||||
|
|
||||||
return GlobalScope.async {
|
return GlobalScope.async(Dispatchers.IO) {
|
||||||
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"),
|
||||||
|
@ -29,6 +29,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.mosad.teapod.R
|
import org.mosad.teapod.R
|
||||||
import org.mosad.teapod.preferences.Preferences
|
import org.mosad.teapod.preferences.Preferences
|
||||||
|
import org.mosad.teapod.util.DataTypes
|
||||||
import org.mosad.teapod.util.Episode
|
import org.mosad.teapod.util.Episode
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -44,6 +45,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
private lateinit var gestureDetector: GestureDetectorCompat
|
private lateinit var gestureDetector: GestureDetectorCompat
|
||||||
private lateinit var timerUpdates: TimerTask
|
private lateinit var timerUpdates: TimerTask
|
||||||
|
|
||||||
|
private var nextEpManually = false
|
||||||
private var playWhenReady = true
|
private var playWhenReady = true
|
||||||
private var currentWindow = 0
|
private var currentWindow = 0
|
||||||
private var playbackPosition: Long = 0
|
private var playbackPosition: Long = 0
|
||||||
@ -129,14 +131,9 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
dataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, "Teapod"))
|
dataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, "Teapod"))
|
||||||
controller = video_view.findViewById(R.id.exo_controller)
|
controller = video_view.findViewById(R.id.exo_controller)
|
||||||
|
|
||||||
val mediaSource = HlsMediaSource.Factory(dataSourceFactory)
|
controller.isAnimationEnabled = false // disable controls (time-bar) animation
|
||||||
.createMediaSource(MediaItem.fromUri(Uri.parse(autoSelectStream(model.currentEpisode))))
|
|
||||||
|
|
||||||
player.playWhenReady = playWhenReady
|
player.playWhenReady = playWhenReady
|
||||||
player.setMediaSource(mediaSource)
|
|
||||||
player.seekTo(playbackPosition)
|
|
||||||
player.prepare()
|
|
||||||
|
|
||||||
player.addListener(object : Player.EventListener {
|
player.addListener(object : Player.EventListener {
|
||||||
override fun onPlaybackStateChanged(state: Int) {
|
override fun onPlaybackStateChanged(state: Int) {
|
||||||
super.onPlaybackStateChanged(state)
|
super.onPlaybackStateChanged(state)
|
||||||
@ -154,14 +151,16 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state == ExoPlayer.STATE_ENDED && model.nextEpisode != null && Preferences.autoplay) {
|
if (state == ExoPlayer.STATE_ENDED && model.nextEpisode != null && Preferences.autoplay) {
|
||||||
playNextEpisode()
|
if (nextEpManually) {
|
||||||
|
nextEpManually = false
|
||||||
|
} else {
|
||||||
|
playNextEpisode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
controller.isAnimationEnabled = false // disable controls (time-bar) animation
|
playCurrentMedia(true)
|
||||||
exo_text_title.text = model.currentEpisode.title // set media title
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@ -187,6 +186,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
rwd_10.setOnButtonClickListener { rewind() }
|
rwd_10.setOnButtonClickListener { rewind() }
|
||||||
ffwd_10.setOnButtonClickListener { fastForward() }
|
ffwd_10.setOnButtonClickListener { fastForward() }
|
||||||
button_next_ep.setOnClickListener { playNextEpisode() }
|
button_next_ep.setOnClickListener { playNextEpisode() }
|
||||||
|
button_next_ep_c.setOnClickListener { playNextEpisode() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initTimeUpdates() {
|
private fun initTimeUpdates() {
|
||||||
@ -299,18 +299,37 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playNextEpisode() = model.nextEpisode?.let { nextEp ->
|
private fun playNextEpisode() = model.nextEpisode?.let {
|
||||||
// update the gui
|
model.nextEpisode() // current = next, next = new or null
|
||||||
exo_text_title.text = nextEp.title
|
|
||||||
hideButtonNextEp()
|
hideButtonNextEp()
|
||||||
|
|
||||||
|
nextEpManually = true
|
||||||
|
playCurrentMedia(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* start playing a episode
|
||||||
|
* Note: movies are episodes too!
|
||||||
|
*/
|
||||||
|
private fun playCurrentMedia(seekToPosition: Boolean) {
|
||||||
|
// update the gui
|
||||||
|
exo_text_title.text = if (model.media.type == DataTypes.MediaType.TVSHOW) {
|
||||||
|
getString(R.string.component_episode_title, model.currentEpisode.number, model.currentEpisode.description)
|
||||||
|
} else {
|
||||||
|
model.currentEpisode.title
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.nextEpisode == null) {
|
||||||
|
button_next_ep_c.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
player.clearMediaItems() //remove previous item
|
player.clearMediaItems() //remove previous item
|
||||||
val mediaSource = HlsMediaSource.Factory(dataSourceFactory)
|
val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
|
||||||
.createMediaSource(MediaItem.fromUri(Uri.parse(autoSelectStream(nextEp))))
|
MediaItem.fromUri(Uri.parse(autoSelectStream(model.currentEpisode)))
|
||||||
|
)
|
||||||
|
if (seekToPosition) player.seekTo(playbackPosition)
|
||||||
player.setMediaSource(mediaSource)
|
player.setMediaSource(mediaSource)
|
||||||
player.prepare()
|
player.prepare()
|
||||||
|
|
||||||
model.nextEpisode()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.mosad.teapod.player
|
package org.mosad.teapod.player
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.mosad.teapod.parser.AoDParser
|
import org.mosad.teapod.parser.AoDParser
|
||||||
import org.mosad.teapod.ui.fragments.MediaFragment
|
import org.mosad.teapod.ui.fragments.MediaFragment
|
||||||
import org.mosad.teapod.util.DataTypes
|
import org.mosad.teapod.util.DataTypes
|
||||||
@ -25,7 +26,10 @@ class PlayerViewModel : ViewModel() {
|
|||||||
mediaId = iMediaId
|
mediaId = iMediaId
|
||||||
episodeId = iEpisodeId
|
episodeId = iEpisodeId
|
||||||
|
|
||||||
media = AoDParser.getMediaById(mediaId)
|
runBlocking {
|
||||||
|
media = AoDParser.getMediaById(mediaId)
|
||||||
|
}
|
||||||
|
|
||||||
currentEpisode = media.episodes.first { it.id == episodeId }
|
currentEpisode = media.episodes.first { it.id == episodeId }
|
||||||
nextEpisode = selectNextEpisode()
|
nextEpisode = selectNextEpisode()
|
||||||
}
|
}
|
||||||
|
@ -49,12 +49,12 @@ class MediaFragment(private val mediaId: Int) : Fragment() {
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
binding.frameLoading.visibility = View.VISIBLE
|
binding.frameLoading.visibility = View.VISIBLE
|
||||||
|
|
||||||
GlobalScope.launch {
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
// load the streams for the selected media
|
// load the streams for the selected media
|
||||||
media = AoDParser.getMediaById(mediaId)
|
media = AoDParser.getMediaById(mediaId)
|
||||||
tmdb = TMDBApiController().search(media.info.title, media.type)
|
tmdb = TMDBApiController().search(media.info.title, media.type)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
if (this@MediaFragment.isAdded) {
|
||||||
updateGUI()
|
updateGUI()
|
||||||
initActions()
|
initActions()
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,7 @@ package org.mosad.teapod.util
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import org.mosad.teapod.util.DataTypes.MediaType
|
import org.mosad.teapod.util.DataTypes.MediaType
|
||||||
@ -22,12 +20,12 @@ class TMDBApiController {
|
|||||||
|
|
||||||
private val imageUrl = "https://image.tmdb.org/t/p/w500"
|
private val imageUrl = "https://image.tmdb.org/t/p/w500"
|
||||||
|
|
||||||
fun search(title: String, type: MediaType): TMDBResponse {
|
suspend fun search(title: String, type: MediaType): TMDBResponse {
|
||||||
val searchTerm = title.replace("(Sub)", "").trim()
|
val searchTerm = title.replace("(Sub)", "").trim()
|
||||||
|
|
||||||
return when (type) {
|
return when (type) {
|
||||||
MediaType.MOVIE -> searchMovie(searchTerm)
|
MediaType.MOVIE -> searchMovie(searchTerm).await()
|
||||||
MediaType.TVSHOW -> searchTVShow(searchTerm)
|
MediaType.TVSHOW -> searchTVShow(searchTerm).await()
|
||||||
else -> {
|
else -> {
|
||||||
Log.e(javaClass.name, "Wrong Type: $type")
|
Log.e(javaClass.name, "Wrong Type: $type")
|
||||||
TMDBResponse()
|
TMDBResponse()
|
||||||
@ -36,17 +34,17 @@ class TMDBApiController {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchTVShow(title: String) = runBlocking {
|
fun searchTVShow(title: String): Deferred<TMDBResponse> {
|
||||||
val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
val url = URL("$searchTVUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
||||||
|
|
||||||
GlobalScope.async {
|
return GlobalScope.async {
|
||||||
val response = JsonParser.parseString(url.readText()).asJsonObject
|
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||||
//println(response)
|
//println(response)
|
||||||
|
|
||||||
return@async if (response.get("total_results").asInt > 0) {
|
if (response.get("total_results").asInt > 0) {
|
||||||
response.get("results").asJsonArray.first().asJsonObject.let {
|
response.get("results").asJsonArray.first().asJsonObject.let {
|
||||||
val id = getStringNotNull(it,"id").toInt()
|
val id = getStringNotNull(it, "id").toInt()
|
||||||
val overview = getStringNotNull(it,"overview")
|
val overview = getStringNotNull(it, "overview")
|
||||||
val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
|
val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
|
||||||
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
|
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
|
||||||
|
|
||||||
@ -55,18 +53,17 @@ class TMDBApiController {
|
|||||||
} else {
|
} else {
|
||||||
TMDBResponse()
|
TMDBResponse()
|
||||||
}
|
}
|
||||||
}.await()
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchMovie(title: String) = runBlocking {
|
fun searchMovie(title: String): Deferred<TMDBResponse> {
|
||||||
val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
val url = URL("$searchMovieUrl$preparedParameters&query=${URLEncoder.encode(title, "UTF-8")}")
|
||||||
|
|
||||||
GlobalScope.async {
|
return GlobalScope.async {
|
||||||
val response = JsonParser.parseString(url.readText()).asJsonObject
|
val response = JsonParser.parseString(url.readText()).asJsonObject
|
||||||
//println(response)
|
//println(response)
|
||||||
|
|
||||||
return@async if (response.get("total_results").asInt > 0) {
|
if (response.get("total_results").asInt > 0) {
|
||||||
response.get("results").asJsonArray.first().asJsonObject.let {
|
response.get("results").asJsonArray.first().asJsonObject.let {
|
||||||
val id = getStringNotNull(it,"id").toInt()
|
val id = getStringNotNull(it,"id").toInt()
|
||||||
val overview = getStringNotNull(it,"overview")
|
val overview = getStringNotNull(it,"overview")
|
||||||
@ -79,9 +76,7 @@ class TMDBApiController {
|
|||||||
} else {
|
} else {
|
||||||
TMDBResponse()
|
TMDBResponse()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}.await()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
5
app/src/main/res/drawable/ic_baseline_skip_next_24.xml
Normal file
5
app/src/main/res/drawable/ic_baseline_skip_next_24.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
|
||||||
|
</vector>
|
@ -1,10 +1,5 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
android:width="24dp"
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
android:height="24dp"
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:viewportWidth="24"
|
<path android:fillColor="@android:color/white" android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM12,14.5v-9l6,4.5 -6,4.5z"/>
|
||||||
android:viewportHeight="24"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM12,14.5v-9l6,4.5 -6,4.5z"/>
|
|
||||||
</vector>
|
</vector>
|
||||||
|
@ -20,12 +20,12 @@
|
|||||||
app:fastforward_increment="10000"
|
app:fastforward_increment="10000"
|
||||||
app:rewind_increment="10000" />
|
app:rewind_increment="10000" />
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.ProgressIndicator
|
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
android:id="@+id/loading"
|
android:id="@+id/loading"
|
||||||
style="@style/Widget.MaterialComponents.ProgressIndicator.Circular.Indeterminate"
|
|
||||||
android:layout_width="70dp"
|
android:layout_width="70dp"
|
||||||
android:layout_height="70dp"
|
android:layout_height="70dp"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
|
android:indeterminate="true"
|
||||||
app:indicatorColor="@color/exo_white"
|
app:indicatorColor="@color/exo_white"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
@ -167,12 +167,13 @@
|
|||||||
android:background="?themePrimary"
|
android:background="?themePrimary"
|
||||||
android:visibility="gone">
|
android:visibility="gone">
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.ProgressIndicator
|
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
android:id="@+id/loadingIndicator"
|
android:id="@+id/loadingIndicator"
|
||||||
style="@style/Widget.MaterialComponents.ProgressIndicator.Circular.Indeterminate"
|
|
||||||
android:layout_width="70dp"
|
android:layout_width="70dp"
|
||||||
android:layout_height="70dp"
|
android:layout_height="70dp"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
|
android:indeterminate="true"
|
||||||
|
app:indicatorColor="?colorPrimary"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
@ -62,6 +62,6 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="2"
|
android:maxLines="3"
|
||||||
android:textColor="?textSecondary" />
|
android:textColor="?textSecondary" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="195dp"
|
android:layout_width="195dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:backgroundTint="?themeSecondary"
|
android:backgroundTint="?themeSecondary"
|
||||||
@ -23,7 +24,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:srcCompat="@color/md_disabled_text_dark_theme" />
|
tools:srcCompat="@color/md_disabled_text_dark_theme" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_title"
|
android:id="@+id/text_title"
|
||||||
|
@ -108,4 +108,40 @@
|
|||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/exo_bottom_controls"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="42dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginBottom="7dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_next_ep_c"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/episode"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:icon="@drawable/ic_baseline_skip_next_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_episodes"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="7dp"
|
||||||
|
android:text="@string/episodes"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:icon="@drawable/ic_baseline_video_library_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/button_next_ep_c"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
@ -16,15 +16,15 @@
|
|||||||
<string name="button_play">Abspielen</string>
|
<string name="button_play">Abspielen</string>
|
||||||
<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">Flg. %1$d %2$s</string>
|
||||||
<string name="component_episode_title_sub">Episode %1$d %2$s (OmU)</string>
|
<string name="component_episode_title_sub">Flg. %1$d %2$s (OmU)</string>
|
||||||
|
|
||||||
<!-- settings fragment -->
|
<!-- settings fragment -->
|
||||||
<string name="account">Account</string>
|
<string name="account">Account</string>
|
||||||
<string name="account_login_desc">Zum bearbeiten tippen</string>
|
<string name="account_login_desc">Zum bearbeiten tippen</string>
|
||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
||||||
<string name="info_about_dialog">Diese App wird unter den Bedingungen der GNU GPL 3 oder höher zur Verfügung gestellt. Weiter Informationen findest du unter: \ngit.mosad.xyz/Seil0/teapod \n\n© 2020 seil0@mosad.xyz</string>
|
<string name="info_about_dialog">Diese App wird unter den Bedingungen der GNU GPL 3 oder höher zur Verfügung gestellt. Weiter Informationen findest du unter: git.mosad.xyz/Seil0/teapod \n\n© 2020 seil0@mosad.xyz</string>
|
||||||
<string name="licenses">Lizenzen</string>
|
<string name="licenses">Lizenzen</string>
|
||||||
<string name="settings">Einstellungen</string>
|
<string name="settings">Einstellungen</string>
|
||||||
<string name="settings_secondary">Bevorzuge alternativen Stream</string>
|
<string name="settings_secondary">Bevorzuge alternativen Stream</string>
|
||||||
@ -40,7 +40,9 @@
|
|||||||
<string name="rewind_10">10 Sekunden zurück</string>
|
<string name="rewind_10">10 Sekunden zurück</string>
|
||||||
<string name="play_pause">Abspielen/Pause</string>
|
<string name="play_pause">Abspielen/Pause</string>
|
||||||
<string name="forward_10">10 Sekunden vorwärts</string>
|
<string name="forward_10">10 Sekunden vorwärts</string>
|
||||||
<string name="next_episode">Nächste Episode</string>
|
<string name="next_episode">Nächste Folge</string>
|
||||||
|
<string name="episode">Folge</string>
|
||||||
|
<string name="episodes">Folgen</string>
|
||||||
|
|
||||||
<!-- dialogs -->
|
<!-- dialogs -->
|
||||||
<string name="save">speichern</string>
|
<string name="save">speichern</string>
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
<string name="text_age_ex" translatable="false">6</string>
|
<string name="text_age_ex" translatable="false">6</string>
|
||||||
<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">Ep. %1$d %2$s</string>
|
||||||
<string name="component_episode_title_sub">Episode %1$d %2$s (Sub)</string>
|
<string name="component_episode_title_sub">Ep. %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>
|
||||||
|
|
||||||
@ -33,7 +33,7 @@
|
|||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="info_about" translatable="false">Teapod by @Seil0</string>
|
<string name="info_about" translatable="false">Teapod by @Seil0</string>
|
||||||
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
<string name="info_about_desc">Version %1$s (%2$s)</string>
|
||||||
<string name="info_about_dialog">This app is published under the terms and conditions of the GNU GPL 3 or later. For further information visit: \ngit.mosad.xyz/Seil0/teapod \n\n© 2020 seil0@mosad.xyz</string>
|
<string name="info_about_dialog">This app is published under the terms and conditions of the GNU GPL 3 or later. For further information visit: git.mosad.xyz/Seil0/teapod \n\n© 2020 seil0@mosad.xyz</string>
|
||||||
<string name="licenses">Licenses</string>
|
<string name="licenses">Licenses</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
<string name="settings_secondary">Prefer secondary (sub) stream</string>
|
<string name="settings_secondary">Prefer secondary (sub) stream</string>
|
||||||
@ -52,9 +52,11 @@
|
|||||||
<string name="forward_10">forward 10 seconds</string>
|
<string name="forward_10">forward 10 seconds</string>
|
||||||
<string name="rwd_10_s" translatable="false">- 10 s</string>
|
<string name="rwd_10_s" translatable="false">- 10 s</string>
|
||||||
<string name="fwd_10_s" translatable="false">+ 10 s</string>
|
<string name="fwd_10_s" translatable="false">+ 10 s</string>
|
||||||
|
<string name="next_episode">Next Episode</string>
|
||||||
<string name="time_min_sec" translatable="false">%1$02d:%2$02d</string>
|
<string name="time_min_sec" translatable="false">%1$02d:%2$02d</string>
|
||||||
<string name="time_hour_min_sec" translatable="false">%1$d:%2$02d:%3$02d</string>
|
<string name="time_hour_min_sec" translatable="false">%1$d:%2$02d:%3$02d</string>
|
||||||
<string name="next_episode">Next Episode</string>
|
<string name="episode">Episode</string>
|
||||||
|
<string name="episodes">Episodes</string>
|
||||||
|
|
||||||
<!-- dialogs -->
|
<!-- dialogs -->
|
||||||
<string name="save">save</string>
|
<string name="save">save</string>
|
||||||
|
3
fastlane/metadata/android/de-DE/changelogs/2100.txt
Normal file
3
fastlane/metadata/android/de-DE/changelogs/2100.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
* Ein Fehler wurde behoben, bei dem Autoplay Folgen überspringen konnte
|
||||||
|
* Der Player zeigt nun einen Button an, um zur nächsten Folge zu springen
|
||||||
|
* Die UI wurde an einigen Stellen angepasst
|
Reference in New Issue
Block a user