threadstractormf 1.4.0__py3-none-any.whl
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.
- threadstractormf/__init__.py +7 -0
- threadstractormf/_backend.py +283 -0
- threadstractormf/_discovery.py +445 -0
- threadstractormf/_parsing.py +244 -0
- threadstractormf/api.py +1269 -0
- threadstractormf/archive.py +262 -0
- threadstractormf/auth.py +319 -0
- threadstractormf/cli.py +426 -0
- threadstractormf/client.py +221 -0
- threadstractormf/downloader.py +353 -0
- threadstractormf/models.py +123 -0
- threadstractormf/rate_limit.py +201 -0
- threadstractormf/template.py +123 -0
- threadstractormf-1.4.0.dist-info/METADATA +163 -0
- threadstractormf-1.4.0.dist-info/RECORD +18 -0
- threadstractormf-1.4.0.dist-info/WHEEL +4 -0
- threadstractormf-1.4.0.dist-info/entry_points.txt +2 -0
- threadstractormf-1.4.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Template rendering for filenames — compatible with gallery-dl / scrapmf.
|
|
2
|
+
|
|
3
|
+
Supports variables: {post_id}, {media_id}, {username}, {category}, {subcategory},
|
|
4
|
+
{extension}, {date:%Y-%m-%d}, {num}, {num:02d}
|
|
5
|
+
and configurable ordering: "{date:%Y-%m-%d}_{post_id}_{num:02d}.{extension}" (chronological)
|
|
6
|
+
or "{post_id}_{num:02d}.{extension}" (post_id only).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
|
|
14
|
+
from threadstractormf.models import sanitize_filename
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def render_filename(
|
|
18
|
+
template: str,
|
|
19
|
+
*,
|
|
20
|
+
post_id: str,
|
|
21
|
+
media_id: str,
|
|
22
|
+
username: str,
|
|
23
|
+
category: str = "threads",
|
|
24
|
+
subcategory: str = "posts",
|
|
25
|
+
extension: str = "jpg",
|
|
26
|
+
num: int = 1,
|
|
27
|
+
date_iso: str | None = None,
|
|
28
|
+
) -> str:
|
|
29
|
+
# parse date
|
|
30
|
+
dt: datetime | None = None
|
|
31
|
+
if date_iso:
|
|
32
|
+
try:
|
|
33
|
+
dt = datetime.fromisoformat(date_iso.replace("Z", "+00:00"))
|
|
34
|
+
except Exception:
|
|
35
|
+
dt = None
|
|
36
|
+
if dt is None:
|
|
37
|
+
dt = datetime.now(timezone.utc)
|
|
38
|
+
|
|
39
|
+
# handle {date:FORMAT} and {date}
|
|
40
|
+
def repl_date(m: re.Match) -> str:
|
|
41
|
+
fmt = m.group(1)
|
|
42
|
+
if fmt:
|
|
43
|
+
# fmt like %Y-%m-%d or %Y
|
|
44
|
+
try:
|
|
45
|
+
return dt.strftime(fmt)
|
|
46
|
+
except Exception:
|
|
47
|
+
return dt.strftime("%Y-%m-%d")
|
|
48
|
+
return dt.strftime("%Y-%m-%d")
|
|
49
|
+
|
|
50
|
+
# replace {date:%Y-%m-%d} or {date}
|
|
51
|
+
template = re.sub(r"\{date(?::([^}]+))?\}", repl_date, template)
|
|
52
|
+
|
|
53
|
+
# handle {num:02d} and {num}
|
|
54
|
+
def repl_num(m: re.Match) -> str:
|
|
55
|
+
fmt = m.group(1)
|
|
56
|
+
if fmt:
|
|
57
|
+
# support 02d, 2d, etc.
|
|
58
|
+
try:
|
|
59
|
+
# extract width
|
|
60
|
+
width_match = re.search(r"0?(\d+)d", fmt)
|
|
61
|
+
if width_match:
|
|
62
|
+
width = int(width_match.group(1))
|
|
63
|
+
return str(num).zfill(width)
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
return str(num)
|
|
67
|
+
return str(num)
|
|
68
|
+
|
|
69
|
+
template = re.sub(r"\{num(?::([^}]+))?\}", repl_num, template)
|
|
70
|
+
|
|
71
|
+
# simple replacements
|
|
72
|
+
replacements = {
|
|
73
|
+
"{post_id}": post_id,
|
|
74
|
+
"{media_id}": media_id,
|
|
75
|
+
"{username}": username,
|
|
76
|
+
"{category}": category,
|
|
77
|
+
"{subcategory}": subcategory,
|
|
78
|
+
"{extension}": extension.lstrip("."),
|
|
79
|
+
"{ext}": extension.lstrip("."),
|
|
80
|
+
}
|
|
81
|
+
for k, v in replacements.items():
|
|
82
|
+
template = template.replace(k, v)
|
|
83
|
+
|
|
84
|
+
# handle {scrapmf_root} / {username} etc. already done
|
|
85
|
+
return template
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def render_directory(
|
|
89
|
+
template_parts: list[str] | str,
|
|
90
|
+
*,
|
|
91
|
+
username: str,
|
|
92
|
+
category: str = "threads",
|
|
93
|
+
subcategory: str = "posts",
|
|
94
|
+
scrapmf_root: str = "default",
|
|
95
|
+
) -> str:
|
|
96
|
+
if isinstance(template_parts, str):
|
|
97
|
+
parts = [template_parts]
|
|
98
|
+
else:
|
|
99
|
+
parts = template_parts
|
|
100
|
+
# {username} comes from the CLI target, which the user types. It must be
|
|
101
|
+
# reduced to a single harmless path component: otherwise
|
|
102
|
+
# `--directory-template "{username}"` with the target `@..` resolves the
|
|
103
|
+
# destination to the parent of --dest, and files land outside it silently.
|
|
104
|
+
#
|
|
105
|
+
# sanitize_filename is reused rather than a second filter being written, so
|
|
106
|
+
# "untrusted text" has one definition in this package. It maps ".." and "."
|
|
107
|
+
# to "", turns slashes into "_", and collapses runs of underscores.
|
|
108
|
+
safe_user = sanitize_filename(username)
|
|
109
|
+
out: list[str] = []
|
|
110
|
+
for p in parts:
|
|
111
|
+
# simple
|
|
112
|
+
p = p.replace("{username}", safe_user)
|
|
113
|
+
p = p.replace("{user}", safe_user)
|
|
114
|
+
# {category}, {subcategory} and {scrapmf_root} are internal fixed values
|
|
115
|
+
# or orchestrator-supplied, so they are left alone: sanitising them would
|
|
116
|
+
# change the documented scrapmf layout for no gain.
|
|
117
|
+
p = p.replace("{category}", category)
|
|
118
|
+
p = p.replace("{subcategory}", subcategory)
|
|
119
|
+
p = p.replace("{scrapmf_root}", scrapmf_root)
|
|
120
|
+
# handle {scrapmf_root} legacy aliases
|
|
121
|
+
p = p.replace("{scarpmf_root}", scrapmf_root)
|
|
122
|
+
out.append(p)
|
|
123
|
+
return "/".join(out)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: threadstractormf
|
|
3
|
+
Version: 1.4.0
|
|
4
|
+
Summary: Python library for Threads - media download by post_id with Netscape cookies auth (gallery-dl compatible)
|
|
5
|
+
Project-URL: Homepage, https://github.com/ExtractorsMF/ThreadstractorMF
|
|
6
|
+
Project-URL: Repository, https://github.com/ExtractorsMF/ThreadstractorMF
|
|
7
|
+
Author: ExtractorsMF
|
|
8
|
+
License: GPL-3.0-only
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: downloader,gallery-dl,scraper,threads,yt-dlp
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx[http2]>=0.27
|
|
21
|
+
Requires-Dist: pydantic>=2.6
|
|
22
|
+
Requires-Dist: rich>=13
|
|
23
|
+
Requires-Dist: typer>=0.12
|
|
24
|
+
Provides-Extra: antibot
|
|
25
|
+
Requires-Dist: curl-cffi>=0.7; extra == 'antibot'
|
|
26
|
+
Provides-Extra: browser
|
|
27
|
+
Requires-Dist: browser-cookie3>=0.20; extra == 'browser'
|
|
28
|
+
Requires-Dist: playwright>=1.43; extra == 'browser'
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: gitpython<3.1.60; extra == 'dev'
|
|
31
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
33
|
+
Requires-Dist: python-semantic-release<10,>=9; extra == 'dev'
|
|
34
|
+
Requires-Dist: respx>=0.22; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# threadstractormf
|
|
39
|
+
|
|
40
|
+
Threads media downloader — Python library + CLI, gallery-dl/yt-dlp compatible cookies.
|
|
41
|
+
|
|
42
|
+
## Project memory
|
|
43
|
+
|
|
44
|
+
- Index: this README + `docs/` + `docs/memory/`.
|
|
45
|
+
- Compact memory: `docs/memory/architecture.md`, `docs/memory/decisions.md`, `docs/memory/state.md`, `docs/memory/conventions.md`, `docs/memory/testing.md`.
|
|
46
|
+
- Detailed docs: `docs/ARCHITECTURE.md`, `docs/DEVELOPMENT.md`, `docs/SECURITY.md`, `docs/PRIVACY.md`, `docs/GIT_WORKFLOW.md`, `docs/DECISIONS.md`.
|
|
47
|
+
- LLM indexes: `llms.txt`, `llms-full.txt`.
|
|
48
|
+
- Agent instructions: `AGENTS.md`.
|
|
49
|
+
- Last documentation review: 2026-11-03.
|
|
50
|
+
|
|
51
|
+
## Stack
|
|
52
|
+
|
|
53
|
+
Python ≥ 3.10, `httpx[http2]`, `pydantic`, `typer`, `rich`. Optional: `curl_cffi` (antibot), `playwright` + `browser-cookie3` (browser).
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv pip install -e ".[dev]" # dev
|
|
59
|
+
uv pip install -e ".[antibot]" # TLS impersonation
|
|
60
|
+
uv pip install -e ".[browser]" # Playwright + cookies
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quickstart
|
|
64
|
+
|
|
65
|
+
Library:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from threadstractormf import Threadscraper
|
|
69
|
+
scraper = Threadscraper(cookies="cookies.txt")
|
|
70
|
+
posts = scraper.get_posts("user", limit=20)
|
|
71
|
+
for post in posts:
|
|
72
|
+
for media in post.media:
|
|
73
|
+
scraper.download(media, dest="./dl", username=post.username, date_iso=post.datetime_iso)
|
|
74
|
+
scraper.download_profile_pic("user", dest="./dl")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
CLI:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
threadstractormf --cookies cookies.txt --dest ./dl @user
|
|
81
|
+
threadstractormf --cookies-from-browser brave --dest ./dl @user
|
|
82
|
+
threadstractormf --cookies cookies.txt --limit 50 --dest ./dl @user
|
|
83
|
+
threadstractormf --cookies cookies.txt --get-urls --dest ./dl @user
|
|
84
|
+
threadstractormf --version
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Modules
|
|
88
|
+
|
|
89
|
+
| Module | Responsibility |
|
|
90
|
+
|---|---|
|
|
91
|
+
| client.py | Facade, wiring, download orchestration |
|
|
92
|
+
| api.py | GraphQL walk + Playwright DOM fallback |
|
|
93
|
+
| _discovery.py | Capture/cache GraphQL template |
|
|
94
|
+
| _backend.py | httpx/curl_cffi backends |
|
|
95
|
+
| auth.py | Netscape/browser cookies |
|
|
96
|
+
| models.py | Post/Media/Profile |
|
|
97
|
+
| template.py | Filename/directory rendering |
|
|
98
|
+
| rate_limit.py | Cooldowns + backoff |
|
|
99
|
+
| archive.py | Download ledger |
|
|
100
|
+
| downloader.py | CDN download + adoption |
|
|
101
|
+
| cli.py | Typer CLI |
|
|
102
|
+
|
|
103
|
+
## Docker
|
|
104
|
+
|
|
105
|
+
Published to GitHub Container Registry (GHCR) on every push to `main`:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# slim (~150 MB): core only, fast GraphQL path
|
|
109
|
+
docker pull ghcr.io/extractorsmf/threadstractormf:slim
|
|
110
|
+
|
|
111
|
+
# full (~1 GB): slim + antibot (curl_cffi) + browser (Playwright)
|
|
112
|
+
docker pull ghcr.io/extractorsmf/threadstractormf:full # also tagged :latest
|
|
113
|
+
docker pull ghcr.io/extractorsmf/threadstractormf:1.2.0
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Run (mount cookies and destination as volumes; never bake secrets into the image):
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
docker run --rm \\
|
|
120
|
+
-v $(pwd)/cookies.txt:/cookies.txt:ro \\
|
|
121
|
+
-v $(pwd)/dl:/dl \\
|
|
122
|
+
ghcr.io/extractorsmf/threadstractormf:full \\
|
|
123
|
+
--cookies /cookies.txt --dest /dl @user
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- `THREADS_USE_SANDBOX=1` is set in the `full` image so Playwright works unprivileged.
|
|
127
|
+
- Multi-arch: `linux/amd64`, `linux/arm64`.
|
|
128
|
+
|
|
129
|
+
## Install from PyPI
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pip install threadstractormf
|
|
133
|
+
pip install 'threadstractormf[browser]'
|
|
134
|
+
pip install 'threadstractormf[antibot]'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Essential commands
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
uv run pytest -q
|
|
141
|
+
uv run ruff check .
|
|
142
|
+
uv run mypy threadstractormf scripts
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Key constraints
|
|
146
|
+
|
|
147
|
+
- Ledger opt-in (`--archive`); keyed by `(post_id, index)`.
|
|
148
|
+
- Browser fallback runs anonymously.
|
|
149
|
+
- GraphQL doc_id is discovered, not hardcoded.
|
|
150
|
+
- stdout = one line per downloaded file; diagnostics on stderr.
|
|
151
|
+
- Do not commit secrets or personal identities.
|
|
152
|
+
|
|
153
|
+
## More details
|
|
154
|
+
|
|
155
|
+
- Usage/CLI details: `docs/DEVELOPMENT.md` (commands), `docs/ARCHITECTURE.md` (flow).
|
|
156
|
+
- Security/privacy: `docs/SECURITY.md`, `docs/PRIVACY.md`.
|
|
157
|
+
- Git/commits: `docs/GIT_WORKFLOW.md`.
|
|
158
|
+
- Decisions: `docs/DECISIONS.md`.
|
|
159
|
+
- Known issues/state: `docs/memory/state.md`.
|
|
160
|
+
|
|
161
|
+
## Status
|
|
162
|
+
|
|
163
|
+
408 tests pass; ruff and mypy clean. Known gaps: duplicate summary line, library `archive` default, relay flag override, profile-pic ext derivation. See `docs/memory/state.md`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
threadstractormf/__init__.py,sha256=jylkYQvBAftktLeKDF94k2fdsWdLy_SVwjSyBbUj7ao,269
|
|
2
|
+
threadstractormf/_backend.py,sha256=G4bQzoR9gBJfMgDJN14rxJf9eFKxlcE7Ut1rlSEVtZU,11185
|
|
3
|
+
threadstractormf/_discovery.py,sha256=_p0_rUEg6MFaQNgerN1mgOHC77wjTylPzzjy59JRS2A,16982
|
|
4
|
+
threadstractormf/_parsing.py,sha256=2ALeN9WkHfakQgBOrGAWjTDHJsfJkE_Vmqg4Z1-_8mU,9501
|
|
5
|
+
threadstractormf/api.py,sha256=Oy58BK-X2Juv3Mp-uhcfeodGivLPw5UZbOPINz3v0ug,58683
|
|
6
|
+
threadstractormf/archive.py,sha256=8sQVZpZjjOt5lFbTsvvEQOlyhcc6KzMOp5QgvHVgMZU,10187
|
|
7
|
+
threadstractormf/auth.py,sha256=1oiI72O_KQItk6-Pw7Xp9SWhVc21GnS_iGOiCv8Mvnc,13310
|
|
8
|
+
threadstractormf/cli.py,sha256=EtbKGtv7sXPVlAffkYD5cXS4rh2iCewq3JEuSiRBl5o,17361
|
|
9
|
+
threadstractormf/client.py,sha256=2J40HZchUNFOhezymqJRkONPdxkYVFPv9y2hJdT2qyE,7690
|
|
10
|
+
threadstractormf/downloader.py,sha256=nlHsYTNQ1fsA1d4nQ2g7RLRHdR1Rv_h6PiRoqI_uT2M,13931
|
|
11
|
+
threadstractormf/models.py,sha256=uxGOhInfYoOA0vFBGuzYKWY6eHkGVRczuXIZkblngko,3958
|
|
12
|
+
threadstractormf/rate_limit.py,sha256=HhEpjAXB7X9HBBfYMidKFw0DAAY3gOyU__3H0Zs4Kj0,6961
|
|
13
|
+
threadstractormf/template.py,sha256=VXu4kOfDyuhf617m13klKfFtkpeJSmFlLWnzteehwWo,4040
|
|
14
|
+
threadstractormf-1.4.0.dist-info/METADATA,sha256=7V46N1XIN1uJgkKnygsNmBZNidDZSnItCnq72C_Rv10,5464
|
|
15
|
+
threadstractormf-1.4.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
16
|
+
threadstractormf-1.4.0.dist-info/entry_points.txt,sha256=4E6c_W81kApVVt_TEqY96GE02w9NAFoGQ2xREBvoiLs,62
|
|
17
|
+
threadstractormf-1.4.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
18
|
+
threadstractormf-1.4.0.dist-info/RECORD,,
|