diff --git a/docs/supportedsites.md b/docs/supportedsites.md index 21e0146d..b79d1d9f 100644 --- a/docs/supportedsites.md +++ b/docs/supportedsites.md @@ -343,6 +343,12 @@ Consider all listed sites to potentially be NSFW. Models, Videos, Trending Posts, Popular Videos, Top Models, Posts + + FikFap + https://fikfap.com/ + Posts, User Profiles + + Flickr https://www.flickr.com/ diff --git a/gallery_dl/extractor/__init__.py b/gallery_dl/extractor/__init__.py index 5b9b225e..9fa111a9 100644 --- a/gallery_dl/extractor/__init__.py +++ b/gallery_dl/extractor/__init__.py @@ -67,6 +67,7 @@ modules = [ "fantia", "fapello", "fapachi", + "fikfap", "flickr", "furaffinity", "furry34", diff --git a/gallery_dl/extractor/fikfap.py b/gallery_dl/extractor/fikfap.py new file mode 100644 index 00000000..75071c56 --- /dev/null +++ b/gallery_dl/extractor/fikfap.py @@ -0,0 +1,105 @@ +# -*- 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://fikfap.com/""" + +from .common import Extractor, Message +from .. import text, exception + +BASE_PATTERN = r"(?:https?://)?(?:www\.)?fikfap\.com" + + +class FikfapExtractor(Extractor): + """Base class for fikfap extractors""" + category = "fikfap" + root = "https://fikfap.com" + root_api = "https://api.fikfap.com" + directory_fmt = ("{category}", "{author[username]}") + filename_fmt = "{postId} {label[:240]}.{extension}" + archive_fmt = "{postId}" + + def items(self): + headers = { + "Referer" : self.root + "/", + "Origin" : self.root, + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "cross-site", + } + + for post in self.posts(): + if url := post.get("videoFileOriginalUrl"): + post["extension"] = text.ext_from_url(url) + elif url := post.get("videoStreamUrl"): + url = "ytdl:" + url + post["extension"] = "mp4" + post["_ytdl_manifest"] = "hls" + post["_ytdl_manifest_headers"] = headers + else: + self.log.warning("%s: No video available", post["postId"]) + continue + + post["date"] = self.parse_datetime_iso(post["createdAt"]) + post["date_updated"] = self.parse_datetime_iso(post["updatedAt"]) + post["tags"] = [t["label"] for t in post["hashtags"]] + post["filename"] = post["label"] + + yield Message.Directory, "", post + yield Message.Url, url, post + + def request_api(self, url, params): + return self.request_json(url, params=params, headers={ + "Referer" : self.root + "/", + "Authorization-Anonymous": "2527cc30-c3c5-41be-b8bb-104b6ea7a206", + "IsLoggedIn" : "false", + "IsPWA" : "false", + "Origin" : self.root, + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-site", + }) + + +class FikfapPostExtractor(FikfapExtractor): + subcategory = "post" + pattern = rf"{BASE_PATTERN}/user/(\w+)/post/(\d+)" + example = "https://fikfap.com/user/USER/post/12345" + + def posts(self): + user, pid = self.groups + + url = f"{self.root_api}/profile/username/{user}/posts" + params = {"amount" : "1", "startId": pid} + posts = self.request_api(url, params) + + pid = int(pid) + for post in posts: + if post["postId"] == pid: + return (post,) + raise exception.NotFoundError("post") + + +class FikfapUserExtractor(FikfapExtractor): + subcategory = "user" + pattern = rf"{BASE_PATTERN}/user/(\w+)" + example = "https://fikfap.com/user/USER" + + def posts(self): + user = self.groups[0] + + url = f"{self.root_api}/profile/username/{user}/posts" + params = {"amount": "21"} + + while True: + data = self.request_api(url, params) + + yield from data + + if len(data) < 21: + return + params["afterId"] = data[-1]["postId"] diff --git a/scripts/supportedsites.py b/scripts/supportedsites.py index e3ec4e6d..c2a98dbf 100755 --- a/scripts/supportedsites.py +++ b/scripts/supportedsites.py @@ -61,6 +61,7 @@ CATEGORY_MAP = { "fanbox" : "pixivFANBOX", "fappic" : "Fappic.com", "fashionnova" : "Fashion Nova", + "fikfap" : "FikFap", "furaffinity" : "Fur Affinity", "furry34" : "Furry 34 com", "girlswithmuscle": "Girls with Muscle", diff --git a/test/results/fikfap.py b/test/results/fikfap.py new file mode 100644 index 00000000..c1a013bc --- /dev/null +++ b/test/results/fikfap.py @@ -0,0 +1,122 @@ +# -*- 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 fikfap + + +__tests__ = ( +{ + "#url" : "https://fikfap.com/user/alinevs/post/1429486", + "#class" : fikfap.FikfapPostExtractor, + "#pattern" : r"ytdl:https://vz-\w+-178\.b-cdn\.net/bcdn_token=.+/playlist\.m3u8", + + "algorithm" : "user-posts", + "bunnyVideoId" : "89218ae2-d79a-49a0-abcd-590fd70c9800", + "commentsCount" : int, + "createdAt" : "2025-10-21T00:49:00.306Z", + "date" : "dt:2025-10-21 00:49:00", + "date_updated" : "dt:2025-12-10 01:09:26", + "deletedAt" : None, + "duration" : None, + "explicitnessRating": None, + "extension" : "mp4", + "filename" : "⬇️check my FREE VIP OF ⬇️", + "inCollectionsCount": range(20, 50), + "isBunnyVideoReady": True, + "label" : "⬇️check my FREE VIP OF ⬇️", + "likesCount" : range(500, 2000), + "mediaId" : "b821619e-96a1-49a3-a3f8-a8a3e8432a51", + "postId" : 1429486, + "publishedAt" : "2025-10-21T00:50:37.143Z", + "score" : range(500, 2000), + "sexualOrientation": "STRAIGHT", + "tags" : ["lesbian"], + "thumbnailStreamUrl": str, + "updatedAt" : "2025-12-10T01:09:26.902Z", + "uploadMethod" : "USER_FILE", + "userId" : "32f4c8d6-2409-4db8-9e66-d3b5ff0c1a98", + "videoStreamUrl" : str, + "viewsCount" : range(40_000, 200_000), + "hashtags" : [{ + "createdAt" : "2023-09-05T11:03:49.522Z", + "description" : "Lesbian content only.", + "hashtagId" : "287439f9-3210-42e2-98ea-2c7a86628845", + "isModerated" : True, + "label" : "lesbian", + "labelLower" : "lesbian", + "lastCountUpdatedAt": "iso:dt", + "searchTags" : [], + "thumbnailPostId": 301300, + "updatedAt" : "iso:dt", + "sexualOrientations": [ + "STRAIGHT", + "LESBIAN", + "TRANS", + "OTHER", + ], + }], + "author" : { + "countCollections": 1, + "countIncomingFollows": int, + "countIncomingLikes" : int, + "countOutgoingFollows": int, + "countOutgoingLikes" : int, + "countPosts" : int, + "countTotalViews" : int, + "createdAt" : "2023-05-20T15:45:08.702Z", + "deletedAt" : None, + "description" : "Wanna to see EXTRA? PLS FOLLOW!!⬇️👇", + "isAmbassador" : False, + "isPartner" : True, + "isSignedUp" : True, + "isStaff" : False, + "isVerified" : True, + "lastSeenAt" : "iso:dt", + "roles" : [], + "thumbnailId" : "b4b4f444-71d9-4dcd-91e4-0466ff5b9cdd", + "thumbnailUrl" : str, + "updatedAt" : "iso:dt", + "userId" : "32f4c8d6-2409-4db8-9e66-d3b5ff0c1a98", + "username" : "alinevs", + "profileLinks" : list, + }, + "linkDescription": { + "location" : "POST_DESCRIPTION", + "profileLinkId": "92983607-7e6e-42c8-86f2-5a082f594435", + "type" : "ONLYFANS", + "url" : "https://onlyfans.com/alinevs/trial/kw2zxvoaieh5s7qx0gxynmxxp5ggi1tz", + "userId" : "32f4c8d6-2409-4db8-9e66-d3b5ff0c1a98", + }, + "linkSidebar" : { + "location" : "POST_SIDEBAR", + "profileLinkId": "6172dd36-4fa4-412b-bfc8-24c4c56c2b78", + "type" : "OTHER", + "url" : "https://onlyfans.com/alinevs/trial/kw2zxvoaieh5s7qx0gxynmxxp5ggi1tz", + "userId" : "32f4c8d6-2409-4db8-9e66-d3b5ff0c1a98", + }, +}, + +{ + "#url" : "https://fikfap.com/user/alinevs", + "#class" : fikfap.FikfapUserExtractor, + "#range" : "1-50", + "#pattern" : r"ytdl:https://vz-\w+-\w+\.b\-cdn\.net/bcdn_token=.+/playlist\.m3u8", + "#count" : 50, + + "algorithm" : "user-posts", + "date" : "type:datetime", + "date_updated" : "type:datetime", + "extension" : "mp4", + "filename" : str, + "postId" : int, + "tags" : list, + "hashtags" : list, + "author" : dict, + "linkDescription": dict, + "linkSidebar" : dict, +}, + +)