2018-03-08 14:39:24 +01:00
|
|
|
/*
|
|
|
|
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
|
|
|
|
* SubscriptionsImportService.java is part of NewPipe
|
|
|
|
*
|
|
|
|
* License: GPL-3.0+
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2018-04-15 20:29:58 +02:00
|
|
|
package org.schabi.newpipe.local.subscription.services;
|
2018-03-08 14:39:24 +01:00
|
|
|
|
|
|
|
import android.content.Intent;
|
2020-03-31 19:20:15 +02:00
|
|
|
import android.text.TextUtils;
|
|
|
|
import android.util.Log;
|
|
|
|
|
2019-10-04 14:59:08 +02:00
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
2018-03-08 14:39:24 +01:00
|
|
|
|
|
|
|
import org.reactivestreams.Subscriber;
|
|
|
|
import org.reactivestreams.Subscription;
|
|
|
|
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.subscription.SubscriptionItem;
|
|
|
|
import org.schabi.newpipe.util.Constants;
|
2020-03-29 00:10:48 +01:00
|
|
|
import org.schabi.newpipe.util.ExceptionUtils;
|
2018-03-08 14:39:24 +01:00
|
|
|
import org.schabi.newpipe.util.ExtractorHelper;
|
|
|
|
|
|
|
|
import java.io.File;
|
|
|
|
import java.io.FileInputStream;
|
|
|
|
import java.io.FileNotFoundException;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.io.InputStream;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
import io.reactivex.Flowable;
|
|
|
|
import io.reactivex.Notification;
|
|
|
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
|
|
import io.reactivex.functions.Consumer;
|
|
|
|
import io.reactivex.functions.Function;
|
|
|
|
import io.reactivex.schedulers.Schedulers;
|
|
|
|
|
|
|
|
import static org.schabi.newpipe.MainActivity.DEBUG;
|
|
|
|
|
|
|
|
public class SubscriptionsImportService extends BaseImportExportService {
|
|
|
|
public static final int CHANNEL_URL_MODE = 0;
|
|
|
|
public static final int INPUT_STREAM_MODE = 1;
|
|
|
|
public static final int PREVIOUS_EXPORT_MODE = 2;
|
|
|
|
public static final String KEY_MODE = "key_mode";
|
|
|
|
public static final String KEY_VALUE = "key_value";
|
|
|
|
|
|
|
|
/**
|
2020-03-31 19:20:15 +02:00
|
|
|
* A {@link LocalBroadcastManager local broadcast} will be made with this action
|
|
|
|
* when the import is successfully completed.
|
2018-03-08 14:39:24 +01:00
|
|
|
*/
|
2020-03-31 19:20:15 +02:00
|
|
|
public static final String IMPORT_COMPLETE_ACTION = "org.schabi.newpipe.local.subscription"
|
|
|
|
+ ".services.SubscriptionsImportService.IMPORT_COMPLETE";
|
2020-04-02 13:51:10 +02:00
|
|
|
|
2020-03-31 19:20:15 +02:00
|
|
|
/**
|
|
|
|
* How many extractions running in parallel.
|
|
|
|
*/
|
|
|
|
public static final int PARALLEL_EXTRACTIONS = 8;
|
2020-04-02 13:51:10 +02:00
|
|
|
|
2020-03-31 19:20:15 +02:00
|
|
|
/**
|
|
|
|
* Number of items to buffer to mass-insert in the subscriptions table,
|
|
|
|
* this leads to a better performance as we can then use db transactions.
|
|
|
|
*/
|
|
|
|
public static final int BUFFER_COUNT_BEFORE_INSERT = 50;
|
2020-04-02 13:51:10 +02:00
|
|
|
|
2018-03-08 14:39:24 +01:00
|
|
|
private Subscription subscription;
|
|
|
|
private int currentMode;
|
|
|
|
private int currentServiceId;
|
|
|
|
@Nullable
|
|
|
|
private String channelUrl;
|
|
|
|
@Nullable
|
|
|
|
private InputStream inputStream;
|
|
|
|
|
|
|
|
@Override
|
2020-03-31 19:20:15 +02:00
|
|
|
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
|
|
|
if (intent == null || subscription != null) {
|
|
|
|
return START_NOT_STICKY;
|
|
|
|
}
|
2018-03-08 14:39:24 +01:00
|
|
|
|
|
|
|
currentMode = intent.getIntExtra(KEY_MODE, -1);
|
|
|
|
currentServiceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, Constants.NO_SERVICE_ID);
|
|
|
|
|
|
|
|
if (currentMode == CHANNEL_URL_MODE) {
|
|
|
|
channelUrl = intent.getStringExtra(KEY_VALUE);
|
|
|
|
} else {
|
|
|
|
final String filePath = intent.getStringExtra(KEY_VALUE);
|
|
|
|
if (TextUtils.isEmpty(filePath)) {
|
2020-03-31 19:20:15 +02:00
|
|
|
stopAndReportError(new IllegalStateException(
|
|
|
|
"Importing from input stream, but file path is empty or null"),
|
|
|
|
"Importing subscriptions");
|
2018-03-08 14:39:24 +01:00
|
|
|
return START_NOT_STICKY;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
inputStream = new FileInputStream(new File(filePath));
|
2020-08-16 10:24:58 +02:00
|
|
|
} catch (final FileNotFoundException e) {
|
2018-03-08 14:39:24 +01:00
|
|
|
handleError(e);
|
|
|
|
return START_NOT_STICKY;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (currentMode == -1 || currentMode == CHANNEL_URL_MODE && channelUrl == null) {
|
2020-03-31 19:20:15 +02:00
|
|
|
final String errorDescription = "Some important field is null or in illegal state: "
|
|
|
|
+ "currentMode=[" + currentMode + "], "
|
|
|
|
+ "channelUrl=[" + channelUrl + "], "
|
|
|
|
+ "inputStream=[" + inputStream + "]";
|
|
|
|
stopAndReportError(new IllegalStateException(errorDescription),
|
|
|
|
"Importing subscriptions");
|
2018-03-08 14:39:24 +01:00
|
|
|
return START_NOT_STICKY;
|
|
|
|
}
|
|
|
|
|
|
|
|
startImport();
|
|
|
|
return START_NOT_STICKY;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected int getNotificationId() {
|
|
|
|
return 4568;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int getTitle() {
|
|
|
|
return R.string.import_ongoing;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void disposeAll() {
|
|
|
|
super.disposeAll();
|
2020-03-31 19:20:15 +02:00
|
|
|
if (subscription != null) {
|
|
|
|
subscription.cancel();
|
|
|
|
}
|
2018-03-08 14:39:24 +01:00
|
|
|
}
|
|
|
|
|
2020-04-02 13:51:10 +02:00
|
|
|
/*//////////////////////////////////////////////////////////////////////////
|
|
|
|
// Imports
|
|
|
|
//////////////////////////////////////////////////////////////////////////*/
|
|
|
|
|
2018-03-08 14:39:24 +01:00
|
|
|
private void startImport() {
|
|
|
|
showToast(R.string.import_ongoing);
|
|
|
|
|
|
|
|
Flowable<List<SubscriptionItem>> flowable = null;
|
2018-08-28 20:14:26 +02:00
|
|
|
switch (currentMode) {
|
|
|
|
case CHANNEL_URL_MODE:
|
|
|
|
flowable = importFromChannelUrl();
|
|
|
|
break;
|
|
|
|
case INPUT_STREAM_MODE:
|
|
|
|
flowable = importFromInputStream();
|
|
|
|
break;
|
|
|
|
case PREVIOUS_EXPORT_MODE:
|
|
|
|
flowable = importFromPreviousExport();
|
|
|
|
break;
|
2018-03-08 14:39:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (flowable == null) {
|
2020-03-31 19:20:15 +02:00
|
|
|
final String message = "Flowable given by \"importFrom\" is null "
|
|
|
|
+ "(current mode: " + currentMode + ")";
|
2018-03-08 14:39:24 +01:00
|
|
|
stopAndReportError(new IllegalStateException(message), "Importing subscriptions");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-31 19:20:15 +02:00
|
|
|
flowable.doOnNext(subscriptionItems ->
|
|
|
|
eventListener.onSizeReceived(subscriptionItems.size()))
|
2018-03-08 14:39:24 +01:00
|
|
|
.flatMap(Flowable::fromIterable)
|
|
|
|
|
|
|
|
.parallel(PARALLEL_EXTRACTIONS)
|
|
|
|
.runOn(Schedulers.io())
|
|
|
|
.map((Function<SubscriptionItem, Notification<ChannelInfo>>) subscriptionItem -> {
|
|
|
|
try {
|
|
|
|
return Notification.createOnNext(ExtractorHelper
|
2020-03-31 19:20:15 +02:00
|
|
|
.getChannelInfo(subscriptionItem.getServiceId(),
|
|
|
|
subscriptionItem.getUrl(), true)
|
2018-03-08 14:39:24 +01:00
|
|
|
.blockingGet());
|
2020-08-16 10:24:58 +02:00
|
|
|
} catch (final Throwable e) {
|
2018-03-08 14:39:24 +01:00
|
|
|
return Notification.createOnError(e);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.sequential()
|
|
|
|
|
|
|
|
.observeOn(Schedulers.io())
|
|
|
|
.doOnNext(getNotificationsConsumer())
|
2019-04-28 22:43:54 +02:00
|
|
|
|
2018-03-08 14:39:24 +01:00
|
|
|
.buffer(BUFFER_COUNT_BEFORE_INSERT)
|
|
|
|
.map(upsertBatch())
|
|
|
|
|
|
|
|
.subscribeOn(Schedulers.io())
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
.subscribe(getSubscriber());
|
|
|
|
}
|
|
|
|
|
|
|
|
private Subscriber<List<SubscriptionEntity>> getSubscriber() {
|
|
|
|
return new Subscriber<List<SubscriptionEntity>>() {
|
|
|
|
@Override
|
2020-03-31 19:20:15 +02:00
|
|
|
public void onSubscribe(final Subscription s) {
|
2018-03-08 14:39:24 +01:00
|
|
|
subscription = s;
|
|
|
|
s.request(Long.MAX_VALUE);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2020-03-31 19:20:15 +02:00
|
|
|
public void onNext(final List<SubscriptionEntity> successfulInserted) {
|
|
|
|
if (DEBUG) {
|
|
|
|
Log.d(TAG, "startImport() " + successfulInserted.size()
|
|
|
|
+ " items successfully inserted into the database");
|
|
|
|
}
|
2018-03-08 14:39:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2020-03-31 19:20:15 +02:00
|
|
|
public void onError(final Throwable error) {
|
2019-04-28 22:43:54 +02:00
|
|
|
Log.e(TAG, "Got an error!", error);
|
2018-03-08 14:39:24 +01:00
|
|
|
handleError(error);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onComplete() {
|
2020-03-31 19:20:15 +02:00
|
|
|
LocalBroadcastManager.getInstance(SubscriptionsImportService.this)
|
|
|
|
.sendBroadcast(new Intent(IMPORT_COMPLETE_ACTION));
|
2018-03-08 14:39:24 +01:00
|
|
|
showToast(R.string.import_complete_toast);
|
|
|
|
stopService();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private Consumer<Notification<ChannelInfo>> getNotificationsConsumer() {
|
|
|
|
return notification -> {
|
|
|
|
if (notification.isOnNext()) {
|
2020-08-16 10:24:58 +02:00
|
|
|
final String name = notification.getValue().getName();
|
2018-03-08 14:39:24 +01:00
|
|
|
eventListener.onItemCompleted(!TextUtils.isEmpty(name) ? name : "");
|
|
|
|
} else if (notification.isOnError()) {
|
|
|
|
final Throwable error = notification.getError();
|
|
|
|
final Throwable cause = error.getCause();
|
|
|
|
if (error instanceof IOException) {
|
|
|
|
throw (IOException) error;
|
2020-03-29 00:10:48 +01:00
|
|
|
} else if (cause instanceof IOException) {
|
2018-03-08 14:39:24 +01:00
|
|
|
throw (IOException) cause;
|
2020-03-29 00:10:48 +01:00
|
|
|
} else if (ExceptionUtils.isNetworkRelated(error)) {
|
|
|
|
throw new IOException(error);
|
2018-03-08 14:39:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
eventListener.onItemCompleted("");
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private Function<List<Notification<ChannelInfo>>, List<SubscriptionEntity>> upsertBatch() {
|
|
|
|
return notificationList -> {
|
|
|
|
final List<ChannelInfo> infoList = new ArrayList<>(notificationList.size());
|
2020-08-16 10:24:58 +02:00
|
|
|
for (final Notification<ChannelInfo> n : notificationList) {
|
2020-03-31 19:20:15 +02:00
|
|
|
if (n.isOnNext()) {
|
|
|
|
infoList.add(n.getValue());
|
|
|
|
}
|
2018-03-08 14:39:24 +01:00
|
|
|
}
|
|
|
|
|
2019-04-28 22:43:54 +02:00
|
|
|
return subscriptionManager.upsertAll(infoList);
|
2018-03-08 14:39:24 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private Flowable<List<SubscriptionItem>> importFromChannelUrl() {
|
|
|
|
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
|
|
|
|
.getSubscriptionExtractor()
|
|
|
|
.fromChannelUrl(channelUrl));
|
|
|
|
}
|
|
|
|
|
|
|
|
private Flowable<List<SubscriptionItem>> importFromInputStream() {
|
|
|
|
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
|
|
|
|
.getSubscriptionExtractor()
|
|
|
|
.fromInputStream(inputStream));
|
|
|
|
}
|
|
|
|
|
|
|
|
private Flowable<List<SubscriptionItem>> importFromPreviousExport() {
|
|
|
|
return Flowable.fromCallable(() -> ImportExportJsonHelper.readFrom(inputStream, null));
|
|
|
|
}
|
|
|
|
|
2020-03-31 19:20:15 +02:00
|
|
|
protected void handleError(@NonNull final Throwable error) {
|
2018-03-08 14:39:24 +01:00
|
|
|
super.handleError(R.string.subscriptions_import_unsuccessful, error);
|
|
|
|
}
|
2020-03-31 19:20:15 +02:00
|
|
|
}
|