From 6e27430a96542df2bbdce78beab327320400801d Mon Sep 17 00:00:00 2001 From: CyrixJD115 Date: Thu, 9 Apr 2026 15:16:00 -0500 Subject: [PATCH 1/4] Add CLI with flags, args, output modes, download, and portable runner --- bingart.py | 20 +++ bingart/__init__.py | 11 +- bingart/cli.py | 325 ++++++++++++++++++++++++++++++++++++++++++++ setup.py | 5 +- 4 files changed, 359 insertions(+), 2 deletions(-) create mode 100755 bingart.py create mode 100644 bingart/cli.py diff --git a/bingart.py b/bingart.py new file mode 100755 index 0000000..97dfb5d --- /dev/null +++ b/bingart.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +import sys +import os +import importlib.util + +_here = os.path.dirname(os.path.abspath(__file__)) +_pkg_dir = os.path.join(_here, "bingart") + +spec = importlib.util.spec_from_file_location( + "bingart", + os.path.join(_pkg_dir, "__init__.py"), + submodule_search_locations=[_pkg_dir], +) +pkg = importlib.util.module_from_spec(spec) +sys.modules["bingart"] = pkg +spec.loader.exec_module(pkg) + +from bingart.cli import main + +main() diff --git a/bingart/__init__.py b/bingart/__init__.py index bb90d19..69519b8 100644 --- a/bingart/__init__.py +++ b/bingart/__init__.py @@ -1,3 +1,12 @@ from .bingart import BingArt, Model, Aspect, AuthCookieError, PromptRejectedError -__all__ = ["BingArt", "Model", "Aspect", "AuthCookieError", "PromptRejectedError"] +__version__ = "1.5.1" + +__all__ = [ + "BingArt", + "Model", + "Aspect", + "AuthCookieError", + "PromptRejectedError", + "__version__", +] diff --git a/bingart/cli.py b/bingart/cli.py new file mode 100644 index 0000000..e38d5cf --- /dev/null +++ b/bingart/cli.py @@ -0,0 +1,325 @@ +import argparse +import asyncio +import json +import os +import sys +import logging +from pathlib import Path + +from bingart import ( + BingArt, + Model, + Aspect, + AuthCookieError, + PromptRejectedError, + __version__, +) + + +EXIT_SUCCESS = 0 +EXIT_AUTH_ERROR = 1 +EXIT_PROMPT_REJECTED = 2 +EXIT_GENERIC_ERROR = 3 + +MODEL_MAP = { + "dalle": Model.DALLE, + "gpt4o": Model.GPT4O, + "mai1": Model.MAI1, +} + +ASPECT_MAP = { + "square": Aspect.SQUARE, + "landscape": Aspect.LANDSCAPE, + "portrait": Aspect.PORTRAIT, +} + + +def build_parser(): + parser = argparse.ArgumentParser( + prog="bingart", + description=( + "bingart - Unofficial CLI for Bing Image & Video Creator.\n" + "Generate AI-powered images and videos from the command line." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=( + "examples:\n" + ' bingart "sunset over mountains"\n' + ' bingart "cyberpunk city" -m gpt4o -a landscape\n' + ' bingart "dancing robot" -V -A -o json\n' + ' bingart "abstract art" -o urls -d ./output\n' + "\n" + "authentication:\n" + " Auth is resolved in this order:\n" + " 1. --cookie / -c flag\n" + " 2. --auto / -A flag (browser cookie detection)\n" + " 3. BINGART_COOKIE environment variable\n" + " 4. Interactive prompt\n" + "\n" + "exit codes:\n" + " 0 success\n" + " 1 authentication error\n" + " 2 prompt rejected (content policy)\n" + " 3 generic / unknown error\n" + ), + ) + + parser.add_argument( + "prompt", + help="Text prompt for image/video generation.", + ) + + parser.add_argument( + "--version", + action="version", + version=f"bingart {__version__}", + ) + + model_group = parser.add_mutually_exclusive_group() + model_group.add_argument( + "-m", + "--model", + choices=list(MODEL_MAP.keys()), + default="dalle", + help="AI model to use (default: dalle).", + ) + + aspect_group = parser.add_mutually_exclusive_group() + aspect_group.add_argument( + "-a", + "--aspect", + choices=list(ASPECT_MAP.keys()), + default="square", + help="Aspect ratio (default: square).", + ) + + parser.add_argument( + "-V", + "--video", + action="store_true", + default=False, + help="Generate a video instead of an image.", + ) + + auth_group = parser.add_mutually_exclusive_group() + auth_group.add_argument( + "-c", + "--cookie", + default=None, + help="_U auth cookie value for Bing.", + ) + auth_group.add_argument( + "-A", + "--auto", + action="store_true", + default=False, + help="Auto-detect _U cookie from installed browsers.", + ) + + parser.add_argument( + "-o", + "--output", + choices=["text", "json", "urls"], + default="text", + help="Output format (default: text). 'json' prints raw response, 'urls' prints one URL per line.", + ) + + parser.add_argument( + "-d", + "--download", + default=None, + metavar="DIR", + help="Download generated images/video to DIR (created if it doesn't exist).", + ) + + parser.add_argument( + "-v", + "--verbose", + action="store_true", + default=False, + help="Enable verbose/debug logging output.", + ) + + return parser + + +def resolve_cookie(args): + if args.cookie: + return args.cookie, False + if args.auto: + return None, True + env_cookie = os.environ.get("BINGART_COOKIE") + if env_cookie: + return env_cookie, False + try: + cookie = input("Enter your _U cookie value: ").strip() + except (EOFError, KeyboardInterrupt): + print("\nAborted.", file=sys.stderr) + sys.exit(EXIT_GENERIC_ERROR) + if not cookie: + print("Error: no cookie provided.", file=sys.stderr) + sys.exit(EXIT_AUTH_ERROR) + return cookie, False + + +async def download_file(url, dest_path, session=None): + import urllib.request + + try: + urllib.request.urlretrieve(url, str(dest_path)) + return True + except Exception: + pass + try: + from curl_cffi.requests import get + + resp = get(url, allow_redirects=True) + if resp.status_code == 200: + dest_path.write_bytes(resp.content) + return True + except Exception: + pass + return False + + +async def download_results(result, dest_dir, content_type): + dest = Path(dest_dir) + dest.mkdir(parents=True, exist_ok=True) + + if content_type == "video": + video_url = result.get("video", {}).get("video_url") + if not video_url: + print("Warning: no video URL found in response.", file=sys.stderr) + return + ext = "mp4" + filename = f"001.{ext}" + dest_path = dest / filename + print(f"Downloading video -> {dest_path}") + ok = await download_file(video_url, dest_path) + if ok: + print(f" Saved: {dest_path}") + else: + print(f" Failed to download: {video_url}", file=sys.stderr) + return + + images = result.get("images", []) + if not images: + print("Warning: no images found in response.", file=sys.stderr) + return + + for i, img in enumerate(images, 1): + url = img.get("url") + if not url: + continue + ext = "jpg" + if ".png" in url: + ext = "png" + filename = f"{i:03d}.{ext}" + dest_path = dest / filename + print(f"Downloading image {i}/{len(images)} -> {dest_path}") + ok = await download_file(url, dest_path) + if ok: + print(f" Saved: {dest_path}") + else: + print(f" Failed to download: {url}", file=sys.stderr) + + +def format_text(result, content_type): + lines = [] + if content_type == "video": + video_url = result.get("video", {}).get("video_url", "N/A") + lines.append(f"Prompt: {result.get('prompt', 'N/A')}") + lines.append(f"Video URL: {video_url}") + else: + lines.append(f"Model: {result.get('model', 'N/A')}") + lines.append(f"Aspect: {result.get('aspect', 'N/A')}") + lines.append(f"Enhanced Prompt: {result.get('prompt', 'N/A')}") + images = result.get("images", []) + lines.append(f"Images ({len(images)}):") + for idx, img in enumerate(images, 1): + lines.append(f" [{idx}] {img.get('url', 'N/A')}") + return "\n".join(lines) + + +def format_urls(result, content_type): + urls = [] + if content_type == "video": + video_url = result.get("video", {}).get("video_url") + if video_url: + urls.append(video_url) + else: + for img in result.get("images", []): + url = img.get("url") + if url: + urls.append(url) + return "\n".join(urls) + + +async def run(args): + cookie_val, use_auto = resolve_cookie(args) + model = MODEL_MAP[args.model] + aspect = ASPECT_MAP[args.aspect] + content_type = "video" if args.video else "image" + + logger = logging.getLogger("bingart") + if args.verbose: + logger.setLevel(logging.DEBUG) + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) + logger.addHandler(handler) + logger.debug("Model: %s", args.model) + logger.debug("Aspect: %s", args.aspect) + logger.debug("Content type: %s", content_type) + logger.debug("Output format: %s", args.output) + if args.download: + logger.debug("Download dir: %s", args.download) + logger.debug("Auth: %s", "auto-detect" if use_auto else "cookie") + + if use_auto: + bing = BingArt(auto=True) + else: + bing = BingArt(auth_cookie_U=cookie_val) + + try: + result = await bing.generate( + args.prompt, + model=model, + aspect=aspect, + content_type=content_type, + ) + finally: + await bing.close() + + if args.output == "json": + print(json.dumps(result, indent=2)) + elif args.output == "urls": + print(format_urls(result, content_type)) + else: + print(format_text(result, content_type)) + + if args.download: + await download_results(result, args.download, content_type) + + +def main(): + parser = build_parser() + args = parser.parse_args() + + try: + asyncio.run(run(args)) + except AuthCookieError as e: + print(f"Auth error: {e}", file=sys.stderr) + sys.exit(EXIT_AUTH_ERROR) + except PromptRejectedError as e: + print(f"Prompt rejected: {e}", file=sys.stderr) + sys.exit(EXIT_PROMPT_REJECTED) + except KeyboardInterrupt: + print("\nAborted.", file=sys.stderr) + sys.exit(EXIT_GENERIC_ERROR) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(EXIT_GENERIC_ERROR) + + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index 57aec64..85b139f 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -with open("README.md", "r", encoding="utf-8") as f: +with open("README.MD", "r", encoding="utf-8") as f: long_description = f.read() setup( @@ -24,4 +24,7 @@ include_package_data=True, install_requires=["curl_cffi", "rookiepy"], python_requires=">=3.6", + entry_points={ + "console_scripts": ["bingart=bingart.cli:main"], + }, ) From fc474a1e48ca46bd0729091718f421cf13f96b58 Mon Sep 17 00:00:00 2001 From: CyrixJD115 Date: Thu, 9 Apr 2026 15:30:20 -0500 Subject: [PATCH 2/4] Updated README --- README.MD | 249 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 178 insertions(+), 71 deletions(-) diff --git a/README.MD b/README.MD index 2887df9..e58a0df 100644 --- a/README.MD +++ b/README.MD @@ -2,14 +2,13 @@ bingart is an unofficial async API wrapper for Bing Image & Video Creator. It allows you to programmatically generate AI-powered images and videos using Bing's creation tools with support for multiple models and aspect ratios. -> **Warning:** The `_U` auth cookie should be changed every 2-4 weeks for proper functionality. - -## Description +Now includes a full **command-line interface** — no scripting required. -This module uses web scraping and engineering techniques to interface with Bing's internal image and video creation APIs. It is not an official API client. +> **Warning:** The `_U` auth cookie should be changed every 2-4 weeks for proper functionality. -### Key Features +## Key Features +- **CLI support** — generate images and videos directly from the terminal - **Fully asynchronous** — built on `curl_cffi` `AsyncSession` and `asyncio` - **Generate images** with multiple AI models (DALL-E, GPT-4O, MAI1) - **Generate videos** from text prompts @@ -19,14 +18,186 @@ This module uses web scraping and engineering techniques to interface with Bing' - **Enhanced prompts** — get AI-improved versions of your prompts - **Async context manager** support (`async with`) - **Custom exceptions** for common error handling +- **Download support** — save generated images/video directly to disk ## Installation +### From PyPI + ```bash pip install bingart ``` -## Usage +### From Source (Portable) + +```bash +git clone https://github.com/CyrixJD115/bingart.git +cd bingart +python bingart.py --help +``` + +No install needed — `bingart.py` at the project root works as a portable runner. + +## CLI Usage + +### Basic Image Generation + +```bash +python bingart.py "sunset over mountains" -c YOUR_U_COOKIE +``` + +### Auto-Detect Cookie from Browser + +```bash +python bingart.py "cyberpunk city" -A +``` + +### Video Generation + +```bash +python bingart.py "a dancing robot in a futuristic city" -V -A +``` + +### All Flags + +``` +usage: bingart [-h] [--version] [-m {dalle,gpt4o,mai1}] + [-a {square,landscape,portrait}] [-V] [-c COOKIE | -A] + [-o {text,json,urls}] [-d DIR] [-v] + prompt + +positional arguments: + prompt Text prompt for image/video generation. + +options: + -h, --help Show help message and exit + --version Print version + -m, --model AI model: dalle, gpt4o, mai1 (default: dalle) + -a, --aspect Aspect ratio: square, landscape, portrait (default: square) + -V, --video Generate a video instead of an image + -c, --cookie COOKIE _U auth cookie value for Bing + -A, --auto Auto-detect _U cookie from installed browsers + -o, --output {text,json,urls} Output format (default: text) + -d, --download DIR Download generated files to DIR + -v, --verbose Enable verbose/debug output +``` + +### Models + +| Flag | Model | +|------|-------| +| `-m dalle` | DALL-E 3 (default) | +| `-m gpt4o` | GPT-4O image generation | +| `-m mai1` | MAI1 model | + +### Aspect Ratios + +| Flag | Ratio | +|------|-------| +| `-a square` | 1:1 (default) | +| `-a landscape` | 7:4 (wide) | +| `-a portrait` | 4:7 (tall) | + +### Output Formats + +**Text** (default) — human-readable: + +```bash +python bingart.py "abstract art" -A +``` +``` +Model: DALLE +Aspect: SQUARE +Enhanced Prompt: abstract art composition with vibrant colors +Images (4): + [1] https://th.bing.com/th/id/OIG.xxx?pid=ImgGn + [2] https://th.bing.com/th/id/OIG.yyy?pid=ImgGn + [3] https://th.bing.com/th/id/OIG.zzz?pid=ImgGn + [4] https://th.bing.com/th/id/OIG.www?pid=ImgGn +``` + +**JSON** — raw API response: + +```bash +python bingart.py "abstract art" -A -o json +``` +```json +{ + "images": [ + {"url": "https://th.bing.com/th/id/OIG.xxx?pid=ImgGn"}, + {"url": "https://th.bing.com/th/id/OIG.yyy?pid=ImgGn"} + ], + "prompt": "abstract art composition", + "model": "DALLE", + "aspect": "SQUARE" +} +``` + +**URLs** — one per line, pipe-friendly: + +```bash +python bingart.py "abstract art" -A -o urls +``` +``` +https://th.bing.com/th/id/OIG.xxx?pid=ImgGn +https://th.bing.com/th/id/OIG.yyy?pid=ImgGn +``` + +### Download Files + +Save generated images or video directly to a directory: + +```bash +python bingart.py "sunset over mountains" -A -d ./output +``` +``` +Model: DALLE +Aspect: SQUARE +Enhanced Prompt: sunset over mountains with golden light +Images (4): + [1] https://th.bing.com/th/id/OIG.xxx?pid=ImgGn + [2] https://th.bing.com/th/id/OIG.yyy?pid=ImgGn +Downloading image 1/4 -> output/001.jpg + Saved: output/001.jpg +Downloading image 2/4 -> output/002.jpg + Saved: output/002.jpg +``` + +### Authentication + +Auth is resolved in this order: + +1. `--cookie` / `-c` flag — pass the `_U` cookie value directly +2. `--auto` / `-A` flag — auto-detect from installed browsers (Chrome, Edge, Firefox, Brave, Opera, Vivaldi, Chromium) +3. `BINGART_COOKIE` environment variable — set it in your shell profile +4. Interactive prompt — if none of the above are provided, you'll be prompted + +### Exit Codes + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `1` | Authentication error (invalid or expired cookie) | +| `2` | Prompt rejected (content policy violation) | +| `3` | Generic / unknown error | + +### More Examples + +```bash +# GPT-4O landscape image, JSON output +python bingart.py "futuristic cityscape" -m gpt4o -a landscape -A -o json + +# Portrait image, download to folder +python bingart.py "mystical wizard portrait" -m dalle -a portrait -d ./art + +# Video with auto cookie, verbose logging +python bingart.py "cat playing piano" -V -A -v + +# Pipe URLs to wget +python bingart.py "geometric patterns" -o urls -c YOUR_COOKIE | xargs -n1 wget +``` + +## Python API Usage ### Basic Setup @@ -68,7 +239,6 @@ Let bingart automatically fetch cookies from your installed browsers: ```python from bingart import BingArt -# Auto-fetch cookies from Chrome, Edge, Firefox, Brave, Opera, Vivaldi, or Chromium bing_art = BingArt(auto=True) ``` @@ -89,7 +259,6 @@ from bingart import BingArt, Model, Aspect async def main(): async with BingArt(auth_cookie_U='your_cookie_value') as bing_art: - # Generate with GPT-4O model in portrait aspect result = await bing_art.generate( 'a futuristic cityscape', model=Model.GPT4O, @@ -97,7 +266,6 @@ async def main(): ) print(result) - # Generate with MAI1 model in landscape aspect result = await bing_art.generate( 'serene mountain landscape', model=Model.MAI1, @@ -105,7 +273,6 @@ async def main(): ) print(result) - # Generate with DALL-E (default) in square aspect result = await bing_art.generate( 'abstract art composition', model=Model.DALLE, @@ -214,66 +381,6 @@ asyncio.run(main()) 4. Go to Application/Storage → Cookies → `https://www.bing.com` 5. Find the `_U` cookie and copy its value -## Complete Example - -```python -import asyncio -from bingart import BingArt, Model, Aspect, AuthCookieError, PromptRejectedError - -async def main(): - try: - async with BingArt(auto=True) as bing_art: - # Generate multiple images with different settings - prompts = [ - { - "query": "cyberpunk cityscape at night", - "model": Model.GPT4O, - "aspect": Aspect.LANDSCAPE - }, - { - "query": "portrait of a mystical wizard", - "model": Model.DALLE, - "aspect": Aspect.PORTRAIT - }, - { - "query": "abstract geometric patterns", - "model": Model.MAI1, - "aspect": Aspect.SQUARE - } - ] - - for config in prompts: - print(f"\nGenerating: {config['query']}") - result = await bing_art.generate( - config['query'], - model=config['model'], - aspect=config['aspect'] - ) - - print(f"Model: {result['model']}") - print(f"Aspect: {result['aspect']}") - print(f"Enhanced prompt: {result['prompt']}") - print(f"Generated {len(result['images'])} images") - - for idx, img in enumerate(result['images'], 1): - print(f" Image {idx}: {img['url']}") - - # Generate a video - print("\nGenerating video...") - video_result = await bing_art.generate( - 'a cat playing piano', - content_type='video' - ) - print(f"Video URL: {video_result['video']['video_url']}") - - except AuthCookieError as e: - print(f"Authentication error: {e}") - except PromptRejectedError as e: - print(f"Prompt rejected: {e}") - -asyncio.run(main()) -``` - ## Requirements - Python >= 3.6 @@ -286,7 +393,7 @@ Pull requests are welcome! Please open an issue to discuss major changes before ## License -MIT License - see LICENSE file for details +MIT License - see LICENSE file for details. ## Disclaimer From 59bed868157a828e23e7218a34e13eb6fc31ae0f Mon Sep 17 00:00:00 2001 From: CyrixJD115 Date: Thu, 9 Apr 2026 15:55:42 -0500 Subject: [PATCH 3/4] Fix async blocking calls, silent exceptions, README clone URL, and simplify arg parser - Run urllib and curl_cffi blocking calls in run_in_executor to avoid blocking the event loop in download_file - Log debug-level details on download failure instead of silently swallowing exceptions - Update README clone URL to point to canonical DedInc/bingart repo - Remove unnecessary mutually exclusive groups for --model and --aspect --- README.MD | 2 +- bingart/cli.py | 34 ++++++++++++++++++++++------------ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/README.MD b/README.MD index e58a0df..aeffe09 100644 --- a/README.MD +++ b/README.MD @@ -31,7 +31,7 @@ pip install bingart ### From Source (Portable) ```bash -git clone https://github.com/CyrixJD115/bingart.git +git clone https://github.com/DedInc/bingart.git cd bingart python bingart.py --help ``` diff --git a/bingart/cli.py b/bingart/cli.py index e38d5cf..472fe4e 100644 --- a/bingart/cli.py +++ b/bingart/cli.py @@ -75,8 +75,7 @@ def build_parser(): version=f"bingart {__version__}", ) - model_group = parser.add_mutually_exclusive_group() - model_group.add_argument( + parser.add_argument( "-m", "--model", choices=list(MODEL_MAP.keys()), @@ -84,8 +83,7 @@ def build_parser(): help="AI model to use (default: dalle).", ) - aspect_group = parser.add_mutually_exclusive_group() - aspect_group.add_argument( + parser.add_argument( "-a", "--aspect", choices=list(ASPECT_MAP.keys()), @@ -165,20 +163,32 @@ def resolve_cookie(args): async def download_file(url, dest_path, session=None): import urllib.request + logger = logging.getLogger("bingart") + loop = asyncio.get_running_loop() + try: - urllib.request.urlretrieve(url, str(dest_path)) + await loop.run_in_executor( + None, lambda: urllib.request.urlretrieve(url, str(dest_path)) + ) return True - except Exception: - pass + except Exception as exc: + logger.debug("urllib download failed for %s: %s", url, exc) + try: from curl_cffi.requests import get - resp = get(url, allow_redirects=True) - if resp.status_code == 200: - dest_path.write_bytes(resp.content) + def _curl_get(): + resp = get(url, allow_redirects=True) + if resp.status_code == 200: + dest_path.write_bytes(resp.content) + return True + return False + + if await loop.run_in_executor(None, _curl_get): return True - except Exception: - pass + except Exception as exc: + logger.debug("curl_cffi download failed for %s: %s", url, exc) + return False From a38a4d5049f6e587e78ef20db3305aac47db05d4 Mon Sep 17 00:00:00 2001 From: CyrixJD115 Date: Thu, 9 Apr 2026 16:04:24 -0500 Subject: [PATCH 4/4] Add URL scheme validation, use entrypoint in README examples, fix code blocks, guard duplicate handler - Validate URL scheme (http/https only) before download to prevent non-http URLs reaching urllib - Replace python bingart.py with bingart entrypoint in all CLI examples outside the From Source section - Add text language identifiers to bare fenced code blocks in README - Guard StreamHandler registration against duplicate accumulation in run() --- README.MD | 30 +++++++++++++++--------------- bingart/cli.py | 13 ++++++++++--- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/README.MD b/README.MD index aeffe09..0f851a0 100644 --- a/README.MD +++ b/README.MD @@ -43,24 +43,24 @@ No install needed — `bingart.py` at the project root works as a portable runne ### Basic Image Generation ```bash -python bingart.py "sunset over mountains" -c YOUR_U_COOKIE +bingart "sunset over mountains" -c YOUR_U_COOKIE ``` ### Auto-Detect Cookie from Browser ```bash -python bingart.py "cyberpunk city" -A +bingart "cyberpunk city" -A ``` ### Video Generation ```bash -python bingart.py "a dancing robot in a futuristic city" -V -A +bingart "a dancing robot in a futuristic city" -V -A ``` ### All Flags -``` +```text usage: bingart [-h] [--version] [-m {dalle,gpt4o,mai1}] [-a {square,landscape,portrait}] [-V] [-c COOKIE | -A] [-o {text,json,urls}] [-d DIR] [-v] @@ -103,9 +103,9 @@ options: **Text** (default) — human-readable: ```bash -python bingart.py "abstract art" -A -``` +bingart "abstract art" -A ``` +```text Model: DALLE Aspect: SQUARE Enhanced Prompt: abstract art composition with vibrant colors @@ -119,7 +119,7 @@ Images (4): **JSON** — raw API response: ```bash -python bingart.py "abstract art" -A -o json +bingart "abstract art" -A -o json ``` ```json { @@ -136,9 +136,9 @@ python bingart.py "abstract art" -A -o json **URLs** — one per line, pipe-friendly: ```bash -python bingart.py "abstract art" -A -o urls -``` +bingart "abstract art" -A -o urls ``` +```text https://th.bing.com/th/id/OIG.xxx?pid=ImgGn https://th.bing.com/th/id/OIG.yyy?pid=ImgGn ``` @@ -148,9 +148,9 @@ https://th.bing.com/th/id/OIG.yyy?pid=ImgGn Save generated images or video directly to a directory: ```bash -python bingart.py "sunset over mountains" -A -d ./output -``` +bingart "sunset over mountains" -A -d ./output ``` +```text Model: DALLE Aspect: SQUARE Enhanced Prompt: sunset over mountains with golden light @@ -185,16 +185,16 @@ Auth is resolved in this order: ```bash # GPT-4O landscape image, JSON output -python bingart.py "futuristic cityscape" -m gpt4o -a landscape -A -o json +bingart "futuristic cityscape" -m gpt4o -a landscape -A -o json # Portrait image, download to folder -python bingart.py "mystical wizard portrait" -m dalle -a portrait -d ./art +bingart "mystical wizard portrait" -m dalle -a portrait -d ./art # Video with auto cookie, verbose logging -python bingart.py "cat playing piano" -V -A -v +bingart "cat playing piano" -V -A -v # Pipe URLs to wget -python bingart.py "geometric patterns" -o urls -c YOUR_COOKIE | xargs -n1 wget +bingart "geometric patterns" -o urls -c YOUR_COOKIE | xargs -n1 wget ``` ## Python API Usage diff --git a/bingart/cli.py b/bingart/cli.py index 472fe4e..adb7faa 100644 --- a/bingart/cli.py +++ b/bingart/cli.py @@ -161,11 +161,17 @@ def resolve_cookie(args): async def download_file(url, dest_path, session=None): + import urllib.parse import urllib.request logger = logging.getLogger("bingart") loop = asyncio.get_running_loop() + parsed = urllib.parse.urlparse(url) + if parsed.scheme not in ("http", "https"): + logger.debug("blocked non-http(s) URL: %s", url) + return False + try: await loop.run_in_executor( None, lambda: urllib.request.urlretrieve(url, str(dest_path)) @@ -274,9 +280,10 @@ async def run(args): logger = logging.getLogger("bingart") if args.verbose: logger.setLevel(logging.DEBUG) - handler = logging.StreamHandler(sys.stderr) - handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) - logger.addHandler(handler) + if not any(isinstance(h, logging.StreamHandler) for h in logger.handlers): + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) + logger.addHandler(handler) logger.debug("Model: %s", args.model) logger.debug("Aspect: %s", args.aspect) logger.debug("Content type: %s", content_type)