2021-07-20 10:20:51 +00:00
|
|
|
package org.schabi.newpipe.local.feed.service
|
|
|
|
|
|
|
|
import android.content.Context
|
2023-08-22 10:37:02 +00:00
|
|
|
import android.content.SharedPreferences
|
2021-07-20 10:20:51 +00:00
|
|
|
import androidx.preference.PreferenceManager
|
|
|
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
|
|
import io.reactivex.rxjava3.core.Completable
|
|
|
|
import io.reactivex.rxjava3.core.Flowable
|
|
|
|
import io.reactivex.rxjava3.core.Notification
|
|
|
|
import io.reactivex.rxjava3.core.Single
|
|
|
|
import io.reactivex.rxjava3.functions.Consumer
|
|
|
|
import io.reactivex.rxjava3.processors.PublishProcessor
|
|
|
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
|
|
import org.schabi.newpipe.R
|
|
|
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
2021-11-21 21:53:10 +00:00
|
|
|
import org.schabi.newpipe.database.subscription.NotificationMode
|
2023-08-22 10:37:02 +00:00
|
|
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
2023-04-14 08:19:58 +00:00
|
|
|
import org.schabi.newpipe.extractor.Info
|
|
|
|
import org.schabi.newpipe.extractor.NewPipe
|
|
|
|
import org.schabi.newpipe.extractor.feed.FeedInfo
|
2021-07-20 10:20:51 +00:00
|
|
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
|
|
|
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
|
|
|
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
2023-04-14 08:19:58 +00:00
|
|
|
import org.schabi.newpipe.util.ChannelTabHelper
|
|
|
|
import org.schabi.newpipe.util.ExtractorHelper.getChannelInfo
|
|
|
|
import org.schabi.newpipe.util.ExtractorHelper.getChannelTab
|
|
|
|
import org.schabi.newpipe.util.ExtractorHelper.getMoreChannelTabItems
|
2021-07-20 10:20:51 +00:00
|
|
|
import java.time.OffsetDateTime
|
|
|
|
import java.time.ZoneOffset
|
|
|
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
|
|
import java.util.concurrent.atomic.AtomicInteger
|
|
|
|
|
|
|
|
class FeedLoadManager(private val context: Context) {
|
|
|
|
|
|
|
|
private val subscriptionManager = SubscriptionManager(context)
|
|
|
|
private val feedDatabaseManager = FeedDatabaseManager(context)
|
|
|
|
|
|
|
|
private val notificationUpdater = PublishProcessor.create<String>()
|
|
|
|
private val currentProgress = AtomicInteger(-1)
|
|
|
|
private val maxProgress = AtomicInteger(-1)
|
|
|
|
private val cancelSignal = AtomicBoolean()
|
|
|
|
private val feedResultsHolder = FeedResultsHolder()
|
|
|
|
|
|
|
|
val notification: Flowable<FeedLoadState> = notificationUpdater.map { description ->
|
|
|
|
FeedLoadState(description, maxProgress.get(), currentProgress.get())
|
|
|
|
}
|
|
|
|
|
2021-10-25 13:06:15 +00:00
|
|
|
/**
|
|
|
|
* Start checking for new streams of a subscription group.
|
2022-02-23 18:45:49 +00:00
|
|
|
* @param groupId The ID of the subscription group to load. When using
|
|
|
|
* [FeedGroupEntity.GROUP_ALL_ID], all subscriptions are loaded. When using
|
|
|
|
* [GROUP_NOTIFICATION_ENABLED], only subscriptions with enabled notifications for new streams
|
|
|
|
* are loaded. Using an id of a group created by the user results in that specific group to be
|
|
|
|
* loaded.
|
2021-10-25 13:06:15 +00:00
|
|
|
* @param ignoreOutdatedThreshold When `false`, only subscriptions which have not been updated
|
2022-02-23 18:45:49 +00:00
|
|
|
* within the `feed_update_threshold` are checked for updates. This threshold can be set by
|
|
|
|
* the user in the app settings. When `true`, all subscriptions are checked for new streams.
|
2021-10-25 13:06:15 +00:00
|
|
|
*/
|
2021-07-20 10:20:51 +00:00
|
|
|
fun startLoading(
|
2021-09-04 11:53:11 +00:00
|
|
|
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
|
|
|
ignoreOutdatedThreshold: Boolean = false,
|
2021-07-20 10:20:51 +00:00
|
|
|
): Single<List<Notification<FeedUpdateInfo>>> {
|
|
|
|
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
|
|
|
val useFeedExtractor = defaultSharedPreferences.getBoolean(
|
|
|
|
context.getString(R.string.feed_use_dedicated_fetch_method_key),
|
|
|
|
false
|
|
|
|
)
|
|
|
|
|
2021-09-04 11:53:11 +00:00
|
|
|
val outdatedThreshold = if (ignoreOutdatedThreshold) {
|
|
|
|
OffsetDateTime.now(ZoneOffset.UTC)
|
|
|
|
} else {
|
|
|
|
val thresholdOutdatedSeconds = (
|
|
|
|
defaultSharedPreferences.getString(
|
|
|
|
context.getString(R.string.feed_update_threshold_key),
|
|
|
|
context.getString(R.string.feed_update_threshold_default_value)
|
|
|
|
) ?: context.getString(R.string.feed_update_threshold_default_value)
|
|
|
|
).toInt()
|
|
|
|
OffsetDateTime.now(ZoneOffset.UTC).minusSeconds(thresholdOutdatedSeconds.toLong())
|
|
|
|
}
|
2021-07-20 10:20:51 +00:00
|
|
|
|
2021-10-25 13:06:15 +00:00
|
|
|
/**
|
|
|
|
* subscriptions which have not been updated within the feed updated threshold
|
|
|
|
*/
|
|
|
|
val outdatedSubscriptions = when (groupId) {
|
2023-04-16 14:30:40 +00:00
|
|
|
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(
|
|
|
|
outdatedThreshold
|
|
|
|
)
|
2021-11-21 21:53:10 +00:00
|
|
|
GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.outdatedSubscriptionsWithNotificationMode(
|
|
|
|
outdatedThreshold, NotificationMode.ENABLED
|
|
|
|
)
|
2021-07-20 10:20:51 +00:00
|
|
|
else -> feedDatabaseManager.outdatedSubscriptionsForGroup(groupId, outdatedThreshold)
|
|
|
|
}
|
|
|
|
|
2021-10-25 13:06:15 +00:00
|
|
|
return outdatedSubscriptions
|
2021-07-20 10:20:51 +00:00
|
|
|
.take(1)
|
|
|
|
.doOnNext {
|
|
|
|
currentProgress.set(0)
|
|
|
|
maxProgress.set(it.size)
|
|
|
|
}
|
|
|
|
.filter { it.isNotEmpty() }
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
.doOnNext {
|
|
|
|
notificationUpdater.onNext("")
|
|
|
|
broadcastProgress()
|
|
|
|
}
|
|
|
|
.observeOn(Schedulers.io())
|
|
|
|
.flatMap { Flowable.fromIterable(it) }
|
|
|
|
.takeWhile { !cancelSignal.get() }
|
|
|
|
.parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2)
|
|
|
|
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
|
|
|
|
.filter { !cancelSignal.get() }
|
|
|
|
.map { subscriptionEntity ->
|
2023-08-22 10:37:02 +00:00
|
|
|
loadStreams(subscriptionEntity, useFeedExtractor, defaultSharedPreferences)
|
2021-07-20 10:20:51 +00:00
|
|
|
}
|
|
|
|
.sequential()
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
.doOnNext(NotificationConsumer())
|
|
|
|
.observeOn(Schedulers.io())
|
|
|
|
.buffer(BUFFER_COUNT_BEFORE_INSERT)
|
|
|
|
.doOnNext(DatabaseConsumer())
|
|
|
|
.subscribeOn(Schedulers.io())
|
|
|
|
.toList()
|
|
|
|
.flatMap { x -> postProcessFeed().toSingleDefault(x.flatten()) }
|
|
|
|
}
|
|
|
|
|
|
|
|
fun cancel() {
|
|
|
|
cancelSignal.set(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun broadcastProgress() {
|
2023-04-16 14:30:40 +00:00
|
|
|
FeedEventManager.postEvent(
|
|
|
|
FeedEventManager.Event.ProgressEvent(
|
|
|
|
currentProgress.get(),
|
|
|
|
maxProgress.get()
|
|
|
|
)
|
|
|
|
)
|
2021-07-20 10:20:51 +00:00
|
|
|
}
|
|
|
|
|
2023-08-22 10:37:02 +00:00
|
|
|
private fun loadStreams(
|
|
|
|
subscriptionEntity: SubscriptionEntity,
|
|
|
|
useFeedExtractor: Boolean,
|
|
|
|
defaultSharedPreferences: SharedPreferences
|
2023-09-18 13:01:17 +00:00
|
|
|
): Notification<FeedUpdateInfo> {
|
2023-08-22 10:37:02 +00:00
|
|
|
var error: Throwable? = null
|
|
|
|
val storeOriginalErrorAndRethrow = { e: Throwable ->
|
|
|
|
// keep original to prevent blockingGet() from wrapping it into RuntimeException
|
|
|
|
error = e
|
|
|
|
throw e
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
// check for and load new streams
|
|
|
|
// either by using the dedicated feed method or by getting the channel info
|
|
|
|
var originalInfo: Info? = null
|
|
|
|
var streams: List<StreamInfoItem>? = null
|
|
|
|
val errors = ArrayList<Throwable>()
|
|
|
|
|
|
|
|
if (useFeedExtractor) {
|
|
|
|
NewPipe.getService(subscriptionEntity.serviceId)
|
|
|
|
.getFeedExtractor(subscriptionEntity.url)
|
|
|
|
?.also { feedExtractor ->
|
|
|
|
// the user wants to use a feed extractor and there is one, use it
|
|
|
|
val feedInfo = FeedInfo.getInfo(feedExtractor)
|
|
|
|
errors.addAll(feedInfo.errors)
|
|
|
|
originalInfo = feedInfo
|
|
|
|
streams = feedInfo.relatedItems
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (originalInfo == null) {
|
|
|
|
// use the normal channel tabs extractor if either the user wants it, or
|
|
|
|
// the current service does not have a dedicated feed extractor
|
|
|
|
|
|
|
|
val channelInfo = getChannelInfo(
|
|
|
|
subscriptionEntity.serviceId,
|
|
|
|
subscriptionEntity.url, true
|
|
|
|
)
|
|
|
|
.onErrorReturn(storeOriginalErrorAndRethrow)
|
|
|
|
.blockingGet()
|
|
|
|
errors.addAll(channelInfo.errors)
|
|
|
|
originalInfo = channelInfo
|
|
|
|
|
|
|
|
streams = channelInfo.tabs
|
|
|
|
.filter { tab ->
|
|
|
|
ChannelTabHelper.fetchFeedChannelTab(
|
|
|
|
context,
|
|
|
|
defaultSharedPreferences,
|
|
|
|
tab
|
|
|
|
)
|
|
|
|
}
|
|
|
|
.map {
|
|
|
|
Pair(
|
|
|
|
getChannelTab(subscriptionEntity.serviceId, it, true)
|
|
|
|
.onErrorReturn(storeOriginalErrorAndRethrow)
|
|
|
|
.blockingGet(),
|
|
|
|
it
|
|
|
|
)
|
|
|
|
}
|
|
|
|
.flatMap { (channelTabInfo, linkHandler) ->
|
|
|
|
errors.addAll(channelTabInfo.errors)
|
|
|
|
if (channelTabInfo.relatedItems.isEmpty() &&
|
|
|
|
channelTabInfo.nextPage != null
|
|
|
|
) {
|
|
|
|
val infoItemsPage = getMoreChannelTabItems(
|
|
|
|
subscriptionEntity.serviceId,
|
|
|
|
linkHandler, channelTabInfo.nextPage
|
|
|
|
)
|
|
|
|
.blockingGet()
|
|
|
|
|
|
|
|
errors.addAll(infoItemsPage.errors)
|
|
|
|
return@flatMap infoItemsPage.items
|
|
|
|
} else {
|
|
|
|
return@flatMap channelTabInfo.relatedItems
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.filterIsInstance<StreamInfoItem>()
|
|
|
|
}
|
|
|
|
|
|
|
|
return Notification.createOnNext(
|
|
|
|
FeedUpdateInfo(
|
|
|
|
subscriptionEntity,
|
|
|
|
originalInfo!!,
|
|
|
|
streams!!,
|
|
|
|
errors,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
} catch (e: Throwable) {
|
|
|
|
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
|
|
|
|
val wrapper = FeedLoadService.RequestException(
|
|
|
|
subscriptionEntity.uid,
|
|
|
|
request,
|
|
|
|
// do this to prevent blockingGet() from wrapping into RuntimeException
|
|
|
|
error ?: e
|
|
|
|
)
|
|
|
|
return Notification.createOnError(wrapper)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-25 13:06:15 +00:00
|
|
|
/**
|
|
|
|
* Keep the feed and the stream tables small
|
|
|
|
* to reduce loading times when trying to display the feed.
|
|
|
|
* <br>
|
|
|
|
* Remove streams from the feed which are older than [FeedDatabaseManager.FEED_OLDEST_ALLOWED_DATE].
|
|
|
|
* Remove streams from the database which are not linked / used by any table.
|
|
|
|
*/
|
2021-07-20 10:20:51 +00:00
|
|
|
private fun postProcessFeed() = Completable.fromRunnable {
|
|
|
|
FeedEventManager.postEvent(FeedEventManager.Event.ProgressEvent(R.string.feed_processing_message))
|
|
|
|
feedDatabaseManager.removeOrphansOrOlderStreams()
|
|
|
|
|
|
|
|
FeedEventManager.postEvent(FeedEventManager.Event.SuccessResultEvent(feedResultsHolder.itemsErrors))
|
|
|
|
}.doOnSubscribe {
|
|
|
|
currentProgress.set(-1)
|
|
|
|
maxProgress.set(-1)
|
|
|
|
|
|
|
|
notificationUpdater.onNext(context.getString(R.string.feed_processing_message))
|
|
|
|
FeedEventManager.postEvent(FeedEventManager.Event.ProgressEvent(R.string.feed_processing_message))
|
|
|
|
}.subscribeOn(Schedulers.io())
|
|
|
|
|
|
|
|
private inner class NotificationConsumer : Consumer<Notification<FeedUpdateInfo>> {
|
|
|
|
override fun accept(item: Notification<FeedUpdateInfo>) {
|
|
|
|
currentProgress.incrementAndGet()
|
|
|
|
notificationUpdater.onNext(item.value?.name.orEmpty())
|
|
|
|
|
|
|
|
broadcastProgress()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private inner class DatabaseConsumer : Consumer<List<Notification<FeedUpdateInfo>>> {
|
|
|
|
|
|
|
|
override fun accept(list: List<Notification<FeedUpdateInfo>>) {
|
|
|
|
feedDatabaseManager.database().runInTransaction {
|
|
|
|
for (notification in list) {
|
|
|
|
when {
|
|
|
|
notification.isOnNext -> {
|
2023-04-14 08:19:58 +00:00
|
|
|
val info = notification.value!!
|
2021-07-20 10:20:51 +00:00
|
|
|
|
2023-04-14 08:19:58 +00:00
|
|
|
notification.value!!.newStreams = filterNewStreams(info.streams)
|
2021-11-30 22:31:36 +00:00
|
|
|
|
2023-04-14 08:19:58 +00:00
|
|
|
feedDatabaseManager.upsertAll(info.uid, info.streams)
|
2023-12-20 19:22:45 +00:00
|
|
|
subscriptionManager.updateFromInfo(info)
|
2021-07-20 10:20:51 +00:00
|
|
|
|
|
|
|
if (info.errors.isNotEmpty()) {
|
2021-10-25 13:06:15 +00:00
|
|
|
feedResultsHolder.addErrors(
|
2023-04-14 08:19:58 +00:00
|
|
|
info.errors.map {
|
|
|
|
FeedLoadService.RequestException(
|
|
|
|
info.uid,
|
2023-12-20 19:22:45 +00:00
|
|
|
"${info.serviceId}:${info.url}",
|
2023-04-14 08:19:58 +00:00
|
|
|
it
|
|
|
|
)
|
|
|
|
}
|
2021-10-25 13:06:15 +00:00
|
|
|
)
|
2023-04-14 08:19:58 +00:00
|
|
|
feedDatabaseManager.markAsOutdated(info.uid)
|
2021-07-20 10:20:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
notification.isOnError -> {
|
|
|
|
val error = notification.error
|
2021-11-28 16:09:20 +00:00
|
|
|
feedResultsHolder.addError(error!!)
|
2021-07-20 10:20:51 +00:00
|
|
|
|
|
|
|
if (error is FeedLoadService.RequestException) {
|
|
|
|
feedDatabaseManager.markAsOutdated(error.subscriptionId)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-30 22:31:36 +00:00
|
|
|
private fun filterNewStreams(list: List<StreamInfoItem>): List<StreamInfoItem> {
|
|
|
|
return list.filter {
|
|
|
|
!feedDatabaseManager.doesStreamExist(it) &&
|
|
|
|
it.uploadDate != null &&
|
|
|
|
// Streams older than this date are automatically removed from the feed.
|
|
|
|
// Therefore, streams which are not in the database,
|
|
|
|
// but older than this date, are considered old.
|
|
|
|
it.uploadDate!!.offsetDateTime().isAfter(
|
|
|
|
FeedDatabaseManager.FEED_OLDEST_ALLOWED_DATE
|
|
|
|
)
|
2021-07-20 10:20:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-21 21:53:10 +00:00
|
|
|
companion object {
|
|
|
|
|
|
|
|
/**
|
2021-12-10 22:52:28 +00:00
|
|
|
* Constant used to check for updates of subscriptions with [NotificationMode.ENABLED].
|
2021-11-21 21:53:10 +00:00
|
|
|
*/
|
|
|
|
const val GROUP_NOTIFICATION_ENABLED = -2L
|
2021-07-20 10:20:51 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* How many extractions will be running in parallel.
|
|
|
|
*/
|
2021-11-21 21:53:10 +00:00
|
|
|
private const val PARALLEL_EXTRACTIONS = 6
|
2021-07-20 10:20:51 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Number of items to buffer to mass-insert in the database.
|
|
|
|
*/
|
2021-11-21 21:53:10 +00:00
|
|
|
private const val BUFFER_COUNT_BEFORE_INSERT = 20
|
2021-07-20 10:20:51 +00:00
|
|
|
}
|
|
|
|
}
|