Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mikf
GitHub Repository: mikf/gallery-dl
Path: blob/master/gallery_dl/option.py
8861 views
1
# -*- coding: utf-8 -*-
2
3
# Copyright 2017-2026 Mike Fährmann
4
#
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License version 2 as
7
# published by the Free Software Foundation.
8
9
"""Command line option parsing"""
10
11
import argparse
12
import logging
13
import os.path
14
import sys
15
from . import job, util, version
16
17
18
class ConfigAction(argparse.Action):
19
"""Set argparse results as config values"""
20
def __call__(self, parser, namespace, values, option_string=None):
21
namespace.options.append(((), self.dest, values))
22
23
24
class ConfigConstAction(argparse.Action):
25
"""Set argparse const values as config values"""
26
def __call__(self, parser, namespace, values, option_string=None):
27
namespace.options.append(((), self.dest, self.const))
28
29
30
class AppendCommandAction(argparse.Action):
31
def __call__(self, parser, namespace, values, option_string=None):
32
items = getattr(namespace, self.dest, None) or []
33
val = self.const.copy()
34
val["command"] = values
35
items.append(val)
36
setattr(namespace, self.dest, items)
37
38
39
class DeprecatedConfigConstAction(argparse.Action):
40
"""Set argparse const values as config values + deprecation warning"""
41
def __call__(self, parser, namespace, values, option_string=None):
42
sys.stderr.write(
43
f"Warning: {'/'.join(self.option_strings)} is deprecated. "
44
f"Use {self.choices} instead.\n")
45
namespace.options.append(((), self.dest, self.const))
46
47
48
class ConfigParseAction(argparse.Action):
49
"""Parse KEY=VALUE config options"""
50
def __call__(self, parser, namespace, values, option_string=None):
51
key, value = _parse_option(values)
52
key = key.split(".") # splitting an empty string becomes [""]
53
namespace.options.append((key[:-1], key[-1], value))
54
55
56
class PPParseAction(argparse.Action):
57
"""Parse KEY=VALUE post processor options"""
58
def __call__(self, parser, namespace, values, option_string=None):
59
key, value = _parse_option(values)
60
namespace.options_pp[key] = value
61
62
63
class InputfileAction(argparse.Action):
64
"""Collect input files"""
65
def __call__(self, parser, namespace, value, option_string=None):
66
namespace.input_files.append((value, self.const))
67
68
69
class MtimeAction(argparse.Action):
70
"""Configure mtime post processors"""
71
def __call__(self, parser, namespace, value, option_string=None):
72
namespace.postprocessors.append({
73
"name": "mtime",
74
"value": f"{{{self.const or value}}}",
75
})
76
77
78
class RenameAction(argparse.Action):
79
"""Configure rename post processors"""
80
def __call__(self, parser, namespace, value, option_string=None):
81
if self.const:
82
namespace.postprocessors.append({
83
"name": "rename",
84
"to" : value,
85
})
86
else:
87
namespace.postprocessors.append({
88
"name": "rename",
89
"from": value,
90
})
91
92
93
class UgoiraAction(argparse.Action):
94
"""Configure ugoira post processors"""
95
def __call__(self, parser, namespace, value, option_string=None):
96
value = self.const or value.strip().lower()
97
98
if value in {"webm", "vp9"}:
99
pp = {
100
"extension" : "webm",
101
"ffmpeg-args" : ("-c:v", "libvpx-vp9",
102
"-crf", "12",
103
"-b:v", "0",
104
"-pix_fmt", "yuv420p", "-an"),
105
}
106
elif value == "vp9-lossless":
107
pp = {
108
"extension" : "webm",
109
"ffmpeg-args" : ("-c:v", "libvpx-vp9",
110
"-lossless", "1",
111
"-pix_fmt", "yuv420p", "-an"),
112
}
113
elif value == "vp8":
114
pp = {
115
"extension" : "webm",
116
"ffmpeg-args" : ("-c:v", "libvpx",
117
"-crf", "4",
118
"-b:v", "5M",
119
"-pix_fmt", "yuv420p", "-an"),
120
}
121
elif value == "mp4":
122
pp = {
123
"extension" : "mp4",
124
"ffmpeg-args" : ("-c:v", "libx264",
125
"-b:v", "5M",
126
"-pix_fmt", "yuv420p", "-an"),
127
"libx264-prevent-odd": True,
128
}
129
elif value == "gif":
130
pp = {
131
"extension" : "gif",
132
"ffmpeg-args" : ("-filter_complex", "[0:v] split [a][b];"
133
"[a] palettegen [p];[b][p] paletteuse"),
134
"repeat-last-frame": False,
135
}
136
elif value in {"mkv", "copy"}:
137
pp = {
138
"extension" : "mkv",
139
"ffmpeg-args" : ("-c:v", "copy"),
140
"repeat-last-frame": False,
141
}
142
elif value in {"zip", "archive"}:
143
pp = {
144
"mode" : "archive",
145
}
146
else:
147
parser.error(f"Unsupported Ugoira format '{value}'")
148
149
pp["name"] = "ugoira"
150
pp["whitelist"] = ("pixiv", "danbooru")
151
152
namespace.options.append((("extractor",), "ugoira", "original"))
153
namespace.postprocessors.append(pp)
154
155
156
class PrintAction(argparse.Action):
157
def __call__(self, parser, namespace, value, option_string=None):
158
if self.const:
159
if self.const == "-":
160
namespace.options.append(((), "skip", False))
161
namespace.options.append(((), "download", False))
162
namespace.options.append((("output",), "mode", False))
163
filename = "-"
164
base = None
165
mode = "w"
166
else:
167
if self.const is None:
168
namespace.options.append(((), "skip", False))
169
namespace.options.append(((), "download", False))
170
value, path = value
171
base, filename = os.path.split(path)
172
mode = "a"
173
174
event, sep, format_string = value.partition(":")
175
if not sep:
176
format_string = event
177
event = ("prepare",)
178
else:
179
event = event.strip().lower()
180
if event not in {"init", "file", "after", "skip", "error",
181
"prepare", "prepare-after", "post", "post-after",
182
"finalize", "finalize-success", "finalize-error",
183
"child", "child-after"}:
184
format_string = value
185
event = ("prepare",)
186
187
if not format_string:
188
return
189
190
if format_string.startswith("\\f"):
191
format_string = "\f" + format_string[2:]
192
193
if format_string[0] == "\f":
194
if format_string[1] == "F" and format_string[-1] != "\n":
195
format_string += "\n"
196
elif "{" not in format_string and " " not in format_string:
197
format_string = f"{{{format_string}}}\n"
198
elif format_string[-1] != "\n":
199
format_string += "\n"
200
201
namespace.postprocessors.append({
202
"name" : "metadata",
203
"event" : event,
204
"filename" : filename,
205
"base-directory": base or ".",
206
"content-format": format_string,
207
"open" : mode,
208
})
209
210
211
class Formatter(argparse.HelpFormatter):
212
"""Custom HelpFormatter class to customize help output"""
213
def __init__(self, prog):
214
argparse.HelpFormatter.__init__(self, prog, max_help_position=30)
215
216
def _format_action_invocation(self, action):
217
opts = action.option_strings
218
if action.metavar:
219
opts = opts.copy()
220
opts[-1] = f"{opts[-1]} {action.metavar}"
221
return ", ".join(opts)
222
223
def _format_usage(self, usage, actions, groups, prefix):
224
return f"Usage: {self._prog} [OPTIONS] URL [URL...]\n"
225
226
227
def _parse_option(opt):
228
key, _, value = opt.partition("=")
229
try:
230
value = util.json_loads(value)
231
except ValueError:
232
pass
233
return key, value
234
235
236
def build_parser():
237
"""Build and configure an ArgumentParser object"""
238
SUPPRESS = argparse.SUPPRESS
239
parser = argparse.ArgumentParser(
240
formatter_class=Formatter,
241
add_help=False,
242
)
243
244
general = parser.add_argument_group("General Options")
245
general.add_argument(
246
"-h", "--help",
247
action="help",
248
help="Print this help message and exit",
249
)
250
general.add_argument(
251
"--version",
252
action="version", version=version.__version__,
253
help="Print program version and exit",
254
)
255
general.add_argument(
256
"-f", "--filename",
257
dest="filename", metavar="FORMAT",
258
help=("Filename format string for downloaded files "
259
"('/O' for \"original\" filenames)"),
260
)
261
general.add_argument(
262
"-d", "--destination",
263
dest="base-directory", metavar="PATH", action=ConfigAction,
264
help="Target location for file downloads",
265
)
266
general.add_argument(
267
"-D", "--directory",
268
dest="directory", metavar="PATH",
269
help="Exact location for file downloads",
270
)
271
general.add_argument(
272
"--restrict-filenames",
273
dest="path-restrict", metavar="VALUE", action=ConfigAction,
274
help=("Replace restricted filename characters with underscores. "
275
"One of 'windows', 'unix', 'ascii', 'ascii+', "
276
"or a custom set of characters"),
277
)
278
general.add_argument(
279
"--windows-filenames",
280
dest="path-restrict", nargs=0, action=ConfigConstAction,
281
const="windows",
282
help="Force filenames to be Windows-compatible",
283
)
284
general.add_argument(
285
"-X", "--extractors",
286
dest="extractor_sources", metavar="PATH", action="append",
287
help="Load external extractors from PATH",
288
)
289
general.add_argument(
290
"--clear-cache",
291
dest="clear_cache", metavar="MODULE",
292
help="Delete cached login sessions, cookies, etc. for MODULE "
293
"(ALL to delete everything)",
294
)
295
general.add_argument(
296
"--compat",
297
dest="category-map", nargs=0, action=ConfigConstAction, const="compat",
298
help="Restore legacy 'category' names",
299
)
300
301
update = parser.add_argument_group("Update Options")
302
if util.EXECUTABLE:
303
update.add_argument(
304
"-U", "--update",
305
dest="update", action="store_const", const="latest",
306
help="Update to the latest version",
307
)
308
update.add_argument(
309
"--update-to",
310
dest="update", metavar="CHANNEL[@TAG]",
311
help=("Switch to a dfferent release channel (stable or dev) "
312
"or upgrade/downgrade to a specific version"),
313
)
314
update.add_argument(
315
"--update-check",
316
dest="update", action="store_const", const="check",
317
help="Check if a newer version is available",
318
)
319
else:
320
update.add_argument(
321
"-U", "--update-check",
322
dest="update", action="store_const", const="check",
323
help="Check if a newer version is available",
324
)
325
326
input = parser.add_argument_group("Input Options")
327
input.add_argument(
328
"urls",
329
metavar="URL", nargs="*",
330
help=SUPPRESS,
331
)
332
input.add_argument(
333
"-i", "--input-file",
334
dest="input_files", metavar="FILE", action=InputfileAction, const=None,
335
default=[],
336
help=("Download URLs found in FILE ('-' for stdin). "
337
"More than one --input-file can be specified"),
338
)
339
input.add_argument(
340
"-I", "--input-file-comment",
341
dest="input_files", metavar="FILE", action=InputfileAction, const="c",
342
help=("Download URLs found in FILE. "
343
"Comment them out after they were downloaded successfully."),
344
)
345
input.add_argument(
346
"-x", "--input-file-delete",
347
dest="input_files", metavar="FILE", action=InputfileAction, const="d",
348
help=("Download URLs found in FILE. "
349
"Delete them after they were downloaded successfully."),
350
)
351
input.add_argument(
352
"--no-input",
353
dest="input", nargs=0, action=ConfigConstAction, const=False,
354
help="Do not prompt for passwords/tokens",
355
)
356
357
output = parser.add_argument_group("Output Options")
358
output.add_argument(
359
"-q", "--quiet",
360
dest="loglevel", default=logging.INFO,
361
action="store_const", const=logging.ERROR,
362
help="Activate quiet mode",
363
)
364
output.add_argument(
365
"-w", "--warning",
366
dest="loglevel",
367
action="store_const", const=logging.WARNING,
368
help="Print only warnings and errors",
369
)
370
output.add_argument(
371
"-v", "--verbose",
372
dest="loglevel",
373
action="store_const", const=logging.DEBUG,
374
help="Print various debugging information",
375
)
376
output.add_argument(
377
"-g", "--get-urls",
378
dest="list_urls", action="count",
379
help="Print URLs instead of downloading",
380
)
381
output.add_argument(
382
"-G", "--resolve-urls",
383
dest="list_urls", action="store_const", const=128,
384
help="Print URLs instead of downloading; resolve intermediary URLs",
385
)
386
output.add_argument(
387
"-j", "--dump-json",
388
dest="dump_json", action="count",
389
help="Print JSON information",
390
)
391
output.add_argument(
392
"-J", "--resolve-json",
393
dest="dump_json", action="store_const", const=128,
394
help="Print JSON information; resolve intermediary URLs",
395
)
396
output.add_argument(
397
"-s", "--simulate",
398
dest="jobtype", action="store_const", const=job.SimulationJob,
399
help="Simulate data extraction; do not download anything",
400
)
401
output.add_argument(
402
"-E", "--extractor-info",
403
dest="jobtype", action="store_const", const=job.InfoJob,
404
help="Print extractor defaults and settings",
405
)
406
output.add_argument(
407
"-K", "--list-keywords",
408
dest="jobtype", action="store_const", const=job.KeywordJob,
409
help=("Print a list of available keywords and example values "
410
"for the given URLs"),
411
)
412
output.add_argument(
413
"-e", "--error-file",
414
dest="errorfile", metavar="FILE", action=ConfigAction,
415
help="Add input URLs which returned an error to FILE",
416
)
417
output.add_argument(
418
"-N", "--print",
419
dest="postprocessors", metavar="[EVENT:]FORMAT",
420
action=PrintAction, const="-", default=[],
421
help=("Write FORMAT during EVENT (default 'prepare') to standard "
422
"output instead of downloading files. "
423
"Can be used multiple times. "
424
"Examples: 'id' or 'post:{md5[:8]}'"),
425
)
426
output.add_argument(
427
"--Print",
428
dest="postprocessors", metavar="[EVENT:]FORMAT",
429
action=PrintAction, const="+",
430
help="Like --print, but downloads files as well",
431
)
432
output.add_argument(
433
"--print-to-file",
434
dest="postprocessors", metavar="[EVENT:]FORMAT FILE",
435
action=PrintAction, const=None, nargs=2,
436
help=("Append FORMAT during EVENT to FILE instead of downloading "
437
"files. Can be used multiple times"),
438
)
439
output.add_argument(
440
"--Print-to-file",
441
dest="postprocessors", metavar="[EVENT:]FORMAT FILE",
442
action=PrintAction, const=False, nargs=2,
443
help="Like --print-to-file, but downloads files as well",
444
)
445
output.add_argument(
446
"--list-modules",
447
dest="list_modules", action="store_true",
448
help="Print a list of available extractor modules",
449
)
450
output.add_argument(
451
"--list-extractors",
452
dest="list_extractors", metavar="[CATEGORIES]", nargs="*",
453
help=("Print a list of extractor classes "
454
"with description, (sub)category and example URL"),
455
)
456
output.add_argument(
457
"--write-log",
458
dest="logfile", metavar="FILE", action=ConfigAction,
459
help="Write logging output to FILE",
460
)
461
output.add_argument(
462
"--write-unsupported",
463
dest="unsupportedfile", metavar="FILE", action=ConfigAction,
464
help=("Write URLs, which get emitted by other extractors but cannot "
465
"be handled, to FILE"),
466
)
467
output.add_argument(
468
"--write-pages",
469
dest="write-pages", nargs=0, action=ConfigConstAction, const=True,
470
help=("Write downloaded intermediary pages to files "
471
"in the current directory to debug problems"),
472
)
473
output.add_argument(
474
"--print-traffic",
475
dest="print_traffic", action="store_true",
476
help="Display sent and read HTTP traffic",
477
)
478
output.add_argument(
479
"--no-colors",
480
dest="colors", action="store_false",
481
help="Do not emit ANSI color codes in output",
482
)
483
484
networking = parser.add_argument_group("Networking Options")
485
networking.add_argument(
486
"-R", "--retries",
487
dest="retries", metavar="N", type=int, action=ConfigAction,
488
help=("Maximum number of retries for failed HTTP requests "
489
"or -1 for infinite retries (default: 4)"),
490
)
491
networking.add_argument(
492
"-a", "--user-agent",
493
dest="user-agent", metavar="UA", action=ConfigAction,
494
help="User-Agent request header",
495
)
496
networking.add_argument(
497
"--http-timeout",
498
dest="timeout", metavar="SECONDS", type=float, action=ConfigAction,
499
help="Timeout for HTTP connections (default: 30.0)",
500
)
501
networking.add_argument(
502
"--proxy",
503
dest="proxy", metavar="URL", action=ConfigAction,
504
help="Use the specified proxy",
505
)
506
networking.add_argument(
507
"--xff",
508
dest="geo-bypass", metavar="VALUE", action=ConfigAction,
509
help=("Use a fake 'X-Forwarded-For' HTTP header to try bypassing "
510
"geographic restrictions. Can be IP blocks in CIDR notation "
511
"or two-letter ISO 3166-2 country codes (12.0.0.0/8,FR,CN)")
512
)
513
networking.add_argument(
514
"--source-address",
515
dest="source-address", metavar="IP", action=ConfigAction,
516
help="Client-side IP address to bind to",
517
)
518
networking.add_argument(
519
"-4", "--force-ipv4",
520
dest="source-address", nargs=0, action=ConfigConstAction,
521
const="0.0.0.0",
522
help="Make all connections via IPv4",
523
)
524
networking.add_argument(
525
"-6", "--force-ipv6",
526
dest="source-address", nargs=0, action=ConfigConstAction, const="::",
527
help="Make all connections via IPv6",
528
)
529
networking.add_argument(
530
"--no-check-certificate",
531
dest="verify", nargs=0, action=ConfigConstAction, const=False,
532
help="Disable HTTPS certificate validation",
533
)
534
535
downloader = parser.add_argument_group("Downloader Options")
536
downloader.add_argument(
537
"-r", "--limit-rate",
538
dest="rate", metavar="RATE", action=ConfigAction,
539
help="Maximum download rate (e.g. 500k, 2.5M, or 800k-2M)",
540
)
541
downloader.add_argument(
542
"--chunk-size",
543
dest="chunk-size", metavar="SIZE", action=ConfigAction,
544
help="Size of in-memory data chunks (default: 32k)",
545
)
546
downloader.add_argument(
547
"--sleep",
548
dest="sleep", metavar="SECONDS", action=ConfigAction,
549
help=("Number of seconds to wait before each download. "
550
"This can be either a constant value or a range "
551
"(e.g. 2.7 or 2.0-3.5)"),
552
)
553
downloader.add_argument(
554
"--sleep-skip",
555
dest="sleep-skip", metavar="SECONDS", action=ConfigAction,
556
help=("Number of seconds to wait after skipping a file download"),
557
)
558
downloader.add_argument(
559
"--sleep-request",
560
dest="sleep-request", metavar="SECONDS", action=ConfigAction,
561
help=("Number of seconds to wait between HTTP requests "
562
"during data extraction"),
563
)
564
downloader.add_argument(
565
"--sleep-429",
566
dest="sleep-429", metavar="SECONDS", action=ConfigAction,
567
help=("Number of seconds to wait when receiving a "
568
"'429 Too Many Requests' response"),
569
)
570
downloader.add_argument(
571
"--sleep-extractor",
572
dest="sleep-extractor", metavar="SECONDS", action=ConfigAction,
573
help=("Number of seconds to wait before starting data extraction "
574
"for an input URL"),
575
)
576
downloader.add_argument(
577
"--no-part",
578
dest="part", nargs=0, action=ConfigConstAction, const=False,
579
help="Do not use .part files",
580
)
581
downloader.add_argument(
582
"--no-skip",
583
dest="skip", nargs=0, action=ConfigConstAction, const=False,
584
help="Do not skip downloads; overwrite existing files",
585
)
586
downloader.add_argument(
587
"--no-mtime",
588
dest="mtime", nargs=0, action=ConfigConstAction, const=False,
589
help=("Do not set file modification times according to "
590
"Last-Modified HTTP response headers")
591
)
592
downloader.add_argument(
593
"--no-download",
594
dest="download", nargs=0, action=ConfigConstAction, const=False,
595
help=("Do not download any files")
596
)
597
598
configuration = parser.add_argument_group("Configuration Options")
599
configuration.add_argument(
600
"-o", "--option",
601
dest="options", metavar="KEY=VALUE",
602
action=ConfigParseAction, default=[],
603
help=("Additional options. "
604
"Example: -o browser=firefox") ,
605
)
606
configuration.add_argument(
607
"-c", "--config",
608
dest="configs_json", metavar="FILE", action="append",
609
help="Additional configuration files",
610
)
611
configuration.add_argument(
612
"--config-yaml",
613
dest="configs_yaml", metavar="FILE", action="append",
614
help="Additional configuration files in YAML format",
615
)
616
configuration.add_argument(
617
"--config-toml",
618
dest="configs_toml", metavar="FILE", action="append",
619
help="Additional configuration files in TOML format",
620
)
621
configuration.add_argument(
622
"--config-create",
623
dest="config", action="store_const", const="init",
624
help="Create a basic configuration file",
625
)
626
configuration.add_argument(
627
"--config-status",
628
dest="config", action="store_const", const="status",
629
help="Show configuration file status",
630
)
631
configuration.add_argument(
632
"--config-open",
633
dest="config", action="store_const", const="open",
634
help="Open configuration file in external application",
635
)
636
configuration.add_argument(
637
"--config-type",
638
dest="config_type", metavar="TYPE",
639
help=("Set filetype of default configuration files "
640
"(json, yaml, toml)"),
641
)
642
configuration.add_argument(
643
"--config-ignore",
644
dest="config_load", action="store_false",
645
help="Do not read default configuration files",
646
)
647
configuration.add_argument(
648
"--ignore-config",
649
dest="config_load", action="store_false",
650
help=SUPPRESS,
651
)
652
653
authentication = parser.add_argument_group("Authentication Options")
654
authentication.add_argument(
655
"-u", "--username",
656
dest="username", metavar="USER", action=ConfigAction,
657
help="Username to login with",
658
)
659
authentication.add_argument(
660
"-p", "--password",
661
dest="password", metavar="PASS", action=ConfigAction,
662
help="Password belonging to the given username",
663
)
664
authentication.add_argument(
665
"--netrc",
666
dest="netrc", nargs=0, action=ConfigConstAction, const=True,
667
help="Enable .netrc authentication data",
668
)
669
670
cookies = parser.add_argument_group("Cookie Options")
671
cookies.add_argument(
672
"-C", "--cookies",
673
dest="cookies", metavar="FILE", action=ConfigAction,
674
help="File to load additional cookies from",
675
)
676
cookies.add_argument(
677
"--cookies-export",
678
dest="cookies-update", metavar="FILE", action=ConfigAction,
679
help="Export session cookies to FILE",
680
)
681
cookies.add_argument(
682
"--cookies-from-browser",
683
dest="cookies_from_browser",
684
metavar="BROWSER[/DOMAIN][+KEYRING][:PROFILE][::CONTAINER]",
685
help=("Name of the browser to load cookies from, with optional "
686
"domain prefixed with '/', "
687
"keyring name prefixed with '+', "
688
"profile prefixed with ':', and "
689
"container prefixed with '::' "
690
"('none' for no container (default), 'all' for all containers)"),
691
)
692
693
selection = parser.add_argument_group("Selection Options")
694
selection.add_argument(
695
"-A", "--abort",
696
dest="abort", metavar="N[:TARGET]",
697
help=("Stop current extractor(s) "
698
"after N consecutive file downloads were skipped. "
699
"Specify a TARGET to set how many levels to ascend or "
700
"to which subcategory to jump to. "
701
"Examples: '-A 3', '-A 3:2', '-A 3:manga'"),
702
)
703
selection.add_argument(
704
"-T", "--terminate",
705
dest="terminate", metavar="N",
706
help=("Stop current & parent extractors "
707
"and proceed with the next input URL "
708
"after N consecutive file downloads were skipped"),
709
)
710
selection.add_argument(
711
"--filesize-min",
712
dest="filesize-min", metavar="SIZE", action=ConfigAction,
713
help="Do not download files smaller than SIZE (e.g. 500k or 2.5M)",
714
)
715
selection.add_argument(
716
"--filesize-max",
717
dest="filesize-max", metavar="SIZE", action=ConfigAction,
718
help="Do not download files larger than SIZE (e.g. 500k or 2.5M)",
719
)
720
selection.add_argument(
721
"--download-archive",
722
dest="archive", metavar="FILE", action=ConfigAction,
723
help=("Record successfully downloaded files in FILE and "
724
"skip downloading any file already in it"),
725
)
726
selection.add_argument(
727
"--range",
728
dest="file-range", metavar="RANGE", action=ConfigAction,
729
help=("Index range(s) specifying which files to download. "
730
"These can be either a constant value, range, or slice "
731
"(e.g. '5', '8-20', or '1:24:3')"),
732
)
733
selection.add_argument(
734
"--post-range",
735
dest="post-range", metavar="RANGE", action=ConfigAction,
736
help=("Like '--range', but for posts"),
737
)
738
selection.add_argument(
739
"--child-range",
740
dest="child-range", metavar="RANGE", action=ConfigAction,
741
help=("Like '--range', but for child extractors handling "
742
"manga chapters, external URLs, etc."),
743
)
744
selection.add_argument(
745
"--filter",
746
dest="file-filter", metavar="EXPR", action=ConfigAction,
747
help=("Python expression controlling which files to download. "
748
"Files for which the expression evaluates to False are ignored. "
749
"Available keys are the filename-specific ones listed by '-K'. "
750
"Example: --filter \"image_width >= 1000 and "
751
"rating in ('s', 'q')\""),
752
)
753
selection.add_argument(
754
"--post-filter",
755
dest="post-filter", metavar="EXPR", action=ConfigAction,
756
help=("Like '--filter', but for posts"),
757
)
758
selection.add_argument(
759
"--child-filter",
760
dest="child-filter", metavar="EXPR", action=ConfigAction,
761
help=("Like '--filter', but for child extractors handling "
762
"manga chapters, external URLs, etc."),
763
)
764
selection.add_argument(
765
"--file-range", "--image-range",
766
dest="file-range", action=ConfigAction, help=SUPPRESS)
767
selection.add_argument(
768
"--chapter-range",
769
dest="child-range", action=ConfigAction, help=SUPPRESS)
770
selection.add_argument(
771
"--file-filter", "--image-filter",
772
dest="file-filter", action=ConfigAction, help=SUPPRESS)
773
selection.add_argument(
774
"--chapter-filter",
775
dest="child-filter", action=ConfigAction, help=SUPPRESS)
776
777
infojson = {
778
"name" : "metadata",
779
"event" : "init",
780
"filename": "info.json",
781
}
782
postprocessor = parser.add_argument_group("Post-processing Options")
783
postprocessor.add_argument(
784
"-P", "--postprocessor",
785
dest="postprocessors", metavar="NAME", action="append",
786
help="Activate the specified post processor",
787
)
788
postprocessor.add_argument(
789
"--no-postprocessors",
790
dest="postprocess", nargs=0, action=ConfigConstAction, const=False,
791
help=("Do not run any post processors")
792
)
793
postprocessor.add_argument(
794
"-O", "--postprocessor-option",
795
dest="options_pp", metavar="KEY=VALUE",
796
action=PPParseAction, default={},
797
help="Additional post processor options",
798
)
799
postprocessor.add_argument(
800
"--write-metadata",
801
dest="postprocessors",
802
action="append_const", const="metadata",
803
help="Write metadata to separate JSON files",
804
)
805
postprocessor.add_argument(
806
"--write-info-json",
807
dest="postprocessors",
808
action="append_const", const=infojson,
809
help="Write gallery metadata to a info.json file",
810
)
811
postprocessor.add_argument(
812
"--write-infojson",
813
dest="postprocessors",
814
action="append_const", const=infojson,
815
help=SUPPRESS,
816
)
817
postprocessor.add_argument(
818
"--write-tags",
819
dest="postprocessors",
820
action="append_const", const={"name": "metadata", "mode": "tags"},
821
help="Write image tags to separate text files",
822
)
823
postprocessor.add_argument(
824
"--zip",
825
dest="postprocessors",
826
action="append_const", const="zip",
827
help="Store downloaded files in a ZIP archive",
828
)
829
postprocessor.add_argument(
830
"--cbz",
831
dest="postprocessors",
832
action="append_const", const={
833
"name" : "zip",
834
"extension": "cbz",
835
},
836
help="Store downloaded files in a CBZ archive",
837
)
838
postprocessor.add_argument(
839
"--mtime",
840
dest="postprocessors", metavar="NAME", action=MtimeAction,
841
help=("Set file modification times according to metadata "
842
"selected by NAME. Examples: 'date' or 'status[date]'"),
843
)
844
postprocessor.add_argument(
845
"--mtime-from-date",
846
dest="postprocessors", nargs=0, action=MtimeAction,
847
const="date|status[date]",
848
help=SUPPRESS,
849
)
850
postprocessor.add_argument(
851
"--rename",
852
dest="postprocessors", metavar="FORMAT", action=RenameAction, const=0,
853
help=("Rename previously downloaded files from FORMAT "
854
"to the current filename format"),
855
)
856
postprocessor.add_argument(
857
"--rename-to",
858
dest="postprocessors", metavar="FORMAT", action=RenameAction, const=1,
859
help=("Rename previously downloaded files from the current filename "
860
"format to FORMAT"),
861
)
862
postprocessor.add_argument(
863
"--ugoira",
864
dest="postprocessors", metavar="FMT", action=UgoiraAction,
865
help=("Convert Pixiv Ugoira to FMT using FFmpeg. "
866
"Supported formats are 'webm', 'mp4', 'gif', "
867
"'vp8', 'vp9', 'vp9-lossless', 'copy', 'zip'."),
868
)
869
postprocessor.add_argument(
870
"--ugoira-conv",
871
dest="postprocessors", nargs=0, action=UgoiraAction, const="vp8",
872
help=SUPPRESS,
873
)
874
postprocessor.add_argument(
875
"--ugoira-conv-lossless",
876
dest="postprocessors", nargs=0, action=UgoiraAction,
877
const="vp9-lossless",
878
help=SUPPRESS,
879
)
880
postprocessor.add_argument(
881
"--ugoira-conv-copy",
882
dest="postprocessors", nargs=0, action=UgoiraAction, const="copy",
883
help=SUPPRESS,
884
)
885
postprocessor.add_argument(
886
"--exec",
887
dest="postprocessors", metavar="CMD",
888
action=AppendCommandAction, const={"name": "exec"},
889
help=("Execute CMD for each downloaded file. "
890
"Supported replacement fields are "
891
"{} or {_path}, {_directory}, {_filename}. "
892
"Example: --exec \"convert {} {}.png && rm {}\""),
893
)
894
postprocessor.add_argument(
895
"--exec-after",
896
dest="postprocessors", metavar="CMD",
897
action=AppendCommandAction, const={
898
"name": "exec", "event": "finalize"},
899
help=("Execute CMD after all files were downloaded. "
900
"Example: --exec-after \"cd {_directory} "
901
"&& convert * ../doc.pdf\""),
902
)
903
904
try:
905
# restore normal behavior when adding '-4' or '-6' as arguments
906
parser._has_negative_number_optionals.clear()
907
except Exception:
908
pass
909
910
return parser
911
912