From 087da66565dfea0f020172f47939e00fb1691def Mon Sep 17 00:00:00 2001 From: RobinFrcd <29704178+RobinFrcd@users.noreply.github.com> Date: Sat, 12 Apr 2025 00:24:25 +0200 Subject: [PATCH] [feat] add SensCritique (FR) engine Closes: https://github.com/searxng/searxng/issues/4623 --- searx/engines/senscritique.py | 151 ++++++++++++++++++++++++++++++++++ searx/settings.yml | 6 ++ 2 files changed, 157 insertions(+) create mode 100644 searx/engines/senscritique.py diff --git a/searx/engines/senscritique.py b/searx/engines/senscritique.py new file mode 100644 index 000000000..3abd7d358 --- /dev/null +++ b/searx/engines/senscritique.py @@ -0,0 +1,151 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +"""SensCritique (movies) +""" +from __future__ import annotations + +from json import dumps, loads +from typing import Any, Optional +from searx.result_types import EngineResults, MainResult + +about = { + "website": 'https://www.senscritique.com/', + "wikidata_id": 'Q16676060', + "official_api_documentation": None, + "use_official_api": False, + "require_api_key": False, + "results": 'JSON', + 'language': 'fr', +} + +categories = ['movies'] +paging = True +page_size = 16 +graphql_url = 'https://apollo.senscritique.com/' + +graphql_query = """query SearchProductExplorer($query: String, $offset: Int, $limit: Int, + $sortBy: SearchProductExplorerSort) { + searchProductExplorer( + query: $query + filters: [] + sortBy: $sortBy + offset: $offset + limit: $limit + ) { + items { + category + dateRelease + duration + id + originalTitle + rating + title + url + yearOfProduction + medias { + picture + } + countries { + name + } + genresInfos { + label + } + directors { + name + } + stats { + ratingCount + } + } + } +}""" + + +def request(query: str, params: dict[str, Any]) -> dict[str, Any]: + offset = (params['pageno'] - 1) * page_size + + data = { + "operationName": "SearchProductExplorer", + "variables": {"offset": offset, "limit": page_size, "query": query, "sortBy": "RELEVANCE"}, + "query": graphql_query, + } + + params['url'] = graphql_url + params['method'] = 'POST' + params['headers']['Content-Type'] = 'application/json' + params['data'] = dumps(data) + + return params + + +def response(resp) -> EngineResults: + res = EngineResults() + response_data = loads(resp.text) + + items = response_data.get('data', {}).get('searchProductExplorer', {}).get('items', []) + if not items: + return res + + for item in items: + result = parse_item(item) + if not result: + continue + res.add(result=result) + + return res + + +def parse_item(item: dict[str, Any]) -> MainResult | None: + """Parse a single item from the SensCritique API response""" + title = item.get('title', '') + if not title: + return None + year = item.get('yearOfProduction') + original_title = item.get('originalTitle') + + thumbnail: str = "" + if item.get('medias', {}) and item['medias'].get('picture'): + thumbnail = item['medias']['picture'] + + content_parts = build_content_parts(item, title, original_title) + url = f"https://www.senscritique.com{item['url']}" + + return MainResult( + url=url, + title=title + (f' ({year})' if year else ''), + content=' | '.join(content_parts), + thumbnail=thumbnail, + ) + + +def build_content_parts(item: dict[str, Any], title: str, original_title: Optional[str]) -> list[str]: + """Build the content parts for an item""" + content_parts = [] + + if item.get('category'): + content_parts.append(item['category']) + + if original_title and original_title != title: + content_parts.append(f"Original title: {original_title}") + + if item.get('directors'): + directors = [director['name'] for director in item['directors']] + content_parts.append(f"Director(s): {', '.join(directors)}") + + if item.get('countries'): + countries = [country['name'] for country in item['countries']] + content_parts.append(f"Country: {', '.join(countries)}") + + if item.get('genresInfos'): + genres = [genre['label'] for genre in item['genresInfos']] + content_parts.append(f"Genre(s): {', '.join(genres)}") + + if item.get('duration'): + minutes = item['duration'] // 60 + if minutes > 0: + content_parts.append(f"Duration: {minutes} min") + + if item.get('rating') and item.get('stats', {}).get('ratingCount'): + content_parts.append(f"Rating: {item['rating']}/10 ({item['stats']['ratingCount']} votes)") + + return content_parts diff --git a/searx/settings.yml b/searx/settings.yml index ec72f8736..435862723 100644 --- a/searx/settings.yml +++ b/searx/settings.yml @@ -2642,6 +2642,12 @@ engines: shortcut: pgo disabled: true + - name: senscritique + engine: senscritique + shortcut: scr + timeout: 4.0 + disabled: true + # Doku engine lets you access to any Doku wiki instance: # A public one or a privete/corporate one. # - name: ubuntuwiki