threads-cleaner 0.2.0__tar.gz → 0.2.2__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.
- {threads_cleaner-0.2.0 → threads_cleaner-0.2.2}/PKG-INFO +6 -5
- {threads_cleaner-0.2.0 → threads_cleaner-0.2.2}/README.md +5 -4
- {threads_cleaner-0.2.0 → threads_cleaner-0.2.2}/pyproject.toml +1 -1
- {threads_cleaner-0.2.0 → threads_cleaner-0.2.2}/src/threads_cleaner/browser.py +71 -35
- {threads_cleaner-0.2.0 → threads_cleaner-0.2.2}/src/threads_cleaner/main.py +26 -6
- {threads_cleaner-0.2.0 → threads_cleaner-0.2.2}/.gitignore +0 -0
- {threads_cleaner-0.2.0 → threads_cleaner-0.2.2}/src/threads_cleaner/__init__.py +0 -0
- {threads_cleaner-0.2.0 → threads_cleaner-0.2.2}/src/threads_cleaner/__main__.py +0 -0
- {threads_cleaner-0.2.0 → threads_cleaner-0.2.2}/src/threads_cleaner/config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: threads-cleaner
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Bulk-delete your Threads posts and replies. Free, open-source, runs locally.
|
|
5
5
|
Project-URL: Homepage, https://github.com/vincentmathis/threads-cleaner
|
|
6
6
|
Project-URL: Repository, https://github.com/vincentmathis/threads-cleaner
|
|
@@ -32,14 +32,14 @@ pip install threads-cleaner
|
|
|
32
32
|
uv add threads-cleaner
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Then install the
|
|
35
|
+
Then install the Chromium browser:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
|
|
39
|
-
# or
|
|
40
|
-
uv run playwright install chromium
|
|
38
|
+
threads-cleaner install-browser
|
|
41
39
|
```
|
|
42
40
|
|
|
41
|
+
(The `install-browser` command runs `playwright install chromium` in your environment.)
|
|
42
|
+
|
|
43
43
|
## Quick start
|
|
44
44
|
|
|
45
45
|
```bash
|
|
@@ -57,6 +57,7 @@ threads-cleaner browser-delete --headed --max 5
|
|
|
57
57
|
|
|
58
58
|
| Command | Description |
|
|
59
59
|
|---------|-------------|
|
|
60
|
+
| `install-browser` | Download the Chromium browser required by Playwright |
|
|
60
61
|
| `browser-login` | Opens a headed browser — log into Threads and session is saved automatically |
|
|
61
62
|
| `browser-delete` | Deletes posts by clicking the Threads UI |
|
|
62
63
|
| `browser-delete --include-replies` | Also delete your replies |
|
|
@@ -11,14 +11,14 @@ pip install threads-cleaner
|
|
|
11
11
|
uv add threads-cleaner
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Then install the
|
|
14
|
+
Then install the Chromium browser:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
|
|
18
|
-
# or
|
|
19
|
-
uv run playwright install chromium
|
|
17
|
+
threads-cleaner install-browser
|
|
20
18
|
```
|
|
21
19
|
|
|
20
|
+
(The `install-browser` command runs `playwright install chromium` in your environment.)
|
|
21
|
+
|
|
22
22
|
## Quick start
|
|
23
23
|
|
|
24
24
|
```bash
|
|
@@ -36,6 +36,7 @@ threads-cleaner browser-delete --headed --max 5
|
|
|
36
36
|
|
|
37
37
|
| Command | Description |
|
|
38
38
|
|---------|-------------|
|
|
39
|
+
| `install-browser` | Download the Chromium browser required by Playwright |
|
|
39
40
|
| `browser-login` | Opens a headed browser — log into Threads and session is saved automatically |
|
|
40
41
|
| `browser-delete` | Deletes posts by clicking the Threads UI |
|
|
41
42
|
| `browser-delete --include-replies` | Also delete your replies |
|
|
@@ -37,10 +37,18 @@ class BrowserDeleter:
|
|
|
37
37
|
def start(self):
|
|
38
38
|
from playwright.sync_api import sync_playwright
|
|
39
39
|
self._pw = sync_playwright().start()
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
try:
|
|
41
|
+
self._browser = self._pw.chromium.launch(
|
|
42
|
+
headless=not self._headed,
|
|
43
|
+
args=["--disable-blink-features=AutomationControlled", "--no-sandbox"],
|
|
44
|
+
)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
msg = str(e)
|
|
47
|
+
if "Executable doesn't exist" in msg or "executable" in msg.lower() and "playwright" in msg.lower():
|
|
48
|
+
console.print("[red]Chromium browser not found.[/]")
|
|
49
|
+
console.print("Run: [bold]threads-cleaner install-browser[/]")
|
|
50
|
+
raise RuntimeError("Playwright browser not installed") from e
|
|
51
|
+
raise
|
|
44
52
|
self._context = self._browser.new_context(
|
|
45
53
|
viewport={"width": 1280, "height": 720},
|
|
46
54
|
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
@@ -98,7 +106,7 @@ class BrowserDeleter:
|
|
|
98
106
|
for (const el of all) {
|
|
99
107
|
if (el.offsetParent === null) continue;
|
|
100
108
|
const txt = el.innerText || el.textContent || '';
|
|
101
|
-
if (txt.trim() === 'Delete'
|
|
109
|
+
if (txt.trim() === 'Delete') {
|
|
102
110
|
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
|
103
111
|
return true;
|
|
104
112
|
}
|
|
@@ -126,8 +134,8 @@ class BrowserDeleter:
|
|
|
126
134
|
for (const el of all) {
|
|
127
135
|
if (el.offsetParent === null) continue;
|
|
128
136
|
const txt = el.innerText || el.textContent || '';
|
|
129
|
-
if (txt.trim() === 'Delete'
|
|
130
|
-
if (el.closest('[role="dialog"]')) {
|
|
137
|
+
if (txt.trim() === 'Delete') {
|
|
138
|
+
if (el.closest('[role="dialog"], [role="alertdialog"]')) {
|
|
131
139
|
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
|
132
140
|
return true;
|
|
133
141
|
}
|
|
@@ -137,6 +145,20 @@ class BrowserDeleter:
|
|
|
137
145
|
})()
|
|
138
146
|
""")
|
|
139
147
|
|
|
148
|
+
def _mark_current_more_tried(self):
|
|
149
|
+
self._page.evaluate("""
|
|
150
|
+
const icons = document.querySelectorAll('svg[aria-label="More"]');
|
|
151
|
+
for (const icon of icons) {
|
|
152
|
+
if (icon.offsetParent === null) continue;
|
|
153
|
+
if (icon.dataset.tried) continue;
|
|
154
|
+
const r = icon.getBoundingClientRect();
|
|
155
|
+
if (r.width < 5 || r.height < 5) continue;
|
|
156
|
+
if (r.y < 100) continue;
|
|
157
|
+
icon.dataset.tried = '1';
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
""")
|
|
161
|
+
|
|
140
162
|
def _dismiss_toasts(self):
|
|
141
163
|
try:
|
|
142
164
|
self._page.keyboard.press("Escape")
|
|
@@ -168,11 +190,13 @@ class BrowserDeleter:
|
|
|
168
190
|
if not self._click_menu_delete():
|
|
169
191
|
self._page.keyboard.press("Escape")
|
|
170
192
|
time.sleep(0.3)
|
|
193
|
+
self._mark_current_more_tried()
|
|
171
194
|
return False
|
|
172
195
|
time.sleep(0.8)
|
|
173
196
|
if not self._click_confirm_delete():
|
|
174
197
|
self._page.keyboard.press("Escape")
|
|
175
198
|
time.sleep(0.3)
|
|
199
|
+
self._mark_current_more_tried()
|
|
176
200
|
return False
|
|
177
201
|
time.sleep(1.5)
|
|
178
202
|
had_error = self._has_error_toast()
|
|
@@ -240,6 +264,10 @@ class BrowserDeleter:
|
|
|
240
264
|
except:
|
|
241
265
|
pass
|
|
242
266
|
time.sleep(4)
|
|
267
|
+
if "login" in self._page.url.lower():
|
|
268
|
+
console.print(f"[red]Not logged in (current URL: {self._page.url}).[/]")
|
|
269
|
+
console.print("[yellow]Run [bold]threads-cleaner browser-login[/] to refresh the session.[/]")
|
|
270
|
+
raise RuntimeError("Session expired or invalid")
|
|
243
271
|
self._dismiss_popups()
|
|
244
272
|
return self._delete_loop("replies", max_deletes)
|
|
245
273
|
|
|
@@ -275,34 +303,42 @@ def run_browser_delete(*, include_replies=False, max_deletes=None, dry_run=False
|
|
|
275
303
|
def run_browser_login() -> dict:
|
|
276
304
|
from playwright.sync_api import sync_playwright, TimeoutError as PwTimeout
|
|
277
305
|
console.print("[bold]Threads Cleaner - Browser Login[/bold]\n")
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
306
|
+
try:
|
|
307
|
+
with sync_playwright() as pw:
|
|
308
|
+
browser = pw.chromium.launch(headless=False, args=["--no-sandbox"])
|
|
309
|
+
ctx = browser.new_context(viewport={"width": 1280, "height": 800})
|
|
310
|
+
page = ctx.new_page()
|
|
311
|
+
page.goto("about:blank")
|
|
312
|
+
console.print("[yellow]Go to [bold]https://www.threads.net/[/bold] -> log in -> go to your profile -> wait[/]")
|
|
313
|
+
try:
|
|
314
|
+
page.wait_for_url("**/***@**", timeout=600000)
|
|
315
|
+
except PwTimeout:
|
|
316
|
+
browser.close()
|
|
317
|
+
raise RuntimeError("Login timed out")
|
|
318
|
+
time.sleep(2)
|
|
319
|
+
cookies_raw = ctx.cookies()
|
|
320
|
+
cookies = {c["name"]: c["value"] for c in cookies_raw if c["name"] in ("sessionid", "csrftoken", "ds_user_id", "rur")}
|
|
321
|
+
if not cookies.get("sessionid"):
|
|
322
|
+
browser.close()
|
|
323
|
+
raise RuntimeError("No session cookie")
|
|
324
|
+
sessionid = cookies["sessionid"]
|
|
325
|
+
csrftoken = cookies.get("csrftoken", "")
|
|
326
|
+
ds_user_id = cookies.get("ds_user_id", "")
|
|
327
|
+
cookies["sessionid"] = sessionid
|
|
328
|
+
cookies["csrftoken"] = csrftoken
|
|
329
|
+
cookies["ds_user_id"] = ds_user_id
|
|
330
|
+
# Extract username from the URL
|
|
331
|
+
username = page.url.split("/@")[-1].split("/")[0].split("?")[0]
|
|
332
|
+
user_id = ds_user_id or ""
|
|
333
|
+
session = {"sessionid": sessionid, "csrftoken": csrftoken, "ds_user_id": user_id, "rur": cookies.get("rur", ""), "user_id": user_id, "username": username}
|
|
334
|
+
config.save_session(session)
|
|
293
335
|
browser.close()
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
# Extract username from the URL
|
|
302
|
-
username = page.url.split("/@")[-1].split("/")[0].split("?")[0]
|
|
303
|
-
user_id = ds_user_id or ""
|
|
304
|
-
session = {"sessionid": sessionid, "csrftoken": csrftoken, "ds_user_id": user_id, "rur": cookies.get("rur", ""), "user_id": user_id, "username": username}
|
|
305
|
-
config.save_session(session)
|
|
306
|
-
browser.close()
|
|
336
|
+
except Exception as e:
|
|
337
|
+
msg = str(e)
|
|
338
|
+
if "Executable doesn't exist" in msg or ("executable" in str(e).lower() and "playwright" in str(e).lower()):
|
|
339
|
+
console.print("[red]Chromium browser not found.[/]")
|
|
340
|
+
console.print("Run: [bold]threads-cleaner install-browser[/]")
|
|
341
|
+
raise RuntimeError("Playwright browser not installed") from e
|
|
342
|
+
raise
|
|
307
343
|
console.print(f"[green]Session saved.[/] Logged in as [bold]@{username}[/]")
|
|
308
344
|
return session
|
|
@@ -41,13 +41,33 @@ def browser_delete(
|
|
|
41
41
|
),
|
|
42
42
|
):
|
|
43
43
|
"""Delete posts (and optionally replies) via a real browser (Playwright). Clicks the UI like a human."""
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
try:
|
|
45
|
+
run_browser_delete(
|
|
46
|
+
include_replies=include_replies,
|
|
47
|
+
max_deletes=max_deletes,
|
|
48
|
+
dry_run=dry_run,
|
|
49
|
+
yes=yes,
|
|
50
|
+
headed=headed,
|
|
51
|
+
)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
console.print(f"[red]Error:[/] {e}")
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command()
|
|
58
|
+
def install_browser():
|
|
59
|
+
"""Install the Chromium browser required by Playwright."""
|
|
60
|
+
import subprocess, sys
|
|
61
|
+
console.print("[yellow]Installing Chromium browser for Playwright...[/]")
|
|
62
|
+
result = subprocess.run(
|
|
63
|
+
[sys.executable, "-m", "playwright", "install", "chromium"],
|
|
64
|
+
capture_output=False,
|
|
50
65
|
)
|
|
66
|
+
if result.returncode == 0:
|
|
67
|
+
console.print("[green]Chromium installed![/] Run [bold]threads-cleaner browser-login[/] to start.")
|
|
68
|
+
else:
|
|
69
|
+
console.print("[red]Installation failed.[/] Try manually: [bold]playwright install chromium[/]")
|
|
70
|
+
raise typer.Exit(1)
|
|
51
71
|
|
|
52
72
|
|
|
53
73
|
if __name__ == "__main__":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|