fap-dl 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fap_dl-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Reveren
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
fap_dl-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: fap-dl
3
+ Version: 0.1.0
4
+ Summary: A command-line downloader for Fapello profiles.
5
+ Author: Michael Horton
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/Reveren/fap-dl
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: playwright>=1.55
12
+ Requires-Dist: requests>=2.32
13
+ Dynamic: license-file
14
+
15
+ # fap-dl
16
+
17
+ A command-line downloader for images and videos from Fapello profiles.
18
+
19
+ `fap-dl` uses Chromium to discover the contents of a profile, then closes the
20
+ browser and downloads the media directly.
21
+
22
+ ## Features
23
+
24
+ - Downloads images and videos
25
+ - Automatically discovers lazy-loaded posts
26
+ - Detects video posts
27
+ - Streams downloads directly to disk
28
+ - Skips media that has already been downloaded
29
+ - Uses `.part` files for incomplete downloads
30
+ - Separates images and videos by default
31
+ - Supports combined output
32
+ - Supports image-only and video-only downloads
33
+ - Automatically installs the required Playwright Chromium browser when needed
34
+
35
+ ## Requirements
36
+
37
+ - Python 3.10 or newer
38
+ - Internet connection
39
+
40
+ ### Platform support
41
+
42
+ `fap-dl` is currently developed and tested on macOS.
43
+
44
+ The underlying Python, Playwright, Chromium, and Requests components are
45
+ cross-platform, so the program is expected to work on Linux and Windows as
46
+ well. Linux and Windows have not yet been tested with this project.
47
+
48
+ ## Usage
49
+
50
+ Download a profile:
51
+
52
+ ```bash
53
+ fap-dl PROFILE
54
+ ```
55
+
56
+ For example:
57
+
58
+ ```bash
59
+ fap-dl diamondnips-1
60
+ ```
61
+
62
+ You can also provide the full profile URL:
63
+
64
+ ```bash
65
+ fap-dl https://fapello.com/diamondnips-1/
66
+ ```
67
+
68
+ By default, files are stored in:
69
+
70
+ ```text
71
+ ~/Downloads/fap-dl/PROFILE/
72
+ ├── images/
73
+ └── videos/
74
+ ```
75
+
76
+ ### Combined folder
77
+
78
+ ```bash
79
+ fap-dl PROFILE --combined
80
+ ```
81
+
82
+ ### Images only
83
+
84
+ ```bash
85
+ fap-dl PROFILE --images-only
86
+ ```
87
+
88
+ ### Videos only
89
+
90
+ ```bash
91
+ fap-dl PROFILE --videos-only
92
+ ```
93
+
94
+ ### Custom output directory
95
+
96
+ ```bash
97
+ fap-dl PROFILE --output ~/Desktop/media
98
+ ```
99
+
100
+ This creates:
101
+
102
+ ```text
103
+ ~/Desktop/media/PROFILE/
104
+ ```
105
+
106
+ ## Why does Chromium open?
107
+
108
+ Fapello is protected by Cloudflare and may challenge or block ordinary
109
+ automated HTTP requests. During development, headless Chromium was also unable
110
+ to access profile content reliably even when using a previously established
111
+ browser session.
112
+
113
+ For that reason, `fap-dl` briefly launches a normal, visible Chromium window.
114
+ This allows the profile to load in a regular browser environment and also
115
+ allows you to complete a Cloudflare verification challenge if one appears.
116
+
117
+ Chromium is used only for the discovery stage:
118
+
119
+ 1. Chromium opens the requested profile.
120
+ 2. `fap-dl` scrolls through the profile to discover all available posts.
121
+ 3. Images and video posts are identified.
122
+ 4. Browser session information is transferred to the downloader.
123
+ 5. Chromium closes.
124
+ 6. Images and videos are streamed directly to disk without keeping the
125
+ browser open.
126
+
127
+ Most runs should therefore require no interaction with the Chromium window.
128
+ If Fapello presents a verification challenge, complete it in Chromium and
129
+ follow the prompt in the terminal.
130
+
131
+ Browser data is retained in:
132
+
133
+ ```text
134
+ ~/.fap-dl/browser/
135
+ ```
136
+
137
+ This allows browser session information to persist between runs and can reduce
138
+ the need for repeated verification.
139
+
140
+ ## Downloads and existing files
141
+
142
+ Existing non-empty media files are skipped. This makes it possible to run
143
+ `fap-dl` against the same profile later and download newly discovered media
144
+ without downloading the entire collection again.
145
+
146
+ Downloads are streamed directly to disk rather than being held entirely in
147
+ memory.
148
+
149
+ While a file is downloading, it uses the `.part` extension:
150
+
151
+ ```text
152
+ example.mp4.part
153
+ ```
154
+
155
+ After the download completes successfully, it becomes:
156
+
157
+ ```text
158
+ example.mp4
159
+ ```
160
+
161
+ This prevents an interrupted or incomplete download from being mistaken for a
162
+ completed media file.
163
+
164
+ ## Video fallback
165
+
166
+ Video URLs can normally be determined directly from the profile gallery.
167
+
168
+ If a predicted video URL fails, `fap-dl` can reopen Chromium and inspect the
169
+ individual post to locate its actual MP4 source. Multiple failed videos are
170
+ handled in the same fallback browser session rather than opening a separate
171
+ browser for each one.
172
+
173
+ ## Disclaimer
174
+
175
+ This project is not affiliated with or endorsed by Fapello.
176
+
177
+ Users are responsible for ensuring that their use of this software complies
178
+ with applicable laws, copyright restrictions, website terms, and the rights
179
+ of content creators.
180
+
181
+ ## License
182
+
183
+ MIT
fap_dl-0.1.0/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # fap-dl
2
+
3
+ A command-line downloader for images and videos from Fapello profiles.
4
+
5
+ `fap-dl` uses Chromium to discover the contents of a profile, then closes the
6
+ browser and downloads the media directly.
7
+
8
+ ## Features
9
+
10
+ - Downloads images and videos
11
+ - Automatically discovers lazy-loaded posts
12
+ - Detects video posts
13
+ - Streams downloads directly to disk
14
+ - Skips media that has already been downloaded
15
+ - Uses `.part` files for incomplete downloads
16
+ - Separates images and videos by default
17
+ - Supports combined output
18
+ - Supports image-only and video-only downloads
19
+ - Automatically installs the required Playwright Chromium browser when needed
20
+
21
+ ## Requirements
22
+
23
+ - Python 3.10 or newer
24
+ - Internet connection
25
+
26
+ ### Platform support
27
+
28
+ `fap-dl` is currently developed and tested on macOS.
29
+
30
+ The underlying Python, Playwright, Chromium, and Requests components are
31
+ cross-platform, so the program is expected to work on Linux and Windows as
32
+ well. Linux and Windows have not yet been tested with this project.
33
+
34
+ ## Usage
35
+
36
+ Download a profile:
37
+
38
+ ```bash
39
+ fap-dl PROFILE
40
+ ```
41
+
42
+ For example:
43
+
44
+ ```bash
45
+ fap-dl diamondnips-1
46
+ ```
47
+
48
+ You can also provide the full profile URL:
49
+
50
+ ```bash
51
+ fap-dl https://fapello.com/diamondnips-1/
52
+ ```
53
+
54
+ By default, files are stored in:
55
+
56
+ ```text
57
+ ~/Downloads/fap-dl/PROFILE/
58
+ ├── images/
59
+ └── videos/
60
+ ```
61
+
62
+ ### Combined folder
63
+
64
+ ```bash
65
+ fap-dl PROFILE --combined
66
+ ```
67
+
68
+ ### Images only
69
+
70
+ ```bash
71
+ fap-dl PROFILE --images-only
72
+ ```
73
+
74
+ ### Videos only
75
+
76
+ ```bash
77
+ fap-dl PROFILE --videos-only
78
+ ```
79
+
80
+ ### Custom output directory
81
+
82
+ ```bash
83
+ fap-dl PROFILE --output ~/Desktop/media
84
+ ```
85
+
86
+ This creates:
87
+
88
+ ```text
89
+ ~/Desktop/media/PROFILE/
90
+ ```
91
+
92
+ ## Why does Chromium open?
93
+
94
+ Fapello is protected by Cloudflare and may challenge or block ordinary
95
+ automated HTTP requests. During development, headless Chromium was also unable
96
+ to access profile content reliably even when using a previously established
97
+ browser session.
98
+
99
+ For that reason, `fap-dl` briefly launches a normal, visible Chromium window.
100
+ This allows the profile to load in a regular browser environment and also
101
+ allows you to complete a Cloudflare verification challenge if one appears.
102
+
103
+ Chromium is used only for the discovery stage:
104
+
105
+ 1. Chromium opens the requested profile.
106
+ 2. `fap-dl` scrolls through the profile to discover all available posts.
107
+ 3. Images and video posts are identified.
108
+ 4. Browser session information is transferred to the downloader.
109
+ 5. Chromium closes.
110
+ 6. Images and videos are streamed directly to disk without keeping the
111
+ browser open.
112
+
113
+ Most runs should therefore require no interaction with the Chromium window.
114
+ If Fapello presents a verification challenge, complete it in Chromium and
115
+ follow the prompt in the terminal.
116
+
117
+ Browser data is retained in:
118
+
119
+ ```text
120
+ ~/.fap-dl/browser/
121
+ ```
122
+
123
+ This allows browser session information to persist between runs and can reduce
124
+ the need for repeated verification.
125
+
126
+ ## Downloads and existing files
127
+
128
+ Existing non-empty media files are skipped. This makes it possible to run
129
+ `fap-dl` against the same profile later and download newly discovered media
130
+ without downloading the entire collection again.
131
+
132
+ Downloads are streamed directly to disk rather than being held entirely in
133
+ memory.
134
+
135
+ While a file is downloading, it uses the `.part` extension:
136
+
137
+ ```text
138
+ example.mp4.part
139
+ ```
140
+
141
+ After the download completes successfully, it becomes:
142
+
143
+ ```text
144
+ example.mp4
145
+ ```
146
+
147
+ This prevents an interrupted or incomplete download from being mistaken for a
148
+ completed media file.
149
+
150
+ ## Video fallback
151
+
152
+ Video URLs can normally be determined directly from the profile gallery.
153
+
154
+ If a predicted video URL fails, `fap-dl` can reopen Chromium and inspect the
155
+ individual post to locate its actual MP4 source. Multiple failed videos are
156
+ handled in the same fallback browser session rather than opening a separate
157
+ browser for each one.
158
+
159
+ ## Disclaimer
160
+
161
+ This project is not affiliated with or endorsed by Fapello.
162
+
163
+ Users are responsible for ensuring that their use of this software complies
164
+ with applicable laws, copyright restrictions, website terms, and the rights
165
+ of content creators.
166
+
167
+ ## License
168
+
169
+ MIT
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "fap-dl"
7
+ version = "0.1.0"
8
+ description = "A command-line downloader for Fapello profiles."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Michael Horton" }
14
+ ]
15
+ dependencies = [
16
+ "playwright>=1.55",
17
+ "requests>=2.32"
18
+ ]
19
+
20
+ [project.scripts]
21
+ fap-dl = "fap_dl.cli:main"
22
+
23
+ [project.urls]
24
+ Repository = "https://github.com/Reveren/fap-dl"
25
+
26
+ [tool.setuptools.packages.find]
27
+ where = ["src"]
fap_dl-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,194 @@
1
+ from pathlib import Path
2
+ import subprocess
3
+ import sys
4
+
5
+ from playwright.sync_api import (
6
+ Error as PlaywrightError,
7
+ TimeoutError as PlaywrightTimeoutError,
8
+ )
9
+
10
+ from .gallery import get_video_fallback_url
11
+
12
+
13
+ def get_browser_data_dir():
14
+ return Path.home() / ".fap-dl" / "browser"
15
+
16
+
17
+ def install_chromium():
18
+ print()
19
+ print("Chromium is required for browser discovery.")
20
+ print("Installing Chromium for fap-dl...")
21
+ print()
22
+
23
+ try:
24
+ subprocess.run(
25
+ [
26
+ sys.executable,
27
+ "-m",
28
+ "playwright",
29
+ "install",
30
+ "chromium",
31
+ ],
32
+ check=True,
33
+ )
34
+ except subprocess.CalledProcessError as exc:
35
+ raise RuntimeError(
36
+ "Unable to install Chromium automatically. "
37
+ "Run: python -m playwright install chromium"
38
+ ) from exc
39
+
40
+ print()
41
+ print("Chromium installation complete.")
42
+
43
+
44
+ def launch_browser(p):
45
+ browser_data_dir = get_browser_data_dir()
46
+ browser_data_dir.mkdir(
47
+ parents=True,
48
+ exist_ok=True,
49
+ )
50
+
51
+ try:
52
+ return p.chromium.launch_persistent_context(
53
+ user_data_dir=str(browser_data_dir),
54
+ headless=False,
55
+ )
56
+
57
+ except PlaywrightError as exc:
58
+ message = str(exc)
59
+
60
+ # Playwright gives an "Executable doesn't exist"
61
+ # error when its Chromium build has not yet been installed.
62
+ if "Executable doesn't exist" not in message:
63
+ raise
64
+
65
+ install_chromium()
66
+
67
+ return p.chromium.launch_persistent_context(
68
+ user_data_dir=str(browser_data_dir),
69
+ headless=False,
70
+ )
71
+
72
+
73
+ def get_page(context):
74
+ if context.pages:
75
+ return context.pages[0]
76
+
77
+ return context.new_page()
78
+
79
+
80
+ def open_profile(
81
+ p,
82
+ profile_url,
83
+ model,
84
+ post_pattern,
85
+ get_post_count,
86
+ ):
87
+ context = launch_browser(p)
88
+ page = get_page(context)
89
+
90
+ print(f"\nOpening {profile_url}")
91
+
92
+ try:
93
+ page.goto(
94
+ profile_url,
95
+ wait_until="domcontentloaded",
96
+ timeout=60000,
97
+ )
98
+ except PlaywrightTimeoutError:
99
+ print(
100
+ "Initial page load timed out; "
101
+ "checking page anyway."
102
+ )
103
+
104
+ page.wait_for_timeout(2000)
105
+
106
+ initial_count = get_post_count(
107
+ page,
108
+ model,
109
+ post_pattern,
110
+ )
111
+
112
+ if initial_count == 0:
113
+ print()
114
+ print("No profile posts detected yet.")
115
+ print(
116
+ "If browser verification is displayed, "
117
+ "complete it in the browser."
118
+ )
119
+ print()
120
+
121
+ input(
122
+ "Press Enter here when the profile is visible..."
123
+ )
124
+
125
+ page.wait_for_timeout(1000)
126
+
127
+ initial_count = get_post_count(
128
+ page,
129
+ model,
130
+ post_pattern,
131
+ )
132
+
133
+ if initial_count == 0:
134
+ context.close()
135
+
136
+ raise RuntimeError(
137
+ "Still unable to detect profile posts."
138
+ )
139
+
140
+ return context, page
141
+
142
+
143
+ def resolve_video_fallbacks(
144
+ p,
145
+ failed_videos,
146
+ ):
147
+ if not failed_videos:
148
+ return {}
149
+
150
+ print()
151
+ print("Resolving failed video URLs...")
152
+ print("Opening browser for fallback lookup...")
153
+
154
+ context = launch_browser(p)
155
+ page = get_page(context)
156
+
157
+ resolved = {}
158
+
159
+ try:
160
+ for index, item in enumerate(
161
+ failed_videos,
162
+ start=1,
163
+ ):
164
+ post_number = item["post_number"]
165
+ post_url = item["post_url"]
166
+
167
+ print(
168
+ f"[{index}/{len(failed_videos)}] "
169
+ f"Post {post_number}",
170
+ end=" ",
171
+ flush=True,
172
+ )
173
+
174
+ try:
175
+ video_url = get_video_fallback_url(
176
+ page,
177
+ post_url,
178
+ )
179
+
180
+ if video_url:
181
+ resolved[post_number] = video_url
182
+ print("✓ found")
183
+ else:
184
+ print("✗ no MP4 source")
185
+
186
+ except Exception as exc:
187
+ print(f"✗ {exc}")
188
+
189
+ finally:
190
+ context.close()
191
+
192
+ print("Fallback browser closed.")
193
+
194
+ return resolved