Channels are now an Info

The previous "main" tab is now just a normal tab returned in getTabs().
Various part of the code that used to handle channels as ListInfo now either take the first (playable, i.e. with streams) tab (e.g. the ChannelTabPlayQueue), or take all of them combined (e.g. the feed).
This commit is contained in:
Stypox 2023-04-14 10:19:58 +02:00
parent a735063c5c
commit aa4ec2f39e
19 changed files with 301 additions and 362 deletions

View file

@ -58,7 +58,7 @@ class NotificationHelper(val context: Context) {
.setAutoCancel(true)
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
.setGroupSummary(true)
.setGroup(data.listInfo.url)
.setGroup(data.originalInfo.url)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
// Build a summary notification for Android versions < 7.0
@ -73,7 +73,7 @@ class NotificationHelper(val context: Context) {
context,
data.pseudoId,
NavigationHelper
.getChannelIntent(context, data.listInfo.serviceId, data.listInfo.url)
.getChannelIntent(context, data.originalInfo.serviceId, data.originalInfo.url)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
0,
false
@ -88,7 +88,7 @@ class NotificationHelper(val context: Context) {
// Show individual stream notifications, set channel icon only if there is actually
// one
showStreamNotifications(newStreams, data.listInfo.serviceId, bitmap)
showStreamNotifications(newStreams, data.originalInfo.serviceId, bitmap)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
@ -97,7 +97,7 @@ class NotificationHelper(val context: Context) {
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
// Show individual stream notifications
showStreamNotifications(newStreams, data.listInfo.serviceId, null)
showStreamNotifications(newStreams, data.originalInfo.serviceId, null)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected

View file

@ -13,11 +13,16 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.Info
import org.schabi.newpipe.extractor.NewPipe
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.subscription.SubscriptionManager
import org.schabi.newpipe.util.ExtractorHelper
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
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.concurrent.atomic.AtomicBoolean
@ -102,49 +107,88 @@ class FeedLoadManager(private val context: Context) {
.filter { !cancelSignal.get() }
.map { subscriptionEntity ->
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
val listInfo = if (useFeedExtractor) {
ExtractorHelper
.getFeedInfoFallbackToChannelInfo(
subscriptionEntity.serviceId,
subscriptionEntity.url
)
.onErrorReturn {
error = it // store error, otherwise wrapped into RuntimeException
throw it
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()
} else {
ExtractorHelper
.getChannelInfo(
subscriptionEntity.serviceId,
subscriptionEntity.url,
true
)
.onErrorReturn {
error = it // store error, otherwise wrapped into RuntimeException
throw it
errors.addAll(channelInfo.errors)
originalInfo = channelInfo
streams = channelInfo.tabs
.filter(ChannelTabHelper::isStreamsTab)
.map {
Pair(
getChannelTab(subscriptionEntity.serviceId, it, true)
.onErrorReturn(storeOriginalErrorAndRethrow)
.blockingGet(),
it
)
}
.blockingGet()
} as ListInfo<StreamInfoItem>
.flatMap { (channelTabInfo, linkHandler) ->
errors.addAll(channelTabInfo.errors)
if (channelTabInfo.relatedItems.isEmpty()) {
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@map Notification.createOnNext(
FeedUpdateInfo(
subscriptionEntity,
listInfo
originalInfo!!,
streams!!,
errors,
)
)
} catch (e: Throwable) {
if (error == null) {
// do this to prevent blockingGet() from wrapping into RuntimeException
error = e
}
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
val wrapper =
FeedLoadService.RequestException(subscriptionEntity.uid, request, error!!)
val wrapper = FeedLoadService.RequestException(
subscriptionEntity.uid,
request,
// do this to prevent blockingGet() from wrapping into RuntimeException
error ?: e
)
return@map Notification.createOnError<FeedUpdateInfo>(wrapper)
}
}
@ -203,24 +247,24 @@ class FeedLoadManager(private val context: Context) {
for (notification in list) {
when {
notification.isOnNext -> {
val subscriptionId = notification.value!!.uid
val info = notification.value!!.listInfo
val info = notification.value!!
notification.value!!.newStreams = filterNewStreams(
notification.value!!.listInfo.relatedItems
)
notification.value!!.newStreams = filterNewStreams(info.streams)
feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
subscriptionManager.updateFromInfo(subscriptionId, info)
feedDatabaseManager.upsertAll(info.uid, info.streams)
subscriptionManager.updateFromInfo(info.uid, info.originalInfo)
if (info.errors.isNotEmpty()) {
feedResultsHolder.addErrors(
FeedLoadService.RequestException.wrapList(
subscriptionId,
info
)
info.errors.map {
FeedLoadService.RequestException(
info.uid,
"${info.originalInfo.serviceId}:${info.originalInfo.url}",
it
)
}
)
feedDatabaseManager.markAsOutdated(subscriptionId)
feedDatabaseManager.markAsOutdated(info.uid)
}
}
notification.isOnError -> {

View file

@ -39,8 +39,6 @@ import org.schabi.newpipe.App
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.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
import java.util.concurrent.TimeUnit
@ -126,17 +124,7 @@ class FeedLoadService : Service() {
// Loading & Handling
// /////////////////////////////////////////////////////////////////////////
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)
info.errors.mapTo(toReturn) {
RequestException(subscriptionId, info.serviceId.toString() + ":" + info.url, it)
}
return toReturn
}
}
}
class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause)
// /////////////////////////////////////////////////////////////////////////
// Notification

View file

@ -2,7 +2,7 @@ package org.schabi.newpipe.local.feed.service
import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.Info
import org.schabi.newpipe.extractor.stream.StreamInfoItem
data class FeedUpdateInfo(
@ -11,24 +11,30 @@ data class FeedUpdateInfo(
val notificationMode: Int,
val name: String,
val avatarUrl: String,
val listInfo: ListInfo<StreamInfoItem>,
val originalInfo: Info,
val streams: List<StreamInfoItem>,
val errors: List<Throwable>,
) {
constructor(
subscription: SubscriptionEntity,
listInfo: ListInfo<StreamInfoItem>,
originalInfo: Info,
streams: List<StreamInfoItem>,
errors: List<Throwable>,
) : this(
uid = subscription.uid,
notificationMode = subscription.notificationMode,
name = subscription.name,
avatarUrl = subscription.avatarUrl,
listInfo = listInfo,
originalInfo = originalInfo,
streams = streams,
errors = errors,
)
/**
* Integer id, can be used as notification id, etc.
*/
val pseudoId: Int
get() = listInfo.url.hashCode()
get() = originalInfo.url.hashCode()
lateinit var newStreams: List<StreamInfoItem>
}

View file

@ -1,6 +1,7 @@
package org.schabi.newpipe.local.subscription
import android.content.Context
import android.util.Pair
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
@ -11,8 +12,9 @@ import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.database.subscription.SubscriptionDAO
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.Info
import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.channel.ChannelTabInfo
import org.schabi.newpipe.extractor.feed.FeedInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.FeedDatabaseManager
@ -46,28 +48,33 @@ class SubscriptionManager(context: Context) {
}
}
fun upsertAll(infoList: List<ChannelInfo>): List<SubscriptionEntity> {
fun upsertAll(infoList: List<Pair<ChannelInfo, List<ChannelTabInfo>>>): List<SubscriptionEntity> {
val listEntities = subscriptionTable.upsertAll(
infoList.map { SubscriptionEntity.from(it) }
infoList.map { SubscriptionEntity.from(it.first) }
)
database.runInTransaction {
infoList.forEachIndexed { index, info ->
feedDatabaseManager.upsertAll(listEntities[index].uid, info.relatedItems)
info.second.forEach {
feedDatabaseManager.upsertAll(
listEntities[index].uid,
it.relatedItems.filterIsInstance<StreamInfoItem>()
)
}
}
}
return listEntities
}
fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url)
.flatMapCompletable {
Completable.fromRunnable {
it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount)
subscriptionTable.update(it)
feedDatabaseManager.upsertAll(it.uid, info.relatedItems)
fun updateChannelInfo(info: ChannelInfo): Completable =
subscriptionTable.getSubscription(info.serviceId, info.url)
.flatMapCompletable {
Completable.fromRunnable {
it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount)
subscriptionTable.update(it)
}
}
}
fun updateNotificationMode(serviceId: Int, url: String, @NotificationMode mode: Int): Completable {
return subscriptionTable().getSubscription(serviceId, url)
@ -84,7 +91,7 @@ class SubscriptionManager(context: Context) {
}
}
fun updateFromInfo(subscriptionId: Long, info: ListInfo<StreamInfoItem>) {
fun updateFromInfo(subscriptionId: Long, info: Info) {
val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId)
if (info is FeedInfo) {
@ -107,11 +114,8 @@ class SubscriptionManager(context: Context) {
.observeOn(AndroidSchedulers.mainThread())
}
fun insertSubscription(subscriptionEntity: SubscriptionEntity, info: ChannelInfo) {
database.runInTransaction {
val subscriptionId = subscriptionTable.insert(subscriptionEntity)
feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
}
fun insertSubscription(subscriptionEntity: SubscriptionEntity) {
subscriptionTable.insert(subscriptionEntity)
}
fun deleteSubscription(subscriptionEntity: SubscriptionEntity) {
@ -125,7 +129,10 @@ class SubscriptionManager(context: Context) {
*/
private fun rememberAllStreams(subscription: SubscriptionEntity): Completable {
return ExtractorHelper.getChannelInfo(subscription.serviceId, subscription.url, false)
.map { channel -> channel.relatedItems.map { stream -> StreamEntity(stream) } }
.flatMap { info ->
ExtractorHelper.getChannelTab(subscription.serviceId, info.tabs.first(), false)
}
.map { channel -> channel.relatedItems.filterIsInstance<StreamInfoItem>().map { stream -> StreamEntity(stream) } }
.flatMapCompletable { entities ->
Completable.fromAction {
database.streamDAO().upsertAll(entities)

View file

@ -26,6 +26,7 @@ import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -38,6 +39,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.channel.ChannelTabInfo;
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.streams.io.SharpInputStream;
@ -48,6 +50,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@ -199,12 +202,19 @@ public class SubscriptionsImportService extends BaseImportExportService {
.parallel(PARALLEL_EXTRACTIONS)
.runOn(Schedulers.io())
.map((Function<SubscriptionItem, Notification<ChannelInfo>>) subscriptionItem -> {
.map((Function<SubscriptionItem, Notification<Pair<ChannelInfo,
List<ChannelTabInfo>>>>) subscriptionItem -> {
try {
return Notification.createOnNext(ExtractorHelper
final ChannelInfo channelInfo = ExtractorHelper
.getChannelInfo(subscriptionItem.getServiceId(),
subscriptionItem.getUrl(), true)
.blockingGet());
.blockingGet();
return Notification.createOnNext(new Pair<>(channelInfo,
Collections.singletonList(
ExtractorHelper.getChannelTab(
subscriptionItem.getServiceId(),
channelInfo.getTabs().get(0), true).blockingGet()
)));
} catch (final Throwable e) {
return Notification.createOnError(e);
}
@ -223,7 +233,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
}
private Subscriber<List<SubscriptionEntity>> getSubscriber() {
return new Subscriber<List<SubscriptionEntity>>() {
return new Subscriber<>() {
@Override
public void onSubscribe(final Subscription s) {
subscription = s;
@ -254,10 +264,11 @@ public class SubscriptionsImportService extends BaseImportExportService {
};
}
private Consumer<Notification<ChannelInfo>> getNotificationsConsumer() {
private Consumer<Notification<Pair<ChannelInfo,
List<ChannelTabInfo>>>> getNotificationsConsumer() {
return notification -> {
if (notification.isOnNext()) {
final String name = notification.getValue().getName();
final String name = notification.getValue().first.getName();
eventListener.onItemCompleted(!TextUtils.isEmpty(name) ? name : "");
} else if (notification.isOnError()) {
final Throwable error = notification.getError();
@ -275,10 +286,12 @@ public class SubscriptionsImportService extends BaseImportExportService {
};
}
private Function<List<Notification<ChannelInfo>>, List<SubscriptionEntity>> upsertBatch() {
private Function<List<Notification<Pair<ChannelInfo, List<ChannelTabInfo>>>>,
List<SubscriptionEntity>> upsertBatch() {
return notificationList -> {
final List<ChannelInfo> infoList = new ArrayList<>(notificationList.size());
for (final Notification<ChannelInfo> n : notificationList) {
final List<Pair<ChannelInfo, List<ChannelTabInfo>>> infoList =
new ArrayList<>(notificationList.size());
for (final Notification<Pair<ChannelInfo, List<ChannelTabInfo>>> n : notificationList) {
if (n.isOnNext()) {
infoList.add(n.getValue());
}