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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: threads-cleaner
3
- Version: 0.2.0
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 Playwright browser:
35
+ Then install the Chromium browser:
36
36
 
37
37
  ```bash
38
- playwright install chromium
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 Playwright browser:
14
+ Then install the Chromium browser:
15
15
 
16
16
  ```bash
17
- playwright install chromium
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 |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "threads-cleaner"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Bulk-delete your Threads posts and replies. Free, open-source, runs locally."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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
- self._browser = self._pw.chromium.launch(
41
- headless=not self._headed,
42
- args=["--disable-blink-features=AutomationControlled", "--no-sandbox"],
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' && el.childElementCount === 0) {
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' && el.childElementCount === 0) {
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
- with sync_playwright() as pw:
279
- browser = pw.chromium.launch(headless=False, args=["--no-sandbox"])
280
- ctx = browser.new_context(viewport={"width": 1280, "height": 800})
281
- page = ctx.new_page()
282
- page.goto("about:blank")
283
- console.print("[yellow]Go to [bold]https://www.threads.net/[/bold] -> log in -> go to your profile -> wait[/]")
284
- try:
285
- page.wait_for_url("**/***@**", timeout=600000)
286
- except PwTimeout:
287
- browser.close()
288
- raise RuntimeError("Login timed out")
289
- time.sleep(2)
290
- cookies_raw = ctx.cookies()
291
- cookies = {c["name"]: c["value"] for c in cookies_raw if c["name"] in ("sessionid", "csrftoken", "ds_user_id", "rur")}
292
- if not cookies.get("sessionid"):
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
- raise RuntimeError("No session cookie")
295
- sessionid = cookies["sessionid"]
296
- csrftoken = cookies.get("csrftoken", "")
297
- ds_user_id = cookies.get("ds_user_id", "")
298
- cookies["sessionid"] = sessionid
299
- cookies["csrftoken"] = csrftoken
300
- cookies["ds_user_id"] = ds_user_id
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
- run_browser_delete(
45
- include_replies=include_replies,
46
- max_deletes=max_deletes,
47
- dry_run=dry_run,
48
- yes=yes,
49
- headed=headed,
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__":