forked from recloudstream/cloudstream
Compare commits
7 commits
Author | SHA1 | Date | |
---|---|---|---|
|
7f91ea5cc6 | ||
|
e24dc692d0 | ||
|
415c173524 | ||
|
41fd364401 | ||
|
a5cee36572 | ||
|
f77fe0a31e | ||
|
7619b7e9d9 |
17 changed files with 543 additions and 221 deletions
|
@ -48,7 +48,7 @@ android {
|
|||
targetSdk = 30
|
||||
|
||||
versionCode = 54
|
||||
versionName = "3.2.1"
|
||||
versionName = "3.2.2"
|
||||
|
||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||
|
||||
|
@ -190,7 +190,7 @@ dependencies {
|
|||
// Networking
|
||||
// implementation("com.squareup.okhttp3:okhttp:4.9.2")
|
||||
// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1")
|
||||
implementation("com.github.Blatzar:NiceHttp:0.3.3")
|
||||
implementation("com.github.Blatzar:NiceHttp:0.3.4")
|
||||
|
||||
// Util to skip the URI file fuckery 🙏
|
||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
||||
|
|
|
@ -337,6 +337,9 @@ object CommonActivity {
|
|||
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
|
||||
PlayerEventType.SkipOp
|
||||
}
|
||||
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
|
||||
PlayerEventType.SkipCurrentChapter
|
||||
}
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
|
||||
PlayerEventType.PlayPauseToggle
|
||||
}
|
||||
|
|
|
@ -88,6 +88,9 @@ import kotlinx.android.synthetic.main.activity_main.*
|
|||
import kotlinx.android.synthetic.main.fragment_result_swipe.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import okhttp3.ConnectionSpec
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.internal.applyConnectionSpec
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.nio.charset.Charset
|
||||
|
|
|
@ -12,7 +12,6 @@ import okhttp3.Headers.Companion.toHeaders
|
|||
import okhttp3.OkHttpClient
|
||||
import java.io.File
|
||||
|
||||
|
||||
fun Requests.initClient(context: Context): OkHttpClient {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val dns = settingsManager.getInt(context.getString(R.string.dns_pref), 0)
|
||||
|
|
|
@ -38,6 +38,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
|||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus
|
||||
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
||||
import com.lagradost.cloudstream3.utils.UIHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||
|
@ -103,6 +104,14 @@ abstract class AbstractPlayerFragment(
|
|||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
open fun onTimestamp(timestamp: EpisodeSkip.SkipStamp?) {
|
||||
|
||||
}
|
||||
|
||||
open fun onTimestampSkipped(timestamp: EpisodeSkip.SkipStamp) {
|
||||
|
||||
}
|
||||
|
||||
open fun exitedPipMode() {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
@ -373,7 +382,9 @@ abstract class AbstractPlayerFragment(
|
|||
),
|
||||
subtitlesUpdates = ::subtitlesChanged,
|
||||
embeddedSubtitlesFetched = ::embeddedSubtitlesFetched,
|
||||
onTracksInfoChanged = ::onTracksInfoChanged
|
||||
onTracksInfoChanged = ::onTracksInfoChanged,
|
||||
onTimestampInvoked = ::onTimestamp,
|
||||
onTimestampSkipped = ::onTimestampSkipped
|
||||
)
|
||||
|
||||
if (player is CS3IPlayer) {
|
||||
|
|
|
@ -18,7 +18,10 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
|||
import com.google.android.exoplayer2.trackselection.TrackSelectionOverride
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelector
|
||||
import com.google.android.exoplayer2.ui.SubtitleView
|
||||
import com.google.android.exoplayer2.upstream.*
|
||||
import com.google.android.exoplayer2.upstream.DataSource
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
|
||||
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
||||
|
@ -32,6 +35,7 @@ import com.lagradost.cloudstream3.app
|
|||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
||||
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
|
||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||
|
@ -113,6 +117,8 @@ class CS3IPlayer : IPlayer {
|
|||
private var playerUpdated: ((Any?) -> Unit)? = null
|
||||
private var embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)? = null
|
||||
private var onTracksInfoChanged: (() -> Unit)? = null
|
||||
private var onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)? = null
|
||||
private var onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)? = null
|
||||
|
||||
override fun releaseCallbacks() {
|
||||
playerUpdated = null
|
||||
|
@ -126,7 +132,9 @@ class CS3IPlayer : IPlayer {
|
|||
prevEpisode = null
|
||||
subtitlesUpdates = null
|
||||
onTracksInfoChanged = null
|
||||
onTimestampInvoked = null
|
||||
requestSubtitleUpdate = null
|
||||
onTimestampSkipped = null
|
||||
}
|
||||
|
||||
override fun initCallbacks(
|
||||
|
@ -142,6 +150,8 @@ class CS3IPlayer : IPlayer {
|
|||
subtitlesUpdates: (() -> Unit)?,
|
||||
embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)?,
|
||||
onTracksInfoChanged: (() -> Unit)?,
|
||||
onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)?,
|
||||
onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)?,
|
||||
) {
|
||||
this.playerUpdated = playerUpdated
|
||||
this.updateIsPlaying = updateIsPlaying
|
||||
|
@ -155,6 +165,8 @@ class CS3IPlayer : IPlayer {
|
|||
this.subtitlesUpdates = subtitlesUpdates
|
||||
this.embeddedSubtitlesFetched = embeddedSubtitlesFetched
|
||||
this.onTracksInfoChanged = onTracksInfoChanged
|
||||
this.onTimestampInvoked = onTimestampInvoked
|
||||
this.onTimestampSkipped = onTimestampSkipped
|
||||
}
|
||||
|
||||
// I know, this is not a perfect solution, however it works for fixing subs
|
||||
|
@ -719,7 +731,7 @@ class CS3IPlayer : IPlayer {
|
|||
source
|
||||
}
|
||||
|
||||
println("PLAYBACK POS $playbackPosition")
|
||||
//println("PLAYBACK POS $playbackPosition")
|
||||
return exoPlayerBuilder.build().apply {
|
||||
setPlayWhenReady(playWhenReady)
|
||||
seekTo(currentWindow, playbackPosition)
|
||||
|
@ -735,8 +747,22 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
fun updatedTime() {
|
||||
val position = exoPlayer?.currentPosition
|
||||
private fun getCurrentTimestamp(writePosition : Long? = null): EpisodeSkip.SkipStamp? {
|
||||
val position = writePosition ?: this@CS3IPlayer.getPosition() ?: return null
|
||||
for (lastTimeStamp in lastTimeStamps) {
|
||||
if (lastTimeStamp.startMs <= position && position < lastTimeStamp.endMs) {
|
||||
return lastTimeStamp
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun updatedTime(writePosition : Long? = null) {
|
||||
getCurrentTimestamp(writePosition)?.let { timestamp ->
|
||||
onTimestampInvoked?.invoke(timestamp)
|
||||
}
|
||||
|
||||
val position = writePosition ?: exoPlayer?.currentPosition
|
||||
val duration = exoPlayer?.contentDuration
|
||||
if (duration != null && position != null) {
|
||||
playerPositionChanged?.invoke(Pair(position, duration))
|
||||
|
@ -748,12 +774,12 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
|
||||
override fun seekTo(time: Long) {
|
||||
updatedTime()
|
||||
updatedTime(time)
|
||||
exoPlayer?.seekTo(time)
|
||||
}
|
||||
|
||||
private fun ExoPlayer.seekTime(time: Long) {
|
||||
updatedTime()
|
||||
updatedTime(currentPosition + time)
|
||||
seekTo(currentPosition + time)
|
||||
}
|
||||
|
||||
|
@ -789,6 +815,17 @@ class CS3IPlayer : IPlayer {
|
|||
CSPlayerEvent.SeekBack -> seekTime(-seekActionTime)
|
||||
CSPlayerEvent.NextEpisode -> nextEpisode?.invoke()
|
||||
CSPlayerEvent.PrevEpisode -> prevEpisode?.invoke()
|
||||
CSPlayerEvent.SkipCurrentChapter -> {
|
||||
//val dur = this@CS3IPlayer.getDuration() ?: return@apply
|
||||
getCurrentTimestamp()?.let { lastTimeStamp ->
|
||||
if (lastTimeStamp.skipToNextEpisode) {
|
||||
handleEvent(CSPlayerEvent.NextEpisode)
|
||||
} else {
|
||||
seekTo(lastTimeStamp.endMs + 1L)
|
||||
}
|
||||
onTimestampSkipped?.invoke(lastTimeStamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -1007,6 +1044,24 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
private var lastTimeStamps: List<EpisodeSkip.SkipStamp> = emptyList()
|
||||
override fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>) {
|
||||
lastTimeStamps = timeStamps
|
||||
timeStamps.forEach { timestamp ->
|
||||
exoPlayer?.createMessage { _, _ ->
|
||||
updatedTime()
|
||||
//if (payload is EpisodeSkip.SkipStamp) // this should always be true
|
||||
// onTimestampInvoked?.invoke(payload)
|
||||
}
|
||||
?.setLooper(Looper.getMainLooper())
|
||||
?.setPosition(timestamp.startMs)
|
||||
//?.setPayload(timestamp)
|
||||
?.setDeleteAfterDelivery(false)
|
||||
?.send()
|
||||
}
|
||||
updatedTime()
|
||||
}
|
||||
|
||||
fun onRenderFirst() {
|
||||
if (!hasUsedFirstRender) { // this insures that we only call this once per player load
|
||||
Log.i(TAG, "Rendered first frame")
|
||||
|
|
|
@ -1141,6 +1141,9 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
PlayerEventType.Play -> {
|
||||
player.handleEvent(CSPlayerEvent.Play)
|
||||
}
|
||||
PlayerEventType.SkipCurrentChapter -> {
|
||||
player.handleEvent(CSPlayerEvent.SkipCurrentChapter)
|
||||
}
|
||||
PlayerEventType.Resize -> {
|
||||
nextResize()
|
||||
}
|
||||
|
@ -1254,6 +1257,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
|
||||
}
|
||||
|
||||
skip_chapter_button?.setOnClickListener {
|
||||
player.handleEvent(CSPlayerEvent.SkipCurrentChapter)
|
||||
}
|
||||
|
||||
// init clicks
|
||||
player_resize_btt?.setOnClickListener {
|
||||
autoHide()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
|
@ -13,6 +14,7 @@ import android.view.ViewGroup
|
|||
import android.widget.*
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.animation.addListener
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
|
@ -36,8 +38,8 @@ import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSub
|
|||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||
import com.lagradost.cloudstream3.ui.result.SyncViewModel
|
||||
import com.lagradost.cloudstream3.ui.result.setText
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
|
@ -49,6 +51,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import kotlinx.android.synthetic.main.dialog_online_subtitles.*
|
||||
import kotlinx.android.synthetic.main.dialog_online_subtitles.apply_btt
|
||||
import kotlinx.android.synthetic.main.dialog_online_subtitles.cancel_btt
|
||||
|
@ -58,7 +61,6 @@ import kotlinx.android.synthetic.main.player_select_source_and_subs.*
|
|||
import kotlinx.android.synthetic.main.player_select_source_and_subs.subtitles_click_settings
|
||||
import kotlinx.android.synthetic.main.player_select_tracks.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class GeneratorPlayer : FullScreenPlayer() {
|
||||
companion object {
|
||||
|
@ -67,8 +69,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
Log.i(TAG, "newInstance = $syncData")
|
||||
lastUsedGenerator = generator
|
||||
return Bundle().apply {
|
||||
if (syncData != null)
|
||||
putSerializable("syncData", syncData)
|
||||
if (syncData != null) putSerializable("syncData", syncData)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -165,6 +166,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
isActive = true
|
||||
setPlayerDimen(null)
|
||||
setTitle()
|
||||
hasRequestedStamps = false
|
||||
|
||||
loadExtractorJob(link.first)
|
||||
// load player
|
||||
|
@ -180,12 +182,11 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
},
|
||||
currentSubs,
|
||||
(if (sameEpisode) currentSelectedSubtitles else null) ?: getAutoSelectSubtitle(
|
||||
currentSubs,
|
||||
settings = true,
|
||||
downloads = true
|
||||
currentSubs, settings = true, downloads = true
|
||||
),
|
||||
)
|
||||
}
|
||||
player.addTimeStamps(listOf()) // clear stamps
|
||||
}
|
||||
|
||||
private fun sortLinks(useQualitySettings: Boolean = true): List<Pair<ExtractorLink?, ExtractorUri?>> {
|
||||
|
@ -231,9 +232,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
override fun openOnlineSubPicker(
|
||||
context: Context,
|
||||
imdbId: Long?,
|
||||
dismissCallback: (() -> Unit)
|
||||
context: Context, imdbId: Long?, dismissCallback: (() -> Unit)
|
||||
) {
|
||||
val providers = subsProviders
|
||||
val isSingleProvider = subsProviders.size == 1
|
||||
|
@ -256,8 +255,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val arrayAdapter =
|
||||
object : ArrayAdapter<AbstractSubtitleEntities.SubtitleEntity>(dialog.context, layout) {
|
||||
fun setHearingImpairedIcon(
|
||||
imageViewEnd: ImageView?,
|
||||
position: Int
|
||||
imageViewEnd: ImageView?, position: Int
|
||||
) {
|
||||
if (imageViewEnd == null) return
|
||||
val isHearingImpaired =
|
||||
|
@ -265,13 +263,11 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
val drawableEnd = if (isHearingImpaired) {
|
||||
ContextCompat.getDrawable(
|
||||
context,
|
||||
R.drawable.ic_baseline_hearing_24
|
||||
context, R.drawable.ic_baseline_hearing_24
|
||||
)?.apply {
|
||||
setTint(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
R.color.textColor
|
||||
context, R.color.textColor
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -281,8 +277,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = convertView ?: LayoutInflater.from(context)
|
||||
.inflate(layout, null)
|
||||
val view = convertView ?: LayoutInflater.from(context).inflate(layout, null)
|
||||
|
||||
val item = getItem(position)
|
||||
|
||||
|
@ -337,13 +332,12 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
dialog.search_loading_bar?.show()
|
||||
ioSafe {
|
||||
val search = AbstractSubtitleEntities.SubtitleSearch(
|
||||
query = query ?: return@ioSafe,
|
||||
imdb = imdbId,
|
||||
epNumber = currentTempMeta.episode,
|
||||
seasonNumber = currentTempMeta.season,
|
||||
lang = currentLanguageTwoLetters.ifBlank { null }
|
||||
)
|
||||
val search =
|
||||
AbstractSubtitleEntities.SubtitleSearch(query = query ?: return@ioSafe,
|
||||
imdb = imdbId,
|
||||
epNumber = currentTempMeta.episode,
|
||||
seasonNumber = currentTempMeta.season,
|
||||
lang = currentLanguageTwoLetters.ifBlank { null })
|
||||
val results = providers.amap {
|
||||
try {
|
||||
it.search(search)
|
||||
|
@ -379,14 +373,12 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
dialog.search_filter.setOnClickListener { view ->
|
||||
val lang639_1 = languages.map { it.ISO_639_1 }
|
||||
activity?.showDialog(
|
||||
languages.map { it.languageName },
|
||||
activity?.showDialog(languages.map { it.languageName },
|
||||
lang639_1.indexOf(currentLanguageTwoLetters),
|
||||
view?.context?.getString(R.string.subs_subtitle_languages)
|
||||
?: return@setOnClickListener,
|
||||
true,
|
||||
{ }
|
||||
) { index ->
|
||||
{ }) { index ->
|
||||
currentLanguageTwoLetters = lang639_1[index]
|
||||
dialog.subtitles_search.setQuery(dialog.subtitles_search.query, true)
|
||||
}
|
||||
|
@ -472,8 +464,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
if (uri == null) return@normalSafeApiCall
|
||||
val ctx = context ?: AcraApplication.context ?: return@normalSafeApiCall
|
||||
// RW perms for the path
|
||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
val flags =
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
|
||||
ctx.contentResolver.takePersistableUriPermission(uri, flags)
|
||||
|
||||
|
@ -536,11 +528,9 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
if (subsProvidersIsActive) {
|
||||
val loadFromOpenSubsFooter: TextView =
|
||||
layoutInflater.inflate(
|
||||
R.layout.sort_bottom_footer_add_choice,
|
||||
null
|
||||
) as TextView
|
||||
val loadFromOpenSubsFooter: TextView = layoutInflater.inflate(
|
||||
R.layout.sort_bottom_footer_add_choice, null
|
||||
) as TextView
|
||||
|
||||
loadFromOpenSubsFooter.text =
|
||||
ctx.getString(R.string.player_load_subtitles_online)
|
||||
|
@ -592,8 +582,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val subtitleIndexStart = currentSubtitles.indexOf(currentSelectedSubtitles) + 1
|
||||
var subtitleIndex = subtitleIndexStart
|
||||
|
||||
val subsArrayAdapter =
|
||||
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
||||
val subsArrayAdapter = ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
||||
subsArrayAdapter.add(ctx.getString(R.string.no_subtitles))
|
||||
subsArrayAdapter.addAll(currentSubtitles.map { it.name })
|
||||
|
||||
|
@ -631,8 +620,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values)
|
||||
|
||||
val value = settingsManager.getString(
|
||||
ctx.getString(R.string.subtitles_encoding_key),
|
||||
null
|
||||
ctx.getString(R.string.subtitles_encoding_key), null
|
||||
)
|
||||
val index = prefValues.indexOf(value)
|
||||
text = prefNames[if (index == -1) 0 else index]
|
||||
|
@ -644,28 +632,22 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list)
|
||||
val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values)
|
||||
|
||||
val currentPrefMedia =
|
||||
settingsManager.getString(
|
||||
ctx.getString(R.string.subtitles_encoding_key),
|
||||
null
|
||||
)
|
||||
val currentPrefMedia = settingsManager.getString(
|
||||
ctx.getString(R.string.subtitles_encoding_key), null
|
||||
)
|
||||
|
||||
shouldDismiss = false
|
||||
sourceDialog.dismissSafe(activity)
|
||||
|
||||
val index = prefValues.indexOf(currentPrefMedia)
|
||||
activity?.showDialog(
|
||||
prefNames.toList(),
|
||||
activity?.showDialog(prefNames.toList(),
|
||||
if (index == -1) 0 else index,
|
||||
ctx.getString(R.string.subtitles_encoding),
|
||||
true,
|
||||
{}) {
|
||||
settingsManager.edit()
|
||||
.putString(
|
||||
ctx.getString(R.string.subtitles_encoding_key),
|
||||
prefValues[it]
|
||||
)
|
||||
.apply()
|
||||
settingsManager.edit().putString(
|
||||
ctx.getString(R.string.subtitles_encoding_key), prefValues[it]
|
||||
).apply()
|
||||
|
||||
updateForcedEncoding(ctx)
|
||||
dismiss()
|
||||
|
@ -878,7 +860,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
var maxEpisodeSet: Int? = null
|
||||
|
||||
var hasRequestedStamps: Boolean = false
|
||||
override fun playerPositionChanged(posDur: Pair<Long, Long>) {
|
||||
// Don't save livestream data
|
||||
if ((currentMeta as? ResultEpisode)?.tvType?.isLiveStream() == true) return
|
||||
|
@ -887,11 +869,16 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
if ((currentMeta as? ResultEpisode)?.tvType == TvType.NSFW) return
|
||||
|
||||
val (position, duration) = posDur
|
||||
if (duration == 0L) return // idk how you achieved this, but div by zero crash
|
||||
if (duration <= 0L) return // idk how you achieved this, but div by zero crash
|
||||
if (!hasRequestedStamps) {
|
||||
hasRequestedStamps = true
|
||||
viewModel.loadStamps(duration)
|
||||
}
|
||||
|
||||
viewModel.getId()?.let {
|
||||
DataStoreHelper.setViewPos(it, position, duration)
|
||||
}
|
||||
|
||||
val percentage = position * 100L / duration
|
||||
|
||||
val nextEp = percentage >= NEXT_WATCH_EPISODE_PERCENTAGE
|
||||
|
@ -939,17 +926,14 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
context?.let { ctx ->
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
if (settingsManager.getBoolean(
|
||||
ctx.getString(R.string.episode_sync_enabled_key),
|
||||
true
|
||||
ctx.getString(R.string.episode_sync_enabled_key), true
|
||||
)
|
||||
)
|
||||
maxEpisodeSet = meta.episode
|
||||
) maxEpisodeSet = meta.episode
|
||||
sync.modifyMaxEpisode(meta.episode)
|
||||
}
|
||||
}
|
||||
|
||||
if (meta.tvType.isAnimeOp())
|
||||
isOpVisible = percentage < SKIP_OP_VIDEO_PERCENTAGE
|
||||
if (meta.tvType.isAnimeOp()) isOpVisible = percentage < SKIP_OP_VIDEO_PERCENTAGE
|
||||
}
|
||||
}
|
||||
player_skip_op?.isVisible = isOpVisible
|
||||
|
@ -961,9 +945,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
private fun getAutoSelectSubtitle(
|
||||
subtitles: Set<SubtitleData>,
|
||||
settings: Boolean,
|
||||
downloads: Boolean
|
||||
subtitles: Set<SubtitleData>, settings: Boolean, downloads: Boolean
|
||||
): SubtitleData? {
|
||||
val langCode = preferredAutoSelectSubtitles ?: return null
|
||||
val lang = SubtitleHelper.fromTwoLettersToLanguage(langCode) ?: return null
|
||||
|
@ -1009,23 +991,20 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
player.handleEvent(CSPlayerEvent.Play)
|
||||
return true
|
||||
}
|
||||
} else
|
||||
if (!langCode.isNullOrEmpty()) {
|
||||
getAutoSelectSubtitle(
|
||||
currentSubs,
|
||||
settings = true,
|
||||
downloads = false
|
||||
)?.let { sub ->
|
||||
|
||||
if (setSubtitles(sub)) {
|
||||
player.saveData()
|
||||
player.reloadPlayer(ctx)
|
||||
player.handleEvent(CSPlayerEvent.Play)
|
||||
return true
|
||||
}
|
||||
} else if (!langCode.isNullOrEmpty()) {
|
||||
getAutoSelectSubtitle(
|
||||
currentSubs, settings = true, downloads = false
|
||||
)?.let { sub ->
|
||||
|
||||
if (setSubtitles(sub)) {
|
||||
player.saveData()
|
||||
player.reloadPlayer(ctx)
|
||||
player.handleEvent(CSPlayerEvent.Play)
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -1081,17 +1060,17 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
context?.let { ctx ->
|
||||
//Generate video title
|
||||
val playerVideoTitle = if (headerName != null) {
|
||||
(headerName +
|
||||
if (tvType.isEpisodeBased() && episode != null)
|
||||
if (season == null)
|
||||
" - ${ctx.getString(R.string.episode)} $episode"
|
||||
else
|
||||
" \"${ctx.getString(R.string.season_short)}${season}:${
|
||||
ctx.getString(
|
||||
R.string.episode_short
|
||||
)
|
||||
}${episode}\""
|
||||
else "") + if (subName.isNullOrBlank() || subName == headerName) "" else " - $subName"
|
||||
(headerName + if (tvType.isEpisodeBased() && episode != null) if (season == null) " - ${
|
||||
ctx.getString(
|
||||
R.string.episode
|
||||
)
|
||||
} $episode"
|
||||
else " \"${ctx.getString(R.string.season_short)}${season}:${
|
||||
ctx.getString(
|
||||
R.string.episode_short
|
||||
)
|
||||
}${episode}\""
|
||||
else "") + if (subName.isNullOrBlank() || subName == headerName) "" else " - $subName"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
@ -1131,8 +1110,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
""
|
||||
}
|
||||
|
||||
val source = currentSelectedLink?.first?.name ?: currentSelectedLink?.second?.name
|
||||
?: "NULL"
|
||||
val source = currentSelectedLink?.first?.name ?: currentSelectedLink?.second?.name ?: "NULL"
|
||||
|
||||
player_video_title_rez?.text = when (titleRez) {
|
||||
0 -> ""
|
||||
|
@ -1155,14 +1133,11 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||
): View? {
|
||||
// this is used instead of layout-television to follow the settings and some TV devices are not classified as TV for some reason
|
||||
isTv = isTvSettings()
|
||||
layout =
|
||||
if (isTv) R.layout.fragment_player_tv else R.layout.fragment_player
|
||||
layout = if (isTv) R.layout.fragment_player_tv else R.layout.fragment_player
|
||||
|
||||
viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java]
|
||||
sync = ViewModelProvider(this)[SyncViewModel::class.java]
|
||||
|
@ -1174,6 +1149,68 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
return super.onCreateView(inflater, container, savedInstanceState)
|
||||
}
|
||||
|
||||
var timestampShowState = false
|
||||
|
||||
var skipAnimator: ValueAnimator? = null
|
||||
var skipIndex = 0
|
||||
|
||||
private fun displayTimeStamp(show: Boolean) {
|
||||
if (timestampShowState == show) return
|
||||
skipIndex++
|
||||
println("displayTimeStamp = $show")
|
||||
timestampShowState = show
|
||||
skip_chapter_button?.apply {
|
||||
val showWidth = 170.toPx
|
||||
val noShowWidth = 10.toPx
|
||||
//if((show && width == showWidth) || (!show && width == noShowWidth)) {
|
||||
// return
|
||||
//}
|
||||
val to = if (show) showWidth else noShowWidth
|
||||
val from = if (!show) showWidth else noShowWidth
|
||||
|
||||
skipAnimator?.cancel()
|
||||
isVisible = true
|
||||
|
||||
// just in case
|
||||
val lay = layoutParams
|
||||
lay.width = from
|
||||
layoutParams = lay
|
||||
skipAnimator = ValueAnimator.ofInt(
|
||||
from, to
|
||||
).apply {
|
||||
addListener(onEnd = {
|
||||
if (!show) skip_chapter_button?.isVisible = false
|
||||
})
|
||||
addUpdateListener { valueAnimator ->
|
||||
val value = valueAnimator.animatedValue as Int
|
||||
val layoutParams: ViewGroup.LayoutParams = layoutParams
|
||||
layoutParams.width = value
|
||||
setLayoutParams(layoutParams)
|
||||
}
|
||||
duration = 500
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTimestampSkipped(timestamp: EpisodeSkip.SkipStamp) {
|
||||
displayTimeStamp(false)
|
||||
}
|
||||
|
||||
override fun onTimestamp(timestamp: EpisodeSkip.SkipStamp?) {
|
||||
if (timestamp != null) {
|
||||
skip_chapter_button.setText(timestamp.uiText)
|
||||
displayTimeStamp(true)
|
||||
val currentIndex = skipIndex
|
||||
skip_chapter_button?.handler?.postDelayed({
|
||||
if (skipIndex == currentIndex)
|
||||
displayTimeStamp(false)
|
||||
}, 6000)
|
||||
} else {
|
||||
displayTimeStamp(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
var langFilterList = listOf<String>()
|
||||
|
@ -1189,8 +1226,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
settingsManager.getBoolean(getString(R.string.filter_sub_lang_key), false)
|
||||
if (filterSubByLang) {
|
||||
val langFromPrefMedia = settingsManager.getStringSet(
|
||||
this.getString(R.string.provider_lang_key),
|
||||
mutableSetOf("en")
|
||||
this.getString(R.string.provider_lang_key), mutableSetOf("en")
|
||||
)
|
||||
langFilterList = langFromPrefMedia?.mapNotNull {
|
||||
fromTwoLettersToLanguage(it)?.lowercase() ?: return@mapNotNull null
|
||||
|
@ -1203,7 +1239,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
sync.updateUserData()
|
||||
|
||||
preferredAutoSelectSubtitles = SubtitlesFragment.getAutoSelectLanguageISO639_1()
|
||||
preferredAutoSelectSubtitles = getAutoSelectLanguageISO639_1()
|
||||
|
||||
if (currentSelectedLink == null) {
|
||||
viewModel.loadLinks()
|
||||
|
@ -1218,6 +1254,10 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
activity?.popCurrentPage()
|
||||
}
|
||||
|
||||
observe(viewModel.currentStamps) { stamps ->
|
||||
player.addTimeStamps(stamps)
|
||||
}
|
||||
|
||||
observe(viewModel.loadingLinks) {
|
||||
when (it) {
|
||||
is Resource.Loading -> {
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player
|
|||
|
||||
import android.content.Context
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
||||
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||
|
||||
|
@ -12,9 +13,9 @@ enum class PlayerEventType(val value: Int) {
|
|||
SeekForward(2),
|
||||
SeekBack(3),
|
||||
|
||||
//SkipCurrentChapter(4),
|
||||
SkipCurrentChapter(4),
|
||||
NextEpisode(5),
|
||||
PrevEpisode(5),
|
||||
PrevEpisode(6),
|
||||
PlayPauseToggle(7),
|
||||
ToggleMute(8),
|
||||
Lock(9),
|
||||
|
@ -32,7 +33,7 @@ enum class CSPlayerEvent(val value: Int) {
|
|||
SeekForward(2),
|
||||
SeekBack(3),
|
||||
|
||||
//SkipCurrentChapter(4),
|
||||
SkipCurrentChapter(4),
|
||||
NextEpisode(5),
|
||||
PrevEpisode(6),
|
||||
PlayPauseToggle(7),
|
||||
|
@ -54,7 +55,8 @@ interface Track {
|
|||
**/
|
||||
val id: String?
|
||||
val label: String?
|
||||
// val isCurrentlyPlaying: Boolean
|
||||
|
||||
// val isCurrentlyPlaying: Boolean
|
||||
val language: String?
|
||||
}
|
||||
|
||||
|
@ -124,6 +126,8 @@ interface IPlayer {
|
|||
subtitlesUpdates: (() -> Unit)? = null, // callback from player to inform that subtitles have updated in some way
|
||||
embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)? = null, // callback from player to give all embedded subtitles
|
||||
onTracksInfoChanged: (() -> Unit)? = null, // Callback when tracks are changed, used for UI changes
|
||||
onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)? = null, // Callback when timestamps appear, null when it should disappear
|
||||
onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)? = null, // callback for when a chapter is skipped, aka when event is handled (or for future use when skip automatically ads/sponsor)
|
||||
)
|
||||
|
||||
fun releaseCallbacks()
|
||||
|
@ -131,6 +135,8 @@ interface IPlayer {
|
|||
fun updateSubtitleStyle(style: SaveCaptionStyle)
|
||||
fun saveData()
|
||||
|
||||
fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>)
|
||||
|
||||
fun loadPlayer(
|
||||
context: Context,
|
||||
sameEpisode: Boolean,
|
||||
|
|
|
@ -9,10 +9,12 @@ import com.lagradost.cloudstream3.mvvm.Resource
|
|||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PlayerGeneratorViewModel : ViewModel() {
|
||||
companion object {
|
||||
|
@ -30,6 +32,9 @@ class PlayerGeneratorViewModel : ViewModel() {
|
|||
private val _loadingLinks = MutableLiveData<Resource<Boolean?>>()
|
||||
val loadingLinks: LiveData<Resource<Boolean?>> = _loadingLinks
|
||||
|
||||
private val _currentStamps = MutableLiveData<List<EpisodeSkip.SkipStamp>>(emptyList())
|
||||
val currentStamps: LiveData<List<EpisodeSkip.SkipStamp>> = _currentStamps
|
||||
|
||||
fun getId(): Int? {
|
||||
return generator?.getCurrentId()
|
||||
}
|
||||
|
@ -113,10 +118,31 @@ class PlayerGeneratorViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
private var currentJob: Job? = null
|
||||
private var currentStampJob: Job? = null
|
||||
|
||||
fun loadStamps(duration: Long) {
|
||||
//currentStampJob?.cancel()
|
||||
currentStampJob = ioSafe {
|
||||
val meta = generator?.getCurrent()
|
||||
val page = (generator as? RepoLinkGenerator?)?.page
|
||||
if (page != null && meta is ResultEpisode) {
|
||||
_currentStamps.postValue(listOf())
|
||||
_currentStamps.postValue(
|
||||
EpisodeSkip.getStamps(
|
||||
page,
|
||||
meta,
|
||||
duration,
|
||||
hasNextEpisode() ?: false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadLinks(clearCache: Boolean = false, isCasting: Boolean = false) {
|
||||
Log.i(TAG, "loadLinks")
|
||||
currentJob?.cancel()
|
||||
|
||||
currentJob = viewModelScope.launchSafe {
|
||||
val currentLinks = mutableSetOf<Pair<ExtractorLink?, ExtractorUri?>>()
|
||||
val currentSubs = mutableSetOf<SubtitleData>()
|
||||
|
@ -142,5 +168,6 @@ class PlayerGeneratorViewModel : ViewModel() {
|
|||
_currentLinks.postValue(currentLinks)
|
||||
_currentSubs.postValue(currentSubs)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player
|
|||
|
||||
import android.util.Log
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
|
@ -11,7 +12,8 @@ import kotlin.math.min
|
|||
|
||||
class RepoLinkGenerator(
|
||||
private val episodes: List<ResultEpisode>,
|
||||
private var currentIndex: Int = 0
|
||||
private var currentIndex: Int = 0,
|
||||
val page: LoadResponse? = null,
|
||||
) : IGenerator {
|
||||
companion object {
|
||||
const val TAG = "RepoLink"
|
||||
|
|
|
@ -416,7 +416,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
return this?.firstOrNull { it.season == season }
|
||||
}
|
||||
|
||||
fun updateWatchStatus(currentResponse : LoadResponse, status: WatchType) {
|
||||
fun updateWatchStatus(currentResponse: LoadResponse, status: WatchType) {
|
||||
val currentId = currentResponse.getId()
|
||||
val resultPage = currentResponse
|
||||
|
||||
|
@ -793,7 +793,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
|
||||
fun updateWatchStatus(status: WatchType) {
|
||||
updateWatchStatus(currentResponse ?: return,status)
|
||||
updateWatchStatus(currentResponse ?: return, status)
|
||||
_watchStatus.postValue(status)
|
||||
}
|
||||
|
||||
|
@ -1681,10 +1681,10 @@ class ResultViewModel2 : ViewModel() {
|
|||
preferDubStatus = indexer.dubStatus
|
||||
|
||||
generator = if (isMovie) {
|
||||
getMovie()?.let { RepoLinkGenerator(listOf(it)) }
|
||||
getMovie()?.let { RepoLinkGenerator(listOf(it), page = currentResponse) }
|
||||
} else {
|
||||
episodes?.let { list ->
|
||||
RepoLinkGenerator(list)
|
||||
RepoLinkGenerator(list, page = currentResponse)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
140
app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt
Normal file
140
app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt
Normal file
|
@ -0,0 +1,140 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import java.lang.Long.min
|
||||
|
||||
object EpisodeSkip {
|
||||
private const val TAG = "EpisodeSkip"
|
||||
|
||||
enum class SkipType(@StringRes name: Int) {
|
||||
Opening(R.string.skip_type_op),
|
||||
Ending(R.string.skip_type_ed),
|
||||
Recap(R.string.skip_type_recap),
|
||||
MixedOpening(R.string.skip_type_mixed_op),
|
||||
MixedEnding(R.string.skip_type_mixed_ed),
|
||||
Credits(R.string.skip_type_creddits),
|
||||
Intro(R.string.skip_type_creddits),
|
||||
}
|
||||
|
||||
data class SkipStamp(
|
||||
val type: SkipType,
|
||||
val skipToNextEpisode: Boolean,
|
||||
val startMs: Long,
|
||||
val endMs: Long,
|
||||
) {
|
||||
val uiText = if (skipToNextEpisode) txt(R.string.next_episode) else txt(
|
||||
R.string.skip_type_format,
|
||||
txt(type.name)
|
||||
)
|
||||
}
|
||||
|
||||
private val cachedStamps = HashMap<Int, List<SkipStamp>>()
|
||||
|
||||
private fun shouldSkipToNextEpisode(endMs: Long, episodeDurationMs: Long): Boolean {
|
||||
return episodeDurationMs - endMs < 20_000L // some might have outro that we don't care about tbh
|
||||
}
|
||||
|
||||
suspend fun getStamps(
|
||||
data: LoadResponse,
|
||||
episode: ResultEpisode,
|
||||
episodeDurationMs: Long,
|
||||
hasNextEpisode: Boolean,
|
||||
): List<SkipStamp> {
|
||||
cachedStamps[episode.id]?.let { list ->
|
||||
return list
|
||||
}
|
||||
|
||||
val out = mutableListOf<SkipStamp>()
|
||||
Log.i(TAG, "Requesting SkipStamp from ${data.syncData}")
|
||||
|
||||
if (data is AnimeLoadResponse && (data.type == TvType.Anime || data.type == TvType.OVA)) {
|
||||
data.getMalId()?.toIntOrNull()?.let { malId ->
|
||||
val (resultLength, stamps) = AniSkip.getResult(
|
||||
malId,
|
||||
episode.episode,
|
||||
episodeDurationMs
|
||||
) ?: return@let null
|
||||
// because it also returns an expected episode length we use that just in case it is mismatched with like 2s next episode will still work
|
||||
val dur = min(episodeDurationMs, resultLength)
|
||||
stamps.mapNotNull { stamp ->
|
||||
val skipType = when (stamp.skipType) {
|
||||
"op" -> SkipType.Opening
|
||||
"ed" -> SkipType.Ending
|
||||
"recap" -> SkipType.Recap
|
||||
"mixed-ed" -> SkipType.MixedEnding
|
||||
"mixed-op" -> SkipType.MixedOpening
|
||||
else -> null
|
||||
} ?: return@mapNotNull null
|
||||
val end = (stamp.interval.endTime * 1000.0).toLong()
|
||||
val start = (stamp.interval.startTime * 1000.0).toLong()
|
||||
SkipStamp(
|
||||
type = skipType,
|
||||
skipToNextEpisode = hasNextEpisode && shouldSkipToNextEpisode(
|
||||
end,
|
||||
dur
|
||||
),
|
||||
startMs = start,
|
||||
endMs = end
|
||||
)
|
||||
}?.let { list ->
|
||||
out.addAll(list)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out.isNotEmpty())
|
||||
cachedStamps[episode.id] = out
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
// taken from https://github.com/saikou-app/saikou/blob/3803f8a7a59b826ca193664d46af3a22bbc989f7/app/src/main/java/ani/saikou/others/AniSkip.kt
|
||||
// the following is GPLv3 code https://github.com/saikou-app/saikou/blob/main/LICENSE.md
|
||||
object AniSkip {
|
||||
private const val TAG = "AniSkip"
|
||||
suspend fun getResult(
|
||||
malId: Int,
|
||||
episodeNumber: Int,
|
||||
episodeLength: Long
|
||||
): Pair<Long, List<Stamp>>? {
|
||||
return try {
|
||||
val url =
|
||||
"https://api.aniskip.com/v2/skip-times/$malId/$episodeNumber?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=${episodeLength / 1000L}"
|
||||
Log.i(TAG, "Requesting $url")
|
||||
|
||||
val a = app.get(url)
|
||||
val res = a.parsed<AniSkipResponse>()
|
||||
Log.i(TAG, "Found ${res.found} with ${res.results?.size} results")
|
||||
if (res.found && !res.results.isNullOrEmpty()) (res.results[0].episodeLength * 1000).toLong() to res.results else null
|
||||
} catch (t: Throwable) {
|
||||
Log.i(TAG, "error = ${t.message}")
|
||||
logError(t)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
data class AniSkipResponse(
|
||||
@JsonSerialize val found: Boolean,
|
||||
@JsonSerialize val results: List<Stamp>?,
|
||||
@JsonSerialize val message: String?,
|
||||
@JsonSerialize val statusCode: Int
|
||||
)
|
||||
|
||||
data class Stamp(
|
||||
@JsonSerialize val interval: AniSkipInterval,
|
||||
@JsonSerialize val skipType: String,
|
||||
@JsonSerialize val skipId: String,
|
||||
@JsonSerialize val episodeLength: Double
|
||||
)
|
||||
|
||||
data class AniSkipInterval(
|
||||
@JsonSerialize val startTime: Double,
|
||||
@JsonSerialize val endTime: Double
|
||||
)
|
||||
}
|
|
@ -1,130 +1,130 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:id="@+id/player_background"
|
||||
app:backgroundTint="@android:color/black"
|
||||
android:background="@android:color/black"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
app:surface_type="texture_view">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/player_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black"
|
||||
android:orientation="horizontal"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
app:backgroundTint="@android:color/black"
|
||||
app:surface_type="texture_view">
|
||||
<!--
|
||||
app:fastforward_increment="10000"
|
||||
app:rewind_increment="10000"-->
|
||||
<com.google.android.exoplayer2.ui.PlayerView
|
||||
android:id="@+id/player_view"
|
||||
app:show_timeout="0"
|
||||
app:hide_on_touch="false"
|
||||
app:auto_show="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:backgroundTint="@android:color/black"
|
||||
android:background="@android:color/black"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:controller_layout_id="@layout/player_custom_layout" />
|
||||
android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black"
|
||||
app:auto_show="true"
|
||||
app:backgroundTint="@android:color/black"
|
||||
app:controller_layout_id="@layout/player_custom_layout"
|
||||
app:hide_on_touch="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:show_timeout="0" />
|
||||
|
||||
<FrameLayout
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/player_loading_overlay"
|
||||
android:background="@android:color/black"
|
||||
android:backgroundTint="@android:color/black">
|
||||
android:id="@+id/player_loading_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black"
|
||||
android:backgroundTint="@android:color/black"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
tools:visibility="visible"
|
||||
android:visibility="gone"
|
||||
android:layout_marginTop="70dp"
|
||||
android:layout_gravity="center"
|
||||
app:cornerRadius="4dp"
|
||||
android:id="@+id/overlay_loading_skip_button"
|
||||
android:text="@string/skip_loading"
|
||||
android:id="@+id/overlay_loading_skip_button"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="45dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="70dp"
|
||||
android:backgroundTint="@color/transparent"
|
||||
|
||||
app:rippleColor="?attr/colorPrimary"
|
||||
android:textColor="?attr/textColor"
|
||||
app:iconTint="?attr/textColor"
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_baseline_skip_next_24"
|
||||
android:backgroundTint="@color/transparent"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="45dp" />
|
||||
android:text="@string/skip_loading"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="?attr/textColor"
|
||||
android:visibility="gone"
|
||||
app:cornerRadius="4dp"
|
||||
app:icon="@drawable/ic_baseline_skip_next_24"
|
||||
app:iconTint="?attr/textColor"
|
||||
app:rippleColor="?attr/colorPrimary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_gravity="center"
|
||||
android:id="@+id/main_load" />
|
||||
android:id="@+id/main_load"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/video_go_back_holder_holder"
|
||||
android:layout_margin="5dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
android:id="@+id/video_go_back_holder_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_baseline_arrow_back_24"
|
||||
app:tint="@android:color/white"
|
||||
android:contentDescription="@string/go_back_img_des" />
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@string/go_back_img_des"
|
||||
android:src="@drawable/ic_baseline_arrow_back_24"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/player_loading_go_back"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="70dp"
|
||||
android:layout_gravity="center"
|
||||
android:focusable="true"
|
||||
android:clickable="true"
|
||||
android:background="@drawable/video_tap_button_always_white"
|
||||
android:contentDescription="@string/go_back_img_des" />
|
||||
android:id="@+id/player_loading_go_back"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="70dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/video_tap_button_always_white"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/go_back_img_des"
|
||||
android:focusable="true" />
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:visibility="gone"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:id="@+id/player_torrent_info"
|
||||
android:id="@+id/player_torrent_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_torrent_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="15dp"
|
||||
android:gravity="start"
|
||||
android:textColor="@color/white"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="78% at 18kb/s" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:gravity="start"
|
||||
android:layout_marginTop="15dp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/white"
|
||||
android:id="@+id/video_torrent_progress"
|
||||
tools:text="78% at 18kb/s" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:gravity="start"
|
||||
android:layout_marginTop="0dp"
|
||||
android:textColor="@color/white"
|
||||
android:id="@+id/video_torrent_seeders"
|
||||
tools:text="17 seeders"
|
||||
app:layout_constraintTop_toBottomOf="@+id/player_video_title" />
|
||||
android:id="@+id/video_torrent_seeders"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="0dp"
|
||||
android:gravity="start"
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/player_video_title"
|
||||
tools:text="17 seeders" />
|
||||
</FrameLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -318,6 +318,25 @@
|
|||
</FrameLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
tools:visibility="visible"
|
||||
android:id="@+id/skip_chapter_button"
|
||||
style="@style/NiceButton"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginEnd="100dp"
|
||||
android:visibility="gone"
|
||||
android:maxLines="1"
|
||||
android:backgroundTint="@color/skipOpTransparent"
|
||||
android:padding="10dp"
|
||||
android:textColor="@color/white"
|
||||
app:cornerRadius="@dimen/rounded_button_radius"
|
||||
app:layout_constraintBottom_toTopOf="@+id/bottom_player_bar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:strokeColor="@color/white"
|
||||
app:strokeWidth="1dp"
|
||||
tools:text="Skip Opening" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
|
||||
<color name="black_overlay">#66000000</color>
|
||||
<color name="darkBarTransparent">#C0121212</color>
|
||||
<color name="skipOpTransparent">#4D121212</color>
|
||||
<color name="darkBar">#121212</color>
|
||||
<color name="videoProgress">#66B5B5B5</color> <!--66B5B5B5-->
|
||||
<!--<color name="videoCache">#663D50FA</color>--> <!--66B5B5B5-->
|
||||
|
|
|
@ -638,4 +638,13 @@
|
|||
<string name="player_settings_play_in_browser">Browser</string>
|
||||
<string name="app_not_found_error">App not found</string>
|
||||
<string name="all_languages_preference">All Languages</string>
|
||||
|
||||
<string name="skip_type_format" formatted="true">Skip %s</string>
|
||||
<string name="skip_type_op">Opening</string>
|
||||
<string name="skip_type_ed">Ending</string>
|
||||
<string name="skip_type_recap">Recap</string>
|
||||
<string name="skip_type_mixed_ed">Mixed ending</string>
|
||||
<string name="skip_type_mixed_op">Mixed opening</string>
|
||||
<string name="skip_type_creddits">Credits</string>
|
||||
<string name="skip_type_intro">Intro</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue