Better error handling of terminated channels when loading feed

This commit is contained in:
TobiGr 2021-04-02 21:41:06 +02:00
parent 761e01c3b9
commit 6ad4b425e4
4 changed files with 95 additions and 8 deletions

View File

@ -183,7 +183,7 @@ dependencies {
/** NewPipe libraries **/
// You can use a local version by uncommenting a few lines in settings.gradle
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.4'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:d4186d100b6c6dddfcf3cf4b004f5960a8bf441d'
/** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"

View File

@ -19,6 +19,7 @@
package org.schabi.newpipe.local.feed
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
@ -28,6 +29,7 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.annotation.Nullable
import androidx.appcompat.app.AlertDialog
import androidx.core.content.edit
import androidx.core.os.bundleOf
@ -35,15 +37,24 @@ import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import icepick.State
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.databinding.FragmentFeedBinding
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.fragments.list.BaseListFragment
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.Localization
import java.time.OffsetDateTime
@ -51,6 +62,8 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
private var _feedBinding: FragmentFeedBinding? = null
private val feedBinding get() = _feedBinding!!
private val disposables = CompositeDisposable()
private lateinit var viewModel: FeedViewModel
@State
@JvmField
@ -158,6 +171,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
}
override fun onDestroy() {
disposables.dispose()
super.onDestroy()
activity?.supportActionBar?.subtitle = null
}
@ -243,9 +257,9 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
oldestSubscriptionUpdate = loadedState.oldestUpdate
val loadedCount = loadedState.notLoadedCount > 0
feedBinding.refreshSubtitleText.isVisible = loadedCount
if (loadedCount) {
val feedsNotLoaded = loadedState.notLoadedCount > 0
feedBinding.refreshSubtitleText.isVisible = feedsNotLoaded
if (feedsNotLoaded) {
feedBinding.refreshSubtitleText.text = getString(
R.string.feed_subscription_not_loaded_count,
loadedState.notLoadedCount
@ -264,11 +278,64 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
hideLoading()
false
} else {
showError(ErrorInfo(errorState.error, UserAction.REQUESTED_FEED, "Loading feed"))
if (errorState.error is FeedLoadService.RequestException) {
disposables.add(
Single.fromCallable {
NewPipeDatabase.getInstance(requireContext()).subscriptionDAO()
.getSubscription(errorState.error.subscriptionId)
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
subscriptionEntity ->
handleFeedNotAvailable(
subscriptionEntity,
errorState.error.cause?.cause
)
},
{ throwable -> throwable.printStackTrace() }
)
)
} else {
showError(ErrorInfo(errorState.error, UserAction.REQUESTED_FEED, "Loading feed"))
}
true
}
}
private fun handleFeedNotAvailable(
subscriptionEntity: SubscriptionEntity,
@Nullable cause: Throwable?
) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val isFastFeedModeEnabled = sharedPreferences.getBoolean(
getString(R.string.feed_use_dedicated_fetch_method_key), false
)
val builder = AlertDialog.Builder(requireContext())
.setTitle(R.string.feed_load_error)
.setPositiveButton(
R.string.unsubscribe,
DialogInterface.OnClickListener {
_, _ ->
SubscriptionManager(requireContext()).deleteSubscription(
subscriptionEntity.serviceId, subscriptionEntity.url
).subscribe()
}
)
.setNegativeButton(R.string.cancel, DialogInterface.OnClickListener { _, _ -> })
if (cause is AccountTerminatedException) {
builder.setMessage(R.string.feed_load_error_terminated)
} else if (cause is ContentNotAvailableException && isFastFeedModeEnabled) {
builder.setMessage(R.string.feed_load_error_fast_unknown)
.setNeutralButton(R.string.feed_use_dedicated_fetch_method_disable_button) { _, _ ->
sharedPreferences.edit {
putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
}
}
}
builder.create().show()
}
private fun updateRelativeTimeViews() {
updateRefreshViewState()
infoListAdapter.notifyDataSetChanged()

View File

@ -48,6 +48,7 @@ import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.ktx.isNetworkRelated
@ -162,7 +163,7 @@ class FeedLoadService : Service() {
// Loading & Handling
// /////////////////////////////////////////////////////////////////////////
private class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) {
public class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) {
companion object {
fun wrapList(subscriptionId: Long, info: ListInfo<StreamInfoItem>): List<Throwable> {
val toReturn = ArrayList<Throwable>(info.errors.size)
@ -334,8 +335,9 @@ class FeedLoadService : Service() {
private val errorHandlingConsumer: Consumer<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>
get() = Consumer {
if (it.isOnError) {
var error = it.error!!
if (error is RequestException) error = error.cause!!
var maybeWrapper = it.error!!
val error = if (maybeWrapper is RequestException) maybeWrapper.cause!!
else maybeWrapper
val cause = error.cause
when {
@ -345,6 +347,19 @@ class FeedLoadService : Service() {
error is IOException -> throw error
cause is IOException -> throw cause
error.isNetworkRelated -> throw IOException(error)
cause is ContentNotAvailableException -> {
// maybeWrapper is definitely a RequestException,
// because this is an exception thrown in the extractor
if (maybeWrapper is RequestException) {
throw maybeWrapper
} else {
if (DEBUG) {
Log.d(TAG, "Cause is ContentNotAvailableException, but maybeWrapper is not a RequestException")
}
throw cause // should never be the case
}
}
}
}
}

View File

@ -690,11 +690,16 @@
<string name="feed_update_threshold_title">Feed update threshold</string>
<string name="feed_update_threshold_summary">Time after last update before a subscription is considered outdated — %s</string>
<string name="feed_update_threshold_option_always_update">Always update</string>
<string name="feed_load_error">Error loading feed</string>
<string name="feed_load_error_account_info">Could not load feed for \'%s\'.</string>
<string name="feed_load_error_terminated">The author\'s account has been terminated.\nNewPipe will not be able to load this feed in the future.\Do you want to unsubscribe from this channel?</string>
<string name="feed_load_error_fast_unknown">The fast feed mode does not provide more info on this.</string>
<string name="feed_use_dedicated_fetch_method_title">Fetch from dedicated feed when available</string>
<string name="feed_use_dedicated_fetch_method_summary">Available in some services, it is usually much faster but may return a limited amount of items and often incomplete information (e.g. no duration, item type, no live status).</string>
<string name="feed_use_dedicated_fetch_method_enable_button">Enable fast mode</string>
<string name="feed_use_dedicated_fetch_method_disable_button">Disable fast mode</string>
<string name="feed_use_dedicated_fetch_method_help_text">Do you think feed loading is too slow? If so, try enabling fast loading (you can change it in settings or by pressing the button below).\n\nNewPipe offers two feed loading strategies:\n• Fetching the whole subscription channel, which is slow but complete.\n• Using a dedicated service endpoint, which is fast but usually not complete.\n\nThe difference between the two is that the fast one usually lacks some information, like the item\'s duration or type (can\'t distinguish between live videos and normal ones) and it may return less items.\n\nYouTube is an example of a service that offers this fast method with its RSS feed.\n\nSo the choice boils down to what you prefer: speed or precise information.</string>
<string name="content_not_supported">This content is not yet supported by NewPipe.\n\nIt will hopefully be supported in a future version.</string>
<string name="detail_sub_channel_thumbnail_view_description">Channel\'s avatar thumbnail</string>
<string name="channel_created_by">Created by %s</string>