* aniskip groundwork

* working

* removed prints

* bump nicehttp

* small fix

* small fixes

* bump

Co-authored-by: reduplicated <110570621+reduplicated@users.noreply.github.com>
This commit is contained in:
Osten 2022-11-04 23:36:27 +00:00 committed by GitHub
parent f84259f898
commit c10ec34ab8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 543 additions and 221 deletions

View file

@ -48,7 +48,7 @@ android {
targetSdk = 30 targetSdk = 30
versionCode = 54 versionCode = 54
versionName = "3.2.1" versionName = "3.2.2"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
@ -190,7 +190,7 @@ dependencies {
// Networking // Networking
// implementation("com.squareup.okhttp3:okhttp:4.9.2") // implementation("com.squareup.okhttp3:okhttp:4.9.2")
// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") // 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 🙏 // Util to skip the URI file fuckery 🙏
implementation("com.github.tachiyomiorg:unifile:17bec43") implementation("com.github.tachiyomiorg:unifile:17bec43")

View file

@ -337,6 +337,9 @@ object CommonActivity {
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> { KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
PlayerEventType.SkipOp 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 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 PlayerEventType.PlayPauseToggle
} }

View file

@ -88,6 +88,9 @@ import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_result_swipe.* import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import okhttp3.ConnectionSpec
import okhttp3.OkHttpClient
import okhttp3.internal.applyConnectionSpec
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import java.nio.charset.Charset import java.nio.charset.Charset

View file

@ -12,7 +12,6 @@ import okhttp3.Headers.Companion.toHeaders
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.io.File import java.io.File
fun Requests.initClient(context: Context): OkHttpClient { fun Requests.initClient(context: Context): OkHttpClient {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
val dns = settingsManager.getInt(context.getString(R.string.dns_pref), 0) val dns = settingsManager.getInt(context.getString(R.string.dns_pref), 0)

View file

@ -38,6 +38,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus 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
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
@ -103,6 +104,14 @@ abstract class AbstractPlayerFragment(
throw NotImplementedError() throw NotImplementedError()
} }
open fun onTimestamp(timestamp: EpisodeSkip.SkipStamp?) {
}
open fun onTimestampSkipped(timestamp: EpisodeSkip.SkipStamp) {
}
open fun exitedPipMode() { open fun exitedPipMode() {
throw NotImplementedError() throw NotImplementedError()
} }
@ -373,7 +382,9 @@ abstract class AbstractPlayerFragment(
), ),
subtitlesUpdates = ::subtitlesChanged, subtitlesUpdates = ::subtitlesChanged,
embeddedSubtitlesFetched = ::embeddedSubtitlesFetched, embeddedSubtitlesFetched = ::embeddedSubtitlesFetched,
onTracksInfoChanged = ::onTracksInfoChanged onTracksInfoChanged = ::onTracksInfoChanged,
onTimestampInvoked = ::onTimestamp,
onTimestampSkipped = ::onTimestampSkipped
) )
if (player is CS3IPlayer) { if (player is CS3IPlayer) {

View file

@ -18,7 +18,10 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.trackselection.TrackSelectionOverride import com.google.android.exoplayer2.trackselection.TrackSelectionOverride
import com.google.android.exoplayer2.trackselection.TrackSelector import com.google.android.exoplayer2.trackselection.TrackSelector
import com.google.android.exoplayer2.ui.SubtitleView 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.CacheDataSource
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache 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.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.EpisodeSkip
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.ExtractorUri
@ -113,6 +117,8 @@ class CS3IPlayer : IPlayer {
private var playerUpdated: ((Any?) -> Unit)? = null private var playerUpdated: ((Any?) -> Unit)? = null
private var embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)? = null private var embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)? = null
private var onTracksInfoChanged: (() -> Unit)? = null private var onTracksInfoChanged: (() -> Unit)? = null
private var onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)? = null
private var onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)? = null
override fun releaseCallbacks() { override fun releaseCallbacks() {
playerUpdated = null playerUpdated = null
@ -126,7 +132,9 @@ class CS3IPlayer : IPlayer {
prevEpisode = null prevEpisode = null
subtitlesUpdates = null subtitlesUpdates = null
onTracksInfoChanged = null onTracksInfoChanged = null
onTimestampInvoked = null
requestSubtitleUpdate = null requestSubtitleUpdate = null
onTimestampSkipped = null
} }
override fun initCallbacks( override fun initCallbacks(
@ -142,6 +150,8 @@ class CS3IPlayer : IPlayer {
subtitlesUpdates: (() -> Unit)?, subtitlesUpdates: (() -> Unit)?,
embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)?, embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)?,
onTracksInfoChanged: (() -> Unit)?, onTracksInfoChanged: (() -> Unit)?,
onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)?,
onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)?,
) { ) {
this.playerUpdated = playerUpdated this.playerUpdated = playerUpdated
this.updateIsPlaying = updateIsPlaying this.updateIsPlaying = updateIsPlaying
@ -155,6 +165,8 @@ class CS3IPlayer : IPlayer {
this.subtitlesUpdates = subtitlesUpdates this.subtitlesUpdates = subtitlesUpdates
this.embeddedSubtitlesFetched = embeddedSubtitlesFetched this.embeddedSubtitlesFetched = embeddedSubtitlesFetched
this.onTracksInfoChanged = onTracksInfoChanged this.onTracksInfoChanged = onTracksInfoChanged
this.onTimestampInvoked = onTimestampInvoked
this.onTimestampSkipped = onTimestampSkipped
} }
// I know, this is not a perfect solution, however it works for fixing subs // I know, this is not a perfect solution, however it works for fixing subs
@ -719,7 +731,7 @@ class CS3IPlayer : IPlayer {
source source
} }
println("PLAYBACK POS $playbackPosition") //println("PLAYBACK POS $playbackPosition")
return exoPlayerBuilder.build().apply { return exoPlayerBuilder.build().apply {
setPlayWhenReady(playWhenReady) setPlayWhenReady(playWhenReady)
seekTo(currentWindow, playbackPosition) seekTo(currentWindow, playbackPosition)
@ -735,8 +747,22 @@ class CS3IPlayer : IPlayer {
} }
} }
fun updatedTime() { private fun getCurrentTimestamp(writePosition : Long? = null): EpisodeSkip.SkipStamp? {
val position = exoPlayer?.currentPosition 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 val duration = exoPlayer?.contentDuration
if (duration != null && position != null) { if (duration != null && position != null) {
playerPositionChanged?.invoke(Pair(position, duration)) playerPositionChanged?.invoke(Pair(position, duration))
@ -748,12 +774,12 @@ class CS3IPlayer : IPlayer {
} }
override fun seekTo(time: Long) { override fun seekTo(time: Long) {
updatedTime() updatedTime(time)
exoPlayer?.seekTo(time) exoPlayer?.seekTo(time)
} }
private fun ExoPlayer.seekTime(time: Long) { private fun ExoPlayer.seekTime(time: Long) {
updatedTime() updatedTime(currentPosition + time)
seekTo(currentPosition + time) seekTo(currentPosition + time)
} }
@ -789,6 +815,17 @@ class CS3IPlayer : IPlayer {
CSPlayerEvent.SeekBack -> seekTime(-seekActionTime) CSPlayerEvent.SeekBack -> seekTime(-seekActionTime)
CSPlayerEvent.NextEpisode -> nextEpisode?.invoke() CSPlayerEvent.NextEpisode -> nextEpisode?.invoke()
CSPlayerEvent.PrevEpisode -> prevEpisode?.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) { } 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() { fun onRenderFirst() {
if (!hasUsedFirstRender) { // this insures that we only call this once per player load if (!hasUsedFirstRender) { // this insures that we only call this once per player load
Log.i(TAG, "Rendered first frame") Log.i(TAG, "Rendered first frame")

View file

@ -1141,6 +1141,9 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
PlayerEventType.Play -> { PlayerEventType.Play -> {
player.handleEvent(CSPlayerEvent.Play) player.handleEvent(CSPlayerEvent.Play)
} }
PlayerEventType.SkipCurrentChapter -> {
player.handleEvent(CSPlayerEvent.SkipCurrentChapter)
}
PlayerEventType.Resize -> { PlayerEventType.Resize -> {
nextResize() nextResize()
} }
@ -1254,6 +1257,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
player.handleEvent(CSPlayerEvent.PlayPauseToggle) player.handleEvent(CSPlayerEvent.PlayPauseToggle)
} }
skip_chapter_button?.setOnClickListener {
player.handleEvent(CSPlayerEvent.SkipCurrentChapter)
}
// init clicks // init clicks
player_resize_btt?.setOnClickListener { player_resize_btt?.setOnClickListener {
autoHide() autoHide()

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.ui.player package com.lagradost.cloudstream3.ui.player
import android.animation.ValueAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
@ -13,6 +14,7 @@ import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.animation.addListener
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible 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.ResultEpisode
import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.result.SyncViewModel 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.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.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe 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.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage 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.*
import kotlinx.android.synthetic.main.dialog_online_subtitles.apply_btt import kotlinx.android.synthetic.main.dialog_online_subtitles.apply_btt
import kotlinx.android.synthetic.main.dialog_online_subtitles.cancel_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_source_and_subs.subtitles_click_settings
import kotlinx.android.synthetic.main.player_select_tracks.* import kotlinx.android.synthetic.main.player_select_tracks.*
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
class GeneratorPlayer : FullScreenPlayer() { class GeneratorPlayer : FullScreenPlayer() {
companion object { companion object {
@ -67,8 +69,7 @@ class GeneratorPlayer : FullScreenPlayer() {
Log.i(TAG, "newInstance = $syncData") Log.i(TAG, "newInstance = $syncData")
lastUsedGenerator = generator lastUsedGenerator = generator
return Bundle().apply { return Bundle().apply {
if (syncData != null) if (syncData != null) putSerializable("syncData", syncData)
putSerializable("syncData", syncData)
} }
} }
@ -165,6 +166,7 @@ class GeneratorPlayer : FullScreenPlayer() {
isActive = true isActive = true
setPlayerDimen(null) setPlayerDimen(null)
setTitle() setTitle()
hasRequestedStamps = false
loadExtractorJob(link.first) loadExtractorJob(link.first)
// load player // load player
@ -180,12 +182,11 @@ class GeneratorPlayer : FullScreenPlayer() {
}, },
currentSubs, currentSubs,
(if (sameEpisode) currentSelectedSubtitles else null) ?: getAutoSelectSubtitle( (if (sameEpisode) currentSelectedSubtitles else null) ?: getAutoSelectSubtitle(
currentSubs, currentSubs, settings = true, downloads = true
settings = true,
downloads = true
), ),
) )
} }
player.addTimeStamps(listOf()) // clear stamps
} }
private fun sortLinks(useQualitySettings: Boolean = true): List<Pair<ExtractorLink?, ExtractorUri?>> { private fun sortLinks(useQualitySettings: Boolean = true): List<Pair<ExtractorLink?, ExtractorUri?>> {
@ -231,9 +232,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
override fun openOnlineSubPicker( override fun openOnlineSubPicker(
context: Context, context: Context, imdbId: Long?, dismissCallback: (() -> Unit)
imdbId: Long?,
dismissCallback: (() -> Unit)
) { ) {
val providers = subsProviders val providers = subsProviders
val isSingleProvider = subsProviders.size == 1 val isSingleProvider = subsProviders.size == 1
@ -256,8 +255,7 @@ class GeneratorPlayer : FullScreenPlayer() {
val arrayAdapter = val arrayAdapter =
object : ArrayAdapter<AbstractSubtitleEntities.SubtitleEntity>(dialog.context, layout) { object : ArrayAdapter<AbstractSubtitleEntities.SubtitleEntity>(dialog.context, layout) {
fun setHearingImpairedIcon( fun setHearingImpairedIcon(
imageViewEnd: ImageView?, imageViewEnd: ImageView?, position: Int
position: Int
) { ) {
if (imageViewEnd == null) return if (imageViewEnd == null) return
val isHearingImpaired = val isHearingImpaired =
@ -265,13 +263,11 @@ class GeneratorPlayer : FullScreenPlayer() {
val drawableEnd = if (isHearingImpaired) { val drawableEnd = if (isHearingImpaired) {
ContextCompat.getDrawable( ContextCompat.getDrawable(
context, context, R.drawable.ic_baseline_hearing_24
R.drawable.ic_baseline_hearing_24
)?.apply { )?.apply {
setTint( setTint(
ContextCompat.getColor( ContextCompat.getColor(
context, context, R.color.textColor
R.color.textColor
) )
) )
} }
@ -281,8 +277,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(context) val view = convertView ?: LayoutInflater.from(context).inflate(layout, null)
.inflate(layout, null)
val item = getItem(position) val item = getItem(position)
@ -337,13 +332,12 @@ class GeneratorPlayer : FullScreenPlayer() {
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean {
dialog.search_loading_bar?.show() dialog.search_loading_bar?.show()
ioSafe { ioSafe {
val search = AbstractSubtitleEntities.SubtitleSearch( val search =
query = query ?: return@ioSafe, AbstractSubtitleEntities.SubtitleSearch(query = query ?: return@ioSafe,
imdb = imdbId, imdb = imdbId,
epNumber = currentTempMeta.episode, epNumber = currentTempMeta.episode,
seasonNumber = currentTempMeta.season, seasonNumber = currentTempMeta.season,
lang = currentLanguageTwoLetters.ifBlank { null } lang = currentLanguageTwoLetters.ifBlank { null })
)
val results = providers.amap { val results = providers.amap {
try { try {
it.search(search) it.search(search)
@ -379,14 +373,12 @@ class GeneratorPlayer : FullScreenPlayer() {
dialog.search_filter.setOnClickListener { view -> dialog.search_filter.setOnClickListener { view ->
val lang639_1 = languages.map { it.ISO_639_1 } val lang639_1 = languages.map { it.ISO_639_1 }
activity?.showDialog( activity?.showDialog(languages.map { it.languageName },
languages.map { it.languageName },
lang639_1.indexOf(currentLanguageTwoLetters), lang639_1.indexOf(currentLanguageTwoLetters),
view?.context?.getString(R.string.subs_subtitle_languages) view?.context?.getString(R.string.subs_subtitle_languages)
?: return@setOnClickListener, ?: return@setOnClickListener,
true, true,
{ } { }) { index ->
) { index ->
currentLanguageTwoLetters = lang639_1[index] currentLanguageTwoLetters = lang639_1[index]
dialog.subtitles_search.setQuery(dialog.subtitles_search.query, true) dialog.subtitles_search.setQuery(dialog.subtitles_search.query, true)
} }
@ -472,8 +464,8 @@ class GeneratorPlayer : FullScreenPlayer() {
if (uri == null) return@normalSafeApiCall if (uri == null) return@normalSafeApiCall
val ctx = context ?: AcraApplication.context ?: return@normalSafeApiCall val ctx = context ?: AcraApplication.context ?: return@normalSafeApiCall
// RW perms for the path // RW perms for the path
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or val flags =
Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
ctx.contentResolver.takePersistableUriPermission(uri, flags) ctx.contentResolver.takePersistableUriPermission(uri, flags)
@ -536,10 +528,8 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
if (subsProvidersIsActive) { if (subsProvidersIsActive) {
val loadFromOpenSubsFooter: TextView = val loadFromOpenSubsFooter: TextView = layoutInflater.inflate(
layoutInflater.inflate( R.layout.sort_bottom_footer_add_choice, null
R.layout.sort_bottom_footer_add_choice,
null
) as TextView ) as TextView
loadFromOpenSubsFooter.text = loadFromOpenSubsFooter.text =
@ -592,8 +582,7 @@ class GeneratorPlayer : FullScreenPlayer() {
val subtitleIndexStart = currentSubtitles.indexOf(currentSelectedSubtitles) + 1 val subtitleIndexStart = currentSubtitles.indexOf(currentSelectedSubtitles) + 1
var subtitleIndex = subtitleIndexStart var subtitleIndex = subtitleIndexStart
val subsArrayAdapter = val subsArrayAdapter = ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
subsArrayAdapter.add(ctx.getString(R.string.no_subtitles)) subsArrayAdapter.add(ctx.getString(R.string.no_subtitles))
subsArrayAdapter.addAll(currentSubtitles.map { it.name }) subsArrayAdapter.addAll(currentSubtitles.map { it.name })
@ -631,8 +620,7 @@ class GeneratorPlayer : FullScreenPlayer() {
val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values) val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values)
val value = settingsManager.getString( val value = settingsManager.getString(
ctx.getString(R.string.subtitles_encoding_key), ctx.getString(R.string.subtitles_encoding_key), null
null
) )
val index = prefValues.indexOf(value) val index = prefValues.indexOf(value)
text = prefNames[if (index == -1) 0 else index] 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 prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list)
val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values) val prefValues = ctx.resources.getStringArray(R.array.subtitles_encoding_values)
val currentPrefMedia = val currentPrefMedia = settingsManager.getString(
settingsManager.getString( ctx.getString(R.string.subtitles_encoding_key), null
ctx.getString(R.string.subtitles_encoding_key),
null
) )
shouldDismiss = false shouldDismiss = false
sourceDialog.dismissSafe(activity) sourceDialog.dismissSafe(activity)
val index = prefValues.indexOf(currentPrefMedia) val index = prefValues.indexOf(currentPrefMedia)
activity?.showDialog( activity?.showDialog(prefNames.toList(),
prefNames.toList(),
if (index == -1) 0 else index, if (index == -1) 0 else index,
ctx.getString(R.string.subtitles_encoding), ctx.getString(R.string.subtitles_encoding),
true, true,
{}) { {}) {
settingsManager.edit() settingsManager.edit().putString(
.putString( ctx.getString(R.string.subtitles_encoding_key), prefValues[it]
ctx.getString(R.string.subtitles_encoding_key), ).apply()
prefValues[it]
)
.apply()
updateForcedEncoding(ctx) updateForcedEncoding(ctx)
dismiss() dismiss()
@ -878,7 +860,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
var maxEpisodeSet: Int? = null var maxEpisodeSet: Int? = null
var hasRequestedStamps: Boolean = false
override fun playerPositionChanged(posDur: Pair<Long, Long>) { override fun playerPositionChanged(posDur: Pair<Long, Long>) {
// Don't save livestream data // Don't save livestream data
if ((currentMeta as? ResultEpisode)?.tvType?.isLiveStream() == true) return if ((currentMeta as? ResultEpisode)?.tvType?.isLiveStream() == true) return
@ -887,11 +869,16 @@ class GeneratorPlayer : FullScreenPlayer() {
if ((currentMeta as? ResultEpisode)?.tvType == TvType.NSFW) return if ((currentMeta as? ResultEpisode)?.tvType == TvType.NSFW) return
val (position, duration) = posDur 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 { viewModel.getId()?.let {
DataStoreHelper.setViewPos(it, position, duration) DataStoreHelper.setViewPos(it, position, duration)
} }
val percentage = position * 100L / duration val percentage = position * 100L / duration
val nextEp = percentage >= NEXT_WATCH_EPISODE_PERCENTAGE val nextEp = percentage >= NEXT_WATCH_EPISODE_PERCENTAGE
@ -939,17 +926,14 @@ class GeneratorPlayer : FullScreenPlayer() {
context?.let { ctx -> context?.let { ctx ->
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
if (settingsManager.getBoolean( if (settingsManager.getBoolean(
ctx.getString(R.string.episode_sync_enabled_key), ctx.getString(R.string.episode_sync_enabled_key), true
true
) )
) ) maxEpisodeSet = meta.episode
maxEpisodeSet = meta.episode
sync.modifyMaxEpisode(meta.episode) sync.modifyMaxEpisode(meta.episode)
} }
} }
if (meta.tvType.isAnimeOp()) if (meta.tvType.isAnimeOp()) isOpVisible = percentage < SKIP_OP_VIDEO_PERCENTAGE
isOpVisible = percentage < SKIP_OP_VIDEO_PERCENTAGE
} }
} }
player_skip_op?.isVisible = isOpVisible player_skip_op?.isVisible = isOpVisible
@ -961,9 +945,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
private fun getAutoSelectSubtitle( private fun getAutoSelectSubtitle(
subtitles: Set<SubtitleData>, subtitles: Set<SubtitleData>, settings: Boolean, downloads: Boolean
settings: Boolean,
downloads: Boolean
): SubtitleData? { ): SubtitleData? {
val langCode = preferredAutoSelectSubtitles ?: return null val langCode = preferredAutoSelectSubtitles ?: return null
val lang = SubtitleHelper.fromTwoLettersToLanguage(langCode) ?: return null val lang = SubtitleHelper.fromTwoLettersToLanguage(langCode) ?: return null
@ -1009,12 +991,9 @@ class GeneratorPlayer : FullScreenPlayer() {
player.handleEvent(CSPlayerEvent.Play) player.handleEvent(CSPlayerEvent.Play)
return true return true
} }
} else } else if (!langCode.isNullOrEmpty()) {
if (!langCode.isNullOrEmpty()) {
getAutoSelectSubtitle( getAutoSelectSubtitle(
currentSubs, currentSubs, settings = true, downloads = false
settings = true,
downloads = false
)?.let { sub -> )?.let { sub ->
if (setSubtitles(sub)) { if (setSubtitles(sub)) {
@ -1081,12 +1060,12 @@ class GeneratorPlayer : FullScreenPlayer() {
context?.let { ctx -> context?.let { ctx ->
//Generate video title //Generate video title
val playerVideoTitle = if (headerName != null) { val playerVideoTitle = if (headerName != null) {
(headerName + (headerName + if (tvType.isEpisodeBased() && episode != null) if (season == null) " - ${
if (tvType.isEpisodeBased() && episode != null) ctx.getString(
if (season == null) R.string.episode
" - ${ctx.getString(R.string.episode)} $episode" )
else } $episode"
" \"${ctx.getString(R.string.season_short)}${season}:${ else " \"${ctx.getString(R.string.season_short)}${season}:${
ctx.getString( ctx.getString(
R.string.episode_short R.string.episode_short
) )
@ -1131,8 +1110,7 @@ class GeneratorPlayer : FullScreenPlayer() {
"" ""
} }
val source = currentSelectedLink?.first?.name ?: currentSelectedLink?.second?.name val source = currentSelectedLink?.first?.name ?: currentSelectedLink?.second?.name ?: "NULL"
?: "NULL"
player_video_title_rez?.text = when (titleRez) { player_video_title_rez?.text = when (titleRez) {
0 -> "" 0 -> ""
@ -1155,14 +1133,11 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
container: ViewGroup?,
savedInstanceState: Bundle?
): View? { ): View? {
// this is used instead of layout-television to follow the settings and some TV devices are not classified as TV for some reason // 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() isTv = isTvSettings()
layout = layout = if (isTv) R.layout.fragment_player_tv else R.layout.fragment_player
if (isTv) R.layout.fragment_player_tv else R.layout.fragment_player
viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java] viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java]
sync = ViewModelProvider(this)[SyncViewModel::class.java] sync = ViewModelProvider(this)[SyncViewModel::class.java]
@ -1174,6 +1149,68 @@ class GeneratorPlayer : FullScreenPlayer() {
return super.onCreateView(inflater, container, savedInstanceState) 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
var langFilterList = listOf<String>() var langFilterList = listOf<String>()
@ -1189,8 +1226,7 @@ class GeneratorPlayer : FullScreenPlayer() {
settingsManager.getBoolean(getString(R.string.filter_sub_lang_key), false) settingsManager.getBoolean(getString(R.string.filter_sub_lang_key), false)
if (filterSubByLang) { if (filterSubByLang) {
val langFromPrefMedia = settingsManager.getStringSet( val langFromPrefMedia = settingsManager.getStringSet(
this.getString(R.string.provider_lang_key), this.getString(R.string.provider_lang_key), mutableSetOf("en")
mutableSetOf("en")
) )
langFilterList = langFromPrefMedia?.mapNotNull { langFilterList = langFromPrefMedia?.mapNotNull {
fromTwoLettersToLanguage(it)?.lowercase() ?: return@mapNotNull null fromTwoLettersToLanguage(it)?.lowercase() ?: return@mapNotNull null
@ -1203,7 +1239,7 @@ class GeneratorPlayer : FullScreenPlayer() {
sync.updateUserData() sync.updateUserData()
preferredAutoSelectSubtitles = SubtitlesFragment.getAutoSelectLanguageISO639_1() preferredAutoSelectSubtitles = getAutoSelectLanguageISO639_1()
if (currentSelectedLink == null) { if (currentSelectedLink == null) {
viewModel.loadLinks() viewModel.loadLinks()
@ -1218,6 +1254,10 @@ class GeneratorPlayer : FullScreenPlayer() {
activity?.popCurrentPage() activity?.popCurrentPage()
} }
observe(viewModel.currentStamps) { stamps ->
player.addTimeStamps(stamps)
}
observe(viewModel.loadingLinks) { observe(viewModel.loadingLinks) {
when (it) { when (it) {
is Resource.Loading -> { is Resource.Loading -> {

View file

@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player
import android.content.Context import android.content.Context
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.EpisodeSkip
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.ExtractorUri
@ -12,9 +13,9 @@ enum class PlayerEventType(val value: Int) {
SeekForward(2), SeekForward(2),
SeekBack(3), SeekBack(3),
//SkipCurrentChapter(4), SkipCurrentChapter(4),
NextEpisode(5), NextEpisode(5),
PrevEpisode(5), PrevEpisode(6),
PlayPauseToggle(7), PlayPauseToggle(7),
ToggleMute(8), ToggleMute(8),
Lock(9), Lock(9),
@ -32,7 +33,7 @@ enum class CSPlayerEvent(val value: Int) {
SeekForward(2), SeekForward(2),
SeekBack(3), SeekBack(3),
//SkipCurrentChapter(4), SkipCurrentChapter(4),
NextEpisode(5), NextEpisode(5),
PrevEpisode(6), PrevEpisode(6),
PlayPauseToggle(7), PlayPauseToggle(7),
@ -54,6 +55,7 @@ interface Track {
**/ **/
val id: String? val id: String?
val label: String? val label: String?
// val isCurrentlyPlaying: Boolean // val isCurrentlyPlaying: Boolean
val language: String? val language: String?
} }
@ -124,6 +126,8 @@ interface IPlayer {
subtitlesUpdates: (() -> Unit)? = null, // callback from player to inform that subtitles have updated in some way 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 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 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() fun releaseCallbacks()
@ -131,6 +135,8 @@ interface IPlayer {
fun updateSubtitleStyle(style: SaveCaptionStyle) fun updateSubtitleStyle(style: SaveCaptionStyle)
fun saveData() fun saveData()
fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>)
fun loadPlayer( fun loadPlayer(
context: Context, context: Context,
sameEpisode: Boolean, sameEpisode: Boolean,

View file

@ -9,10 +9,12 @@ import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.safeApiCall 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.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.ExtractorUri
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
class PlayerGeneratorViewModel : ViewModel() { class PlayerGeneratorViewModel : ViewModel() {
companion object { companion object {
@ -30,6 +32,9 @@ class PlayerGeneratorViewModel : ViewModel() {
private val _loadingLinks = MutableLiveData<Resource<Boolean?>>() private val _loadingLinks = MutableLiveData<Resource<Boolean?>>()
val loadingLinks: LiveData<Resource<Boolean?>> = _loadingLinks val loadingLinks: LiveData<Resource<Boolean?>> = _loadingLinks
private val _currentStamps = MutableLiveData<List<EpisodeSkip.SkipStamp>>(emptyList())
val currentStamps: LiveData<List<EpisodeSkip.SkipStamp>> = _currentStamps
fun getId(): Int? { fun getId(): Int? {
return generator?.getCurrentId() return generator?.getCurrentId()
} }
@ -113,10 +118,31 @@ class PlayerGeneratorViewModel : ViewModel() {
} }
private var currentJob: Job? = null 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) { fun loadLinks(clearCache: Boolean = false, isCasting: Boolean = false) {
Log.i(TAG, "loadLinks") Log.i(TAG, "loadLinks")
currentJob?.cancel() currentJob?.cancel()
currentJob = viewModelScope.launchSafe { currentJob = viewModelScope.launchSafe {
val currentLinks = mutableSetOf<Pair<ExtractorLink?, ExtractorUri?>>() val currentLinks = mutableSetOf<Pair<ExtractorLink?, ExtractorUri?>>()
val currentSubs = mutableSetOf<SubtitleData>() val currentSubs = mutableSetOf<SubtitleData>()
@ -142,5 +168,6 @@ class PlayerGeneratorViewModel : ViewModel() {
_currentLinks.postValue(currentLinks) _currentLinks.postValue(currentLinks)
_currentSubs.postValue(currentSubs) _currentSubs.postValue(currentSubs)
} }
} }
} }

View file

@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player
import android.util.Log import android.util.Log
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
@ -11,7 +12,8 @@ import kotlin.math.min
class RepoLinkGenerator( class RepoLinkGenerator(
private val episodes: List<ResultEpisode>, private val episodes: List<ResultEpisode>,
private var currentIndex: Int = 0 private var currentIndex: Int = 0,
val page: LoadResponse? = null,
) : IGenerator { ) : IGenerator {
companion object { companion object {
const val TAG = "RepoLink" const val TAG = "RepoLink"

View file

@ -1681,10 +1681,10 @@ class ResultViewModel2 : ViewModel() {
preferDubStatus = indexer.dubStatus preferDubStatus = indexer.dubStatus
generator = if (isMovie) { generator = if (isMovie) {
getMovie()?.let { RepoLinkGenerator(listOf(it)) } getMovie()?.let { RepoLinkGenerator(listOf(it), page = currentResponse) }
} else { } else {
episodes?.let { list -> episodes?.let { list ->
RepoLinkGenerator(list) RepoLinkGenerator(list, page = currentResponse)
} }
} }

View 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
)
}

View file

@ -2,129 +2,129 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout 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" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/player_background"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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:background="@android:color/black"
android:orientation="horizontal"
android:screenOrientation="sensorLandscape" android:screenOrientation="sensorLandscape"
app:backgroundTint="@android:color/black"
app:surface_type="texture_view"> app:surface_type="texture_view">
<!-- <!--
app:fastforward_increment="10000" app:fastforward_increment="10000"
app:rewind_increment="10000"--> app:rewind_increment="10000"-->
<com.google.android.exoplayer2.ui.PlayerView <com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view" 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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:backgroundTint="@android:color/black"
android:background="@android:color/black" 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_constraintBottom_toBottomOf="parent"
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:controller_layout_id="@layout/player_custom_layout" /> app:show_timeout="0" />
<FrameLayout <FrameLayout
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_constraintBottom_toBottomOf="parent"
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">
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">
<com.google.android.material.button.MaterialButton <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:id="@+id/overlay_loading_skip_button"
android:text="@string/skip_loading"
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" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="45dp" /> android:layout_height="45dp"
android:layout_gravity="center"
android:layout_marginTop="70dp"
android:backgroundTint="@color/transparent"
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 <ProgressBar
android:id="@+id/main_load"
android:layout_width="50dp" android:layout_width="50dp"
android:layout_height="50dp" android:layout_height="50dp"
android:layout_gravity="center" android:layout_gravity="center" />
android:id="@+id/main_load" />
<FrameLayout <FrameLayout
android:id="@+id/video_go_back_holder_holder" android:id="@+id/video_go_back_holder_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp" android:layout_margin="5dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent">
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView <ImageView
android:layout_width="30dp" android:layout_width="30dp"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_gravity="center" android:layout_gravity="center"
android:contentDescription="@string/go_back_img_des"
android:src="@drawable/ic_baseline_arrow_back_24" android:src="@drawable/ic_baseline_arrow_back_24"
app:tint="@android:color/white" app:tint="@android:color/white" />
android:contentDescription="@string/go_back_img_des" />
<ImageView <ImageView
android:id="@+id/player_loading_go_back" android:id="@+id/player_loading_go_back"
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:focusable="true"
android:clickable="true"
android:background="@drawable/video_tap_button_always_white" android:background="@drawable/video_tap_button_always_white"
android:contentDescription="@string/go_back_img_des" /> android:clickable="true"
android:contentDescription="@string/go_back_img_des"
android:focusable="true" />
</FrameLayout> </FrameLayout>
</FrameLayout> </FrameLayout>
<FrameLayout <FrameLayout
android:visibility="gone" android:id="@+id/player_torrent_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp" android:paddingStart="20dp"
android:paddingEnd="20dp" android:paddingEnd="20dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
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">
android:id="@+id/player_torrent_info"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView <TextView
android:id="@+id/video_torrent_progress"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:gravity="start"
android:layout_marginTop="15dp" android:layout_marginTop="15dp"
android:textStyle="bold" android:gravity="start"
android:textColor="@color/white" android:textColor="@color/white"
android:id="@+id/video_torrent_progress" android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="78% at 18kb/s" /> tools:text="78% at 18kb/s" />
<TextView <TextView
android:id="@+id/video_torrent_seeders"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
android:gravity="start"
android:layout_marginTop="0dp" android:layout_marginTop="0dp"
android:gravity="start"
android:textColor="@color/white" android:textColor="@color/white"
android:id="@+id/video_torrent_seeders" app:layout_constraintLeft_toLeftOf="parent"
tools:text="17 seeders" app:layout_constraintTop_toBottomOf="@+id/player_video_title"
app:layout_constraintTop_toBottomOf="@+id/player_video_title" /> tools:text="17 seeders" />
</FrameLayout> </FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -318,6 +318,25 @@
</FrameLayout> </FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </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 <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -41,6 +41,7 @@
<color name="black_overlay">#66000000</color> <color name="black_overlay">#66000000</color>
<color name="darkBarTransparent">#C0121212</color> <color name="darkBarTransparent">#C0121212</color>
<color name="skipOpTransparent">#4D121212</color>
<color name="darkBar">#121212</color> <color name="darkBar">#121212</color>
<color name="videoProgress">#66B5B5B5</color> <!--66B5B5B5--> <color name="videoProgress">#66B5B5B5</color> <!--66B5B5B5-->
<!--<color name="videoCache">#663D50FA</color>--> <!--66B5B5B5--> <!--<color name="videoCache">#663D50FA</color>--> <!--66B5B5B5-->

View file

@ -638,4 +638,13 @@
<string name="player_settings_play_in_browser">Browser</string> <string name="player_settings_play_in_browser">Browser</string>
<string name="app_not_found_error">App not found</string> <string name="app_not_found_error">App not found</string>
<string name="all_languages_preference">All Languages</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> </resources>