summaryrefslogtreecommitdiff
path: root/net/sync
diff options
context:
space:
mode:
authorthrillfall <thrillfall@users.noreply.github.com>2021-10-06 22:12:47 +0200
committerGitHub <noreply@github.com>2021-10-06 22:12:47 +0200
commitbc85ebc806367d863973bc9434e7b0d9d5fd2168 (patch)
tree5a729b84f1a12c3de8d3178ad7d688eb6bb552be /net/sync
parentdab44b68436601f415edb095da605811e985eb00 (diff)
downloadAntennaPod-bc85ebc806367d863973bc9434e7b0d9d5fd2168.zip
Add synchronization with gPodder Nextcloud server app (#5243)
Diffstat (limited to 'net/sync')
-rw-r--r--net/sync/gpoddernet/build.gradle3
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/HostnameParser.java41
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java125
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/mapper/ResponseMapper.java60
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudLoginFlow.java107
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSyncService.java169
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSynchronizationServiceException.java9
7 files changed, 417 insertions, 97 deletions
diff --git a/net/sync/gpoddernet/build.gradle b/net/sync/gpoddernet/build.gradle
index eb5af1b60..77e9ce0f3 100644
--- a/net/sync/gpoddernet/build.gradle
+++ b/net/sync/gpoddernet/build.gradle
@@ -9,4 +9,7 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "org.apache.commons:commons-lang3:$commonslangVersion"
+ implementation 'commons-io:commons-io:2.5'
+ implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
+ implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
}
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/HostnameParser.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/HostnameParser.java
new file mode 100644
index 000000000..ebb415248
--- /dev/null
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/HostnameParser.java
@@ -0,0 +1,41 @@
+package de.danoeh.antennapod.net.sync;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class HostnameParser {
+ public String scheme;
+ public int port;
+ public String host;
+
+ // split into schema, host and port - missing parts are null
+ private static final Pattern URLSPLIT_REGEX = Pattern.compile("(?:(https?)://)?([^:]+)(?::(\\d+))?");
+
+ public HostnameParser(String hosturl) {
+ Matcher m = URLSPLIT_REGEX.matcher(hosturl);
+ if (m.matches()) {
+ scheme = m.group(1);
+ host = m.group(2);
+ if (m.group(3) == null) {
+ port = -1;
+ } else {
+ port = Integer.parseInt(m.group(3)); // regex -> can only be digits
+ }
+ } else {
+ // URL does not match regex: use it anyway -> this will cause an exception on connect
+ scheme = "https";
+ host = hosturl;
+ port = 443;
+ }
+
+ if (scheme == null) { // assume https
+ scheme = "https";
+ }
+
+ if (scheme.equals("https") && port == -1) {
+ port = 443;
+ } else if (scheme.equals("http") && port == -1) {
+ port = 80;
+ }
+ }
+}
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java
index eb18da80b..439a528b7 100644
--- a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java
@@ -1,25 +1,10 @@
package de.danoeh.antennapod.net.sync.gpoddernet;
import android.util.Log;
+
import androidx.annotation.NonNull;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetDevice;
-import de.danoeh.antennapod.net.sync.model.EpisodeAction;
-import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetEpisodeActionPostResponse;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetPodcast;
-import de.danoeh.antennapod.net.sync.model.ISyncService;
-import de.danoeh.antennapod.net.sync.model.SubscriptionChanges;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetTag;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetUploadChangesResponse;
-import de.danoeh.antennapod.net.sync.model.SyncServiceException;
-import de.danoeh.antennapod.net.sync.model.UploadChangesResponse;
-import okhttp3.Credentials;
-import okhttp3.MediaType;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-import okhttp3.ResponseBody;
+
+import de.danoeh.antennapod.net.sync.HostnameParser;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -35,12 +20,28 @@ import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import de.danoeh.antennapod.net.sync.gpoddernet.mapper.ResponseMapper;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetDevice;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetEpisodeActionPostResponse;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetPodcast;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetTag;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetUploadChangesResponse;
+import de.danoeh.antennapod.net.sync.model.EpisodeAction;
+import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges;
+import de.danoeh.antennapod.net.sync.model.ISyncService;
+import de.danoeh.antennapod.net.sync.model.SubscriptionChanges;
+import de.danoeh.antennapod.net.sync.model.SyncServiceException;
+import de.danoeh.antennapod.net.sync.model.UploadChangesResponse;
+import okhttp3.Credentials;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
/**
* Communicates with the gpodder.net service.
@@ -61,43 +62,16 @@ public class GpodnetService implements ISyncService {
private final OkHttpClient httpClient;
- // split into schema, host and port - missing parts are null
- private static final Pattern URLSPLIT_REGEX = Pattern.compile("(?:(https?)://)?([^:]+)(?::(\\d+))?");
-
public GpodnetService(OkHttpClient httpClient, String baseHosturl,
String deviceId, String username, String password) {
this.httpClient = httpClient;
this.deviceId = deviceId;
this.username = username;
this.password = password;
-
- Matcher m = URLSPLIT_REGEX.matcher(baseHosturl);
- if (m.matches()) {
- this.baseScheme = m.group(1);
- this.baseHost = m.group(2);
- if (m.group(3) == null) {
- this.basePort = -1;
- } else {
- this.basePort = Integer.parseInt(m.group(3)); // regex -> can only be digits
- }
- } else {
- // URL does not match regex: use it anyway -> this will cause an exception on connect
- this.baseScheme = "https";
- this.baseHost = baseHosturl;
- this.basePort = 443;
- }
-
- if (this.baseScheme == null) { // assume https
- this.baseScheme = "https";
- }
-
- if (this.baseScheme.equals("https") && this.basePort == -1) {
- this.basePort = 443;
- }
-
- if (this.baseScheme.equals("http") && this.basePort == -1) {
- this.basePort = 80;
- }
+ HostnameParser hostname = new HostnameParser(baseHosturl == null ? DEFAULT_BASE_HOST : baseHosturl);
+ this.baseHost = hostname.host;
+ this.basePort = hostname.port;
+ this.baseScheme = hostname.scheme;
}
private void requireLoggedIn() {
@@ -434,7 +408,7 @@ public class GpodnetService implements ISyncService {
String response = executeRequest(request);
JSONObject changes = new JSONObject(response);
- return readSubscriptionChangesFromJsonObject(changes);
+ return ResponseMapper.readSubscriptionChangesFromJsonObject(changes);
} catch (URISyntaxException e) {
e.printStackTrace();
throw new IllegalStateException(e);
@@ -515,7 +489,7 @@ public class GpodnetService implements ISyncService {
String response = executeRequest(request);
JSONObject json = new JSONObject(response);
- return readEpisodeActionsFromJsonObject(json);
+ return ResponseMapper.readEpisodeActionsFromJsonObject(json);
} catch (URISyntaxException e) {
e.printStackTrace();
throw new IllegalStateException(e);
@@ -526,7 +500,6 @@ public class GpodnetService implements ISyncService {
}
-
/**
* Logs in a specific user. This method must be called if any of the methods
* that require authentication is used.
@@ -689,48 +662,6 @@ public class GpodnetService implements ISyncService {
return new GpodnetDevice(id, caption, type, subscriptions);
}
- private SubscriptionChanges readSubscriptionChangesFromJsonObject(@NonNull JSONObject object)
- throws JSONException {
-
- List<String> added = new LinkedList<>();
- JSONArray jsonAdded = object.getJSONArray("add");
- for (int i = 0; i < jsonAdded.length(); i++) {
- String addedUrl = jsonAdded.getString(i);
- // gpodder escapes colons unnecessarily
- addedUrl = addedUrl.replace("%3A", ":");
- added.add(addedUrl);
- }
-
- List<String> removed = new LinkedList<>();
- JSONArray jsonRemoved = object.getJSONArray("remove");
- for (int i = 0; i < jsonRemoved.length(); i++) {
- String removedUrl = jsonRemoved.getString(i);
- // gpodder escapes colons unnecessarily
- removedUrl = removedUrl.replace("%3A", ":");
- removed.add(removedUrl);
- }
-
- long timestamp = object.getLong("timestamp");
- return new SubscriptionChanges(added, removed, timestamp);
- }
-
- private EpisodeActionChanges readEpisodeActionsFromJsonObject(@NonNull JSONObject object)
- throws JSONException {
-
- List<EpisodeAction> episodeActions = new ArrayList<>();
-
- long timestamp = object.getLong("timestamp");
- JSONArray jsonActions = object.getJSONArray("actions");
- for (int i = 0; i < jsonActions.length(); i++) {
- JSONObject jsonAction = jsonActions.getJSONObject(i);
- EpisodeAction episodeAction = EpisodeAction.readFromJsonObject(jsonAction);
- if (episodeAction != null) {
- episodeActions.add(episodeAction);
- }
- }
- return new EpisodeActionChanges(episodeActions, timestamp);
- }
-
@Override
public void logout() {
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/mapper/ResponseMapper.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/mapper/ResponseMapper.java
new file mode 100644
index 000000000..c8e607d74
--- /dev/null
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/mapper/ResponseMapper.java
@@ -0,0 +1,60 @@
+package de.danoeh.antennapod.net.sync.gpoddernet.mapper;
+
+import androidx.annotation.NonNull;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import de.danoeh.antennapod.net.sync.model.EpisodeAction;
+import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges;
+import de.danoeh.antennapod.net.sync.model.SubscriptionChanges;
+
+public class ResponseMapper {
+
+ public static SubscriptionChanges readSubscriptionChangesFromJsonObject(@NonNull JSONObject object)
+ throws JSONException {
+
+ List<String> added = new LinkedList<>();
+ JSONArray jsonAdded = object.getJSONArray("add");
+ for (int i = 0; i < jsonAdded.length(); i++) {
+ String addedUrl = jsonAdded.getString(i);
+ // gpodder escapes colons unnecessarily
+ addedUrl = addedUrl.replace("%3A", ":");
+ added.add(addedUrl);
+ }
+
+ List<String> removed = new LinkedList<>();
+ JSONArray jsonRemoved = object.getJSONArray("remove");
+ for (int i = 0; i < jsonRemoved.length(); i++) {
+ String removedUrl = jsonRemoved.getString(i);
+ // gpodder escapes colons unnecessarily
+ removedUrl = removedUrl.replace("%3A", ":");
+ removed.add(removedUrl);
+ }
+
+ long timestamp = object.getLong("timestamp");
+ return new SubscriptionChanges(added, removed, timestamp);
+ }
+
+ public static EpisodeActionChanges readEpisodeActionsFromJsonObject(@NonNull JSONObject object)
+ throws JSONException {
+
+ List<EpisodeAction> episodeActions = new ArrayList<>();
+
+ long timestamp = object.getLong("timestamp");
+ JSONArray jsonActions = object.getJSONArray("actions");
+ for (int i = 0; i < jsonActions.length(); i++) {
+ JSONObject jsonAction = jsonActions.getJSONObject(i);
+ EpisodeAction episodeAction = EpisodeAction.readFromJsonObject(jsonAction);
+ if (episodeAction != null) {
+ episodeActions.add(episodeAction);
+ }
+ }
+ return new EpisodeActionChanges(episodeActions, timestamp);
+ }
+}
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudLoginFlow.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudLoginFlow.java
new file mode 100644
index 000000000..b66c44402
--- /dev/null
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudLoginFlow.java
@@ -0,0 +1,107 @@
+package de.danoeh.antennapod.net.sync.nextcloud;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import de.danoeh.antennapod.net.sync.HostnameParser;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import org.json.JSONException;
+import org.json.JSONObject;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
+
+public class NextcloudLoginFlow {
+ private static final String TAG = "NextcloudLoginFlow";
+
+ private final OkHttpClient httpClient;
+ private final HostnameParser hostname;
+ private final Context context;
+ private final AuthenticationCallback callback;
+ private String token;
+ private String endpoint;
+ private Disposable startDisposable;
+ private Disposable pollDisposable;
+
+ public NextcloudLoginFlow(OkHttpClient httpClient, String hostUrl, Context context,
+ AuthenticationCallback callback) {
+ this.httpClient = httpClient;
+ this.hostname = new HostnameParser(hostUrl);
+ this.context = context;
+ this.callback = callback;
+ }
+
+ public void start() {
+ startDisposable = Observable.fromCallable(() -> {
+ URL url = new URI(hostname.scheme, null, hostname.host, hostname.port,
+ "/index.php/login/v2", null, null).toURL();
+ JSONObject result = doRequest(url, "");
+ String loginUrl = result.getString("login");
+ this.token = result.getJSONObject("poll").getString("token");
+ this.endpoint = result.getJSONObject("poll").getString("endpoint");
+ return loginUrl;
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ result -> {
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(result));
+ context.startActivity(browserIntent);
+ poll();
+ }, error -> {
+ Log.e(TAG, Log.getStackTraceString(error));
+ callback.onNextcloudAuthError(error.getLocalizedMessage());
+ });
+ }
+
+ private void poll() {
+ pollDisposable = Observable.fromCallable(() -> doRequest(URI.create(endpoint).toURL(), "token=" + token))
+ .delay(1, TimeUnit.SECONDS)
+ .retry(60 * 10) // 10 minutes
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(result -> {
+ callback.onNextcloudAuthenticated(result.getString("server"),
+ result.getString("loginName"), result.getString("appPassword"));
+ }, Throwable::printStackTrace);
+ }
+
+ public void cancel() {
+ if (startDisposable != null) {
+ startDisposable.dispose();
+ }
+ if (pollDisposable != null) {
+ pollDisposable.dispose();
+ }
+ }
+
+ private JSONObject doRequest(URL url, String bodyContent) throws IOException, JSONException {
+ RequestBody requestBody = RequestBody.create(
+ MediaType.get("application/x-www-form-urlencoded"), bodyContent);
+ Request request = new Request.Builder().url(url).method("POST", requestBody).build();
+ Response response = httpClient.newCall(request).execute();
+ if (response.code() != 200) {
+ throw new IOException("Return code " + response.code());
+ }
+ ResponseBody body = response.body();
+ return new JSONObject(body.string());
+ }
+
+ public interface AuthenticationCallback {
+ void onNextcloudAuthenticated(String server, String username, String password);
+
+ void onNextcloudAuthError(String errorMessage);
+ }
+}
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSyncService.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSyncService.java
new file mode 100644
index 000000000..647a9073c
--- /dev/null
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSyncService.java
@@ -0,0 +1,169 @@
+package de.danoeh.antennapod.net.sync.nextcloud;
+
+import de.danoeh.antennapod.net.sync.HostnameParser;
+import de.danoeh.antennapod.net.sync.gpoddernet.mapper.ResponseMapper;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetUploadChangesResponse;
+import de.danoeh.antennapod.net.sync.model.EpisodeAction;
+import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges;
+import de.danoeh.antennapod.net.sync.model.ISyncService;
+import de.danoeh.antennapod.net.sync.model.SubscriptionChanges;
+import de.danoeh.antennapod.net.sync.model.SyncServiceException;
+import de.danoeh.antennapod.net.sync.model.UploadChangesResponse;
+import okhttp3.Credentials;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.util.HashMap;
+import java.util.List;
+
+public class NextcloudSyncService implements ISyncService {
+ private static final int UPLOAD_BULK_SIZE = 30;
+ private final OkHttpClient httpClient;
+ private final String baseScheme;
+ private final int basePort;
+ private final String baseHost;
+ private final String username;
+ private final String password;
+
+ public NextcloudSyncService(OkHttpClient httpClient, String baseHosturl,
+ String username, String password) {
+ this.httpClient = httpClient;
+ this.username = username;
+ this.password = password;
+ HostnameParser hostname = new HostnameParser(baseHosturl);
+ this.baseHost = hostname.host;
+ this.basePort = hostname.port;
+ this.baseScheme = hostname.scheme;
+ }
+
+ @Override
+ public void login() {
+ }
+
+ @Override
+ public SubscriptionChanges getSubscriptionChanges(long lastSync) throws SyncServiceException {
+ try {
+ HttpUrl.Builder url = makeUrl("/index.php/apps/gpoddersync/subscriptions");
+ url.addQueryParameter("since", "" + lastSync);
+ String responseString = performRequest(url, "GET", null);
+ JSONObject json = new JSONObject(responseString);
+ return ResponseMapper.readSubscriptionChangesFromJsonObject(json);
+ } catch (JSONException | MalformedURLException e) {
+ e.printStackTrace();
+ throw new SyncServiceException(e);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new SyncServiceException(e);
+ }
+ }
+
+ @Override
+ public UploadChangesResponse uploadSubscriptionChanges(List<String> addedFeeds,
+ List<String> removedFeeds)
+ throws NextcloudSynchronizationServiceException {
+ try {
+ HttpUrl.Builder url = makeUrl("/index.php/apps/gpoddersync/subscription_change/create");
+ final JSONObject requestObject = new JSONObject();
+ requestObject.put("add", new JSONArray(addedFeeds));
+ requestObject.put("remove", new JSONArray(removedFeeds));
+ RequestBody requestBody = RequestBody.create(
+ MediaType.get("application/json"), requestObject.toString());
+ performRequest(url, "POST", requestBody);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new NextcloudSynchronizationServiceException(e);
+ }
+
+ return new GpodnetUploadChangesResponse(System.currentTimeMillis() / 1000, new HashMap<>());
+ }
+
+ @Override
+ public EpisodeActionChanges getEpisodeActionChanges(long timestamp) throws SyncServiceException {
+ try {
+ HttpUrl.Builder uri = makeUrl("/index.php/apps/gpoddersync/episode_action");
+ uri.addQueryParameter("since", "" + timestamp);
+ String responseString = performRequest(uri, "GET", null);
+ JSONObject json = new JSONObject(responseString);
+ return ResponseMapper.readEpisodeActionsFromJsonObject(json);
+ } catch (JSONException | MalformedURLException e) {
+ e.printStackTrace();
+ throw new SyncServiceException(e);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new SyncServiceException(e);
+ }
+ }
+
+ @Override
+ public UploadChangesResponse uploadEpisodeActions(List<EpisodeAction> queuedEpisodeActions)
+ throws NextcloudSynchronizationServiceException {
+ for (int i = 0; i < queuedEpisodeActions.size(); i += UPLOAD_BULK_SIZE) {
+ uploadEpisodeActionsPartial(queuedEpisodeActions,
+ i, Math.min(queuedEpisodeActions.size(), i + UPLOAD_BULK_SIZE));
+ }
+ return new NextcloudGpodderEpisodeActionPostResponse(System.currentTimeMillis() / 1000);
+ }
+
+ private void uploadEpisodeActionsPartial(List<EpisodeAction> queuedEpisodeActions, int from, int to)
+ throws NextcloudSynchronizationServiceException {
+ try {
+ final JSONArray list = new JSONArray();
+ for (int i = from; i < to; i++) {
+ EpisodeAction episodeAction = queuedEpisodeActions.get(i);
+ JSONObject obj = episodeAction.writeToJsonObject();
+ if (obj != null) {
+ list.put(obj);
+ }
+ }
+ HttpUrl.Builder url = makeUrl("/index.php/apps/gpoddersync/episode_action/create");
+ RequestBody requestBody = RequestBody.create(
+ MediaType.get("application/json"), list.toString());
+ performRequest(url, "POST", requestBody);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new NextcloudSynchronizationServiceException(e);
+ }
+ }
+
+ private String performRequest(HttpUrl.Builder url, String method, RequestBody body) throws IOException {
+ Request request = new Request.Builder()
+ .url(url.build())
+ .header("Authorization", Credentials.basic(username, password))
+ .header("Accept", "application/json")
+ .method(method, body)
+ .build();
+ Response response = httpClient.newCall(request).execute();
+ if (response.code() != 200) {
+ throw new IOException("Response code: " + response.code());
+ }
+ return response.body().string();
+ }
+
+ private HttpUrl.Builder makeUrl(String path) {
+ return new HttpUrl.Builder()
+ .scheme(baseScheme)
+ .host(baseHost)
+ .port(basePort)
+ .addPathSegments(path);
+ }
+
+ @Override
+ public void logout() {
+ }
+
+ private static class NextcloudGpodderEpisodeActionPostResponse extends UploadChangesResponse {
+ public NextcloudGpodderEpisodeActionPostResponse(long epochSecond) {
+ super(epochSecond);
+ }
+ }
+}
+
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSynchronizationServiceException.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSynchronizationServiceException.java
new file mode 100644
index 000000000..d907c229e
--- /dev/null
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSynchronizationServiceException.java
@@ -0,0 +1,9 @@
+package de.danoeh.antennapod.net.sync.nextcloud;
+
+import de.danoeh.antennapod.net.sync.model.SyncServiceException;
+
+public class NextcloudSynchronizationServiceException extends SyncServiceException {
+ public NextcloudSynchronizationServiceException(Throwable e) {
+ super(e);
+ }
+}