diff --git a/src/main/java/com/rarchives/ripme/ripper/rippers/CoomerPartyRipper.java b/src/main/java/com/rarchives/ripme/ripper/rippers/CoomerPartyRipper.java
new file mode 100644
index 00000000..ca885da1
--- /dev/null
+++ b/src/main/java/com/rarchives/ripme/ripper/rippers/CoomerPartyRipper.java
@@ -0,0 +1,152 @@
+package com.rarchives.ripme.ripper.rippers;
+
+import com.rarchives.ripme.ripper.AbstractJSONRipper;
+import com.rarchives.ripme.utils.Http;
+import com.rarchives.ripme.utils.Utils;
+
+import org.apache.log4j.Logger;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * See this link for the API schema.
+ */
+public class CoomerPartyRipper extends AbstractJSONRipper {
+ private static final Logger LOGGER = Logger.getLogger(CoomerPartyRipper.class);
+ private static final String IMG_URL_BASE = "https://c3.coomer.su/data";
+ private static final String VID_URL_BASE = "https://c1.coomer.su/data";
+ private static final Pattern IMG_PATTERN = Pattern.compile("^.*\\.(jpg|jpeg|png|gif|apng|webp|tif|tiff)$", Pattern.CASE_INSENSITIVE);
+ private static final Pattern VID_PATTERN = Pattern.compile("^.*\\.(webm|mp4|m4v)$", Pattern.CASE_INSENSITIVE);
+
+ // just so we can return a JSONObject from getFirstPage
+ private static final String KEY_WRAPPER_JSON_ARRAY = "array";
+
+ private static final String KEY_FILE = "file";
+ private static final String KEY_PATH = "path";
+ private static final String KEY_ATTACHMENTS = "attachments";
+
+ private final String service;
+ private final String user;
+
+ public CoomerPartyRipper(URL url) throws IOException {
+ super(url);
+ List pathElements = Arrays.stream(url.getPath().split("/"))
+ .filter(element -> !element.isBlank())
+ .collect(Collectors.toList());
+
+ service = pathElements.get(0);
+ user = pathElements.get(2);
+
+ if (service == null || user == null || service.isBlank() || user.isBlank()) {
+ LOGGER.warn("service=" + service + ", user=" + user);
+ throw new MalformedURLException("Invalid coomer.party URL: " + url);
+ }
+ LOGGER.debug("Parsed service=" + service + " and user=" + user + " from " + url);
+ }
+
+ @Override
+ protected String getDomain() {
+ return "coomer.party";
+ }
+
+ @Override
+ public String getHost() {
+ return "coomer.party";
+ }
+
+ @Override
+ public boolean canRip(URL url) {
+ String host = url.getHost();
+ return host.endsWith("coomer.party") || host.endsWith("coomer.su");
+ }
+
+ @Override
+ public String getGID(URL url) {
+ return Utils.filesystemSafe(String.format("%s_%s", service, user));
+ }
+
+ @Override
+ protected JSONObject getFirstPage() throws IOException {
+ String apiUrl = String.format("https://coomer.su/api/v1/%s/user/%s", service, user);
+ String jsonArrayString = Http.url(apiUrl)
+ .ignoreContentType()
+ .response()
+ .body();
+ JSONArray jsonArray = new JSONArray(jsonArrayString);
+
+ // Ideally we'd just return the JSONArray from here, but we have to wrap it in a JSONObject
+ JSONObject wrapperObject = new JSONObject();
+ wrapperObject.put(KEY_WRAPPER_JSON_ARRAY, jsonArray);
+ return wrapperObject;
+ }
+
+ @Override
+ protected List getURLsFromJSON(JSONObject json) {
+ // extract the array from our wrapper JSONObject
+ JSONArray posts = json.getJSONArray(KEY_WRAPPER_JSON_ARRAY);
+ ArrayList urls = new ArrayList<>();
+ for (int i = 0; i < posts.length(); i++) {
+ JSONObject post = posts.getJSONObject(i);
+ pullFileUrl(post, urls);
+ pullAttachmentUrls(post, urls);
+ }
+ LOGGER.debug("Pulled " + urls.size() + " URLs from " + posts.length() + " posts");
+ return urls;
+ }
+
+ @Override
+ protected void downloadURL(URL url, int index) {
+ addURLToDownload(url, getPrefix(index));
+ }
+
+ private void pullFileUrl(JSONObject post, ArrayList results) {
+ try {
+ JSONObject file = post.getJSONObject(KEY_FILE);
+ String path = file.getString(KEY_PATH);
+ if (isImage(path)) {
+ String url = IMG_URL_BASE + path;
+ results.add(url);
+ } else if (isVideo(path)) {
+ String url = VID_URL_BASE + path;
+ results.add(url);
+ } else {
+ LOGGER.error("Unknown extension for coomer.su path: " + path);
+ }
+ } catch (JSONException e) {
+ /* No-op */
+ }
+ }
+
+ private void pullAttachmentUrls(JSONObject post, ArrayList results) {
+ try {
+ JSONArray attachments = post.getJSONArray(KEY_ATTACHMENTS);
+ for (int i = 0; i < attachments.length(); i++) {
+ JSONObject attachment = attachments.getJSONObject(0);
+ pullFileUrl(attachment, results);
+ }
+ } catch (JSONException e) {
+ /* No-op */
+ }
+ }
+
+ private boolean isImage(String path) {
+ Matcher matcher = IMG_PATTERN.matcher(path);
+ return matcher.matches();
+ }
+
+ private boolean isVideo(String path) {
+ Matcher matcher = VID_PATTERN.matcher(path);
+ return matcher.matches();
+ }
+}
diff --git a/src/test/java/com/rarchives/ripme/tst/ripper/rippers/CoomerPartyRipperTest.java b/src/test/java/com/rarchives/ripme/tst/ripper/rippers/CoomerPartyRipperTest.java
new file mode 100644
index 00000000..ccc26a76
--- /dev/null
+++ b/src/test/java/com/rarchives/ripme/tst/ripper/rippers/CoomerPartyRipperTest.java
@@ -0,0 +1,38 @@
+package com.rarchives.ripme.tst.ripper.rippers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.rarchives.ripme.ripper.rippers.CoomerPartyRipper;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.net.URL;
+
+public class CoomerPartyRipperTest extends RippersTest {
+ @Test
+ public void testRip() throws IOException {
+ URL url = new URL("https://coomer.su/onlyfans/user/soogsx");
+ CoomerPartyRipper ripper = new CoomerPartyRipper(url);
+ testRipper(ripper);
+ }
+
+ @Test
+ public void testUrlParsing() throws IOException {
+ String expectedGid = "onlyfans_soogsx";
+ String[] urls = new String[]{
+ "https://coomer.su/onlyfans/user/soogsx", // normal url
+ "http://coomer.su/onlyfans/user/soogsx", // http, not https
+ "https://coomer.su/onlyfans/user/soogsx/", // with slash at the end
+ "https://coomer.su/onlyfans/user/soogsx?whatever=abc", // with url params
+ "https://coomer.party/onlyfans/user/soogsx", // alternate domain
+ };
+ for (String stringUrl : urls) {
+ URL url = new URL(stringUrl);
+ CoomerPartyRipper ripper = new CoomerPartyRipper(url);
+ assertTrue(ripper.canRip(url));
+ assertEquals(expectedGid, ripper.getGID(url));
+ }
+ }
+}
\ No newline at end of file