content: package allen.town.podcast.sync.nextcloud;
  
  import allen.town.podcast.sync.HostnameParser;
  import allen.town.podcast.sync.gpoddernet.mapper.ResponseMapper;
  import allen.town.podcast.sync.gpoddernet.model.GpodnetUploadChangesResponse;
  import allen.town.podcast.sync.model.EpisodeAction;
  import allen.town.podcast.sync.model.EpisodeActionChanges;
  import allen.town.podcast.sync.model.ISyncService;
  import allen.town.podcast.sync.model.SubscriptionChanges;
  import allen.town.podcast.sync.model.SyncServiceException;
  import allen.town.podcast.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 HostnameParser hostname;
      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;
          this.hostname = new HostnameParser(baseHosturl);
      }
  
      @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(hostname.scheme)
                  .host(hostname.host)
                  .port(hostname.port)
                  .addPathSegments(hostname.subfolder + path);
      }
  
      @Override
      public void logout() {
      }
  
      private static class NextcloudGpodderEpisodeActionPostResponse extends UploadChangesResponse {
          public NextcloudGpodderEpisodeActionPostResponse(long epochSecond) {
              super(epochSecond);
          }
      }
  }
  
  