diff --git a/docs/supportedsites.md b/docs/supportedsites.md
index c7a40659..01326dd0 100644
--- a/docs/supportedsites.md
+++ b/docs/supportedsites.md
@@ -1087,6 +1087,12 @@ Consider all listed sites to potentially be NSFW.
Posts, Tag Searches |
|
+
+ | TheFap |
+ https://thefap.net/ |
+ Models, Posts |
+ |
+
| TikTok |
https://www.tiktok.com/ |
diff --git a/gallery_dl/extractor/__init__.py b/gallery_dl/extractor/__init__.py
index 9f182041..e747c5d9 100644
--- a/gallery_dl/extractor/__init__.py
+++ b/gallery_dl/extractor/__init__.py
@@ -206,6 +206,7 @@ modules = [
"tcbscans",
"telegraph",
"tenor",
+ "thefap",
"thehentaiworld",
"tiktok",
"tmohentai",
diff --git a/gallery_dl/extractor/thefap.py b/gallery_dl/extractor/thefap.py
new file mode 100644
index 00000000..71b68873
--- /dev/null
+++ b/gallery_dl/extractor/thefap.py
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+
+"""Extractors for https://thefap.net/"""
+
+from .common import Extractor, Message
+from .. import text, exception
+
+BASE_PATTERN = r"(?:https?://)?(?:www\.)?thefap\.net"
+
+
+class ThefapExtractor(Extractor):
+ """Base class for thefap extractors"""
+ category = "thefap"
+ root = "https://thefap.net"
+ directory_fmt = ("{category}", "{model_name} ({model_id})")
+ filename_fmt = "{model}_{num:>03}.{extension}"
+ archive_fmt = "{model_id}_{filename}"
+
+ def _normalize_url(self, url):
+ if not url:
+ return ""
+ url = url.strip()
+ if "?w=" in url:
+ url = url[:url.rfind("?")]
+ elif url.endswith(":small"):
+ url = url[:-6] + ":orig"
+ if url.startswith("//"):
+ url = "https:" + url
+ elif url.startswith("/"):
+ url = self.root + url
+ return url
+
+
+class ThefapPostExtractor(ThefapExtractor):
+ """Extractor for individual thefap.net posts"""
+ subcategory = "post"
+ pattern = (BASE_PATTERN +
+ r"(/([^/?#]+)-(\d+)/([^/?#]+)/i(\d+))")
+ example = "https://thefap.net/MODEL-12345/KIND/i12345"
+
+ def items(self):
+ path, model, model_id, kind, post_id = self.groups
+
+ page = self.request(self.root + path).text
+ if "Not Found" in page:
+ raise exception.NotFoundError("post")
+
+ if model_name := text.extr(page, "", " / "):
+ model_name = text.unescape(model_name)
+ else:
+ model_name = text.unquote(model).replace(".", " ")
+
+ data = {
+ "model" : model,
+ "model_id" : text.parse_int(model_id),
+ "model_name": model_name,
+ "kind" : kind,
+ "post_id" : text.parse_int(post_id),
+ "_http_headers": {"Referer": None},
+ }
+ yield Message.Directory, "", data
+
+ data["num"] = 0
+ page = text.extract(
+ page, "\n", "\n", page.index(""))[0]
+ for url in text.extract_iter(page, '
"):
+ model_name = text.unescape(model_name[model_name.find(">")+1:])
+ else:
+ model_name = text.unquote(model).replace(".", " ")
+
+ data = {
+ "model" : model,
+ "model_id" : text.parse_int(model_id),
+ "model_name": model_name,
+ "_http_headers": {"Referer": None},
+ }
+ yield Message.Directory, "", data
+
+ base = f"{self.root}/ajax/model/{model_id}/page-"
+ headers = {
+ "X-Requested-With": "XMLHttpRequest",
+ "Sec-Fetch-Dest" : "empty",
+ "Sec-Fetch-Mode" : "cors",
+ "Sec-Fetch-Site" : "same-origin",
+ }
+
+ page = text.extr(page, '