mirror of
				https://github.com/recloudstream/cloudstream.git
				synced 2024-08-15 01:53:11 +00:00 
			
		
		
		
	Aniskip (#195)
* 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:
		
							parent
							
								
									f84259f898
								
							
						
					
					
						commit
						c10ec34ab8
					
				
					 17 changed files with 543 additions and 221 deletions
				
			
		|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue