-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjina.ai_reader
More file actions
executable file
·710 lines (624 loc) · 30.8 KB
/
Copy pathjina.ai_reader
File metadata and controls
executable file
·710 lines (624 loc) · 30.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
#!/usr/bin/env -S uv run
# /// script
# dependencies = [
# "requests>=2.31",
# "python-dotenv>=1.0",
# ]
# requires-python = ">=3.11"
# ///
# Get your Jina AI API key for free: https://jina.ai/?sui=apikey
# NOTE TO DEVELOPERS:
# All verbose or debug messages must go to standard error (stderr).
# Only the fetched output (i.e., the JSON response or extracted content from the Jina Reader API)
# should be printed to standard output (stdout).
import sys
import os
import argparse
import requests
import json
import re
import base64
import mimetypes
from pathlib import Path
from urllib.parse import urlparse, unquote
from typing import Optional, Dict, Any, List
try:
# Use python-dotenv for robust .env parsing (handles comments, quotes, export, etc.)
from dotenv import dotenv_values
except ImportError: # Fallback if library is missing; handled later.
dotenv_values = None
def url_to_filename(url, verbosity=0):
"""
Convert a URL to a safe filename.
Args:
url (str): The URL to convert
verbosity (int): Verbosity level
Returns:
str: A filename-safe string derived from the URL
"""
# Handle special cases for common sites
parsed = urlparse(url)
domain = parsed.netloc.lower()
# GitHub URLs
if 'github.com' in domain:
parts = parsed.path.strip('/').split('/')
if len(parts) >= 2:
# Format: owner_repo[_extra]
result = '_'.join(parts[:3]).replace('/', '_')
return re.sub(r'[^a-zA-Z0-9_-]', '_', result)[:100]
# Wikipedia URLs
if 'wikipedia.org' in domain:
# Extract article name from path
match = re.search(r'/wiki/(.+?)(?:\?|#|$)', parsed.path)
if match:
article = match.group(1).replace('_', '-')
return f"wiki_{re.sub(r'[^a-zA-Z0-9_-]', '_', article)}"[:100]
# YouTube URLs
if 'youtube.com' in domain or 'youtu.be' in domain:
# Extract video ID
video_id = None
if 'youtube.com' in domain:
video_id = re.search(r'[?&]v=([^&]+)', url)
if video_id:
video_id = video_id.group(1)
elif 'youtu.be' in domain:
video_id = parsed.path.strip('/')
if video_id:
return f"youtube_{video_id}"
# Default: use generic handler
return generic_url_to_filename(url, verbosity)
def generic_url_to_filename(url, verbosity=0):
"""
Generic function to convert any URL to a safe filename.
Args:
url (str): The URL to convert
verbosity (int): Verbosity level
Returns:
str: A filename-safe string derived from the URL
"""
if verbosity > 1:
print(f"Converting URL to filename: {url}", file=sys.stderr)
# Parse URL to get meaningful parts
parsed = urlparse(url)
# Start with domain
domain = parsed.netloc.replace('www.', '')
# Add path if present
path = unquote(parsed.path).strip('/')
# Combine domain and path
if path:
clean_url = f"{domain}_{path}"
else:
clean_url = domain
# Replace all characters that aren't alphanumeric, underscore, or hyphen
clean_url = re.sub(r'[^a-zA-Z0-9_-]', '_', clean_url)
# Collapse multiple underscores into one
clean_url = re.sub(r'_+', '_', clean_url).strip('_')
# Limit length to avoid excessively long filenames
if len(clean_url) > 100:
# Try to keep the end part which often has more specific info
clean_url = clean_url[-100:]
# Ensure we have a valid filename
if not clean_url:
clean_url = "jina_output"
if verbosity > 1:
print(f"Generated filename: {clean_url}", file=sys.stderr)
return clean_url
def main():
parser = argparse.ArgumentParser(
description="A script that fetches pages from the Jina AI Reader API (https://r.jina.ai/)."
"Optional flags allow passing certain 'X-' headers. If the environment variable "
"'JINA_API_KEY' is set, the script will use it. Otherwise, it will issue the request without authorization."
)
parser.add_argument("-q", "--quiet", action="store_true",
help="Set verbosity level to 0, suppressing all output except errors.")
parser.add_argument("-v", "--verbose", action="count", default=0,
help="Increase verbosity level. Can be used multiple times.")
parser.add_argument("-u", "--url", required=True,
help="The URL of the webpage to fetch.")
parser.add_argument("--no-cache", action="store_true",
help="If set, passes X-No-Cache: true.")
parser.add_argument("-x", "--remove-selector",
help="Comma-separated CSS selectors to exclude from the page.")
parser.add_argument("-s", "--target-selector",
help="Comma-separated CSS selectors to focus on.")
parser.add_argument("-t", "--timeout", type=int,
help="Specifies the maximum time (in seconds) to wait for the webpage to load.")
parser.add_argument("--wait-for-selector",
help="Comma-separated CSS selectors to wait for before returning.")
parser.add_argument("-l", "--with-links-summary", action="store_true",
help="If set, gather all links at the end of the response.")
parser.add_argument("-i", "--with-images-summary", action="store_true",
help="If set, gather all images at the end of the response.")
parser.add_argument("-a", "--with-generated-alt", action="store_true",
help="If set, generate alt text for images without captions.")
parser.add_argument("-I", "--with-iframe", action="store_true",
help="If set, include iframe content in the response.")
parser.add_argument("-F", "--return-format",
choices=["m", "h", "t", "s", "p", "markdown", "html", "text", "screenshot", "pageshot"],
help="Sets the X-Return-Format header: "
"m (markdown), h (html), t (text), s (screenshot), p (pageshot).")
parser.add_argument("--token-budget", type=int,
help="Specifies the maximum number of tokens to use for the request.")
parser.add_argument("-N", "--retain-images", choices=["none"],
help="Use 'none' to remove images from the response.")
parser.add_argument("-f", "--field",
choices=["content", "links", "images", "title", "description"],
help="Specify a field to print its raw value instead of the whole JSON.")
parser.add_argument("-c", "--content", action="store_true",
help="Equivalent to --field content.")
parser.add_argument("-d", "--description", action="store_true",
help="Equivalent to --field description.")
parser.add_argument("-k", "--key-file", dest="key_file",
help="Path to a .env-like file that contains a line such as "
"'JINA_API_KEY=...' or 'export JINA_API_KEY=...'.")
parser.add_argument("--engine", choices=["browser", "direct", "cf-browser-rendering"],
help="Content retrieval engine (X-Engine header).")
parser.add_argument("--respond-with", choices=["readerlm-v2", "no-content"],
help="Specialized response mode (X-Respond-With header).")
parser.add_argument("--set-cookie",
help="Custom cookie settings (X-Set-Cookie header).")
parser.add_argument("--proxy-url",
help="Proxy URL for accessing the target (X-Proxy-Url header).")
parser.add_argument("--locale",
help="Browser locale for rendering (X-Locale header, e.g., 'en-US').")
# New flags:
parser.add_argument("-o", "--output",
help="Path or filename for the single output file, or prefix for multiple outputs if used with --save-all. "
"Use 'auto' to automatically generate filename based on URL.")
parser.add_argument("--save-all", "-A",
help="Comma-separated list of items or 'all' to produce multiple files from a single request. "
"Possible items include 'json', 'content', 'title', 'description', 'links', 'images', "
"'text', 'markdown', 'html'. "
"Use 'all' to export all recognized fields plus 'json' (if available).")
parser.add_argument("--force", action="store_true",
help="If set, create output files even if the content is empty (default: skip empty files).")
parser.add_argument("--download-images", action="store_true",
help="When used with --save-all and images are included, download actual image files to a subfolder.")
args = parser.parse_args()
# Determine verbosity level
verbosity = 1 # Default verbosity
if args.quiet:
verbosity = 0
else:
verbosity += args.verbose
endpoint = "https://r.jina.ai/"
# Always set Accept to application/json
headers = {
"Accept": "application/json",
"Content-Type": "application/json"
}
# Resolve API key:
# 1. If --key-file is provided, try to read the key from that file.
# The file may contain either `JINA_API_KEY=...` or `export JINA_API_KEY=...`.
# 2. Otherwise, fall back to the JINA_API_KEY environment variable.
jina_api_key = None
if args.key_file:
# Use python-dotenv for robust Bash-compatible .env parsing
if dotenv_values is None:
if verbosity > 0:
print(
"python-dotenv is required to parse key files. "
"Install it with 'pip install python-dotenv'.",
file=sys.stderr,
)
sys.exit(1)
try:
config = dotenv_values(args.key_file)
except Exception as e:
if verbosity > 0:
print(f"Error reading key file '{args.key_file}': {e}", file=sys.stderr)
sys.exit(1)
jina_api_key = config.get("JINA_API_KEY")
if not jina_api_key:
if verbosity > 0:
print(f"No JINA_API_KEY found in key file '{args.key_file}'.", file=sys.stderr)
sys.exit(1)
elif verbosity > 0:
print(f"Using JINA_API_KEY from key file '{args.key_file}'.", file=sys.stderr)
else:
jina_api_key = os.environ.get("JINA_API_KEY")
if jina_api_key and verbosity > 0:
print("Using JINA_API_KEY from environment.", file=sys.stderr)
if jina_api_key:
headers["Authorization"] = f"Bearer {jina_api_key}"
else:
if verbosity > 0:
print("JINA_API_KEY not found; proceeding without authorization.", file=sys.stderr)
# Apply optional headers based on flags
if args.no_cache:
headers["X-No-Cache"] = "true"
if args.remove_selector:
headers["X-Remove-Selector"] = args.remove_selector
if args.target_selector:
headers["X-Target-Selector"] = args.target_selector
if args.timeout is not None:
headers["X-Timeout"] = str(args.timeout)
if args.wait_for_selector:
headers["X-Wait-For-Selector"] = args.wait_for_selector
if args.with_links_summary:
headers["X-With-Links-Summary"] = "true"
if args.with_images_summary:
headers["X-With-Images-Summary"] = "true"
if args.with_generated_alt:
headers["X-With-Generated-Alt"] = "true"
if args.with_iframe:
headers["X-With-Iframe"] = "true"
normalized_rf = None
if args.return_format:
format_map = {
"m": "markdown",
"h": "html",
"t": "text",
"s": "screenshot",
"p": "pageshot"
}
normalized_rf = format_map.get(args.return_format, args.return_format)
headers["X-Return-Format"] = normalized_rf
if args.token_budget is not None:
headers["X-Token-Budget"] = str(args.token_budget)
if args.retain_images:
headers["X-Retain-Images"] = args.retain_images
# Additional headers from new arguments
if args.engine:
headers["X-Engine"] = args.engine
if args.respond_with:
headers["X-Respond-With"] = args.respond_with
if args.set_cookie:
headers["X-Set-Cookie"] = args.set_cookie
if args.proxy_url:
headers["X-Proxy-Url"] = args.proxy_url
if args.locale:
headers["X-Locale"] = args.locale
# Shortcut flags
if args.content:
args.field = "content"
elif args.description:
args.field = "description"
payload = {
"url": args.url
}
# Perform the request (only once)
try:
if verbosity > 0:
print(f"Sending request to {endpoint} with provided parameters...", file=sys.stderr)
# Use timeout from args if provided, otherwise default to 60 seconds
request_timeout = max(args.timeout if args.timeout else 60, 60)
response = requests.post(endpoint, headers=headers, json=payload, timeout=request_timeout)
response.raise_for_status()
data = response.json()
except requests.exceptions.Timeout:
print(f"Error: Request timed out after {request_timeout} seconds", file=sys.stderr)
sys.exit(1)
except requests.exceptions.ConnectionError:
print("Error: Failed to connect to Jina AI Reader API. Check your internet connection.", file=sys.stderr)
sys.exit(1)
except requests.exceptions.HTTPError as e:
print(f"HTTP Error {response.status_code}: {e}", file=sys.stderr)
if response.status_code == 401:
print("Authentication failed. Check your API key.", file=sys.stderr)
elif response.status_code == 429:
print("Rate limit exceeded. Please try again later.", file=sys.stderr)
sys.exit(1)
except requests.exceptions.RequestException as e:
print(f"Error while making request to Jina AI Reader API: {e}", file=sys.stderr)
sys.exit(1)
except ValueError as e:
print(f"Error parsing JSON response from Jina AI Reader API: {e}", file=sys.stderr)
print(f"Response content: {response.text[:500]}...", file=sys.stderr)
sys.exit(1)
# =========================================================
# Logic for new flags --output / --save-all
# =========================================================
# TODO: Enhance --save-all implementation:
# - Handle which --field options work with which --return-format values
# - Different return formats have different structures requiring specific extraction logic
# - Add logic to download images when they are URLs, with appropriate naming conventions
# - Ensure proper standards compliance for xattr metadata
# - Implement overwrite protection options (force, skip, interactive) with sensible defaults
# =========================================================
def get_data_string(item, full_data):
"""
Return the string content for a requested 'item'.
'item' can be:
- 'json': the entire JSON string
- 'text' / 'markdown' / 'html' / 'screenshot' / 'pageshot'
- recognized field among 'content', 'title', 'description', 'links', 'images', 'url'
"""
# Return entire JSON
if item == "json":
return json.dumps(full_data, indent=2, ensure_ascii=False)
# Special case: original URL
if item == "url":
return args.url
# Handle different response structures
# The response has 'data' field which contains the actual content
data_section = full_data.get("data", {})
# For format-specific items - these might be in the root data section
if item in ["text", "markdown", "html", "screenshot", "pageshot"]:
# First check if it's directly in data section
value = data_section.get(item, "")
if not value and "content" in data_section:
# If not found, it might be the content field for certain formats
if item == normalized_rf:
value = data_section.get("content", "")
return value
# For field items
if item in ["content", "title", "description", "url"]:
return str(data_section.get(item, ""))
# For links and images, these are returned as dictionaries with descriptive keys
# according to the docs: {"Image 1": "https://example.com/image.jpg"}
if item in ["links", "images"]:
value = data_section.get(item, {})
if isinstance(value, dict):
# Convert dict values to a list for easier processing
if item == "images":
# Return list of image URLs for download functionality
return json.dumps(list(value.values()), indent=2, ensure_ascii=False)
else:
# Return the full dict for links
return json.dumps(value, indent=2, ensure_ascii=False)
return json.dumps(value, indent=2, ensure_ascii=False)
# Default
return str(data_section.get(item, ""))
def detect_extension(r_format, item):
"""
Choose file extension based on return format or item.
"""
# Direct mappings
extension_map = {
"json": ".json",
"url": ".url",
"markdown": ".md",
"html": ".html",
"text": ".txt",
"screenshot": ".png",
"pageshot": ".png",
"links": ".json",
"images": ".json"
}
# Check item first
if item in extension_map:
return extension_map[item]
# Then check format
if r_format in extension_map:
return extension_map[r_format]
# Item-specific extensions
if item == "content":
# Content extension depends on the return format
if r_format == "markdown":
return ".md"
elif r_format == "html":
return ".html"
# Fallback
return ".txt"
def set_extended_attribute(filename, url_val):
"""
Attempt to store the original url in extended attribute: user.xdg.origin.url
If it fails, ignore unless verbosity > 0, then show a warning.
"""
try:
# Check if system supports extended attributes
if hasattr(os, 'setxattr'):
os.setxattr(filename, "user.xdg.origin.url", url_val.encode('utf-8'))
elif sys.platform == 'darwin':
# macOS requires different attribute name format
import subprocess
subprocess.run(['xattr', '-w', 'com.apple.metadata:kMDItemWhereFroms',
f'("{url_val}")', filename],
capture_output=True, check=False)
except (OSError, AttributeError) as e:
if verbosity > 1: # Only show in verbose mode
print(f"Note: Extended attributes not supported on this system: {e}", file=sys.stderr)
# If --save-all is used, we produce multiple output files
if args.save_all:
if not args.output:
# Must have an -o prefix if we're saving multiple outputs
print("Error: --save-all requires --output as a filename prefix.", file=sys.stderr)
sys.exit(1)
# Handle 'auto' special value for --output
if args.output.lower() == 'auto':
args.output = url_to_filename(args.url, verbosity)
if verbosity > 0:
print(f"Auto-generated filename prefix: {args.output}", file=sys.stderr)
# Build the list of items we want to generate
items_to_export = []
# If user specified a return format, produce the "main" version
# (like entire text or entire markdown) unless that is already
# going to appear in the user list. If no format is specified,
# produce "json" as the main form.
if normalized_rf:
main_item = normalized_rf
else:
main_item = "json"
# parse comma separated items
raw_list = [x.strip() for x in args.save_all.split(",") if x.strip()]
# Expand "all" if present
# We'll define "all" as: json, content, title, description, links, images, text, markdown, html
# We won't forcibly produce screenshot/pageshot unless user specifically asks, to limit confusion
def all_list():
return ["json", "content", "title", "description", "links", "images", "text", "markdown", "html", "url"]
# Validate and expand the list of requested items
VALID_SAVE_ITEMS = set(all_list() + ["screenshot", "pageshot"])
expanded_items = []
for token in raw_list:
token_lower = token.lower()
if token_lower == "all":
expanded_items.extend(all_list())
elif token_lower in VALID_SAVE_ITEMS:
expanded_items.append(token_lower)
else:
print(
f"Error: invalid token '{token}' for --save-all. "
f"Valid options are: {', '.join(sorted(VALID_SAVE_ITEMS | {'all'}))}.",
file=sys.stderr,
)
sys.exit(1)
# Ensure we add the main_item at the front if not present
all_seen = set()
if main_item not in expanded_items:
items_to_export.append(main_item)
all_seen.add(main_item)
# Add expansions
for it in expanded_items:
if it not in all_seen:
items_to_export.append(it)
all_seen.add(it)
# Now produce each item in items_to_export
for item in items_to_export:
out_str = get_data_string(item, data)
if out_str is None:
out_str = ""
# Special handling for images - download actual image files
if item == "images" and args.download_images:
# Parse the images JSON and download each image
try:
images_data = json.loads(out_str) if isinstance(out_str, str) else out_str
if isinstance(images_data, list) and images_data:
images_dir = f"{args.output}_images"
os.makedirs(images_dir, exist_ok=True)
downloaded_images = []
for idx, img_url in enumerate(images_data):
if isinstance(img_url, str) and img_url.startswith(('http://', 'https://')):
try:
# Use a proper User-Agent for Wikipedia images
img_headers = {
'User-Agent': 'JinaAIReader/1.0 (https://github.com/user/jina-reader-cli; contact@example.com)'
}
img_response = requests.get(img_url, headers=img_headers, timeout=30)
img_response.raise_for_status()
# Determine file extension from content-type
content_type = img_response.headers.get('content-type', '')
ext = mimetypes.guess_extension(content_type) or '.jpg'
img_filename = os.path.join(images_dir, f"image_{idx:03d}{ext}")
with open(img_filename, 'wb') as img_file:
img_file.write(img_response.content)
downloaded_images.append({
"url": img_url,
"local_path": img_filename
})
if verbosity > 0:
print(f"Downloaded image {idx+1}/{len(images_data)}: {img_filename}", file=sys.stderr)
except Exception as e:
if verbosity > 0:
print(f"Failed to download image {img_url}: {e}", file=sys.stderr)
# Save the mapping of URLs to local files
if downloaded_images:
mapping_file = f"{args.output}.images_mapping.json"
with open(mapping_file, 'w', encoding='utf-8') as f:
json.dump(downloaded_images, f, indent=2)
if verbosity > 0:
print(f"Saved image mapping to {mapping_file}", file=sys.stderr)
except Exception as e:
if verbosity > 0:
print(f"Error processing images: {e}", file=sys.stderr)
# If not --force, skip writing empty files
if not args.force and (out_str == "" or (isinstance(out_str, str) and out_str.strip() == "")):
if verbosity > 1:
print(f"Skipping {item} (empty, not saving file).", file=sys.stderr)
continue
# Deduce extension
ext = detect_extension(normalized_rf, item)
# For the 'main_item', we don't want a .markdown.md scenario, so skip appending the item if it matches
# Example: if user typed -F markdown, main_item=markdown => file.md
if item == main_item:
out_filename = f"{args.output}{ext}"
# Avoid double extension like .url.url
elif ext == f".{item}":
out_filename = f"{args.output}{ext}"
else:
out_filename = f"{args.output}.{item}{ext}"
# Handle existing files
if os.path.exists(out_filename) and not args.force:
if verbosity > 0:
print(f"Warning: File {out_filename} already exists. Use --force to overwrite.", file=sys.stderr)
continue
try:
# Handle base64 encoded images
if item in ["screenshot", "pageshot"] and out_str.startswith("data:image/"):
# Extract base64 data
header, base64_data = out_str.split(",", 1)
image_data = base64.b64decode(base64_data)
with open(out_filename, "wb") as f_out:
f_out.write(image_data)
if verbosity > 0:
print(f"Saved base64 image to {out_filename}", file=sys.stderr)
else:
# Regular text content
with open(out_filename, "w", encoding="utf-8") as f_out:
f_out.write(str(out_str))
if verbosity > 0:
print(f"Saved {item} to {out_filename}", file=sys.stderr)
except OSError as e:
print(f"Error writing to {out_filename}: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Unexpected error writing {out_filename}: {e}", file=sys.stderr)
sys.exit(1)
# Attempt to store extended attribute for the original URL
set_extended_attribute(out_filename, args.url)
# Done with multi-output mode
sys.exit(0)
else:
# Legacy single-output path:
# We either print to stdout or write exactly one file if --output is used.
# Decide what the single run's output should be
def produce_single_output():
"""Return a string representing what we would normally print to stdout."""
# If user explicitly asked for a field
if args.field:
# Check for special case: --return-format text and --field content
# The code below does something similar in original logic.
if normalized_rf == "text" and args.field == "content":
text_value = data.get("data", {}).get("text")
if text_value is not None:
return text_value
else:
# "Field 'text' not found"
return ""
else:
field_value = data.get("data", {}).get(args.field)
if field_value is not None:
# For dict/list fields, return JSON
if isinstance(field_value, (dict, list)):
return json.dumps(field_value, indent=2, ensure_ascii=False)
return str(field_value)
else:
return ""
# If no field is specified but we have a return format
# Check both direct field and content field
if normalized_rf in ["text", "markdown", "html"]:
# First try the specific format field
value = data.get("data", {}).get(normalized_rf, "")
if not value:
# Fall back to content field
value = data.get("data", {}).get("content", "")
return value
elif normalized_rf == "screenshot":
return data.get("data", {}).get("screenshot", "")
elif normalized_rf == "pageshot":
return data.get("data", {}).get("pageshot", "")
# Otherwise, just return the entire JSON
return json.dumps(data, indent=2, ensure_ascii=False)
single_output_str = produce_single_output()
if args.output:
output_filename = args.output
# Handle 'auto' special value for --output
if args.output.lower() == 'auto':
output_filename = url_to_filename(args.url, verbosity)
if verbosity > 0:
print(f"Auto-generated filename: {output_filename}", file=sys.stderr)
# TODO: Implement overwrite protection (force, skip, interactive modes)
try:
with open(output_filename, "w", encoding="utf-8") as f_out:
f_out.write(single_output_str)
except OSError as e:
if verbosity > 0:
print(f"Error writing to {args.output}: {e}", file=sys.stderr)
sys.exit(1)
# Attempt to store extended attribute for the original URL
set_extended_attribute(output_filename, args.url)
else:
# Print to stdout as before
print(single_output_str)
if __name__ == "__main__":
main()