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"{current_time.strftime('%H:%M')} |
"
-
- infobox += generate_condition_table(time)
-
- infobox += "
"
-
- 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