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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ threadstractormf = threadstractormf.cli:app