Skip to content

Commit

Permalink
Better error handling of terminated channels when loading feed
Browse files Browse the repository at this point in the history
  • Loading branch information
TobiGr committed Mar 22, 2021
1 parent c7efa8c commit 5aed9b6
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 8 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ dependencies {

// NewPipe dependencies
// You can use a local version by uncommenting a few lines in settings.gradle
implementation 'com.github.TeamNewPipe:NewPipeExtractor:def745b801b2ef35c1a0fee1be950331ca6a0cd2'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:d7182160a727781e5a72ac2ba0c82fca4334467e'
implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"

implementation "org.jsoup:jsoup:1.13.1"
Expand Down
75 changes: 71 additions & 4 deletions app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,29 +29,41 @@ 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
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

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
Expand Down Expand Up @@ -158,6 +171,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
}

override fun onDestroy() {
disposables.dispose()
super.onDestroy()
activity?.supportActionBar?.subtitle = null
}
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -345,6 +347,17 @@ 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) {
Log.d(TAG, if (error.message == null) "terminated" else error.message)
throw maybeWrapper
} else {
throw cause // should never be the case
}
}
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -688,11 +688,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>
Expand Down

0 comments on commit 5aed9b6

Please sign in to comment.