# SPDX-License-Identifier: AGPL-3.0-or-later """`Adobe Stock`_ is a service that gives access to millions of royalty-free assets. Assets types include photos, vectors, illustrations, templates, 3D assets, videos, motion graphics templates and audio tracks. .. Adobe Stock: https://stock.adobe.com/ Configuration ============= The engine has the following mandatory setting: - SearXNG's :ref:`engine categories` - Adobe-Stock's :py:obj:`adobe_order` - Adobe-Stock's :py:obj:`adobe_content_types` .. code:: yaml - name: adobe stock engine: adobe_stock shortcut: asi categories: [images] adobe_order: relevance adobe_content_types: ["photo", "illustration", "zip_vector", "template", "3d", "image"] - name: adobe stock video engine: adobe_stock network: adobe stock shortcut: asi categories: [videos] adobe_order: relevance adobe_content_types: ["video"] Implementation ============== """ from __future__ import annotations from typing import TYPE_CHECKING from datetime import datetime, timedelta from urllib.parse import urlencode import isodate if TYPE_CHECKING: import logging logger: logging.Logger about = { "website": "https://stock.adobe.com/", "wikidata_id": "Q5977430", "official_api_documentation": None, "use_official_api": False, "require_api_key": False, "results": "JSON", } categories = [] paging = True send_accept_language_header = True results_per_page = 10 base_url = "https://stock.adobe.com" adobe_order: str = "" """Sort order, can be one of: - ``relevance`` or - ``featured`` or - ``creation`` (most recent) or - ``nb_downloads`` (number of downloads) """ ADOBE_VALID_TYPES = ["photo", "illustration", "zip_vector", "video", "template", "3d", "audio", "image"] adobe_content_types: list = [] """A list of of content types. The following content types are offered: - Images: ``image`` - Videos: ``video`` - Templates: ``template`` - 3D: ``3d`` - Audio ``audio`` Additional subcategories: - Photos: ``photo`` - Illustrations: ``illustration`` - Vectors: ``zip_vector`` (Vectors), """ # Do we need support for "free_collection" and "include_stock_enterprise"? def init(_): if not categories: raise ValueError("adobe_stock engine: categories is unset") # adobe_order if not adobe_order: raise ValueError("adobe_stock engine: adobe_order is unset") if adobe_order not in ["relevance", "featured", "creation", "nb_downloads"]: raise ValueError(f"unsupported adobe_order: {adobe_order}") # adobe_content_types if not adobe_content_types: raise ValueError("adobe_stock engine: adobe_content_types is unset") if isinstance(adobe_content_types, list): for t in adobe_content_types: if t not in ADOBE_VALID_TYPES: raise ValueError("adobe_stock engine: adobe_content_types: '%s' is invalid" % t) else: raise ValueError( "adobe_stock engine: adobe_content_types must be a list of strings not %s" % type(adobe_content_types) ) def request(query, params): args = { "k": query, "limit": results_per_page, "order": adobe_order, "search_page": params["pageno"], "search_type": "pagination", } for content_type in ADOBE_VALID_TYPES: args[f"filters[content_type:{content_type}]"] = 1 if content_type in adobe_content_types else 0 params["url"] = f"{base_url}/de/Ajax/Search?{urlencode(args)}" # headers required to bypass bot-detection if params["searxng_locale"] == "all": params["headers"]["Accept-Language"] = "en-US,en;q=0.5" return params def parse_image_item(item): return { "template": "images.html", "url": item["content_url"], "title": item["title"], "content": item["asset_type"], "img_src": item["content_thumb_extra_large_url"], "thumbnail_src": item["thumbnail_url"], "resolution": f"{item['content_original_width']}x{item['content_original_height']}", "img_format": item["format"], "author": item["author"], } def parse_video_item(item): # in video items, the title is more or less a "content description", we try # to reduce the lenght of the title .. title = item["title"] content = "" if "." in title.strip()[:-1]: content = title title = title.split(".", 1)[0] elif "," in title: content = title title = title.split(",", 1)[0] elif len(title) > 50: content = title title = "" for w in content.split(" "): title += f" {w}" if len(title) > 50: title = title.strip() + "\u2026" break return { "template": "videos.html", "url": item["content_url"], "title": title, "content": content, # https://en.wikipedia.org/wiki/ISO_8601#Durations "length": isodate.parse_duration(item["time_duration"]), "publishedDate": datetime.strptime(item["creation_date"], "%Y-%m-%d"), "thumbnail": item["thumbnail_url"], "iframe_src": item["video_small_preview_url"], "metadata": item["asset_type"], } def parse_audio_item(item): audio_data = item["audio_data"] content = audio_data.get("description") or "" if audio_data.get("album"): content = audio_data["album"] + " - " + content return { "url": item["content_url"], "title": item["title"], "content": content, # "thumbnail": base_url + item["thumbnail_url"], "iframe_src": audio_data["preview"]["url"], "publishedDate": datetime.fromisoformat(audio_data["release_date"]) if audio_data["release_date"] else None, "length": timedelta(seconds=round(audio_data["duration"] / 1000)) if audio_data["duration"] else None, "author": item.get("artist_name"), } def response(resp): results = [] json_resp = resp.json() if isinstance(json_resp["items"], list): return None for item in json_resp["items"].values(): if item["asset_type"].lower() in ["image", "premium-image", "illustration", "vector"]: result = parse_image_item(item) elif item["asset_type"].lower() == "video": result = parse_video_item(item) elif item["asset_type"].lower() == "audio": result = parse_audio_item(item) else: logger.error("no handle for %s --> %s", item["asset_type"], item) continue results.append(result) return results