Skip to content

Commit da91c71

Browse files
fix: retry when 403
1 parent a26ae2e commit da91c71

File tree

1 file changed

+157
-8
lines changed

1 file changed

+157
-8
lines changed

android/src/main/kotlin/project/pipepipe/app/service/PlaybackService.kt

Lines changed: 157 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ class PlaybackService : MediaLibraryService() {
5858
// Skip silence setting listener
5959
private var skipSilenceListener: SettingsListener? = null
6060

61+
// Retry mechanism for 403 errors
62+
private data class RetryState(
63+
var retryCount: Int = 0,
64+
var hasRefreshedStream: Boolean = false
65+
)
66+
private val retryStates = mutableMapOf<String, RetryState>()
67+
6168
private enum class PlaybackButtonState(
6269
val repeatMode: Int,
6370
val shuffleEnabled: Boolean,
@@ -113,6 +120,8 @@ class PlaybackService : MediaLibraryService() {
113120

114121
companion object {
115122
private const val SPONSOR_BLOCK_CHECK_INTERVAL_MS = 500L
123+
private const val MAX_RETRIES_BEFORE_REFRESH = 1
124+
private const val MAX_RETRIES_AFTER_REFRESH = 1
116125
}
117126

118127
object CustomCommands {
@@ -588,6 +597,77 @@ class PlaybackService : MediaLibraryService() {
588597
// Return null if prefix not found - don't return the whole error message
589598
return null
590599
}
600+
601+
private fun is403Error(error: PlaybackException): Boolean {
602+
var cause: Throwable? = error
603+
while (cause != null) {
604+
val message = cause.message ?: ""
605+
// Check for HTTP 403 in various forms
606+
if (message.contains("403") ||
607+
message.contains("Forbidden") ||
608+
message.contains("responseCode=403")) {
609+
return true
610+
}
611+
// Check if it's an HttpDataSourceException with response code 403
612+
if (cause.javaClass.simpleName == "HttpDataSourceException") {
613+
try {
614+
val field = cause.javaClass.getDeclaredField("responseCode")
615+
field.isAccessible = true
616+
val responseCode = field.getInt(cause)
617+
if (responseCode == 403) {
618+
return true
619+
}
620+
} catch (e: Exception) {
621+
// Ignore reflection errors
622+
}
623+
}
624+
cause = cause.cause
625+
}
626+
return false
627+
}
628+
629+
private fun refreshStreamAndRetry(mediaId: String, currentIndex: Int) {
630+
serviceScope.launch {
631+
try {
632+
// Get service ID from current media item
633+
val currentItem = player.getMediaItemAt(currentIndex)
634+
val serviceId = currentItem.mediaMetadata.extras?.getString("KEY_SERVICE_ID")
635+
636+
// Fetch fresh stream info
637+
val streamInfo = withContext(Dispatchers.IO) {
638+
executeJobFlow(
639+
SupportedJobType.FETCH_INFO,
640+
mediaId,
641+
serviceId
642+
).info as StreamInfo
643+
}
644+
645+
// Create new media item with fresh stream URLs
646+
val newMediaItem = streamInfo.toMediaItem()
647+
648+
// Replace the media item at the current position
649+
withContext(Dispatchers.Main) {
650+
player.removeMediaItem(currentIndex)
651+
player.addMediaItem(currentIndex, newMediaItem)
652+
653+
// Seek to the same position and try to play
654+
player.seekTo(currentIndex, player.currentPosition)
655+
player.prepare()
656+
player.play()
657+
}
658+
} catch (e: Exception) {
659+
e.printStackTrace()
660+
// If refresh fails, just skip to next
661+
withContext(Dispatchers.Main) {
662+
player.removeMediaItem(currentIndex)
663+
if (player.mediaItemCount > 0) {
664+
player.prepare()
665+
player.play()
666+
}
667+
}
668+
}
669+
}
670+
}
591671
private fun createPlayerListener(): Player.Listener {
592672
return object : Player.Listener {
593673
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
@@ -597,6 +677,8 @@ class PlaybackService : MediaLibraryService() {
597677
mediaItem?.let {
598678
skippedSegments[it.mediaId] = mutableSetOf()
599679
loadSponsorBlockForMedia(it)
680+
// Reset retry state when successfully transitioning to a new item
681+
retryStates.remove(it.mediaId)
600682
}
601683
}
602684

@@ -638,37 +720,104 @@ class PlaybackService : MediaLibraryService() {
638720
}
639721

640722
// Extract failed mediaId from error message
641-
// Use helper function to properly parse the mediaId
642723
val failedMediaId = extractMediaIdFromError(error)
724+
val mediaId = failedMediaId ?: player.currentMediaItem?.mediaId
643725

726+
// Log the error
644727
MainScope().launch {
645728
DatabaseOperations.insertErrorLog(
646729
stacktrace = error.stackTraceToString(),
647-
request = failedMediaId ?: player.currentMediaItem?.mediaId,
730+
request = mediaId,
648731
task = "PLAY_STREAM",
649732
errorCode = "PLAY_000"
650733
)
651734
}
735+
736+
// Check if this is a 403 error and handle retry logic
737+
if (is403Error(error) && mediaId != null) {
738+
val retryState = retryStates.getOrPut(mediaId) { RetryState() }
739+
740+
// Find the index of the failed item
741+
val itemIndex = if (failedMediaId != null) {
742+
(0 until player.mediaItemCount).firstOrNull { i ->
743+
player.getMediaItemAt(i).mediaId == failedMediaId
744+
} ?: player.currentMediaItemIndex
745+
} else {
746+
player.currentMediaItemIndex
747+
}
748+
749+
if (itemIndex == C.INDEX_UNSET) {
750+
// Can't find item, show error and skip
751+
ToastManager.show(MR.strings.playback_error.desc().toString(this@PlaybackService))
752+
if (player.mediaItemCount > 0) {
753+
player.prepare()
754+
player.play()
755+
}
756+
return
757+
}
758+
759+
// First phase: retry up to 5 times before refreshing
760+
if (!retryState.hasRefreshedStream && retryState.retryCount < MAX_RETRIES_BEFORE_REFRESH) {
761+
retryState.retryCount++
762+
ToastManager.show("403 error, retrying (${retryState.retryCount}/$MAX_RETRIES_BEFORE_REFRESH)...")
763+
764+
// Simple retry - just prepare and play again
765+
player.prepare()
766+
player.play()
767+
return
768+
}
769+
770+
// Second phase: refresh stream if we haven't done so yet
771+
if (!retryState.hasRefreshedStream) {
772+
retryState.hasRefreshedStream = true
773+
retryState.retryCount = 0
774+
ToastManager.show("Refreshing stream after $MAX_RETRIES_BEFORE_REFRESH retries...")
775+
776+
refreshStreamAndRetry(mediaId, itemIndex)
777+
return
778+
}
779+
780+
// Third phase: retry up to 5 more times after refresh
781+
if (retryState.retryCount < MAX_RETRIES_AFTER_REFRESH) {
782+
retryState.retryCount++
783+
ToastManager.show("403 error after refresh, retrying (${retryState.retryCount}/$MAX_RETRIES_AFTER_REFRESH)...")
784+
785+
// Simple retry - just prepare and play again
786+
player.prepare()
787+
player.play()
788+
return
789+
}
790+
791+
// Final phase: all retries exhausted, give up and skip to next
792+
ToastManager.show("Failed after all retries, skipping to next...")
793+
retryStates.remove(mediaId) // Clean up retry state
794+
player.removeMediaItem(itemIndex)
795+
796+
if (player.mediaItemCount > 0) {
797+
player.prepare()
798+
player.play()
799+
}
800+
return
801+
}
802+
803+
// For non-403 errors, use the original logic
652804
ToastManager.show(MR.strings.playback_error.desc().toString(this@PlaybackService))
653805

654-
// Find and remove the failed item
655-
// If we found a specific failed mediaId, remove that item
656-
// Otherwise, remove the current item to prevent infinite retry loop
657806
val itemToRemove = if (failedMediaId != null) {
658-
// Try to find the specific failed item by mediaId
659807
(0 until player.mediaItemCount).firstOrNull { i ->
660808
player.getMediaItemAt(i).mediaId == failedMediaId
661809
}
662810
} else {
663-
// Couldn't extract mediaId from error, remove current item as fallback
664811
player.currentMediaItemIndex.takeIf { it != C.INDEX_UNSET }
665812
}
666813

667814
itemToRemove?.let { index ->
815+
// Clean up retry state for this item before removing
816+
val itemMediaId = player.getMediaItemAt(index).mediaId
817+
retryStates.remove(itemMediaId)
668818
player.removeMediaItem(index)
669819
}
670820

671-
// Continue playback if there are items left
672821
if (player.mediaItemCount > 0) {
673822
player.prepare()
674823
player.play()

0 commit comments

Comments
 (0)