Implement subscriptions import/export

- Import subscriptions from YouTube and SoundCloud (all services that the extractor support)
- Import/export a JSON representation of the subscriptions
- [Minor] Remove some javax annotations in favor of the one provided by the android support library
This commit is contained in:
Mauricio Colli 2018-03-08 10:39:24 -03:00
parent 7faba84a09
commit 6b5c818cbf
25 changed files with 1126 additions and 117 deletions

View file

@ -33,12 +33,12 @@ import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.fragments.subscription.SubscriptionService;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.playlist.ChannelPlayQueue;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.subscription.SubscriptionService;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
@ -108,11 +108,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
@Override
public void onAttach(Context context) {
super.onAttach(context);
subscriptionService = SubscriptionService.getInstance();
subscriptionService = SubscriptionService.getInstance(activity);
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_channel, container, false);
}

View file

@ -21,8 +21,8 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.fragments.subscription.SubscriptionService;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.subscription.SubscriptionService;
import java.util.Collections;
import java.util.HashSet;
@ -64,7 +64,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
subscriptionService = SubscriptionService.getInstance();
subscriptionService = SubscriptionService.getInstance(activity);
FEED_LOAD_COUNT = howManyItemsToLoad();
}

View file

@ -27,8 +27,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@ -147,7 +145,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
private void onPlaylistSelected(@NonNull LocalPlaylistManager manager,
@NonNull PlaylistMetadataEntry playlist,
@Nonnull List<StreamEntity> streams) {
@NonNull List<StreamEntity> streams) {
if (getStreams() == null) return;
@SuppressLint("ShowToast")

View file

@ -1,147 +0,0 @@
package org.schabi.newpipe.fragments.subscription;
import android.util.Log;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.util.ExtractorHelper;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import io.reactivex.Completable;
import io.reactivex.CompletableSource;
import io.reactivex.Flowable;
import io.reactivex.Maybe;
import io.reactivex.Scheduler;
import io.reactivex.annotations.NonNull;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
/**
* Subscription Service singleton:
* Provides a basis for channel Subscriptions.
* Provides access to subscription table in database as well as
* up-to-date observations on the subscribed channels
*/
public class SubscriptionService {
private static final SubscriptionService sInstance = new SubscriptionService();
public static SubscriptionService getInstance() {
return sInstance;
}
protected final String TAG = "SubscriptionService@" + Integer.toHexString(hashCode());
protected static final boolean DEBUG = MainActivity.DEBUG;
private static final int SUBSCRIPTION_DEBOUNCE_INTERVAL = 500;
private static final int SUBSCRIPTION_THREAD_POOL_SIZE = 4;
private AppDatabase db;
private Flowable<List<SubscriptionEntity>> subscription;
private Scheduler subscriptionScheduler;
private SubscriptionService() {
db = NewPipeDatabase.getInstance();
subscription = getSubscriptionInfos();
final Executor subscriptionExecutor = Executors.newFixedThreadPool(SUBSCRIPTION_THREAD_POOL_SIZE);
subscriptionScheduler = Schedulers.from(subscriptionExecutor);
}
/**
* Part of subscription observation pipeline
*
* @see SubscriptionService#getSubscription()
*/
private Flowable<List<SubscriptionEntity>> getSubscriptionInfos() {
return subscriptionTable().getAll()
// Wait for a period of infrequent updates and return the latest update
.debounce(SUBSCRIPTION_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS)
.share() // Share allows multiple subscribers on the same observable
.replay(1) // Replay synchronizes subscribers to the last emitted result
.autoConnect();
}
/**
* Provides an observer to the latest update to the subscription table.
* <p>
* This observer may be subscribed multiple times, where each subscriber obtains
* the latest synchronized changes available, effectively share the same data
* across all subscribers.
* <p>
* This observer has a debounce cooldown, meaning if multiple updates are observed
* in the cooldown interval, only the latest changes are emitted to the subscribers.
* This reduces the amount of observations caused by frequent updates to the database.
*/
@android.support.annotation.NonNull
public Flowable<List<SubscriptionEntity>> getSubscription() {
return subscription;
}
public Maybe<ChannelInfo> getChannelInfo(final SubscriptionEntity subscriptionEntity) {
if (DEBUG) Log.d(TAG, "getChannelInfo() called with: subscriptionEntity = [" + subscriptionEntity + "]");
return Maybe.fromSingle(ExtractorHelper
.getChannelInfo(subscriptionEntity.getServiceId(), subscriptionEntity.getUrl(), false))
.subscribeOn(subscriptionScheduler);
}
/**
* Returns the database access interface for subscription table.
*/
public SubscriptionDAO subscriptionTable() {
return db.subscriptionDAO();
}
public Completable updateChannelInfo(final ChannelInfo info) {
final Function<List<SubscriptionEntity>, CompletableSource> update = new Function<List<SubscriptionEntity>, CompletableSource>() {
@Override
public CompletableSource apply(@NonNull List<SubscriptionEntity> subscriptionEntities) throws Exception {
if (DEBUG) Log.d(TAG, "updateChannelInfo() called with: subscriptionEntities = [" + subscriptionEntities + "]");
if (subscriptionEntities.size() == 1) {
SubscriptionEntity subscription = subscriptionEntities.get(0);
// Subscriber count changes very often, making this check almost unnecessary.
// Consider removing it later.
if (!isSubscriptionUpToDate(info, subscription)) {
subscription.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount());
return update(subscription);
}
}
return Completable.complete();
}
};
return subscriptionTable().getSubscription(info.getServiceId(), info.getUrl())
.firstOrError()
.flatMapCompletable(update);
}
private Completable update(final SubscriptionEntity updatedSubscription) {
return Completable.fromRunnable(new Runnable() {
@Override
public void run() {
subscriptionTable().update(updatedSubscription);
}
});
}
private boolean isSubscriptionUpToDate(final ChannelInfo info, final SubscriptionEntity entity) {
return info.getUrl().equals(entity.getUrl()) &&
info.getServiceId() == entity.getServiceId() &&
info.getName().equals(entity.getName()) &&
info.getAvatarUrl().equals(entity.getAvatarUrl()) &&
info.getDescription().equals(entity.getDescription()) &&
info.getSubscriberCount() == entity.getSubscriberCount();
}
}