diff --git a/searx/engines/duckduckgo_weather.py b/searx/engines/duckduckgo_weather.py index 715b0dfd1..40e39eecd 100644 --- a/searx/engines/duckduckgo_weather.py +++ b/searx/engines/duckduckgo_weather.py @@ -9,12 +9,14 @@ from json import loads from urllib.parse import quote from dateutil import parser as date_parser -from flask_babel import gettext from searx.engines.duckduckgo import fetch_traits # pylint: disable=unused-import from searx.engines.duckduckgo import get_ddg_lang from searx.enginelib.traits import EngineTraits +from searx.result_types import EngineResults, WeatherAnswer +from searx import weather + if TYPE_CHECKING: import logging @@ -36,53 +38,60 @@ send_accept_language_header = True # engine dependent config categories = ["weather"] -URL = "https://duckduckgo.com/js/spice/forecast/{query}/{lang}" +base_url = "https://duckduckgo.com/js/spice/forecast/{query}/{lang}" + +# adapted from https://gist.github.com/mikesprague/048a93b832e2862050356ca233ef4dc1 +WEATHERKIT_TO_CONDITION = { + "BlowingDust": "fog", + "Clear": "clear", + "Cloudy": "cloudy", + "Foggy": "fog", + "Haze": "fog", + "MostlyClear": "clear", + "MostlyCloudy": "partly cloudy", + "PartlyCloudy": "partly cloudy", + "Smoky": "fog", + "Breezy": "partly cloudy", + "Windy": "partly cloudy", + "Drizzle": "light rain", + "HeavyRain": "heavy rain", + "IsolatedThunderstorms": "rain and thunder", + "Rain": "rain", + "SunShowers": "rain", + "ScatteredThunderstorms": "heavy rain and thunder", + "StrongStorms": "heavy rain and thunder", + "Thunderstorms": "rain and thunder", + "Frigid": "clear sky", + "Hail": "heavy rain", + "Hot": "clear sky", + "Flurries": "light snow", + "Sleet": "sleet", + "Snow": "light snow", + "SunFlurries": "light snow", + "WintryMix": "sleet", + "Blizzard": "heavy snow", + "BlowingSnow": "heavy snow", + "FreezingDrizzle": "light sleet", + "FreezingRain": "sleet", + "HeavySnow": "heavy snow", + "Hurricane": "rain and thunder", + "TropicalStorm": "rain and thunder", +} -def generate_condition_table(condition): - res = "" - - res += f"{gettext('Condition')}" f"{condition['conditionCode']}" - - res += ( - f"{gettext('Temperature')}" - f"{condition['temperature']}°C / {c_to_f(condition['temperature'])}°F" +def _weather_data(location, data): + return WeatherAnswer.Item( + location=location, + temperature=weather.Temperature(unit="°C", value=data['temperature']), + condition=WEATHERKIT_TO_CONDITION[data["conditionCode"]], + feels_like=weather.Temperature(unit="°C", value=data['temperatureApparent']), + wind_from=weather.Compass(data["windDirection"]), + wind_speed=weather.WindSpeed(data["windSpeed"], unit="mi/h"), + pressure=weather.Pressure(data["pressure"], unit="hPa"), + humidity=weather.RelativeHumidity(data["humidity"] * 100), + cloud_cover=data["cloudCover"] * 100, ) - res += ( - f"{gettext('Feels like')}{condition['temperatureApparent']}°C / " - f"{c_to_f(condition['temperatureApparent'])}°F" - ) - - res += ( - f"{gettext('Wind')}{condition['windDirection']}° — " - f"{(condition['windSpeed'] * 1.6093440006147):.2f} km/h / {condition['windSpeed']} mph" - ) - - res += f"{gettext('Visibility')}{condition['visibility']} m" - - res += f"{gettext('Humidity')}{(condition['humidity'] * 100):.1f}%" - - return res - - -def generate_day_table(day): - res = "" - - res += ( - f"{gettext('Min temp.')}{day['temperatureMin']}°C / " - f"{c_to_f(day['temperatureMin'])}°F" - ) - res += ( - f"{gettext('Max temp.')}{day['temperatureMax']}°C / " - f"{c_to_f(day['temperatureMax'])}°F" - ) - res += f"{gettext('UV index')}{day['maxUvIndex']}" - res += f"{gettext('Sunrise')}{date_parser.parse(day['sunrise']).strftime('%H:%M')}" - res += f"{gettext('Sunset')}{date_parser.parse(day['sunset']).strftime('%H:%M')}" - - return res - def request(query, params): @@ -95,64 +104,30 @@ def request(query, params): params['cookies']['l'] = eng_region logger.debug("cookies: %s", params['cookies']) - params["url"] = URL.format(query=quote(query), lang=eng_lang.split('_')[0]) + params["url"] = base_url.format(query=quote(query), lang=eng_lang.split('_')[0]) return params -def c_to_f(temperature): - return "%.2f" % ((temperature * 1.8) + 32) - - def response(resp): - results = [] + res = EngineResults() if resp.text.strip() == "ddg_spice_forecast();": - return [] + return res - result = loads(resp.text[resp.text.find('\n') + 1 : resp.text.rfind('\n') - 2]) + json_data = loads(resp.text[resp.text.find('\n') + 1 : resp.text.rfind('\n') - 2]) - current = result["currentWeather"] + geoloc = weather.GeoLocation.by_query(resp.search_params["query"]) - title = result['location'] - - infobox = f"

{gettext('Current condition')}

" - - infobox += generate_condition_table(current) - - infobox += "
" - - last_date = None - - for time in result['forecastHourly']['hours']: - current_time = date_parser.parse(time['forecastStart']) - - if last_date != current_time.date(): - if last_date is not None: - infobox += "" - - infobox += f"

{current_time.strftime('%Y-%m-%d')}

" - - infobox += "" - - for day in result['forecastDaily']['days']: - if date_parser.parse(day['forecastStart']).date() == current_time.date(): - infobox += generate_day_table(day) - - infobox += "
" - - last_date = current_time.date() - - infobox += f"" - - infobox += generate_condition_table(time) - - infobox += "
{current_time.strftime('%H:%M')}
" - - results.append( - { - "infobox": title, - "content": infobox, - } + weather_answer = WeatherAnswer( + current=_weather_data(geoloc, json_data["currentWeather"]), + service="duckduckgo weather", ) - return results + for forecast in json_data['forecastHourly']['hours']: + forecast_time = date_parser.parse(forecast['forecastStart']) + forecast_data = _weather_data(geoloc, forecast) + forecast_data.datetime = weather.DateTime(forecast_time) + weather_answer.forecasts.append(forecast_data) + + res.add(weather_answer) + return res