New option to use dedicated feed sources for services that support it

YouTube, for example, has a dedicated feed which was built to be used
like this. It comes with some caveats though, like lacking enough
information about the items and returning a limited amount of them.

Nonetheless, a nice option for users that like speedy updates but don't
mind this issue.
This commit is contained in:
Mauricio Colli 2019-12-16 04:36:04 -03:00
parent b2f317ab7c
commit 5ea323ce02
No known key found for this signature in database
GPG Key ID: F200BFD6F29DDD85
6 changed files with 69 additions and 17 deletions

View File

@ -40,8 +40,9 @@ import org.reactivestreams.Subscriber
import org.reactivestreams.Subscription import org.reactivestreams.Subscription
import org.schabi.newpipe.MainActivity.DEBUG import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.channel.ChannelInfo import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.* import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.*
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
@ -109,11 +110,14 @@ class FeedLoadService : Service() {
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1) val groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1)
val useFeedExtractor = defaultSharedPreferences
.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
val thresholdOutdatedMinutesString = defaultSharedPreferences val thresholdOutdatedMinutesString = defaultSharedPreferences
.getString(getString(R.string.feed_update_threshold_key), getString(R.string.feed_update_threshold_default_value)) .getString(getString(R.string.feed_update_threshold_key), getString(R.string.feed_update_threshold_default_value))
val thresholdOutdatedMinutes = thresholdOutdatedMinutesString!!.toInt() val thresholdOutdatedMinutes = thresholdOutdatedMinutesString!!.toInt()
startLoading(groupId, thresholdOutdatedMinutes) startLoading(groupId, useFeedExtractor, thresholdOutdatedMinutes)
return START_NOT_STICKY return START_NOT_STICKY
} }
@ -142,7 +146,7 @@ class FeedLoadService : Service() {
private class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) { private class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) {
companion object { companion object {
fun wrapList(subscriptionId: Long, info: ChannelInfo): List<Throwable> { fun wrapList(subscriptionId: Long, info: ListInfo<StreamInfoItem>): List<Throwable> {
val toReturn = ArrayList<Throwable>(info.errors.size) val toReturn = ArrayList<Throwable>(info.errors.size)
for (error in info.errors) { for (error in info.errors) {
toReturn.add(RequestException(subscriptionId, info.serviceId.toString() + ":" + info.url, error)) toReturn.add(RequestException(subscriptionId, info.serviceId.toString() + ":" + info.url, error))
@ -152,7 +156,7 @@ class FeedLoadService : Service() {
} }
} }
private fun startLoading(groupId: Long = -1, thresholdOutdatedMinutes: Int) { private fun startLoading(groupId: Long = -1, useFeedExtractor: Boolean, thresholdOutdatedMinutes: Int) {
feedResultsHolder = ResultsHolder() feedResultsHolder = ResultsHolder()
val outdatedThreshold = Calendar.getInstance().apply { val outdatedThreshold = Calendar.getInstance().apply {
@ -187,14 +191,21 @@ class FeedLoadService : Service() {
.runOn(Schedulers.io()) .runOn(Schedulers.io())
.map { subscriptionEntity -> .map { subscriptionEntity ->
try { try {
val channelInfo = ExtractorHelper val listInfo = if (useFeedExtractor) {
ExtractorHelper
.getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url)
.blockingGet()
} else {
ExtractorHelper
.getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true) .getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true)
.blockingGet() .blockingGet()
return@map Notification.createOnNext(Pair(subscriptionEntity.uid, channelInfo)) } as ListInfo<StreamInfoItem>
return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo))
} catch (e: Throwable) { } catch (e: Throwable) {
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}" val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
val wrapper = RequestException(subscriptionEntity.uid, request, e) val wrapper = RequestException(subscriptionEntity.uid, request, e)
return@map Notification.createOnError<Pair<Long, ChannelInfo>>(wrapper) return@map Notification.createOnError<Pair<Long, ListInfo<StreamInfoItem>>>(wrapper)
} }
} }
.sequential() .sequential()
@ -219,14 +230,14 @@ class FeedLoadService : Service() {
} }
private val resultSubscriber private val resultSubscriber
get() = object : Subscriber<List<Notification<Pair<Long, ChannelInfo>>>> { get() = object : Subscriber<List<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>> {
override fun onSubscribe(s: Subscription) { override fun onSubscribe(s: Subscription) {
loadingSubscription = s loadingSubscription = s
s.request(java.lang.Long.MAX_VALUE) s.request(java.lang.Long.MAX_VALUE)
} }
override fun onNext(notification: List<Notification<Pair<Long, ChannelInfo>>>) { override fun onNext(notification: List<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>) {
if (DEBUG) Log.v(TAG, "onNext() → $notification") if (DEBUG) Log.v(TAG, "onNext() → $notification")
} }
@ -271,7 +282,7 @@ class FeedLoadService : Service() {
} }
} }
private val databaseConsumer: Consumer<List<Notification<Pair<Long, ChannelInfo>>>> private val databaseConsumer: Consumer<List<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>>
get() = Consumer { get() = Consumer {
feedDatabaseManager.database().runInTransaction { feedDatabaseManager.database().runInTransaction {
for (notification in it) { for (notification in it) {
@ -300,7 +311,8 @@ class FeedLoadService : Service() {
} }
} }
private val errorHandlingConsumer: Consumer<Notification<Pair<Long, ChannelInfo>>>
private val errorHandlingConsumer: Consumer<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>
get() = Consumer { get() = Consumer {
if (it.isOnError) { if (it.isOnError) {
var error = it.error!! var error = it.error!!
@ -317,7 +329,7 @@ class FeedLoadService : Service() {
} }
} }
private val notificationsConsumer: Consumer<Notification<Pair<Long, ChannelInfo>>> private val notificationsConsumer: Consumer<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>
get() = Consumer { onItemCompleted(it.value?.second?.name) } get() = Consumer { onItemCompleted(it.value?.second?.name) }
private fun onItemCompleted(updateDescription: String?) { private fun onItemCompleted(updateDescription: String?) {

View File

@ -7,7 +7,10 @@ import io.reactivex.schedulers.Schedulers
import org.schabi.newpipe.NewPipeDatabase import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.database.subscription.SubscriptionDAO import org.schabi.newpipe.database.subscription.SubscriptionDAO
import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.channel.ChannelInfo import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.feed.FeedInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.feed.FeedDatabaseManager
class SubscriptionManager(context: Context) { class SubscriptionManager(context: Context) {
@ -40,9 +43,14 @@ class SubscriptionManager(context: Context) {
} }
} }
fun updateFromInfo(subscriptionId: Long, info: ChannelInfo) { fun updateFromInfo(subscriptionId: Long, info: ListInfo<StreamInfoItem>) {
val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId) val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId)
if (info is FeedInfo) {
subscriptionEntity.name = info.name
} else if (info is ChannelInfo) {
subscriptionEntity.setData(info.name, info.avatarUrl, info.description, info.subscriberCount) subscriptionEntity.setData(info.name, info.avatarUrl, info.description, info.subscriberCount)
}
subscriptionTable.update(subscriptionEntity) subscriptionTable.update(subscriptionEntity)
} }

View File

@ -31,18 +31,23 @@ import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.Info; import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfo; import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.feed.FeedExtractor;
import org.schabi.newpipe.extractor.feed.FeedInfo;
import org.schabi.newpipe.extractor.kiosk.KioskInfo; import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.search.SearchInfo; import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.report.UserAction;
@ -131,6 +136,22 @@ public final class ExtractorHelper {
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl)); ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl));
} }
public static Single<ListInfo<StreamInfoItem>> getFeedInfoFallbackToChannelInfo(final int serviceId,
final String url) {
final Maybe<ListInfo<StreamInfoItem>> maybeFeedInfo = Maybe.fromCallable(() -> {
final StreamingService service = NewPipe.getService(serviceId);
final FeedExtractor feedExtractor = service.getFeedExtractor(url);
if (feedExtractor == null) {
return null;
}
return FeedInfo.getInfo(feedExtractor);
});
return maybeFeedInfo.switchIfEmpty(getChannelInfo(serviceId, url, true));
}
public static Single<CommentsInfo> getCommentsInfo(final int serviceId, public static Single<CommentsInfo> getCommentsInfo(final int serviceId,
final String url, final String url,
boolean forceLoad) { boolean forceLoad) {

View File

@ -202,6 +202,7 @@
<item>720</item> <item>720</item>
<item>1440</item> <item>1440</item>
</string-array> </string-array>
<string name="feed_use_dedicated_fetch_method_key" translatable="false">feed_use_dedicated_fetch_method</string>
<string name="import_data" translatable="false">import_data</string> <string name="import_data" translatable="false">import_data</string>
<string name="export_data" translatable="false">export_data</string> <string name="export_data" translatable="false">export_data</string>

View File

@ -616,4 +616,6 @@
<string name="feed_update_threshold_title">Feed update threshold</string> <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_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_update_threshold_option_always_update">Always update</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>
</resources> </resources>

View File

@ -102,5 +102,13 @@
android:entryValues="@array/feed_update_threshold_values" android:entryValues="@array/feed_update_threshold_values"
android:title="@string/feed_update_threshold_title" android:title="@string/feed_update_threshold_title"
android:summary="@string/feed_update_threshold_summary"/> android:summary="@string/feed_update_threshold_summary"/>
<SwitchPreference
app:iconSpaceReserved="false"
android:defaultValue="false"
android:key="@string/feed_use_dedicated_fetch_method_key"
android:title="@string/feed_use_dedicated_fetch_method_title"
android:summary="@string/feed_use_dedicated_fetch_method_summary"/>
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>