pywaybackup 4.2.1__tar.gz → 4.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.
Files changed (28) hide show
  1. {pywaybackup-4.2.1/pywaybackup.egg-info → pywaybackup-4.2.2}/PKG-INFO +49 -1
  2. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/README.md +48 -0
  3. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pyproject.toml +2 -2
  4. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup/PyWayBackup.py +63 -9
  5. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup/Snapshot.py +8 -2
  6. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup/SnapshotCollection.py +26 -4
  7. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup/Worker.py +3 -2
  8. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup/archive_download.py +4 -2
  9. pywaybackup-4.2.2/pywaybackup/arguments.py +72 -0
  10. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup/helper.py +21 -0
  11. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup/interactive.py +43 -35
  12. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup/main.py +1 -1
  13. {pywaybackup-4.2.1 → pywaybackup-4.2.2/pywaybackup.egg-info}/PKG-INFO +49 -1
  14. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup.egg-info/SOURCES.txt +1 -2
  15. pywaybackup-4.2.1/pywaybackup/arg_parser.py +0 -55
  16. pywaybackup-4.2.1/pywaybackup/arg_specs.py +0 -257
  17. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/LICENSE +0 -0
  18. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup/Exception.py +0 -0
  19. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup/Verbosity.py +0 -0
  20. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup/__init__.py +0 -0
  21. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup/archive_save.py +0 -0
  22. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup/db.py +0 -0
  23. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup/files.py +0 -0
  24. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup.egg-info/dependency_links.txt +0 -0
  25. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup.egg-info/entry_points.txt +0 -0
  26. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup.egg-info/requires.txt +0 -0
  27. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/pywaybackup.egg-info/top_level.txt +0 -0
  28. {pywaybackup-4.2.1 → pywaybackup-4.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pywaybackup
3
- Version: 4.2.1
3
+ Version: 4.2.2
4
4
  Summary: Query and download archive.org as simple as possible.
5
5
  Author-email: bitdruid <bitdruid@outlook.com>
6
6
  License: MIT License
@@ -75,6 +75,16 @@ Prebuilt executables for Windows, Linux and macOS are attached to each [release]
75
75
  - Run from a terminal with arguments like the pip version: `waybackup -h`
76
76
  - Or start it without arguments (e.g. double-click on Windows) to enter **interactive mode** — the tool will prompt you for URL, mode and optional settings.
77
77
 
78
+ On Linux/macOS the downloaded file has to be made executable first:
79
+
80
+ ```bash
81
+ curl -L -o waybackup https://github.com/bitdruid/python-wayback-machine-downloader/releases/latest/download/waybackup-linux
82
+ chmod +x waybackup
83
+ ./waybackup -h
84
+ ```
85
+
86
+ Move it to a directory in your `PATH` (e.g. `~/.local/bin`) to call it as `waybackup` from anywhere.
87
+
78
88
  ### Manual
79
89
 
80
90
  1. Clone the repository <br>
@@ -100,6 +110,8 @@ You can import pywaybackup into your own scripts and run it. Args are the same a
100
110
  Additional args:
101
111
  - `silent` (default False): If True, suppresses all output to the console.
102
112
  - `debug` (default True): If False, disables writing errors to the error log file.
113
+ - `progress_callback` (default None): A function receiving the `status()` dict on every progress update.
114
+ - `progress_interval` (default 5): Seconds between callback updates while snapshots are downloading.
103
115
 
104
116
  Use:
105
117
  - `run()`
@@ -160,6 +172,39 @@ output:
160
172
  }
161
173
  ```
162
174
 
175
+ Instead of polling `status()`, pass a callback to receive the `status` dict every `progress_interval` seconds (works on `silent` and `progress` disabled).
176
+
177
+ ```python
178
+ from pywaybackup import PyWayBackup
179
+
180
+ def on_progress(status):
181
+ print(status)
182
+
183
+ backup = PyWayBackup(
184
+ url="https://example.com",
185
+ last=True,
186
+ silent=True,
187
+ progress=False,
188
+ progress_callback=on_progress,
189
+ progress_interval=5
190
+ )
191
+
192
+ backup.run()
193
+ ```
194
+ output:
195
+ ```bash
196
+ {'task': 'downloading cdx', 'current': 0, 'total': 0, 'progress': '0'}
197
+ {'task': 'preparing snapshots', 'current': 0, 'total': 0, 'progress': '0'}
198
+ {'task': 'downloading snapshots', 'current': 15, 'total': 84, 'progress': '18%'}
199
+ {'task': 'downloading snapshots', 'current': 54, 'total': 84, 'progress': '64%'}
200
+ {'task': 'downloading snapshots', 'current': 84, 'total': 84, 'progress': '100%'}
201
+ {'task': 'done', 'current': 84, 'total': 84, 'progress': '100%'}
202
+ ```
203
+
204
+ Any callable taking one argument works - a function, a bound method, or a lambda for something short.
205
+
206
+ An exception raised inside the callback is logged and ignored - it will not abort a running job. When using `run(daemon=True)`, the callback runs inside the spawned process, so its side effects are not visible to the parent.
207
+
163
208
  ## cli
164
209
 
165
210
  - `-h`, `--help`: Show the help message and exit. Version info is shown in the help header.
@@ -245,6 +290,9 @@ Parameters will change the download behavior for snapshots.
245
290
  - **`--no-redirect`**:<br>
246
291
  Disables following redirects of snapshots. Can prevent timestamp-folder mismatches caused by redirects.
247
292
 
293
+ - **`--no-merge-www`**:<br>
294
+ Keeps `www.example.com` and `example.com` in separate folders. By default both are treated as the same site and written into one folder, as archive.org returns them mixed together for a single query. Only use this if the two hosts served genuinely different content.
295
+
248
296
  - **`--retry`** `<attempts>`:<br>
249
297
  Retry attempts for failed downloads.
250
298
 
@@ -38,6 +38,16 @@ Prebuilt executables for Windows, Linux and macOS are attached to each [release]
38
38
  - Run from a terminal with arguments like the pip version: `waybackup -h`
39
39
  - Or start it without arguments (e.g. double-click on Windows) to enter **interactive mode** — the tool will prompt you for URL, mode and optional settings.
40
40
 
41
+ On Linux/macOS the downloaded file has to be made executable first:
42
+
43
+ ```bash
44
+ curl -L -o waybackup https://github.com/bitdruid/python-wayback-machine-downloader/releases/latest/download/waybackup-linux
45
+ chmod +x waybackup
46
+ ./waybackup -h
47
+ ```
48
+
49
+ Move it to a directory in your `PATH` (e.g. `~/.local/bin`) to call it as `waybackup` from anywhere.
50
+
41
51
  ### Manual
42
52
 
43
53
  1. Clone the repository <br>
@@ -63,6 +73,8 @@ You can import pywaybackup into your own scripts and run it. Args are the same a
63
73
  Additional args:
64
74
  - `silent` (default False): If True, suppresses all output to the console.
65
75
  - `debug` (default True): If False, disables writing errors to the error log file.
76
+ - `progress_callback` (default None): A function receiving the `status()` dict on every progress update.
77
+ - `progress_interval` (default 5): Seconds between callback updates while snapshots are downloading.
66
78
 
67
79
  Use:
68
80
  - `run()`
@@ -123,6 +135,39 @@ output:
123
135
  }
124
136
  ```
125
137
 
138
+ Instead of polling `status()`, pass a callback to receive the `status` dict every `progress_interval` seconds (works on `silent` and `progress` disabled).
139
+
140
+ ```python
141
+ from pywaybackup import PyWayBackup
142
+
143
+ def on_progress(status):
144
+ print(status)
145
+
146
+ backup = PyWayBackup(
147
+ url="https://example.com",
148
+ last=True,
149
+ silent=True,
150
+ progress=False,
151
+ progress_callback=on_progress,
152
+ progress_interval=5
153
+ )
154
+
155
+ backup.run()
156
+ ```
157
+ output:
158
+ ```bash
159
+ {'task': 'downloading cdx', 'current': 0, 'total': 0, 'progress': '0'}
160
+ {'task': 'preparing snapshots', 'current': 0, 'total': 0, 'progress': '0'}
161
+ {'task': 'downloading snapshots', 'current': 15, 'total': 84, 'progress': '18%'}
162
+ {'task': 'downloading snapshots', 'current': 54, 'total': 84, 'progress': '64%'}
163
+ {'task': 'downloading snapshots', 'current': 84, 'total': 84, 'progress': '100%'}
164
+ {'task': 'done', 'current': 84, 'total': 84, 'progress': '100%'}
165
+ ```
166
+
167
+ Any callable taking one argument works - a function, a bound method, or a lambda for something short.
168
+
169
+ An exception raised inside the callback is logged and ignored - it will not abort a running job. When using `run(daemon=True)`, the callback runs inside the spawned process, so its side effects are not visible to the parent.
170
+
126
171
  ## cli
127
172
 
128
173
  - `-h`, `--help`: Show the help message and exit. Version info is shown in the help header.
@@ -208,6 +253,9 @@ Parameters will change the download behavior for snapshots.
208
253
  - **`--no-redirect`**:<br>
209
254
  Disables following redirects of snapshots. Can prevent timestamp-folder mismatches caused by redirects.
210
255
 
256
+ - **`--no-merge-www`**:<br>
257
+ Keeps `www.example.com` and `example.com` in separate folders. By default both are treated as the same site and written into one folder, as archive.org returns them mixed together for a single query. Only use this if the two hosts served genuinely different content.
258
+
211
259
  - **`--retry`** `<attempts>`:<br>
212
260
  Retry attempts for failed downloads.
213
261
 
@@ -16,7 +16,7 @@ dependencies = [
16
16
  "python-magic-standalone==0.4.28",
17
17
  ]
18
18
  name = "pywaybackup"
19
- version = "4.2.1"
19
+ version = "4.2.2"
20
20
  description = "Query and download archive.org as simple as possible."
21
21
  authors = [{ name = "bitdruid", email = "bitdruid@outlook.com" }]
22
22
  license = { file = "LICENSE" }
@@ -34,4 +34,4 @@ homepage = "https://github.com/bitdruid/python-wayback-machine-downloader"
34
34
 
35
35
  [tool.ruff]
36
36
  line-length = 120
37
- exclude = ["pywaybackup/Arguments.py"]
37
+ exclude = ["pywaybackup/arguments.py"]
@@ -2,16 +2,17 @@ import multiprocessing
2
2
  import os
3
3
  import signal
4
4
  import sys
5
+ import threading
5
6
  import time
6
7
  from importlib.metadata import version
7
- from typing import Union
8
+ from typing import Callable, Optional, Union
8
9
 
9
10
  import pywaybackup.archive_save as archive_save
10
11
  from pywaybackup.archive_download import DownloadArchive
11
12
  from pywaybackup.db import Database as db
12
13
  from pywaybackup.Exception import Exception as ex
13
14
  from pywaybackup.files import CDXfile, CDXquery, CSVfile
14
- from pywaybackup.helper import sanitize_filename, url_split
15
+ from pywaybackup.helper import normalize_domain, sanitize_filename, url_split
15
16
  from pywaybackup.SnapshotCollection import SnapshotCollection
16
17
  from pywaybackup.Verbosity import Verbosity as vb
17
18
 
@@ -89,6 +90,7 @@ class PyWayBackup:
89
90
  log (bool): Enable writing logs to a file.
90
91
  progress (bool): Show a progress bar.
91
92
  no_redirect (bool): Disable handling redirects.
93
+ no_merge_www (bool): Keep www and non-www snapshots in separate folders instead of merging them.
92
94
  retry (int): Retry attempts for failed downloads.
93
95
  workers (int): Number of download workers (default: 1).
94
96
  delay (int): Delay between download requests in seconds.
@@ -96,6 +98,9 @@ class PyWayBackup:
96
98
  keep (bool): Retain all job metadata after completion.
97
99
  silent (bool): Suppress all output (for programmatic use).
98
100
  debug (bool): Enable debug mode.
101
+ progress_callback (callable): Called with the status() dict on every task change and, during the
102
+ download phase, every `progress_interval` seconds. Fires independently of `silent`/`progress`.
103
+ progress_interval (float): Seconds between callback ticks while downloading snapshots (default: 5).
99
104
  **kwargs: Catch-all for future expansion or external integration.
100
105
 
101
106
  Methods:
@@ -127,6 +132,7 @@ class PyWayBackup:
127
132
  log: bool = False,
128
133
  progress: bool = False,
129
134
  no_redirect: bool = False,
135
+ no_merge_www: bool = False,
130
136
  retry: int = 0,
131
137
  workers: int = 1,
132
138
  delay: int = 0,
@@ -135,6 +141,8 @@ class PyWayBackup:
135
141
  keep: bool = False,
136
142
  silent: bool = True,
137
143
  debug: bool = False,
144
+ progress_callback: Optional[Callable[[dict], None]] = None,
145
+ progress_interval: float = 5.0,
138
146
  **kwargs: dict,
139
147
  ):
140
148
  self._url = url
@@ -155,6 +163,7 @@ class PyWayBackup:
155
163
  self._log = log
156
164
  self._progress = progress
157
165
  self._no_redirect = no_redirect
166
+ self._merge_www = not no_merge_www
158
167
  self._retry = retry
159
168
  self._workers = workers
160
169
  self._delay = delay
@@ -166,6 +175,8 @@ class PyWayBackup:
166
175
  # module exclusive
167
176
  self._silent = silent
168
177
  self._debug = debug
178
+ self._progress_callback = progress_callback
179
+ self._progress_interval = progress_interval
169
180
 
170
181
  # internal
171
182
  self._status = _Status()
@@ -328,7 +339,7 @@ class PyWayBackup:
328
339
  SnapshotCollection: The initialized and loaded snapshot collection.
329
340
  """
330
341
  collection = SnapshotCollection()
331
- collection.load(mode=self._mode, cdxfile=self._cdxfile, csvfile=self._csvfile)
342
+ collection.load(mode=self._mode, cdxfile=self._cdxfile, csvfile=self._csvfile, merge_www=self._merge_www)
332
343
  collection.print_calculation()
333
344
  return collection
334
345
 
@@ -350,9 +361,43 @@ class PyWayBackup:
350
361
  delay=self._delay,
351
362
  wait=self._wait,
352
363
  workers=self._workers,
364
+ merge_www=self._merge_www,
353
365
  )
354
366
  downloader.run(SnapshotCollection=collection)
355
367
 
368
+ def _notify(self, task: str = None):
369
+ """
370
+ Set the current task (if given) and push the status to the progress callback.
371
+
372
+ Never gated by `silent`/`progress` - those only control the tqdm bar, while
373
+ embedders using the callback are exactly the ones running without one. A
374
+ failing callback is logged and swallowed, it must not abort a running job.
375
+
376
+ Args:
377
+ task (str, optional): New task name to set before notifying.
378
+ """
379
+ if task is not None:
380
+ self._status.task = task
381
+ if not self._progress_callback:
382
+ return
383
+ try:
384
+ self._progress_callback(self._status.status)
385
+ except Exception as e:
386
+ ex.exception(message="\nprogress_callback raised", e=e)
387
+
388
+ def _notify_loop(self, stop: threading.Event):
389
+ """
390
+ Push a status update every `progress_interval` seconds until stopped.
391
+
392
+ Runs in a daemon thread during the download phase, which can last hours
393
+ without any task change to notify on.
394
+
395
+ Args:
396
+ stop (threading.Event): Set by the workflow to end the loop.
397
+ """
398
+ while not stop.wait(self._progress_interval):
399
+ self._notify()
400
+
356
401
  def _workflow(self):
357
402
  """
358
403
  Executes the steps required to perform the backup process:
@@ -365,18 +410,23 @@ class PyWayBackup:
365
410
 
366
411
  """
367
412
  collection = None
413
+ ticker = None
414
+ ticker_stop = threading.Event()
368
415
  try:
369
416
  self._startup()
370
417
 
371
- self._status.task = "downloading cdx"
418
+ self._notify(task="downloading cdx")
372
419
  cdx = self._prep_cdx()
373
420
 
374
421
  if cdx:
375
- self._status.task = "preparing snapshots"
422
+ self._notify(task="preparing snapshots")
376
423
  collection = self._prep_collection()
377
424
 
378
425
  if collection:
379
- self._status.task = "downloading snapshots"
426
+ self._notify(task="downloading snapshots")
427
+ if self._progress_callback:
428
+ ticker = threading.Thread(target=self._notify_loop, args=(ticker_stop,), daemon=True)
429
+ ticker.start()
380
430
  self._dl_download(collection=collection)
381
431
 
382
432
  except KeyboardInterrupt:
@@ -386,6 +436,10 @@ class PyWayBackup:
386
436
  self._keep = True
387
437
  ex.exception(message="", e=e)
388
438
  finally:
439
+ ticker_stop.set() # stop ticking before the db teardown below
440
+ if ticker:
441
+ ticker.join()
442
+ self._notify() # last frame before "done" freezes the counts
389
443
  # if a collection was created during the workflow, close its DB session cleanly
390
444
  try:
391
445
  if collection:
@@ -412,7 +466,7 @@ class PyWayBackup:
412
466
  ... }
413
467
  """
414
468
  files = {
415
- "snapshots": os.path.join(self._output, self._domain),
469
+ "snapshots": os.path.join(self._output, normalize_domain(self._domain, merge_www=self._merge_www)),
416
470
  "cdxfile": self._cdxfile.filepath,
417
471
  "dbfile": self._dbfile,
418
472
  "csvfile": self._csvfile.filepath,
@@ -492,7 +546,7 @@ class PyWayBackup:
492
546
 
493
547
  def _startup(self):
494
548
  if db.query_exist:
495
- self._status.task = "resuming"
549
+ self._notify(task="resuming")
496
550
  vb.write(
497
551
  content=f"\nDOWNLOAD job exist - processed: {db.query_progress}\nResuming download... (to reset the job use '--reset')"
498
552
  )
@@ -506,7 +560,7 @@ class PyWayBackup:
506
560
  time.sleep(1)
507
561
 
508
562
  def _shutdown(self):
509
- self._status.task = "done"
563
+ self._notify(task="done") # counts stay frozen at the last live update
510
564
  collection = SnapshotCollection()
511
565
  collection.close()
512
566
  self._csvfile.store_result()
@@ -2,7 +2,7 @@ import os
2
2
  import threading
3
3
 
4
4
  from pywaybackup.db import Database, select, update, waybackup_snapshots, and_
5
- from pywaybackup.helper import url_split
5
+ from pywaybackup.helper import normalize_domain, url_split
6
6
  from pywaybackup.Verbosity import Verbosity as vb
7
7
 
8
8
 
@@ -22,7 +22,7 @@ class Snapshot:
22
22
 
23
23
  __sqlite_lock = threading.Lock()
24
24
 
25
- def __init__(self, db: Database, output: str, mode: str):
25
+ def __init__(self, db: Database, output: str, mode: str, merge_www: bool = True):
26
26
  """
27
27
  Initialize a Snapshot instance and fetch its database row if available.
28
28
 
@@ -30,10 +30,12 @@ class Snapshot:
30
30
  db (Database): Database connection/session manager.
31
31
  output (str): Output directory for downloaded files.
32
32
  mode (str): Download mode ('first', 'last', or default).
33
+ merge_www (bool): Write www and non-www snapshots into the same folder.
33
34
  """
34
35
  self._db = db
35
36
  self.output = output
36
37
  self.mode = mode
38
+ self.merge_www = merge_www
37
39
 
38
40
  self._redirect_url = None
39
41
  self._redirect_timestamp = None
@@ -164,10 +166,14 @@ class Snapshot:
164
166
  If mode is 'first' or 'last', the path does not include the timestamp.
165
167
  Otherwise, the timestamp is included in the path.
166
168
 
169
+ The domain is normalized so that the www and non-www variants of a site
170
+ end up in the same folder instead of two separate ones.
171
+
167
172
  Returns:
168
173
  str: Absolute path to the output file for the snapshot.
169
174
  """
170
175
  domain, subdir, filename = url_split(self.url_archive.split("id_/")[1], index=True)
176
+ domain = normalize_domain(domain, merge_www=self.merge_www)
171
177
 
172
178
  if self.mode == "last" or self.mode == "first":
173
179
  download_dir = os.path.join(self.output, domain, subdir)
@@ -28,6 +28,9 @@ class SnapshotCollection:
28
28
 
29
29
  self._snapshot_faulty = 0 # error while parsing cdx line
30
30
 
31
+ self._merge_www = True # treat www and non-www as the same url
32
+
33
+ self._filter_mailto = 0 # mailto: links in the cdx results
31
34
  self._filter_duplicates = 0 # with identical url_archive
32
35
  self._filter_mode = 0 # all snapshots filtered by the MODE (last or first)
33
36
  self._filter_skip = 0 # content of the csv file
@@ -60,12 +63,13 @@ class SnapshotCollection:
60
63
  self.db.write_progress(self._snapshot_handled, self._snapshot_total)
61
64
  self.db.session.close()
62
65
 
63
- def load(self, mode: str, cdxfile: CDXfile, csvfile: CSVfile):
66
+ def load(self, mode: str, cdxfile: CDXfile, csvfile: CSVfile, merge_www: bool = True):
64
67
  """
65
68
  Insert the content of the cdx and csv file into the snapshot table.
66
69
  """
67
70
  self.cdxfile = cdxfile
68
71
  self.csvfile = csvfile
72
+ self._merge_www = merge_www
69
73
  if mode == "first":
70
74
  self._mode_first = True
71
75
  if mode == "last":
@@ -114,6 +118,9 @@ class SnapshotCollection:
114
118
  "statuscode": line[3],
115
119
  "origin": line[4],
116
120
  }
121
+ # cdx results contain mailto: links, which are no downloadable resources
122
+ if line["origin"].lower().startswith("mailto"):
123
+ return None
117
124
  url_archive = f"https://web.archive.org/web/{line['timestamp']}id_/{line['origin']}"
118
125
  statuscode = line["statuscode"] if line["statuscode"] in ("301", "404") else None
119
126
  return {
@@ -189,10 +196,14 @@ class SnapshotCollection:
189
196
  line = line.rsplit(",", 1)[0]
190
197
 
191
198
  try:
192
- line_batch.append(__parse_line(line))
199
+ parsed = __parse_line(line)
193
200
  except json.decoder.JSONDecodeError:
194
201
  self._snapshot_faulty += 1
195
202
  continue
203
+ if parsed is None:
204
+ self._filter_mailto += 1
205
+ continue
206
+ line_batch.append(parsed)
196
207
 
197
208
  if len(line_batch) >= line_batchsize:
198
209
  total_inserted += _insert_batch_safe(line_batch=line_batch)
@@ -253,6 +264,11 @@ class SnapshotCollection:
253
264
 
254
265
  - MODE_LAST → keep only the latest snapshot (highest timestamp) per url_origin.
255
266
  - MODE_FIRST → keep only the earliest snapshot (lowest timestamp) per url_origin.
267
+
268
+ Grouped by the url as it maps to disk, not by the raw url_origin: the output
269
+ path drops the scheme and normalizes the domain, so `http://www.example.com/`
270
+ and `https://example.com/` are one and the same file. Grouping by the raw value
271
+ would download both and let the second silently overwrite the first.
256
272
  """
257
273
 
258
274
  def _filter_mode():
@@ -261,11 +277,15 @@ class SnapshotCollection:
261
277
  ordering = (
262
278
  waybackup_snapshots.timestamp.desc() if self._mode_last else waybackup_snapshots.timestamp.asc()
263
279
  )
264
- # assign row numbers per url_origin
280
+ # same url as far as the output path is concerned (see helper.normalize_domain)
281
+ url_key = func.replace(func.lower(waybackup_snapshots.url_origin), "https://", "http://")
282
+ if self._merge_www:
283
+ url_key = func.replace(url_key, "http://www.", "http://")
284
+ # assign row numbers per url
265
285
  rownum = (
266
286
  func.row_number()
267
287
  .over(
268
- partition_by=waybackup_snapshots.url_origin,
288
+ partition_by=url_key,
269
289
  order_by=ordering,
270
290
  )
271
291
  .label("rn")
@@ -377,6 +397,8 @@ class SnapshotCollection:
377
397
  vb.write(content="\nSnapshot calculation:")
378
398
  vb.write(content=f"-----> {'in CDX file'.ljust(18)}: {self._cdx_total:,}")
379
399
 
400
+ if self._filter_mailto > 0:
401
+ vb.write(content=f"-----> {'removed mailto'.ljust(18)}: {self._filter_mailto:,}")
380
402
  if self._filter_duplicates > 0:
381
403
  vb.write(content=f"-----> {'removed duplicates'.ljust(18)}: {self._filter_duplicates:,}")
382
404
  if self._filter_mode > 0:
@@ -11,10 +11,11 @@ class Worker:
11
11
  Worker buffers its messages in a Message object. Output has to be done with write() method.
12
12
  """
13
13
 
14
- def __init__(self, id: int, output: str, mode: str):
14
+ def __init__(self, id: int, output: str, mode: str, merge_www: bool = True):
15
15
  self.id = id
16
16
  self.output = output
17
17
  self.mode = mode
18
+ self.merge_www = merge_www
18
19
  self.message = Message(self)
19
20
 
20
21
  def init(self):
@@ -43,7 +44,7 @@ class Worker:
43
44
  pass
44
45
 
45
46
  def assign_snapshot(self, total_amount: int):
46
- self.snapshot = Snapshot(self.db, output=self.output, mode=self.mode)
47
+ self.snapshot = Snapshot(self.db, output=self.output, mode=self.mode, merge_www=self.merge_www)
47
48
  self.total_amount = total_amount
48
49
  if not self.snapshot.counter: # counter only if a row was fetched
49
50
  self.snapshot = None
@@ -81,7 +81,7 @@ class DownloadArchive:
81
81
  sc (SnapshotCollection): The snapshot collection being processed.
82
82
  """
83
83
 
84
- def __init__(self, mode: str, output: str, retry: int, no_redirect: bool, delay: int, wait: int, workers: int):
84
+ def __init__(self, mode: str, output: str, retry: int, no_redirect: bool, delay: int, wait: int, workers: int, merge_www: bool = True):
85
85
  """
86
86
  Initialize the download manager with configuration options.
87
87
 
@@ -92,9 +92,11 @@ class DownloadArchive:
92
92
  no_redirect (bool): Disable redirect handling if True.
93
93
  delay (int): Delay between downloads in seconds.
94
94
  workers (int): Number of worker threads.
95
+ merge_www (bool): Write www and non-www snapshots into the same folder.
95
96
  """
96
97
  self.mode = mode
97
98
  self.output = output
99
+ self.merge_www = merge_www
98
100
  self.retry = retry
99
101
  self.no_redirect = no_redirect
100
102
  self.delay = delay
@@ -127,7 +129,7 @@ class DownloadArchive:
127
129
 
128
130
  threads = []
129
131
  for i in range(self.workers):
130
- worker = Worker(id=i + 1, output=self.output, mode=self.mode)
132
+ worker = Worker(id=i + 1, output=self.output, mode=self.mode, merge_www=self.merge_www)
131
133
  vb.write(verbose=True, content=f"\n-----> Starting Worker: {worker.id}")
132
134
  thread = threading.Thread(target=self._download_loop, args=(worker,), daemon=True)
133
135
  threads.append(thread)
@@ -0,0 +1,72 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from argparse import RawTextHelpFormatter
5
+
6
+ from importlib.metadata import version
7
+
8
+
9
+ def build_parser() -> argparse.ArgumentParser:
10
+ """
11
+ Build the argument parser. Single source of truth for all cli arguments.
12
+
13
+ Split from parsing so that Interactive can read the argument definitions
14
+ without argparse touching sys.argv. Interactive walks the argument groups
15
+ below as they are, so a new argument shows up there without further work.
16
+ """
17
+ parser = argparse.ArgumentParser(
18
+ description=f"<<< python-wayback-machine-downloader v{version('pywaybackup')} >>>\nby @bitdruid -> https://github.com/bitdruid",
19
+ formatter_class=RawTextHelpFormatter,
20
+ )
21
+
22
+ required = parser.add_argument_group("required (one exclusive)")
23
+ required.add_argument("-u", "--url", type=str, metavar="", help="url (with subdir/subdomain) to download")
24
+ exclusive_required = required.add_mutually_exclusive_group(required=True)
25
+ exclusive_required.add_argument("-a", "--all", action="store_true", help="download snapshots of all timestamps")
26
+ exclusive_required.add_argument("-l", "--last", action="store_true", help="download the last version of each file snapshot")
27
+ exclusive_required.add_argument("-f", "--first", action="store_true", help="download the first version of each file snapshot")
28
+ exclusive_required.add_argument("-s", "--save", action="store_true", help="save a page to the wayback machine")
29
+
30
+ optional = parser.add_argument_group("optional query parameters")
31
+ optional.add_argument("-e", "--explicit", action="store_true", help="search only for the explicit given url")
32
+ optional.add_argument("-r", "--range", type=int, metavar="", help="range in years to search")
33
+ optional.add_argument("--start", type=int, metavar="", help="start timestamp format: YYYYMMDDHHMMSS")
34
+ optional.add_argument("--end", type=int, metavar="", help="end timestamp format: YYYYMMDDHHMMSS")
35
+ optional.add_argument("--limit", type=int, nargs="?", const=True, metavar="int", help="limit the number of snapshots to download")
36
+ optional.add_argument("--filetype", type=str, metavar="", help="filetypes to download comma separated (js,css,...)")
37
+ optional.add_argument("--statuscode", type=str, metavar="", help="statuscodes to download comma separated (200,404,...)")
38
+
39
+ behavior = parser.add_argument_group("manipulate behavior")
40
+ behavior.add_argument("-o", "--output", type=str, metavar="", help="output for all files - defaults to current directory")
41
+ behavior.add_argument("-m", "--metadata", type=str, metavar="", help="change directory for db/cdx/csv/log files")
42
+ behavior.add_argument("-v", "--verbose", type=str, nargs="?", const="default", metavar="", help="verbosity level: low, default, high (default if flag set without value)")
43
+ behavior.add_argument("--log", action="store_true", help="save a log file into the output folder")
44
+ behavior.add_argument("--progress", action="store_true", help="show a progress bar")
45
+ behavior.add_argument("--no-redirect", action="store_true", help="do not follow redirects by archive.org")
46
+ behavior.add_argument("--no-merge-www", action="store_true", help="keep www and non-www snapshots in separate folders")
47
+ behavior.add_argument("--retry", type=int, default=0, metavar="", help="retry failed downloads (opt tries as int, else infinite)")
48
+ behavior.add_argument("--workers", type=int, default=1, metavar="", help="number of workers (simultaneous downloads)")
49
+ behavior.add_argument("--delay", type=int, default=0, metavar="", help="delay between each download in seconds")
50
+ behavior.add_argument("--wait", type=int, default=15, metavar="", help="seconds to wait before renewing connection after HTTP errors or snapshot download errors (default: 15)")
51
+
52
+ special = parser.add_argument_group("special")
53
+ special.add_argument("--reset", action="store_true", help="reset the job and ignore existing cdx/db/csv files")
54
+ special.add_argument("--keep", action="store_true", help="keep all files after the job finished")
55
+
56
+ return parser
57
+
58
+
59
+ class Arguments:
60
+ def __init__(self):
61
+ parser = build_parser()
62
+
63
+ args = parser.parse_args(args=None if sys.argv[1:] else ["--help"]) # if no arguments are given, print help
64
+
65
+ args.silent = False
66
+ args.debug = True
67
+
68
+ self.args = args
69
+
70
+ def get_args(self) -> dict:
71
+ """Returns the parsed arguments as a dictionary."""
72
+ return vars(self.args)
@@ -47,6 +47,27 @@ def url_get_timestamp(url):
47
47
  return timestamp
48
48
 
49
49
 
50
+ def normalize_domain(domain: str, merge_www: bool = True) -> str:
51
+ """
52
+ Normalize a domain for use as a folder name.
53
+
54
+ The cdx api canonicalizes hosts, so a single query returns `example.com`,
55
+ `www.example.com`, `www.example.com.` and `www.EXAMPLE.com` mixed together.
56
+ Without normalizing these all become separate output folders for the same site.
57
+
58
+ The `www.` prefix is only stripped if a dot remains, so `www.com` stays intact.
59
+ Real subdomains (`blog.example.com`) are left alone.
60
+
61
+ Args:
62
+ domain (str): The domain to normalize.
63
+ merge_www (bool): If False, keeps the `www.` prefix (--no-merge-www).
64
+ """
65
+ domain = domain.lower().rstrip(".")
66
+ if merge_www and domain.startswith("www.") and "." in domain[4:]:
67
+ domain = domain[4:]
68
+ return domain
69
+
70
+
50
71
  def url_split(url, index=False):
51
72
  """
52
73
  Split a URL into domain, subdir, and filename.
@@ -3,13 +3,14 @@ Interactive mode: prompt the user for arguments instead of parsing sys.argv.
3
3
 
4
4
  Used when waybackup is launched without CLI arguments (e.g. double-clicking
5
5
  the Windows .exe). Produces the same dict shape as Arguments.get_args() so
6
- PyWayBackup(**args) works either way. Argument metadata is read from
7
- arg_specs.ARG_SPECS so flags only have to be declared in one place.
6
+ PyWayBackup(**args) works either way. The arguments and their grouping are read
7
+ from the argparse parser, so a new argument is offered here automatically - it
8
+ only has to be added in arguments.py.
8
9
  """
9
10
 
10
11
  from importlib.metadata import version
11
12
 
12
- from pywaybackup.arg_specs import ARG_SPECS, EXCLUSIVE_GROUPS, default_args
13
+ from pywaybackup.arguments import build_parser
13
14
 
14
15
 
15
16
  class Interactive:
@@ -17,28 +18,35 @@ class Interactive:
17
18
  print(f"<<< python-wayback-machine-downloader v{version('pywaybackup')} >>>")
18
19
  print("Interactive mode - press Ctrl+C to abort.\n")
19
20
 
20
- args = default_args()
21
-
22
- # 1. Required URL
23
- url_spec = _spec_by_name("url")
24
- args["url"] = self._prompt_required(url_spec.prompt or url_spec.help)
25
-
26
- # 2. Required exclusive group(s) — pick exactly one member
27
- for ex_name in EXCLUSIVE_GROUPS:
28
- members = [s for s in ARG_SPECS if s.exclusive_group == ex_name]
29
- choice = self._prompt_choice(
30
- ex_name.capitalize(),
31
- [(s.name, s.help) for s in members],
32
- )
33
- for s in members:
34
- args[s.name] = s.name == choice
35
-
36
- # 3. Advanced options
37
- if self._prompt_yes_no("Configure advanced options?", default=False):
38
- for spec in ARG_SPECS:
39
- if not spec.advanced:
40
- continue
41
- args[spec.name] = self._prompt_for(spec, args[spec.name])
21
+ parser = build_parser()
22
+ actions = [action for action in parser._actions if action.dest != "help"]
23
+ args = {action.dest: action.default for action in actions}
24
+
25
+ # 1. Required url
26
+ url = _action_by_dest(actions, "url")
27
+ args["url"] = self._prompt_required(url.help)
28
+
29
+ # 2. Required exclusive group(s) - pick exactly one member
30
+ exclusive = set()
31
+ for group in parser._mutually_exclusive_groups:
32
+ members = group._group_actions
33
+ exclusive.update(action.dest for action in members)
34
+ if not group.required:
35
+ continue
36
+ choice = self._prompt_choice("Mode", [(a.dest, a.help) for a in members])
37
+ for action in members:
38
+ args[action.dest] = action.dest == choice
39
+
40
+ # 3. Every other argument, offered group by group as defined in the parser
41
+ handled = exclusive | {"url"}
42
+ for group in parser._action_groups:
43
+ members = [a for a in group._group_actions if a.dest not in handled and a.dest != "help"]
44
+ if not members:
45
+ continue
46
+ if not self._prompt_yes_no(f"\nConfigure {group.title}?", default=False):
47
+ continue
48
+ for action in members:
49
+ args[action.dest] = self._prompt_for(action, args[action.dest])
42
50
 
43
51
  # internal flags (parity with Arguments.py)
44
52
  args["silent"] = False
@@ -50,11 +58,11 @@ class Interactive:
50
58
  def get_args(self) -> dict:
51
59
  return self.args
52
60
 
53
- def _prompt_for(self, spec, current):
54
- label = spec.prompt or spec.help
55
- if spec.action == "store_true":
56
- return self._prompt_yes_no(f"{label}", default=bool(current))
57
- if spec.type is int:
61
+ def _prompt_for(self, action, current):
62
+ label = action.help
63
+ if action.nargs == 0: # store_true
64
+ return self._prompt_yes_no(label, default=bool(current))
65
+ if action.type is int:
58
66
  if current is None:
59
67
  return self._prompt_optional_int(label)
60
68
  return self._prompt_int(label, default=current)
@@ -137,8 +145,8 @@ class Interactive:
137
145
  print(f" (please enter one of: {', '.join(sorted(valid))})")
138
146
 
139
147
 
140
- def _spec_by_name(name):
141
- for s in ARG_SPECS:
142
- if s.name == name:
143
- return s
144
- raise KeyError(name)
148
+ def _action_by_dest(actions, dest):
149
+ for action in actions:
150
+ if action.dest == dest:
151
+ return action
152
+ raise KeyError(dest)
@@ -2,7 +2,7 @@ import signal
2
2
  import sys
3
3
 
4
4
  from pywaybackup import PyWayBackup
5
- from pywaybackup.arg_parser import Arguments
5
+ from pywaybackup.arguments import Arguments
6
6
  from pywaybackup.interactive import Interactive
7
7
 
8
8
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pywaybackup
3
- Version: 4.2.1
3
+ Version: 4.2.2
4
4
  Summary: Query and download archive.org as simple as possible.
5
5
  Author-email: bitdruid <bitdruid@outlook.com>
6
6
  License: MIT License
@@ -75,6 +75,16 @@ Prebuilt executables for Windows, Linux and macOS are attached to each [release]
75
75
  - Run from a terminal with arguments like the pip version: `waybackup -h`
76
76
  - Or start it without arguments (e.g. double-click on Windows) to enter **interactive mode** — the tool will prompt you for URL, mode and optional settings.
77
77
 
78
+ On Linux/macOS the downloaded file has to be made executable first:
79
+
80
+ ```bash
81
+ curl -L -o waybackup https://github.com/bitdruid/python-wayback-machine-downloader/releases/latest/download/waybackup-linux
82
+ chmod +x waybackup
83
+ ./waybackup -h
84
+ ```
85
+
86
+ Move it to a directory in your `PATH` (e.g. `~/.local/bin`) to call it as `waybackup` from anywhere.
87
+
78
88
  ### Manual
79
89
 
80
90
  1. Clone the repository <br>
@@ -100,6 +110,8 @@ You can import pywaybackup into your own scripts and run it. Args are the same a
100
110
  Additional args:
101
111
  - `silent` (default False): If True, suppresses all output to the console.
102
112
  - `debug` (default True): If False, disables writing errors to the error log file.
113
+ - `progress_callback` (default None): A function receiving the `status()` dict on every progress update.
114
+ - `progress_interval` (default 5): Seconds between callback updates while snapshots are downloading.
103
115
 
104
116
  Use:
105
117
  - `run()`
@@ -160,6 +172,39 @@ output:
160
172
  }
161
173
  ```
162
174
 
175
+ Instead of polling `status()`, pass a callback to receive the `status` dict every `progress_interval` seconds (works on `silent` and `progress` disabled).
176
+
177
+ ```python
178
+ from pywaybackup import PyWayBackup
179
+
180
+ def on_progress(status):
181
+ print(status)
182
+
183
+ backup = PyWayBackup(
184
+ url="https://example.com",
185
+ last=True,
186
+ silent=True,
187
+ progress=False,
188
+ progress_callback=on_progress,
189
+ progress_interval=5
190
+ )
191
+
192
+ backup.run()
193
+ ```
194
+ output:
195
+ ```bash
196
+ {'task': 'downloading cdx', 'current': 0, 'total': 0, 'progress': '0'}
197
+ {'task': 'preparing snapshots', 'current': 0, 'total': 0, 'progress': '0'}
198
+ {'task': 'downloading snapshots', 'current': 15, 'total': 84, 'progress': '18%'}
199
+ {'task': 'downloading snapshots', 'current': 54, 'total': 84, 'progress': '64%'}
200
+ {'task': 'downloading snapshots', 'current': 84, 'total': 84, 'progress': '100%'}
201
+ {'task': 'done', 'current': 84, 'total': 84, 'progress': '100%'}
202
+ ```
203
+
204
+ Any callable taking one argument works - a function, a bound method, or a lambda for something short.
205
+
206
+ An exception raised inside the callback is logged and ignored - it will not abort a running job. When using `run(daemon=True)`, the callback runs inside the spawned process, so its side effects are not visible to the parent.
207
+
163
208
  ## cli
164
209
 
165
210
  - `-h`, `--help`: Show the help message and exit. Version info is shown in the help header.
@@ -245,6 +290,9 @@ Parameters will change the download behavior for snapshots.
245
290
  - **`--no-redirect`**:<br>
246
291
  Disables following redirects of snapshots. Can prevent timestamp-folder mismatches caused by redirects.
247
292
 
293
+ - **`--no-merge-www`**:<br>
294
+ Keeps `www.example.com` and `example.com` in separate folders. By default both are treated as the same site and written into one folder, as archive.org returns them mixed together for a single query. Only use this if the two hosts served genuinely different content.
295
+
248
296
  - **`--retry`** `<attempts>`:<br>
249
297
  Retry attempts for failed downloads.
250
298
 
@@ -10,8 +10,7 @@ pywaybackup/Worker.py
10
10
  pywaybackup/__init__.py
11
11
  pywaybackup/archive_download.py
12
12
  pywaybackup/archive_save.py
13
- pywaybackup/arg_parser.py
14
- pywaybackup/arg_specs.py
13
+ pywaybackup/arguments.py
15
14
  pywaybackup/db.py
16
15
  pywaybackup/files.py
17
16
  pywaybackup/helper.py
@@ -1,55 +0,0 @@
1
- import argparse
2
- import sys
3
- from argparse import RawTextHelpFormatter
4
- from importlib.metadata import version
5
-
6
- from pywaybackup.arg_specs import ARG_GROUPS, ARG_SPECS, EXCLUSIVE_GROUPS
7
-
8
-
9
- class Arguments:
10
- def __init__(self):
11
- parser = argparse.ArgumentParser(
12
- description=f"<<< python-wayback-machine-downloader v{version('pywaybackup')} >>>\nby @bitdruid -> https://github.com/bitdruid",
13
- formatter_class=RawTextHelpFormatter,
14
- )
15
-
16
- groups = {name: parser.add_argument_group(name) for name in ARG_GROUPS}
17
-
18
- exclusive = {
19
- ex_name: groups[ex_meta["parent_group"]].add_mutually_exclusive_group(required=ex_meta["required"])
20
- for ex_name, ex_meta in EXCLUSIVE_GROUPS.items()
21
- }
22
-
23
- for spec in ARG_SPECS:
24
- target = exclusive[spec.exclusive_group] if spec.exclusive_group else groups[spec.group]
25
- target.add_argument(*spec.flags, **_argparse_kwargs(spec))
26
-
27
- args = parser.parse_args(args=None if sys.argv[1:] else ["--help"]) # if no arguments are given, print help
28
-
29
- args.silent = False
30
- args.debug = True
31
-
32
- self.args = args
33
-
34
- def get_args(self) -> dict:
35
- """Returns the parsed arguments as a dictionary."""
36
- return vars(self.args)
37
-
38
-
39
- def _argparse_kwargs(spec) -> dict:
40
- """Translate an ArgSpec into kwargs for argparse.add_argument()."""
41
- kwargs = {"help": spec.help}
42
- if spec.action == "store_true":
43
- kwargs["action"] = "store_true"
44
- kwargs["default"] = bool(spec.default)
45
- elif spec.action == "optional_value":
46
- kwargs["type"] = spec.type
47
- kwargs["nargs"] = "?"
48
- kwargs["const"] = spec.const
49
- kwargs["metavar"] = spec.metavar
50
- kwargs["default"] = spec.default
51
- else:
52
- kwargs["type"] = spec.type
53
- kwargs["metavar"] = spec.metavar
54
- kwargs["default"] = spec.default
55
- return kwargs
@@ -1,257 +0,0 @@
1
- """
2
- Single source of truth for waybackup CLI arguments.
3
-
4
- Both Arguments (argparse) and Interactive (input prompts) consume this list,
5
- so a new flag only needs to be added in one place. PyWayBackup.__init__ keeps
6
- its explicit signature (vscode autocomplete).
7
- """
8
-
9
- from dataclasses import dataclass
10
- from typing import Any, List, Optional
11
-
12
-
13
- @dataclass
14
- class ArgSpec:
15
- name: str # internal key (matches PyWayBackup.__init__ kwarg)
16
- flags: List[str] # CLI flags, e.g. ["-u", "--url"]
17
- group: str # argparse group label
18
- help: str # CLI help text
19
- prompt: Optional[str] = None # interactive prompt label (None = skip in interactive)
20
- type: Optional[type] = None # str / int / None for store_true
21
- default: Any = None
22
- action: str = "store" # "store" | "store_true" | "optional_value"
23
- const: Any = None # used when action="optional_value"
24
- metavar: str = "" # argparse metavar; ignored for store_true
25
- exclusive_group: Optional[str] = None # name of a mutex group below
26
- advanced: bool = False # show in interactive only when advanced opts enabled
27
-
28
-
29
- # Mutually exclusive groups, keyed by name. Specs join them via exclusive_group=...
30
- EXCLUSIVE_GROUPS = {
31
- "mode": {"required": True, "parent_group": "required (one exclusive)"},
32
- }
33
-
34
- # argparse groups in display order
35
- ARG_GROUPS = [
36
- "required (one exclusive)",
37
- "optional query parameters",
38
- "manipulate behavior",
39
- "special",
40
- ]
41
-
42
-
43
- ARG_SPECS: List[ArgSpec] = [
44
- # required
45
- ArgSpec(
46
- name="url",
47
- flags=["-u", "--url"],
48
- group="required (one exclusive)",
49
- help="url (with subdir/subdomain) to download",
50
- prompt="URL to download (with subdir/subdomain)",
51
- type=str,
52
- ),
53
- ArgSpec(
54
- name="all",
55
- flags=["-a", "--all"],
56
- group="required (one exclusive)",
57
- help="download snapshots of all timestamps",
58
- action="store_true",
59
- default=False,
60
- exclusive_group="mode",
61
- ),
62
- ArgSpec(
63
- name="last",
64
- flags=["-l", "--last"],
65
- group="required (one exclusive)",
66
- help="download the last version of each file snapshot",
67
- action="store_true",
68
- default=False,
69
- exclusive_group="mode",
70
- ),
71
- ArgSpec(
72
- name="first",
73
- flags=["-f", "--first"],
74
- group="required (one exclusive)",
75
- help="download the first version of each file snapshot",
76
- action="store_true",
77
- default=False,
78
- exclusive_group="mode",
79
- ),
80
- ArgSpec(
81
- name="save",
82
- flags=["-s", "--save"],
83
- group="required (one exclusive)",
84
- help="save a page to the wayback machine",
85
- action="store_true",
86
- default=False,
87
- exclusive_group="mode",
88
- ),
89
- # -------------------- optional query --------------------
90
- ArgSpec(
91
- name="explicit",
92
- flags=["-e", "--explicit"],
93
- group="optional query parameters",
94
- help="search only for the explicit given url",
95
- action="store_true",
96
- default=False,
97
- ),
98
- ArgSpec(
99
- name="range",
100
- flags=["-r", "--range"],
101
- group="optional query parameters",
102
- help="range in years to search",
103
- prompt="Range in years to search",
104
- type=int,
105
- advanced=True,
106
- ),
107
- ArgSpec(
108
- name="start",
109
- flags=["--start"],
110
- group="optional query parameters",
111
- help="start timestamp format: YYYYMMDDHHMMSS",
112
- type=int,
113
- ),
114
- ArgSpec(
115
- name="end",
116
- flags=["--end"],
117
- group="optional query parameters",
118
- help="end timestamp format: YYYYMMDDHHMMSS",
119
- type=int,
120
- ),
121
- ArgSpec(
122
- name="limit",
123
- flags=["--limit"],
124
- group="optional query parameters",
125
- help="limit the number of snapshots to download",
126
- prompt="Limit number of snapshots",
127
- type=int,
128
- action="optional_value",
129
- const=True,
130
- metavar="int",
131
- advanced=True,
132
- ),
133
- ArgSpec(
134
- name="filetype",
135
- flags=["--filetype"],
136
- group="optional query parameters",
137
- help="filetypes to download comma separated (js,css,...)",
138
- type=str,
139
- ),
140
- ArgSpec(
141
- name="statuscode",
142
- flags=["--statuscode"],
143
- group="optional query parameters",
144
- help="statuscodes to download comma separated (200,404,...)",
145
- type=str,
146
- ),
147
- # behavior
148
- ArgSpec(
149
- name="output",
150
- flags=["-o", "--output"],
151
- group="manipulate behavior",
152
- help="output for all files - defaults to current directory",
153
- prompt="Output directory",
154
- type=str,
155
- advanced=True,
156
- ),
157
- ArgSpec(
158
- name="metadata",
159
- flags=["-m", "--metadata"],
160
- group="manipulate behavior",
161
- help="change directory for db/cdx/csv/log files",
162
- type=str,
163
- ),
164
- ArgSpec(
165
- name="verbose",
166
- flags=["-v", "--verbose"],
167
- group="manipulate behavior",
168
- help="verbosity level: low, default, high (default if flag set without value)",
169
- type=str,
170
- action="optional_value",
171
- const="default",
172
- ),
173
- ArgSpec(
174
- name="log",
175
- flags=["--log"],
176
- group="manipulate behavior",
177
- help="save a log file into the output folder",
178
- prompt="Save log file?",
179
- action="store_true",
180
- default=False,
181
- advanced=True,
182
- ),
183
- ArgSpec(
184
- name="progress",
185
- flags=["--progress"],
186
- group="manipulate behavior",
187
- help="show a progress bar",
188
- prompt="Show progress bar?",
189
- action="store_true",
190
- default=False,
191
- advanced=True,
192
- ),
193
- ArgSpec(
194
- name="no_redirect",
195
- flags=["--no-redirect"],
196
- group="manipulate behavior",
197
- help="do not follow redirects by archive.org",
198
- action="store_true",
199
- default=False,
200
- ),
201
- ArgSpec(
202
- name="retry",
203
- flags=["--retry"],
204
- group="manipulate behavior",
205
- help="retry failed downloads (opt tries as int, else infinite)",
206
- type=int,
207
- default=0,
208
- ),
209
- ArgSpec(
210
- name="workers",
211
- flags=["--workers"],
212
- group="manipulate behavior",
213
- help="number of workers (simultaneous downloads)",
214
- prompt="Workers (parallel downloads)",
215
- type=int,
216
- default=1,
217
- advanced=True,
218
- ),
219
- ArgSpec(
220
- name="delay",
221
- flags=["--delay"],
222
- group="manipulate behavior",
223
- help="delay between each download in seconds",
224
- type=int,
225
- default=0,
226
- ),
227
- ArgSpec(
228
- name="wait",
229
- flags=["--wait"],
230
- group="manipulate behavior",
231
- help="seconds to wait before renewing connection after HTTP errors or snapshot download errors (default: 15)",
232
- type=int,
233
- default=15,
234
- ),
235
- # special
236
- ArgSpec(
237
- name="reset",
238
- flags=["--reset"],
239
- group="special",
240
- help="reset the job and ignore existing cdx/db/csv files",
241
- action="store_true",
242
- default=False,
243
- ),
244
- ArgSpec(
245
- name="keep",
246
- flags=["--keep"],
247
- group="special",
248
- help="keep all files after the job finished",
249
- action="store_true",
250
- default=False,
251
- ),
252
- ]
253
-
254
-
255
- def default_args() -> dict:
256
- """Return a dict of {name: default} for every spec — the canonical empty arg payload."""
257
- return {spec.name: spec.default for spec in ARG_SPECS}
File without changes
File without changes