Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ class ErrorInfo private constructor(
)

throwable is ContentNotAvailableException ->
ErrorMessage(R.string.content_not_available)
getLiveStreamOfflineMessage(throwable)
? : ErrorMessage(R.string.content_not_available)

// other extractor exceptions
throwable is ContentNotSupportedException ->
Expand Down Expand Up @@ -309,7 +310,7 @@ class ErrorInfo private constructor(

// if the service explicitly said that content is not available (e.g. age
// restrictions, video deleted, etc.), there is no use in letting users report it
is ContentNotAvailableException -> !isContentSurelyNotAvailable(throwable)
is ContentNotAvailableException -> !isContentSurelyNotAvailable(throwable) && !isLiveStreamOffline(throwable)

// we know the content is not supported, no need to let the user report it
is ContentNotSupportedException -> false
Expand Down Expand Up @@ -360,5 +361,26 @@ class ErrorInfo private constructor(
else -> false
}
}

fun isLiveStreamOffline(e: ContentNotAvailableException): Boolean {
return e.message?.contains("LIVE_STREAM_OFFLINE", ignoreCase = true) == true
}

fun getLiveStreamOfflineMessage(e: ContentNotAvailableException): ErrorMessage? {
if (!isLiveStreamOffline(e)) return null
// YouTube typically embeds the human-readable start hint in quotes, e.g.:
// "Got error LIVE_STREAM_OFFLINE: \"This live event will begin in 27 minutes.\""
val hint = e.message
?.substringAfter("LIVE_STREAM_OFFLINE:", missingDelimiterValue = "")
?.trim()
?.trim('"')
?.trim()
?.takeIf { it.isNotEmpty() }
return if (hint != null) {
ErrorMessage(R.string.live_stream_starts_in, hint)
} else {
ErrorMessage(R.string.live_stream_not_started_yet)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.AudioStream;
Expand Down Expand Up @@ -210,6 +211,9 @@ public final class VideoDetailFragment
@Nullable
private StreamInfo currentInfo = null;
private Disposable currentWorker;
/** Disposable for the auto-retry timer used when a live stream has not started yet. */
@Nullable
private Disposable liveStreamRetryDisposable;
@NonNull
private final CompositeDisposable disposables = new CompositeDisposable();
@Nullable
Expand Down Expand Up @@ -420,6 +424,7 @@ public void onDestroy() {
if (currentWorker != null) {
currentWorker.dispose();
}
cancelLiveStreamAutoRetry();
disposables.clear();
positionSubscriber = null;
currentWorker = null;
Expand Down Expand Up @@ -564,10 +569,10 @@ private void setOnLongClickListeners() {
}));

binding.detailControlsBackground.setOnLongClickListener(makeOnLongClickListener(info ->
openBackgroundPlayer(true)
openBackgroundPlayer(true)
));
binding.detailControlsPopup.setOnLongClickListener(makeOnLongClickListener(info ->
openPopupPlayer(true)
openPopupPlayer(true)
));
binding.detailControlsDownload.setOnLongClickListener(makeOnLongClickListener(info ->
NavigationHelper.openDownloads(activity)));
Expand Down Expand Up @@ -833,6 +838,7 @@ protected void prepareAndLoadInfo() {
public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad);

cancelLiveStreamAutoRetry();
initTabs();
currentInfo = null;
if (currentWorker != null) {
Expand All @@ -845,6 +851,7 @@ public void startLoading(final boolean forceLoad) {
private void startLoading(final boolean forceLoad, final boolean addToBackStack) {
super.startLoading(forceLoad);

cancelLiveStreamAutoRetry();
initTabs();
currentInfo = null;
if (currentWorker != null) {
Expand Down Expand Up @@ -882,8 +889,52 @@ private void runWorker(final boolean forceLoad, final boolean addToBackStack) {
openVideoPlayerAutoFullscreen();
}
}
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
url == null ? "no url" : url, serviceId, url)));
}, throwable -> {
// If the stream is a scheduled live event not yet started,
// show a helpful message with the start hint and a retry button
// instead of the generic monkey "Content unavailable" error.
if (throwable instanceof ContentNotAvailableException
&& ErrorInfo.Companion.isLiveStreamOffline(
(ContentNotAvailableException) throwable)) {
scheduleAutoRetryForLiveStream();
}
showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
url == null ? "no url" : url, serviceId, url));
});
}

/**
* When a scheduled live stream has not started yet, automatically retry loading
* after {@link #LIVE_STREAM_RETRY_DELAY_SECONDS} seconds so the user does not
* have to manually tap Retry when the stream eventually goes live.
* Any pending retry is cancelled the moment the user navigates away or triggers
* a manual reload (disposables are cleared in {@link #onDestroyView}).
*/
private static final long LIVE_STREAM_RETRY_DELAY_SECONDS = 60;

private void scheduleAutoRetryForLiveStream() {
// Cancel any previously scheduled retry before starting a new one
cancelLiveStreamAutoRetry();
liveStreamRetryDisposable =
io.reactivex.rxjava3.core.Completable
.timer(LIVE_STREAM_RETRY_DELAY_SECONDS, TimeUnit.SECONDS, Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
() -> {
if (isAdded() && !isDetached() && !isRemoving()) {
reloadContent();
}
},
throwable -> { /* timer disposed (user navigated away) */ }
);
disposables.add(liveStreamRetryDisposable);
}

private void cancelLiveStreamAutoRetry() {
if (liveStreamRetryDisposable != null && !liveStreamRetryDisposable.isDisposed()) {
liveStreamRetryDisposable.dispose();
}
liveStreamRetryDisposable = null;
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -1617,14 +1668,13 @@ public void handleResult(@NonNull final StreamInfo info) {
}

if (!info.getErrors().isEmpty()) {
// Bandcamp fan pages are not yet supported and thus a ContentNotAvailableException is
// thrown. This is not an error and thus should not be shown to the user.
for (final Throwable throwable : info.getErrors()) {
if (throwable instanceof ContentNotSupportedException
&& "Fan pages are not supported".equals(throwable.getMessage())) {
info.getErrors().remove(throwable);
}
}
// Bandcamp fan pages are not yet supported and thus a ContentNotSupportedException
// is thrown. This is not an error and thus should not be shown to the user.
// Use removeIf to avoid ConcurrentModificationException when modifying the list
// while iterating over it.
info.getErrors().removeIf(throwable ->
throwable instanceof ContentNotSupportedException
&& "Fan pages are not supported".equals(throwable.getMessage()));

if (!info.getErrors().isEmpty()) {
showSnackBarError(new ErrorInfo(info.getErrors(), UserAction.REQUESTED_STREAM,
Expand Down Expand Up @@ -2518,4 +2568,4 @@ private void updateBottomSheetState(final int newState) {
lastStableBottomSheetState = newState;
}
}
}
}
50 changes: 26 additions & 24 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@
<string name="youtube_restricted_mode_enabled_summary">YouTube provides a \"Restricted Mode\" which hides potentially mature content</string>
<string name="restricted_video">This video is age restricted.\n\nTurn on \"%1$s\" in the settings if you want to see it.</string>
<string name="restricted_video_no_stream">This video is age-restricted.
\nDue to new YouTube policies with age-restricted videos, NewPipe cannot access any of its video streams and thus is unable to play it.</string>
\nDue to new YouTube policies with age-restricted videos, NewPipe cannot access any of its video streams and thus is unable to play it.</string>
<string name="duration_live">Live</string>
<string name="downloads">Downloads</string>
<string name="downloads_title">Downloads</string>
Expand Down Expand Up @@ -238,6 +238,8 @@
<string name="could_not_load_thumbnails">Could not load all thumbnails</string>
<string name="parsing_error">Could not parse website</string>
<string name="content_not_available">Content unavailable</string>
<string name="live_stream_not_started_yet">This live stream has not started yet</string>
<string name="live_stream_starts_in">This live stream starts in: %s</string>
<string name="could_not_setup_download_menu">Could not set up download menu</string>
<string name="app_ui_crash">App/UI crashed</string>
<string name="player_stream_failure">Could not play this stream</string>
Expand Down Expand Up @@ -528,14 +530,14 @@
<string name="subscriptions_import_unsuccessful">Could not import subscriptions</string>
<string name="subscriptions_export_unsuccessful">Could not export subscriptions</string>
<string name="import_youtube_instructions">Import YouTube subscriptions from Google takeout:
\n
\n1. Go to this URL: %1$s
\n2. Log in when asked
\n3. Click on \"All data included\", then on \"Deselect all\", then select only \"subscriptions\" and click \"OK\"
\n4. Click on \"Next step\" and then on \"Create export\"
\n5. Click on the \"Download\" button after it appears
\n6. Click on IMPORT FILE below and select the downloaded .zip file
\n7. [If the .zip import fails] Extract the .csv file (usually under \"YouTube and YouTube Music/subscriptions/subscriptions.csv\"), click on IMPORT FILE below and select the extracted csv file</string>
\n
\n1. Go to this URL: %1$s
\n2. Log in when asked
\n3. Click on \"All data included\", then on \"Deselect all\", then select only \"subscriptions\" and click \"OK\"
\n4. Click on \"Next step\" and then on \"Create export\"
\n5. Click on the \"Download\" button after it appears
\n6. Click on IMPORT FILE below and select the downloaded .zip file
\n7. [If the .zip import fails] Extract the .csv file (usually under \"YouTube and YouTube Music/subscriptions/subscriptions.csv\"), click on IMPORT FILE below and select the extracted csv file</string>
<string name="import_soundcloud_instructions">Import a SoundCloud profile by typing either the URL or your ID:\n\n1. Enable \"desktop mode\" in a web-browser (the site is not available for mobile devices)\n2. Go to this URL: %1$s\n3. Log in when asked\n4. Copy the profile URL you were redirected to.</string>
<string name="import_soundcloud_instructions_hint">yourID, soundcloud.com/yourid</string>
<string name="import_network_expensive_warning">Keep in mind this operation can be network expensive.\n\nDo you want to continue?</string>
Expand All @@ -551,7 +553,7 @@
<string name="semitone">Semitone</string>
<!-- GDPR dialog -->
<string name="start_accept_privacy_policy">In order to comply with the European General Data Protection Regulation (GDPR), we hereby draw your attention to NewPipe\'s privacy policy. Please read it carefully.
\nYou must accept it to send us the bug report.</string>
\nYou must accept it to send us the bug report.</string>
<string name="accept">Accept</string>
<string name="decline">Decline</string>
<!-- Limit mobile data usage -->
Expand Down Expand Up @@ -687,7 +689,7 @@
<string name="remove_duplicates_title">Remove duplicates?</string>
<string name="remove_duplicates_message">Do you want to remove all duplicate streams in this playlist?</string>
<string name="remove_watched_popup_warning">Streams that have been watched before and after being added to the playlist will be removed.
\nAre you sure\?</string>
\nAre you sure\?</string>
<string name="remove_watched_popup_partially_watched_streams">Remove partially watched streams</string>
<string name="new_seek_duration_toast">Due to ExoPlayer constraints the seek duration was set to %d seconds</string>
<!-- Time duration plurals -->
Expand Down Expand Up @@ -739,16 +741,16 @@
<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>
\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="feed_hide_streams_title">Show the following streams</string>
<string name="feed_show_hide_streams">Show/Hide streams</string>
<string name="feed_fetch_channel_tabs">Fetch channel tabs</string>
Expand All @@ -764,9 +766,9 @@
<string name="chapters">Chapters</string>
<string name="no_app_to_open_intent">No app on your device can open this</string>
<string name="no_appropriate_file_manager_message">No appropriate file manager was found for this action.
\nPlease install a file manager or try to disable \'%s\' in the download settings</string>
\nPlease install a file manager or try to disable \'%s\' in the download settings</string>
<string name="no_appropriate_file_manager_message_android_10">No appropriate file manager was found for this action.
\nPlease install a Storage Access Framework compatible file manager</string>
\nPlease install a Storage Access Framework compatible file manager</string>
<string name="georestricted_content">This content is not available in your country.</string>
<string name="soundcloud_go_plus_content">This is a SoundCloud Go+ track, at least in your country, so it cannot be streamed or downloaded by NewPipe.</string>
<string name="private_content">This content is private, so it cannot be streamed or downloaded by NewPipe.</string>
Expand Down Expand Up @@ -903,4 +905,4 @@
<string name="api23_requirement_dialog_title">NewPipe is dropping support for Android 5</string>
<string name="api23_requirement_dialog_message">Unfortunately NewPipe depends on a few libraries that dropped support for Android 5.0 and 5.1. The next NewPipe release will therefore only work on devices with Android 6 or higher, sadly. Read more in the blogpost.</string>
<string name="api23_requirement_dialog_blogpost">Blogpost</string>
</resources>
</resources>
Loading