mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-12-22 16:10:31 +00:00
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:
parent
dfbd39e898
commit
c076a0f771
@ -197,7 +197,7 @@ dependencies {
|
|||||||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
// 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/
|
// This works thanks to JitPack: https://jitpack.io/
|
||||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
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'
|
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||||
|
|
||||||
/** Checkstyle **/
|
/** Checkstyle **/
|
||||||
|
@ -65,6 +65,7 @@ import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
|
|||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
|
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
|
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.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
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.PlayerType;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
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.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
|
import org.schabi.newpipe.util.ChannelTabHelper;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.DeviceUtils;
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
@ -1022,7 +1024,16 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||||
} else if (info instanceof ChannelInfo) {
|
} 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) {
|
} else if (info instanceof PlaylistInfo) {
|
||||||
playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
|
playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
|
||||||
} else {
|
} else {
|
||||||
|
@ -16,7 +16,6 @@ import org.schabi.newpipe.extractor.InfoItem;
|
|||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
import org.schabi.newpipe.extractor.Page;
|
import org.schabi.newpipe.extractor.Page;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.views.NewPipeRecyclerView;
|
import org.schabi.newpipe.views.NewPipeRecyclerView;
|
||||||
@ -236,9 +235,7 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
|
|||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
// showEmptyState should be called only if there is no item as
|
// showEmptyState should be called only if there is no item as
|
||||||
// well as no header in infoListAdapter
|
// well as no header in infoListAdapter
|
||||||
if (!(result instanceof ChannelInfo && infoListAdapter.getItemCount() == 1)) {
|
showEmptyState();
|
||||||
showEmptyState();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,10 +277,9 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
}, onError));
|
}, onError));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription,
|
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
|
||||||
final ChannelInfo info) {
|
|
||||||
return (@NonNull Object o) -> {
|
return (@NonNull Object o) -> {
|
||||||
subscriptionManager.insertSubscription(subscription, info);
|
subscriptionManager.insertSubscription(subscription);
|
||||||
return o;
|
return o;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -355,7 +354,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
info.getSubscriberCount());
|
info.getSubscriberCount());
|
||||||
channelSubscription = null;
|
channelSubscription = null;
|
||||||
updateNotifyButton(null);
|
updateNotifyButton(null);
|
||||||
subscribeButtonMonitor = monitorSubscribeButton(mapOnSubscribe(channel, info));
|
subscribeButtonMonitor = monitorSubscribeButton(mapOnSubscribe(channel));
|
||||||
} else {
|
} else {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "Found subscription to this channel!");
|
Log.d(TAG, "Found subscription to this channel!");
|
||||||
@ -451,8 +450,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
tabAdapter.clearAllItems();
|
tabAdapter.clearAllItems();
|
||||||
|
|
||||||
if (currentInfo != null && !channelContentNotSupported) {
|
if (currentInfo != null && !channelContentNotSupported) {
|
||||||
tabAdapter.addFragment(new ChannelVideosFragment(currentInfo), "Videos");
|
|
||||||
|
|
||||||
final Context context = requireContext();
|
final Context context = requireContext();
|
||||||
final SharedPreferences preferences = PreferenceManager
|
final SharedPreferences preferences = PreferenceManager
|
||||||
.getDefaultSharedPreferences(context);
|
.getDefaultSharedPreferences(context);
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -58,7 +58,7 @@ class NotificationHelper(val context: Context) {
|
|||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
|
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
|
||||||
.setGroupSummary(true)
|
.setGroupSummary(true)
|
||||||
.setGroup(data.listInfo.url)
|
.setGroup(data.originalInfo.url)
|
||||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
|
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
|
||||||
|
|
||||||
// Build a summary notification for Android versions < 7.0
|
// Build a summary notification for Android versions < 7.0
|
||||||
@ -73,7 +73,7 @@ class NotificationHelper(val context: Context) {
|
|||||||
context,
|
context,
|
||||||
data.pseudoId,
|
data.pseudoId,
|
||||||
NavigationHelper
|
NavigationHelper
|
||||||
.getChannelIntent(context, data.listInfo.serviceId, data.listInfo.url)
|
.getChannelIntent(context, data.originalInfo.serviceId, data.originalInfo.url)
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
0,
|
0,
|
||||||
false
|
false
|
||||||
@ -88,7 +88,7 @@ class NotificationHelper(val context: Context) {
|
|||||||
|
|
||||||
// Show individual stream notifications, set channel icon only if there is actually
|
// Show individual stream notifications, set channel icon only if there is actually
|
||||||
// one
|
// one
|
||||||
showStreamNotifications(newStreams, data.listInfo.serviceId, bitmap)
|
showStreamNotifications(newStreams, data.originalInfo.serviceId, bitmap)
|
||||||
// Show summary notification
|
// Show summary notification
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ class NotificationHelper(val context: Context) {
|
|||||||
|
|
||||||
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
||||||
// Show individual stream notifications
|
// Show individual stream notifications
|
||||||
showStreamNotifications(newStreams, data.listInfo.serviceId, null)
|
showStreamNotifications(newStreams, data.originalInfo.serviceId, null)
|
||||||
// Show summary notification
|
// Show summary notification
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
||||||
|
@ -13,11 +13,16 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
|||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
import org.schabi.newpipe.database.subscription.NotificationMode
|
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.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
||||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
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.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@ -102,49 +107,88 @@ class FeedLoadManager(private val context: Context) {
|
|||||||
.filter { !cancelSignal.get() }
|
.filter { !cancelSignal.get() }
|
||||||
.map { subscriptionEntity ->
|
.map { subscriptionEntity ->
|
||||||
var error: Throwable? = null
|
var error: Throwable? = null
|
||||||
|
val storeOriginalErrorAndRethrow = { e: Throwable ->
|
||||||
|
// keep original to prevent blockingGet() from wrapping it into RuntimeException
|
||||||
|
error = e
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// check for and load new streams
|
// check for and load new streams
|
||||||
// either by using the dedicated feed method or by getting the channel info
|
// either by using the dedicated feed method or by getting the channel info
|
||||||
val listInfo = if (useFeedExtractor) {
|
var originalInfo: Info? = null
|
||||||
ExtractorHelper
|
var streams: List<StreamInfoItem>? = null
|
||||||
.getFeedInfoFallbackToChannelInfo(
|
val errors = ArrayList<Throwable>()
|
||||||
subscriptionEntity.serviceId,
|
|
||||||
subscriptionEntity.url
|
if (useFeedExtractor) {
|
||||||
)
|
NewPipe.getService(subscriptionEntity.serviceId)
|
||||||
.onErrorReturn {
|
.getFeedExtractor(subscriptionEntity.url)
|
||||||
error = it // store error, otherwise wrapped into RuntimeException
|
?.also { feedExtractor ->
|
||||||
throw it
|
// 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()
|
.blockingGet()
|
||||||
} else {
|
errors.addAll(channelInfo.errors)
|
||||||
ExtractorHelper
|
originalInfo = channelInfo
|
||||||
.getChannelInfo(
|
|
||||||
subscriptionEntity.serviceId,
|
streams = channelInfo.tabs
|
||||||
subscriptionEntity.url,
|
.filter(ChannelTabHelper::isStreamsTab)
|
||||||
true
|
.map {
|
||||||
)
|
Pair(
|
||||||
.onErrorReturn {
|
getChannelTab(subscriptionEntity.serviceId, it, true)
|
||||||
error = it // store error, otherwise wrapped into RuntimeException
|
.onErrorReturn(storeOriginalErrorAndRethrow)
|
||||||
throw it
|
.blockingGet(),
|
||||||
|
it
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.blockingGet()
|
.flatMap { (channelTabInfo, linkHandler) ->
|
||||||
} as ListInfo<StreamInfoItem>
|
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(
|
return@map Notification.createOnNext(
|
||||||
FeedUpdateInfo(
|
FeedUpdateInfo(
|
||||||
subscriptionEntity,
|
subscriptionEntity,
|
||||||
listInfo
|
originalInfo!!,
|
||||||
|
streams!!,
|
||||||
|
errors,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
if (error == null) {
|
|
||||||
// do this to prevent blockingGet() from wrapping into RuntimeException
|
|
||||||
error = e
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
|
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
|
||||||
val wrapper =
|
val wrapper = FeedLoadService.RequestException(
|
||||||
FeedLoadService.RequestException(subscriptionEntity.uid, request, error!!)
|
subscriptionEntity.uid,
|
||||||
|
request,
|
||||||
|
// do this to prevent blockingGet() from wrapping into RuntimeException
|
||||||
|
error ?: e
|
||||||
|
)
|
||||||
return@map Notification.createOnError<FeedUpdateInfo>(wrapper)
|
return@map Notification.createOnError<FeedUpdateInfo>(wrapper)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,24 +247,24 @@ class FeedLoadManager(private val context: Context) {
|
|||||||
for (notification in list) {
|
for (notification in list) {
|
||||||
when {
|
when {
|
||||||
notification.isOnNext -> {
|
notification.isOnNext -> {
|
||||||
val subscriptionId = notification.value!!.uid
|
val info = notification.value!!
|
||||||
val info = notification.value!!.listInfo
|
|
||||||
|
|
||||||
notification.value!!.newStreams = filterNewStreams(
|
notification.value!!.newStreams = filterNewStreams(info.streams)
|
||||||
notification.value!!.listInfo.relatedItems
|
|
||||||
)
|
|
||||||
|
|
||||||
feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
|
feedDatabaseManager.upsertAll(info.uid, info.streams)
|
||||||
subscriptionManager.updateFromInfo(subscriptionId, info)
|
subscriptionManager.updateFromInfo(info.uid, info.originalInfo)
|
||||||
|
|
||||||
if (info.errors.isNotEmpty()) {
|
if (info.errors.isNotEmpty()) {
|
||||||
feedResultsHolder.addErrors(
|
feedResultsHolder.addErrors(
|
||||||
FeedLoadService.RequestException.wrapList(
|
info.errors.map {
|
||||||
subscriptionId,
|
FeedLoadService.RequestException(
|
||||||
info
|
info.uid,
|
||||||
)
|
"${info.originalInfo.serviceId}:${info.originalInfo.url}",
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
feedDatabaseManager.markAsOutdated(subscriptionId)
|
feedDatabaseManager.markAsOutdated(info.uid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notification.isOnError -> {
|
notification.isOnError -> {
|
||||||
|
@ -39,8 +39,6 @@ import org.schabi.newpipe.App
|
|||||||
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.database.feed.model.FeedGroupEntity
|
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.Event.ErrorResultEvent
|
||||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
|
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -126,17 +124,7 @@ class FeedLoadService : Service() {
|
|||||||
// Loading & Handling
|
// Loading & Handling
|
||||||
// /////////////////////////////////////////////////////////////////////////
|
// /////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) {
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// /////////////////////////////////////////////////////////////////////////
|
// /////////////////////////////////////////////////////////////////////////
|
||||||
// Notification
|
// Notification
|
||||||
|
@ -2,7 +2,7 @@ package org.schabi.newpipe.local.feed.service
|
|||||||
|
|
||||||
import org.schabi.newpipe.database.subscription.NotificationMode
|
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||||
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.Info
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
|
||||||
data class FeedUpdateInfo(
|
data class FeedUpdateInfo(
|
||||||
@ -11,24 +11,30 @@ data class FeedUpdateInfo(
|
|||||||
val notificationMode: Int,
|
val notificationMode: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val avatarUrl: String,
|
val avatarUrl: String,
|
||||||
val listInfo: ListInfo<StreamInfoItem>,
|
val originalInfo: Info,
|
||||||
|
val streams: List<StreamInfoItem>,
|
||||||
|
val errors: List<Throwable>,
|
||||||
) {
|
) {
|
||||||
constructor(
|
constructor(
|
||||||
subscription: SubscriptionEntity,
|
subscription: SubscriptionEntity,
|
||||||
listInfo: ListInfo<StreamInfoItem>,
|
originalInfo: Info,
|
||||||
|
streams: List<StreamInfoItem>,
|
||||||
|
errors: List<Throwable>,
|
||||||
) : this(
|
) : this(
|
||||||
uid = subscription.uid,
|
uid = subscription.uid,
|
||||||
notificationMode = subscription.notificationMode,
|
notificationMode = subscription.notificationMode,
|
||||||
name = subscription.name,
|
name = subscription.name,
|
||||||
avatarUrl = subscription.avatarUrl,
|
avatarUrl = subscription.avatarUrl,
|
||||||
listInfo = listInfo,
|
originalInfo = originalInfo,
|
||||||
|
streams = streams,
|
||||||
|
errors = errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integer id, can be used as notification id, etc.
|
* Integer id, can be used as notification id, etc.
|
||||||
*/
|
*/
|
||||||
val pseudoId: Int
|
val pseudoId: Int
|
||||||
get() = listInfo.url.hashCode()
|
get() = originalInfo.url.hashCode()
|
||||||
|
|
||||||
lateinit var newStreams: List<StreamInfoItem>
|
lateinit var newStreams: List<StreamInfoItem>
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.schabi.newpipe.local.subscription
|
package org.schabi.newpipe.local.subscription
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Pair
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Completable
|
import io.reactivex.rxjava3.core.Completable
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
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.NotificationMode
|
||||||
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.Info
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
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.feed.FeedInfo
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.local.feed.FeedDatabaseManager
|
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(
|
val listEntities = subscriptionTable.upsertAll(
|
||||||
infoList.map { SubscriptionEntity.from(it) }
|
infoList.map { SubscriptionEntity.from(it.first) }
|
||||||
)
|
)
|
||||||
|
|
||||||
database.runInTransaction {
|
database.runInTransaction {
|
||||||
infoList.forEachIndexed { index, info ->
|
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
|
return listEntities
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url)
|
fun updateChannelInfo(info: ChannelInfo): Completable =
|
||||||
.flatMapCompletable {
|
subscriptionTable.getSubscription(info.serviceId, info.url)
|
||||||
Completable.fromRunnable {
|
.flatMapCompletable {
|
||||||
it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount)
|
Completable.fromRunnable {
|
||||||
subscriptionTable.update(it)
|
it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount)
|
||||||
feedDatabaseManager.upsertAll(it.uid, info.relatedItems)
|
subscriptionTable.update(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun updateNotificationMode(serviceId: Int, url: String, @NotificationMode mode: Int): Completable {
|
fun updateNotificationMode(serviceId: Int, url: String, @NotificationMode mode: Int): Completable {
|
||||||
return subscriptionTable().getSubscription(serviceId, url)
|
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)
|
val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId)
|
||||||
|
|
||||||
if (info is FeedInfo) {
|
if (info is FeedInfo) {
|
||||||
@ -107,11 +114,8 @@ class SubscriptionManager(context: Context) {
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertSubscription(subscriptionEntity: SubscriptionEntity, info: ChannelInfo) {
|
fun insertSubscription(subscriptionEntity: SubscriptionEntity) {
|
||||||
database.runInTransaction {
|
subscriptionTable.insert(subscriptionEntity)
|
||||||
val subscriptionId = subscriptionTable.insert(subscriptionEntity)
|
|
||||||
feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteSubscription(subscriptionEntity: SubscriptionEntity) {
|
fun deleteSubscription(subscriptionEntity: SubscriptionEntity) {
|
||||||
@ -125,7 +129,10 @@ class SubscriptionManager(context: Context) {
|
|||||||
*/
|
*/
|
||||||
private fun rememberAllStreams(subscription: SubscriptionEntity): Completable {
|
private fun rememberAllStreams(subscription: SubscriptionEntity): Completable {
|
||||||
return ExtractorHelper.getChannelInfo(subscription.serviceId, subscription.url, false)
|
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 ->
|
.flatMapCompletable { entities ->
|
||||||
Completable.fromAction {
|
Completable.fromAction {
|
||||||
database.streamDAO().upsertAll(entities)
|
database.streamDAO().upsertAll(entities)
|
||||||
|
@ -26,6 +26,7 @@ import android.content.Intent;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@ -38,6 +39,7 @@ import org.schabi.newpipe.R;
|
|||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
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.extractor.subscription.SubscriptionItem;
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||||
import org.schabi.newpipe.streams.io.SharpInputStream;
|
import org.schabi.newpipe.streams.io.SharpInputStream;
|
||||||
@ -48,6 +50,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@ -199,12 +202,19 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
|||||||
|
|
||||||
.parallel(PARALLEL_EXTRACTIONS)
|
.parallel(PARALLEL_EXTRACTIONS)
|
||||||
.runOn(Schedulers.io())
|
.runOn(Schedulers.io())
|
||||||
.map((Function<SubscriptionItem, Notification<ChannelInfo>>) subscriptionItem -> {
|
.map((Function<SubscriptionItem, Notification<Pair<ChannelInfo,
|
||||||
|
List<ChannelTabInfo>>>>) subscriptionItem -> {
|
||||||
try {
|
try {
|
||||||
return Notification.createOnNext(ExtractorHelper
|
final ChannelInfo channelInfo = ExtractorHelper
|
||||||
.getChannelInfo(subscriptionItem.getServiceId(),
|
.getChannelInfo(subscriptionItem.getServiceId(),
|
||||||
subscriptionItem.getUrl(), true)
|
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) {
|
} catch (final Throwable e) {
|
||||||
return Notification.createOnError(e);
|
return Notification.createOnError(e);
|
||||||
}
|
}
|
||||||
@ -223,7 +233,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Subscriber<List<SubscriptionEntity>> getSubscriber() {
|
private Subscriber<List<SubscriptionEntity>> getSubscriber() {
|
||||||
return new Subscriber<List<SubscriptionEntity>>() {
|
return new Subscriber<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(final Subscription s) {
|
public void onSubscribe(final Subscription s) {
|
||||||
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 -> {
|
return notification -> {
|
||||||
if (notification.isOnNext()) {
|
if (notification.isOnNext()) {
|
||||||
final String name = notification.getValue().getName();
|
final String name = notification.getValue().first.getName();
|
||||||
eventListener.onItemCompleted(!TextUtils.isEmpty(name) ? name : "");
|
eventListener.onItemCompleted(!TextUtils.isEmpty(name) ? name : "");
|
||||||
} else if (notification.isOnError()) {
|
} else if (notification.isOnError()) {
|
||||||
final Throwable error = notification.getError();
|
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 -> {
|
return notificationList -> {
|
||||||
final List<ChannelInfo> infoList = new ArrayList<>(notificationList.size());
|
final List<Pair<ChannelInfo, List<ChannelTabInfo>>> infoList =
|
||||||
for (final Notification<ChannelInfo> n : notificationList) {
|
new ArrayList<>(notificationList.size());
|
||||||
|
for (final Notification<Pair<ChannelInfo, List<ChannelTabInfo>>> n : notificationList) {
|
||||||
if (n.isOnNext()) {
|
if (n.isOnNext()) {
|
||||||
infoList.add(n.getValue());
|
infoList.add(n.getValue());
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import android.util.Log;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.ListInfo;
|
import org.schabi.newpipe.extractor.ListInfo;
|
||||||
import org.schabi.newpipe.extractor.Page;
|
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.core.SingleObserver;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
abstract class AbstractInfoPlayQueue<T extends ListInfo<? extends InfoItem>>
|
||||||
extends PlayQueue {
|
extends PlayQueue {
|
||||||
boolean isInitial;
|
boolean isInitial;
|
||||||
private boolean isComplete;
|
private boolean isComplete;
|
||||||
@ -27,7 +28,10 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
|||||||
private transient Disposable fetchReactor;
|
private transient Disposable fetchReactor;
|
||||||
|
|
||||||
protected AbstractInfoPlayQueue(final T info) {
|
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,
|
protected AbstractInfoPlayQueue(final int serviceId,
|
||||||
@ -72,7 +76,10 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
|||||||
}
|
}
|
||||||
nextPage = result.getNextPage();
|
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.dispose();
|
||||||
fetchReactor = null;
|
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<>() {
|
return new SingleObserver<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(@NonNull final Disposable d) {
|
public void onSubscribe(@NonNull final Disposable d) {
|
||||||
@ -101,13 +108,16 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(
|
public void onSuccess(
|
||||||
@NonNull final ListExtractor.InfoItemsPage<StreamInfoItem> result) {
|
@NonNull final ListExtractor.InfoItemsPage<? extends InfoItem> result) {
|
||||||
if (!result.hasNextPage()) {
|
if (!result.hasNextPage()) {
|
||||||
isComplete = true;
|
isComplete = true;
|
||||||
}
|
}
|
||||||
nextPage = result.getNextPage();
|
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.dispose();
|
||||||
fetchReactor = null;
|
fetchReactor = null;
|
||||||
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,7 +19,7 @@ import org.schabi.newpipe.extractor.NewPipe;
|
|||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.fragments.BlankFragment;
|
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.DefaultKioskFragment;
|
||||||
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
||||||
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
|
||||||
@ -432,8 +432,8 @@ public abstract class Tab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ChannelVideosFragment getFragment(final Context context) {
|
public ChannelFragment getFragment(final Context context) {
|
||||||
return new ChannelVideosFragment(channelServiceId, channelUrl, channelName);
|
return ChannelFragment.getInstance(channelServiceId, channelUrl, channelName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -7,24 +7,58 @@ import androidx.annotation.StringRes;
|
|||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ChannelTabs;
|
import org.schabi.newpipe.extractor.linkhandler.ChannelTabs;
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public final class ChannelTabHelper {
|
public final class ChannelTabHelper {
|
||||||
private 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
|
@StringRes
|
||||||
private static int getShowTabKey(final String tab) {
|
private static int getShowTabKey(final String tab) {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case ChannelTabs.PLAYLISTS:
|
case ChannelTabs.VIDEOS:
|
||||||
return R.string.show_channel_tabs_playlists;
|
return R.string.show_channel_tabs_videos;
|
||||||
case ChannelTabs.LIVESTREAMS:
|
case ChannelTabs.TRACKS:
|
||||||
return R.string.show_channel_tabs_livestreams;
|
return R.string.show_channel_tabs_tracks;
|
||||||
case ChannelTabs.SHORTS:
|
case ChannelTabs.SHORTS:
|
||||||
return R.string.show_channel_tabs_shorts;
|
return R.string.show_channel_tabs_shorts;
|
||||||
|
case ChannelTabs.LIVESTREAMS:
|
||||||
|
return R.string.show_channel_tabs_livestreams;
|
||||||
case ChannelTabs.CHANNELS:
|
case ChannelTabs.CHANNELS:
|
||||||
return R.string.show_channel_tabs_channels;
|
return R.string.show_channel_tabs_channels;
|
||||||
|
case ChannelTabs.PLAYLISTS:
|
||||||
|
return R.string.show_channel_tabs_playlists;
|
||||||
case ChannelTabs.ALBUMS:
|
case ChannelTabs.ALBUMS:
|
||||||
return R.string.show_channel_tabs_albums;
|
return R.string.show_channel_tabs_albums;
|
||||||
}
|
}
|
||||||
@ -34,14 +68,18 @@ public final class ChannelTabHelper {
|
|||||||
@StringRes
|
@StringRes
|
||||||
public static int getTranslationKey(final String tab) {
|
public static int getTranslationKey(final String tab) {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case ChannelTabs.PLAYLISTS:
|
case ChannelTabs.VIDEOS:
|
||||||
return R.string.channel_tab_playlists;
|
return R.string.channel_tab_videos;
|
||||||
case ChannelTabs.LIVESTREAMS:
|
case ChannelTabs.TRACKS:
|
||||||
return R.string.channel_tab_livestreams;
|
return R.string.channel_tab_tracks;
|
||||||
case ChannelTabs.SHORTS:
|
case ChannelTabs.SHORTS:
|
||||||
return R.string.channel_tab_shorts;
|
return R.string.channel_tab_shorts;
|
||||||
|
case ChannelTabs.LIVESTREAMS:
|
||||||
|
return R.string.channel_tab_livestreams;
|
||||||
case ChannelTabs.CHANNELS:
|
case ChannelTabs.CHANNELS:
|
||||||
return R.string.channel_tab_channels;
|
return R.string.channel_tab_channels;
|
||||||
|
case ChannelTabs.PLAYLISTS:
|
||||||
|
return R.string.channel_tab_playlists;
|
||||||
case ChannelTabs.ALBUMS:
|
case ChannelTabs.ALBUMS:
|
||||||
return R.string.channel_tab_albums;
|
return R.string.channel_tab_albums;
|
||||||
}
|
}
|
||||||
|
@ -36,17 +36,13 @@ import org.schabi.newpipe.R;
|
|||||||
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.MetaInfo;
|
import org.schabi.newpipe.extractor.MetaInfo;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.Page;
|
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.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelTabInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelTabInfo;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
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.kiosk.KioskInfo;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
@ -129,30 +125,6 @@ public final class ExtractorHelper {
|
|||||||
ChannelInfo.getInfo(NewPipe.getService(serviceId), url)));
|
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,
|
public static Single<ChannelTabInfo> getChannelTab(final int serviceId,
|
||||||
final ListLinkHandler listLinkHandler,
|
final ListLinkHandler listLinkHandler,
|
||||||
final boolean forceLoad) {
|
final boolean forceLoad) {
|
||||||
|
@ -275,25 +275,31 @@
|
|||||||
|
|
||||||
<!-- Content & History -->
|
<!-- Content & History -->
|
||||||
<string name="show_channel_tabs_key">channel_tabs</string>
|
<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_videos">show_channel_tabs_videos</string>
|
||||||
<string name="show_channel_tabs_livestreams">show_channel_tabs_live</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_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_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_albums">show_channel_tabs_albums</string>
|
||||||
<string name="show_channel_tabs_about">show_channel_tabs_about</string>
|
<string name="show_channel_tabs_about">show_channel_tabs_about</string>
|
||||||
<string-array name="show_channel_tabs_value_list">
|
<string-array name="show_channel_tabs_value_list">
|
||||||
<item>@string/show_channel_tabs_playlists</item>
|
<item>@string/show_channel_tabs_videos</item>
|
||||||
<item>@string/show_channel_tabs_livestreams</item>
|
<item>@string/show_channel_tabs_tracks</item>
|
||||||
<item>@string/show_channel_tabs_shorts</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_channels</item>
|
||||||
|
<item>@string/show_channel_tabs_playlists</item>
|
||||||
<item>@string/show_channel_tabs_albums</item>
|
<item>@string/show_channel_tabs_albums</item>
|
||||||
<item>@string/show_channel_tabs_about</item>
|
<item>@string/show_channel_tabs_about</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="show_channel_tabs_description_list">
|
<string-array name="show_channel_tabs_description_list">
|
||||||
<item>@string/channel_tab_playlists</item>
|
<item>@string/channel_tab_videos</item>
|
||||||
<item>@string/channel_tab_livestreams</item>
|
<item>@string/channel_tab_tracks</item>
|
||||||
<item>@string/channel_tab_shorts</item>
|
<item>@string/channel_tab_shorts</item>
|
||||||
|
<item>@string/channel_tab_livestreams</item>
|
||||||
<item>@string/channel_tab_channels</item>
|
<item>@string/channel_tab_channels</item>
|
||||||
|
<item>@string/channel_tab_playlists</item>
|
||||||
<item>@string/channel_tab_albums</item>
|
<item>@string/channel_tab_albums</item>
|
||||||
<item>@string/channel_tab_about</item>
|
<item>@string/channel_tab_about</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
@ -798,10 +798,11 @@
|
|||||||
<string name="audio_track_type_dubbed">dubbed</string>
|
<string name="audio_track_type_dubbed">dubbed</string>
|
||||||
<string name="audio_track_type_descriptive">descriptive</string>
|
<string name="audio_track_type_descriptive">descriptive</string>
|
||||||
<string name="channel_tab_videos">Videos</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_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_channels">Channels</string>
|
||||||
|
<string name="channel_tab_playlists">Playlists</string>
|
||||||
<string name="channel_tab_albums">Albums</string>
|
<string name="channel_tab_albums">Albums</string>
|
||||||
<string name="channel_tab_about">About</string>
|
<string name="channel_tab_about">About</string>
|
||||||
<string name="show_channel_tabs">Channel tabs</string>
|
<string name="show_channel_tabs">Channel tabs</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user