From bcefcd5ae1d6d41033177b884e729c13440197f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20F=C3=A4hrmann?= Date: Wed, 1 Oct 2025 15:26:34 +0200 Subject: [PATCH] [s3ndpics] add initial support (#8322) --- docs/supportedsites.md | 6 ++ gallery_dl/extractor/__init__.py | 1 + gallery_dl/extractor/s3ndpics.py | 101 +++++++++++++++++++++++++++++ scripts/supportedsites.py | 1 + test/results/s3ndpics.py | 105 +++++++++++++++++++++++++++++++ 5 files changed, 214 insertions(+) create mode 100644 gallery_dl/extractor/s3ndpics.py create mode 100644 test/results/s3ndpics.py diff --git a/docs/supportedsites.md b/docs/supportedsites.md index 250d4d86..e4a06a19 100644 --- a/docs/supportedsites.md +++ b/docs/supportedsites.md @@ -871,6 +871,12 @@ Consider all listed sites to potentially be NSFW. Playlists, Posts, Tag Searches Supported + + S3ND + https://s3nd.pics/ + Posts, Search Results, User Profiles + + Saint https://saint2.su/ diff --git a/gallery_dl/extractor/__init__.py b/gallery_dl/extractor/__init__.py index d57c1074..5b00374f 100644 --- a/gallery_dl/extractor/__init__.py +++ b/gallery_dl/extractor/__init__.py @@ -167,6 +167,7 @@ modules = [ "rule34us", "rule34vault", "rule34xyz", + "s3ndpics", "saint", "sankaku", "sankakucomplex", diff --git a/gallery_dl/extractor/s3ndpics.py b/gallery_dl/extractor/s3ndpics.py new file mode 100644 index 00000000..215f160e --- /dev/null +++ b/gallery_dl/extractor/s3ndpics.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +# Copyright 2025 Mike Fährmann +# +# 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://s3nd.pics/""" + +from .common import Extractor, Message +from .. import text + +BASE_PATTERN = r"(?:https?://)?(?:www\.)?s3nd\.pics" + + +class S3ndpicsExtractor(Extractor): + """Base class for s3ndpics extractors""" + category = "s3ndpics" + root = "https://s3nd.pics" + root_api = f"{root}/api" + directory_fmt = ("{category}", "{user[username]}", + "{date} {title:?/ /}({id})") + filename_fmt = "{num:>02}.{extension}" + archive_fmt = "{id}_{num}" + + def items(self): + base = "https://s3.s3nd.pics/s3nd-pics/" + + for post in self.posts(): + post["id"] = post.pop("_id", None) + post["user"] = post.pop("userId", None) + post["date"] = text.parse_datetime( + post["createdAt"], "%Y-%m-%dT%H:%M:%S.%fZ") + post["date_updated"] = text.parse_datetime( + post["updatedAt"], "%Y-%m-%dT%H:%M:%S.%fZ") + + files = post.pop("files", ()) + post["count"] = len(files) + + yield Message.Directory, post + for post["num"], file in enumerate(files, 1): + post["type"] = file["type"] + path = file["url"] + text.nameext_from_url(path, post) + yield Message.Url, f"{base}{path}", post + + def _pagination(self, url, params): + params["page"] = 1 + + while True: + data = self.request_json(url, params=params) + + self.kwdict["total"] = data["pagination"]["total"] + yield from data["posts"] + + if params["page"] >= data["pagination"]["pages"]: + return + params["page"] += 1 + + +class S3ndpicsPostExtractor(S3ndpicsExtractor): + subcategory = "post" + pattern = rf"{BASE_PATTERN}/post/([0-9a-f]+)" + example = "https://s3nd.pics/post/0123456789abcdef01234567" + + def posts(self): + url = f"{self.root_api}/posts/{self.groups[0]}" + return (self.request_json(url)["post"],) + + +class S3ndpicsUserExtractor(S3ndpicsExtractor): + subcategory = "user" + pattern = rf"{BASE_PATTERN}/user/(\w+)" + example = "https://s3nd.pics/user/USER" + + def posts(self): + url = f"{self.root_api}/users/username/{self.groups[0]}" + self.kwdict["user"] = user = self.request_json(url)["user"] + + url = f"{self.root_api}/posts" + params = { + "userId": user["_id"], + "limit" : "12", + "sortBy": "newest", + } + return self._pagination(url, params) + + +class S3ndpicsSearchExtractor(S3ndpicsExtractor): + subcategory = "search" + pattern = rf"{BASE_PATTERN}/search/?\?([^#]+)" + example = "https://s3nd.pics/search?QUERY" + + def posts(self): + url = f"{self.root_api}/posts" + params = text.parse_query(self.groups[0]) + params.setdefault("limit", "20") + self.kwdict["search_tags"] = \ + params.get("tag") or params.get("tags") or params.get("q") + return self._pagination(url, params) diff --git a/scripts/supportedsites.py b/scripts/supportedsites.py index 456f2144..dc702c3a 100755 --- a/scripts/supportedsites.py +++ b/scripts/supportedsites.py @@ -105,6 +105,7 @@ CATEGORY_MAP = { "kabeuchi" : "かべうち", "mangafire" : "MangaFire", "mangataro" : "MangaTaro", + "s3ndpics" : "S3ND", "schalenetwork" : "Schale Network", "leakgallery" : "Leak Gallery", "livedoor" : "livedoor Blog", diff --git a/test/results/s3ndpics.py b/test/results/s3ndpics.py new file mode 100644 index 00000000..3a629c9d --- /dev/null +++ b/test/results/s3ndpics.py @@ -0,0 +1,105 @@ +# -*- 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. + +from gallery_dl.extractor import s3ndpics + + +__tests__ = ( +{ + "#url" : "https://s3nd.pics/post/68dce8098ef7a9effbdbafa1", + "#class" : s3ndpics.S3ndpicsPostExtractor, + "#pattern" : r"https://s3\.s3nd\.pics/s3nd\-pics/uploads/68be9efde7238de1080bbeec/\d+\-\w+\.(jpe?g|mp4)", + "#count" : 10, + + "id" : "68dce8098ef7a9effbdbafa1", + "title" : "Little meme dump 'o the mornin'", + "count" : 10, + "num" : range(1, 10), + "date" : "dt:2025-10-01 08:36:25", + "date_updated" : "type:datetime", + "type" : {"image", "video"}, + "extension" : {"jpeg", "mp4"}, + "imageProcessed" : True, + "isPublic" : True, + "favorites" : int, + "downvotes" : int, + "likes" : int, + "upvotes" : int, + "views" : int, + "locked" : False, + "lockedAt" : None, + "lockedBy" : None, + "moderatorTags" : [], + "photodnaConfidence": 0, + "photodnaFlagged": False, + "pinWeight" : 0, + "pinned" : False, + "pinnedAt" : None, + "suspendReason" : None, + "suspended" : False, + "suspendedAt" : None, + "suspendedBy" : None, + "description" : """\ +Did you know that penguins do a thing called "pebbling", where they offer a nice rock or pebble to other penguins they like, to make them feel nice and valued? + +Consider then that I am not peddling my stolen memes, but pebbling them to ye wonderful sickos. + +Hope you'll like it!\ +""", + "tags" : [ + "#dump", + "#meme", + ], + "user" : { + "_id" : "68be9efde7238de1080bbeec", + "avatar" : "avatars/68be9efde7238de1080bbeec/1758616570175-avatar.jpg", + "filteredUsers": [], + "username" : "wildscarf", + }, +}, + +{ + "#url" : "https://s3nd.pics/post/68dce8098ef7a9effbdbafa1?tag=%23dump&context=search", + "#class" : s3ndpics.S3ndpicsPostExtractor, +}, + +{ + "#url" : "https://s3nd.pics/user/cloacadeepinadragon", + "#class" : s3ndpics.S3ndpicsUserExtractor, + "#pattern" : r"https://s3\.s3nd\.pics/s3nd\-pics/uploads/.+", + "#count" : 18, + + "date" : "type:datetime", + "date_updated" : "type:datetime", + "description" : str, + "filename" : str, + "extension" : {"jpg", "jpeg", "png"}, + "id" : str, + "title" : str, + "total" : 18, + "type" : "image", + "tags" : list, + "user" : { + "_id" : "68ba04803ffea95858f47613", + "avatar" : "avatars/68ba04803ffea95858f47613/1757021580703-avatar.jpg", + "createdAt" : "2025-09-04T21:28:32.452Z", + "email" : "egk251@gmail.com", + "role" : "user", + "username" : "cloacadeepinadragon", + }, +}, + +{ + "#url" : "https://s3nd.pics/search?tag=%23memes", + "#class" : s3ndpics.S3ndpicsSearchExtractor, + "#pattern" : r"https://s3\.s3nd\.pics/s3nd\-pics/uploads/\w+/.+\.(jpe?g|png|mp4)$", + "#range" : "1-50", + "#count" : 50, + + "search_tags": "#memes", +}, + +)