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 dfbd39e898
commit c076a0f771
No known key found for this signature in database
GPG Key ID: 4BDF1B40A49FDD23
19 changed files with 301 additions and 362 deletions

View File

@ -197,7 +197,7 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.Theta-Dev:NewPipeExtractor:e278a2d6d428dec82a304d271803d35afbd7340c'
implementation 'com.github.Theta-Dev:NewPipeExtractor:c3651bef5c622abf0cdfc34c9985ba8c33d1491e'
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
/** Checkstyle **/

View File

@ -65,6 +65,7 @@ import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.ktx.ExceptionUtils;
@ -72,10 +73,11 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.ChannelTabHelper;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
@ -1022,7 +1024,16 @@ public class RouterActivity extends AppCompatActivity {
}
playQueue = new SinglePlayQueue((StreamInfo) info);
} else if (info instanceof ChannelInfo) {
playQueue = new ChannelPlayQueue((ChannelInfo) info);
final Optional<ListLinkHandler> playableTab = ((ChannelInfo) info).getTabs()
.stream()
.filter(ChannelTabHelper::isStreamsTab)
.findFirst();
if (playableTab.isPresent()) {
playQueue = new ChannelTabPlayQueue(info.getServiceId(), playableTab.get());
} else {
return; // there is no playable tab
}
} else if (info instanceof PlaylistInfo) {
playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
} else {

View File

@ -16,7 +16,6 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.views.NewPipeRecyclerView;
@ -236,9 +235,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
infoListAdapter.clearStreamItemList();
// showEmptyState should be called only if there is no item as
// well as no header in infoListAdapter
if (!(result instanceof ChannelInfo && infoListAdapter.getItemCount() == 1)) {
showEmptyState();
}
showEmptyState();
}
}

View File

@ -277,10 +277,9 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
}, onError));
}
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription,
final ChannelInfo info) {
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
return (@NonNull Object o) -> {
subscriptionManager.insertSubscription(subscription, info);
subscriptionManager.insertSubscription(subscription);
return o;
};
}
@ -355,7 +354,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
info.getSubscriberCount());
channelSubscription = null;
updateNotifyButton(null);
subscribeButtonMonitor = monitorSubscribeButton(mapOnSubscribe(channel, info));
subscribeButtonMonitor = monitorSubscribeButton(mapOnSubscribe(channel));
} else {
if (DEBUG) {
Log.d(TAG, "Found subscription to this channel!");
@ -451,8 +450,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
tabAdapter.clearAllItems();
if (currentInfo != null && !channelContentNotSupported) {
tabAdapter.addFragment(new ChannelVideosFragment(currentInfo), "Videos");
final Context context = requireContext();
final SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(context);

View File

@ -1,157 +0,0 @@
package org.schabi.newpipe.fragments.list.channel;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.databinding.FragmentChannelVideosBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class ChannelVideosFragment extends BaseListInfoFragment<StreamInfoItem, ChannelInfo> {
private final CompositeDisposable disposables = new CompositeDisposable();
private FragmentChannelVideosBinding channelBinding;
private PlaylistControlBinding playlistControlBinding;
/*//////////////////////////////////////////////////////////////////////////
// Constructors and lifecycle
//////////////////////////////////////////////////////////////////////////*/
// required by the Android framework to restore fragments after saving
public ChannelVideosFragment() {
super(UserAction.REQUESTED_CHANNEL);
}
public ChannelVideosFragment(final int serviceId, final String url, final String name) {
this();
setInitialData(serviceId, url, name);
}
public ChannelVideosFragment(@NonNull final ChannelInfo info) {
this(info.getServiceId(), info.getUrl(), info.getName());
this.currentInfo = info;
this.currentNextPage = info.getNextPage();
}
@Override
public void onResume() {
super.onResume();
if (activity != null && useAsFrontPage) {
setTitle(currentInfo != null ? currentInfo.getName() : name);
}
}
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(false);
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
channelBinding = FragmentChannelVideosBinding.inflate(inflater, container, false);
return channelBinding.getRoot();
}
@Override
public void onDestroy() {
super.onDestroy();
disposables.clear();
channelBinding = null;
playlistControlBinding = null;
}
@Override
protected Supplier<View> getListHeaderSupplier() {
playlistControlBinding = PlaylistControlBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
return playlistControlBinding::getRoot;
}
/*//////////////////////////////////////////////////////////////////////////
// Loading
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.InfoItemsPage<StreamInfoItem>> loadMoreItemsLogic() {
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage);
}
@Override
protected Single<ChannelInfo> loadResult(final boolean forceLoad) {
return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void handleResult(@NonNull final ChannelInfo result) {
super.handleResult(result);
// PlaylistControls should be visible only if there is some item in
// infoListAdapter other than header
if (infoListAdapter.getItemCount() != 1) {
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
} else {
playlistControlBinding.getRoot().setVisibility(View.GONE);
}
disposables.clear();
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(
view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
return true;
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
return true;
});
}
private PlayQueue getPlayQueue() {
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
.filter(StreamInfoItem.class::isInstance)
.map(StreamInfoItem.class::cast)
.collect(Collectors.toList());
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
currentInfo.getNextPage(), streamItems, 0);
}
}

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());
}

View File

@ -4,6 +4,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.Page;
@ -15,7 +16,7 @@ import java.util.stream.Collectors;
import io.reactivex.rxjava3.core.SingleObserver;
import io.reactivex.rxjava3.disposables.Disposable;
abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
abstract class AbstractInfoPlayQueue<T extends ListInfo<? extends InfoItem>>
extends PlayQueue {
boolean isInitial;
private boolean isComplete;
@ -27,7 +28,10 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
private transient Disposable fetchReactor;
protected AbstractInfoPlayQueue(final T info) {
this(info.getServiceId(), info.getUrl(), info.getNextPage(), info.getRelatedItems(), 0);
this(info.getServiceId(), info.getUrl(), info.getNextPage(),
info.getRelatedItems().stream().filter(StreamInfoItem.class::isInstance)
.map(StreamInfoItem.class::cast).collect(
Collectors.toList()), 0);
}
protected AbstractInfoPlayQueue(final int serviceId,
@ -72,7 +76,10 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
}
nextPage = result.getNextPage();
append(extractListItems(result.getRelatedItems()));
append(extractListItems(result.getRelatedItems().stream()
.filter(StreamInfoItem.class::isInstance)
.map(StreamInfoItem.class::cast).collect(
Collectors.toList())));
fetchReactor.dispose();
fetchReactor = null;
@ -87,7 +94,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
};
}
SingleObserver<ListExtractor.InfoItemsPage<StreamInfoItem>> getNextPageObserver() {
SingleObserver<ListExtractor.InfoItemsPage<? extends InfoItem>> getNextPageObserver() {
return new SingleObserver<>() {
@Override
public void onSubscribe(@NonNull final Disposable d) {
@ -101,13 +108,16 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
@Override
public void onSuccess(
@NonNull final ListExtractor.InfoItemsPage<StreamInfoItem> result) {
@NonNull final ListExtractor.InfoItemsPage<? extends InfoItem> result) {
if (!result.hasNextPage()) {
isComplete = true;
}
nextPage = result.getNextPage();
append(extractListItems(result.getItems()));
append(extractListItems(result.getItems().stream()
.filter(StreamInfoItem.class::isInstance)
.map(StreamInfoItem.class::cast).collect(
Collectors.toList())));
fetchReactor.dispose();
fetchReactor = null;

View File

@ -1,47 +0,0 @@
package org.schabi.newpipe.player.playqueue;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.util.ExtractorHelper;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class ChannelPlayQueue extends AbstractInfoPlayQueue<ChannelInfo> {
public ChannelPlayQueue(final ChannelInfo info) {
super(info);
}
public ChannelPlayQueue(final int serviceId,
final String url,
final Page nextPage,
final List<StreamInfoItem> streams,
final int index) {
super(serviceId, url, nextPage, streams, index);
}
@Override
protected String getTag() {
return "ChannelPlayQueue@" + Integer.toHexString(hashCode());
}
@Override
public void fetch() {
if (this.isInitial) {
ExtractorHelper.getChannelInfo(this.serviceId, this.baseUrl, false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getHeadListObserver());
} else {
ExtractorHelper.getMoreChannelItems(this.serviceId, this.baseUrl, this.nextPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getNextPageObserver());
}
}
}

View File

@ -0,0 +1,53 @@
package org.schabi.newpipe.player.playqueue;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.channel.ChannelTabInfo;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.util.ExtractorHelper;
import java.util.Collections;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class ChannelTabPlayQueue extends AbstractInfoPlayQueue<ChannelTabInfo> {
final ListLinkHandler linkHandler;
public ChannelTabPlayQueue(final int serviceId,
final ListLinkHandler linkHandler,
final Page nextPage,
final List<StreamInfoItem> streams,
final int index) {
super(serviceId, linkHandler.getUrl(), nextPage, streams, index);
this.linkHandler = linkHandler;
}
public ChannelTabPlayQueue(final int serviceId,
final ListLinkHandler linkHandler) {
this(serviceId, linkHandler, null, Collections.emptyList(), 0);
}
@Override
protected String getTag() {
return "ChannelTabPlayQueue@" + Integer.toHexString(hashCode());
}
@Override
public void fetch() {
if (isInitial) {
ExtractorHelper.getChannelTab(this.serviceId, this.linkHandler, false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getHeadListObserver());
} else {
ExtractorHelper.getMoreChannelTabItems(this.serviceId, this.linkHandler, this.nextPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getNextPageObserver());
}
}
}

View File

@ -19,7 +19,7 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.fragments.BlankFragment;
import org.schabi.newpipe.fragments.list.channel.ChannelVideosFragment;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
import org.schabi.newpipe.fragments.list.kiosk.DefaultKioskFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
@ -432,8 +432,8 @@ public abstract class Tab {
}
@Override
public ChannelVideosFragment getFragment(final Context context) {
return new ChannelVideosFragment(channelServiceId, channelUrl, channelName);
public ChannelFragment getFragment(final Context context) {
return ChannelFragment.getInstance(channelServiceId, channelUrl, channelName);
}
@Override

View File

@ -7,24 +7,58 @@ import androidx.annotation.StringRes;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.linkhandler.ChannelTabs;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import java.util.List;
import java.util.Set;
public final class ChannelTabHelper {
private ChannelTabHelper() {
}
/**
* @param tab the channel tab to check
* @return whether the tab should contain (playable) streams or not
*/
public static boolean isStreamsTab(final String tab) {
switch (tab) {
case ChannelTabs.VIDEOS:
case ChannelTabs.TRACKS:
case ChannelTabs.SHORTS:
case ChannelTabs.LIVESTREAMS:
return true;
}
return false;
}
/**
* @param tab the channel tab link handler to check
* @return whether the tab should contain (playable) streams or not
*/
public static boolean isStreamsTab(final ListLinkHandler tab) {
final List<String> contentFilters = tab.getContentFilters();
if (contentFilters.isEmpty()) {
return false; // this should never happen, but check just to be sure
} else {
return isStreamsTab(contentFilters.get(0));
}
}
@StringRes
private static int getShowTabKey(final String tab) {
switch (tab) {
case ChannelTabs.PLAYLISTS:
return R.string.show_channel_tabs_playlists;
case ChannelTabs.LIVESTREAMS:
return R.string.show_channel_tabs_livestreams;
case ChannelTabs.VIDEOS:
return R.string.show_channel_tabs_videos;
case ChannelTabs.TRACKS:
return R.string.show_channel_tabs_tracks;
case ChannelTabs.SHORTS:
return R.string.show_channel_tabs_shorts;
case ChannelTabs.LIVESTREAMS:
return R.string.show_channel_tabs_livestreams;
case ChannelTabs.CHANNELS:
return R.string.show_channel_tabs_channels;
case ChannelTabs.PLAYLISTS:
return R.string.show_channel_tabs_playlists;
case ChannelTabs.ALBUMS:
return R.string.show_channel_tabs_albums;
}
@ -34,14 +68,18 @@ public final class ChannelTabHelper {
@StringRes
public static int getTranslationKey(final String tab) {
switch (tab) {
case ChannelTabs.PLAYLISTS:
return R.string.channel_tab_playlists;
case ChannelTabs.LIVESTREAMS:
return R.string.channel_tab_livestreams;
case ChannelTabs.VIDEOS:
return R.string.channel_tab_videos;
case ChannelTabs.TRACKS:
return R.string.channel_tab_tracks;
case ChannelTabs.SHORTS:
return R.string.channel_tab_shorts;
case ChannelTabs.LIVESTREAMS:
return R.string.channel_tab_livestreams;
case ChannelTabs.CHANNELS:
return R.string.channel_tab_channels;
case ChannelTabs.PLAYLISTS:
return R.string.channel_tab_playlists;
case ChannelTabs.ALBUMS:
return R.string.channel_tab_albums;
}

View File

@ -36,17 +36,13 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.channel.ChannelTabInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
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.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
@ -129,30 +125,6 @@ public final class ExtractorHelper {
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
}
public static Single<InfoItemsPage<StreamInfoItem>> getMoreChannelItems(final int serviceId,
final String url,
final Page nextPage) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage));
}
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<ChannelTabInfo> getChannelTab(final int serviceId,
final ListLinkHandler listLinkHandler,
final boolean forceLoad) {

View File

@ -275,25 +275,31 @@
<!-- Content & History -->
<string name="show_channel_tabs_key">channel_tabs</string>
<string name="show_channel_tabs_playlists">show_channel_tabs_playlists</string>
<string name="show_channel_tabs_livestreams">show_channel_tabs_live</string>
<string name="show_channel_tabs_videos">show_channel_tabs_videos</string>
<string name="show_channel_tabs_tracks">show_channel_tabs_tracks</string>
<string name="show_channel_tabs_shorts">show_channel_tabs_shorts</string>
<string name="show_channel_tabs_livestreams">show_channel_tabs_live</string>
<string name="show_channel_tabs_channels">show_channel_tabs_channels</string>
<string name="show_channel_tabs_playlists">show_channel_tabs_playlists</string>
<string name="show_channel_tabs_albums">show_channel_tabs_albums</string>
<string name="show_channel_tabs_about">show_channel_tabs_about</string>
<string-array name="show_channel_tabs_value_list">
<item>@string/show_channel_tabs_playlists</item>
<item>@string/show_channel_tabs_livestreams</item>
<item>@string/show_channel_tabs_videos</item>
<item>@string/show_channel_tabs_tracks</item>
<item>@string/show_channel_tabs_shorts</item>
<item>@string/show_channel_tabs_livestreams</item>
<item>@string/show_channel_tabs_channels</item>
<item>@string/show_channel_tabs_playlists</item>
<item>@string/show_channel_tabs_albums</item>
<item>@string/show_channel_tabs_about</item>
</string-array>
<string-array name="show_channel_tabs_description_list">
<item>@string/channel_tab_playlists</item>
<item>@string/channel_tab_livestreams</item>
<item>@string/channel_tab_videos</item>
<item>@string/channel_tab_tracks</item>
<item>@string/channel_tab_shorts</item>
<item>@string/channel_tab_livestreams</item>
<item>@string/channel_tab_channels</item>
<item>@string/channel_tab_playlists</item>
<item>@string/channel_tab_albums</item>
<item>@string/channel_tab_about</item>
</string-array>

View File

@ -798,10 +798,11 @@
<string name="audio_track_type_dubbed">dubbed</string>
<string name="audio_track_type_descriptive">descriptive</string>
<string name="channel_tab_videos">Videos</string>
<string name="channel_tab_livestreams">Live</string>
<string name="channel_tab_tracks">Tracks</string>
<string name="channel_tab_shorts">Shorts</string>
<string name="channel_tab_playlists">Playlists</string>
<string name="channel_tab_livestreams">Live</string>
<string name="channel_tab_channels">Channels</string>
<string name="channel_tab_playlists">Playlists</string>
<string name="channel_tab_albums">Albums</string>
<string name="channel_tab_about">About</string>
<string name="show_channel_tabs">Channel tabs</string>