From 966988e36072aa935ccb3bdec0fe523a4bf8812a Mon Sep 17 00:00:00 2001 From: Austin-Olacsi <138650713+Austin-Olacsi@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:18:46 -0700 Subject: [PATCH] [feat] engines: add artstation engine (#5689) --- searx/engines/artstation.py | 105 ++++++++++++++++++++++++++++++++++++ searx/settings.yml | 6 +++ 2 files changed, 111 insertions(+) create mode 100644 searx/engines/artstation.py diff --git a/searx/engines/artstation.py b/searx/engines/artstation.py new file mode 100644 index 000000000..e1726094c --- /dev/null +++ b/searx/engines/artstation.py @@ -0,0 +1,105 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +"""Artstation (images)""" + +import re +import typing as t +from json import dumps + +from searx.result_types import EngineResults +from searx.network import post +from searx.enginelib import EngineCache + +# Engine metadata +about = { + "website": 'https://www.artstation.com/', + "wikidata_id": 'Q65551500', + "official_api_documentation": None, + "use_official_api": False, + "require_api_key": False, + "results": 'JSON', +} + +# Engine configuration +paging = True +categories = ['images'] +results_per_page = 20 + +# Search URL +base_url = "https://www.artstation.com/api/v2/search/projects.json" + +# Cache keys & expiration +CSRF_PUBLICKEY_CACHE = "public_csrf_token" +CSRF_PRIVATEKEY_CACHE = "private_csrf_token" +KEY_EXPIRATION_SECONDS = 3600 + +CACHE: EngineCache + + +def setup(engine_settings: dict[str, t.Any]) -> bool: + global CACHE # pylint: disable=global-statement + CACHE = EngineCache(engine_settings["name"]) + return True + + +def fetch_csrf_tokens(): + + public_token: str | None = CACHE.get(CSRF_PUBLICKEY_CACHE) + private_token: str | None = CACHE.get(CSRF_PRIVATEKEY_CACHE) + + if public_token and private_token: + return public_token, private_token + + resp = post("https://www.artstation.com/api/v2/csrf_protection/token.json") + public_token = resp.json()["public_csrf_token"] + private_token = resp.cookies["PRIVATE-CSRF-TOKEN"] + + CACHE.set(key=CSRF_PUBLICKEY_CACHE, value=public_token, expire=KEY_EXPIRATION_SECONDS) + CACHE.set(key=CSRF_PRIVATEKEY_CACHE, value=private_token, expire=KEY_EXPIRATION_SECONDS) + + return public_token, private_token + + +def request(query, params): + + public_token, private_token = fetch_csrf_tokens() + + form_data = { + "query": query, + "page": params["pageno"], + "per_page": results_per_page, + "sorting": "relevance", + "pro_first": 1, + } + + params["url"] = base_url + params["method"] = 'POST' + params['headers']['content-type'] = "application/json" + params['headers']['PUBLIC-CSRF-TOKEN'] = public_token + params["cookies"] = {"PRIVATE-CSRF-TOKEN": private_token} + params['data'] = dumps(form_data) + + return params + + +def response(resp) -> EngineResults: + results = EngineResults() + search_res = resp.json() + + for item in search_res["data"]: + thumb = item["smaller_square_cover_url"] + fullsize_image = re.sub(r'/\d{6,}/', '/', thumb).replace("smaller_square", "large") + + results.add( + results.types.LegacyResult( + { + "template": 'images.html', + "title": item["title"], + "url": item["url"], + "author": f"{item['user']['username']} ({item['user']['full_name']})", + "img_src": fullsize_image, + "thumbnail_src": thumb, + } + ) + ) + + return results diff --git a/searx/settings.yml b/searx/settings.yml index d54d146a9..8ecfd6f1c 100644 --- a/searx/settings.yml +++ b/searx/settings.yml @@ -475,6 +475,12 @@ engines: shortcut: arc timeout: 4.0 + - name: artstation + engine: artstation + shortcut: as + categories: images + disabled: true + - name: arxiv engine: arxiv shortcut: arx