| 1 | """Pixabay provider. |
| 2 | |
| 3 | Requires ``PIXABAY_API_KEY`` in the environment. Pixabay's Content License |
| 4 | allows commercial use without attribution, so all returned candidates are |
| 5 | classified as ``no-attribution``. |
| 6 | |
| 7 | API docs: https://pixabay.com/api/docs/ |
| 8 | """ |
| 9 | |
| 10 | from __future__ import annotations |
| 11 | |
| 12 | import sys |
| 13 | from pathlib import Path |
| 14 | |
| 15 | _SCRIPTS_DIR = Path(__file__).resolve().parents[1] |
| 16 | if str(_SCRIPTS_DIR) not in sys.path: |
| 17 | sys.path.insert(0, str(_SCRIPTS_DIR)) |
| 18 | |
| 19 | from console_encoding import configure_utf8_stdio # noqa: E402 |
| 20 | |
| 21 | configure_utf8_stdio() |
| 22 | |
| 23 | if __name__ == "__main__": |
| 24 | print(__doc__) |
| 25 | print("Use via: python3 skills/ppt-master/scripts/image_search.py ...") |
| 26 | raise SystemExit(0 if any(arg in {"-h", "--help", "help"} for arg in sys.argv[1:]) else 1) |
| 27 | |
| 28 | import os |
| 29 | |
| 30 | import requests |
| 31 | |
| 32 | from image_sources.provider_common import ( |
| 33 | AssetCandidate, |
| 34 | ImageSearchRequest, |
| 35 | LICENSE_TIER_NO_ATTRIBUTION, |
| 36 | USER_AGENT, |
| 37 | build_query_progression, |
| 38 | normalize_license_name, |
| 39 | ) |
| 40 | |
| 41 | |
| 42 | API_URL = "https://pixabay.com/api/" |
| 43 | DEFAULT_PAGE_SIZE = 20 |
| 44 | DEFAULT_TIMEOUT = 30 |
| 45 | |
| 46 | # Pixabay uses ``horizontal`` / ``vertical`` rather than landscape/portrait, |
| 47 | # and has no ``square`` value (it falls back to ``all``). |
| 48 | _ORIENTATION_MAP = { |
| 49 | "landscape": "horizontal", |
| 50 | "portrait": "vertical", |
| 51 | } |
| 52 | |
| 53 | |
| 54 | def _require_api_key() -> str: |
| 55 | key = (os.environ.get("PIXABAY_API_KEY") or "").strip() |
| 56 | if not key: |
| 57 | raise RuntimeError( |
| 58 | "PIXABAY_API_KEY is not set. Add it to your environment or .env file. " |
| 59 | "Get one at https://pixabay.com/api/docs/" |
| 60 | ) |
| 61 | return key |
| 62 | |
| 63 | |
| 64 | def parse_results(payload: dict) -> list[AssetCandidate]: |
| 65 | """Translate a Pixabay response into candidates.""" |
| 66 | candidates: list[AssetCandidate] = [] |
| 67 | for item in payload.get("hits", []) or []: |
| 68 | download_url = ( |
| 69 | item.get("largeImageURL") |
| 70 | or item.get("webformatURL") |
| 71 | or item.get("previewURL") |
| 72 | or "" |
| 73 | ).strip() |
| 74 | if not download_url: |
| 75 | continue |
| 76 | |
| 77 | candidates.append( |
| 78 | AssetCandidate( |
| 79 | provider="pixabay", |
| 80 | title=(item.get("tags") or "").strip() or "Pixabay image", |
| 81 | asset_id=str(item.get("id") or ""), |
| 82 | source_page_url=(item.get("pageURL") or "").strip(), |
| 83 | license_name=normalize_license_name("Pixabay Content License"), |
| 84 | license_url="https://pixabay.com/service/license-summary/", |
| 85 | license_tier=LICENSE_TIER_NO_ATTRIBUTION, |
| 86 | width=int(item.get("imageWidth") or 0), |
| 87 | height=int(item.get("imageHeight") or 0), |
| 88 | download_url=download_url, |
| 89 | author=(item.get("user") or "").strip(), |
| 90 | raw=item, |
| 91 | ) |
| 92 | ) |
| 93 | return candidates |
| 94 | |
| 95 | |
| 96 | def search( |
| 97 | request: ImageSearchRequest, |
| 98 | *, |
| 99 | license_tier_filter: str = "no-attribution-only", |
| 100 | page_size: int = DEFAULT_PAGE_SIZE, |
| 101 | timeout: int = DEFAULT_TIMEOUT, |
| 102 | ) -> list[AssetCandidate]: |
| 103 | """Search Pixabay for candidates. |
| 104 | |
| 105 | Pixabay images are uniformly ``no-attribution``, so the ``"all"`` filter |
| 106 | behaves identically to ``"no-attribution-only"``. |
| 107 | """ |
| 108 | if license_tier_filter not in {"no-attribution-only", "all"}: |
| 109 | raise ValueError(f"unsupported license_tier_filter: {license_tier_filter!r}") |
| 110 | |
| 111 | api_key = _require_api_key() |
| 112 | orientation = (request.orientation or "").strip().lower() |
| 113 | |
| 114 | for query in build_query_progression(request.query): |
| 115 | params: dict[str, str | int] = { |
| 116 | "key": api_key, |
| 117 | "q": query, |
| 118 | "image_type": "photo", |
| 119 | "per_page": page_size, |
| 120 | "safesearch": "true", |
| 121 | } |
| 122 | if orientation in _ORIENTATION_MAP: |
| 123 | params["orientation"] = _ORIENTATION_MAP[orientation] |
| 124 | |
| 125 | response = requests.get( |
| 126 | API_URL, |
| 127 | params=params, |
| 128 | headers={ |
| 129 | "User-Agent": USER_AGENT, |
| 130 | "Accept": "application/json", |
| 131 | }, |
| 132 | timeout=timeout, |
| 133 | ) |
| 134 | response.raise_for_status() |
| 135 | candidates = parse_results(response.json()) |
| 136 | if candidates: |
| 137 | return candidates |
| 138 | |
| 139 | return [] |
| 140 |