diff --git a/vrobbler/apps/moods/models.py b/vrobbler/apps/moods/models.py index f36bc2b..19bc5fa 100644 --- a/vrobbler/apps/moods/models.py +++ b/vrobbler/apps/moods/models.py @@ -37,6 +37,8 @@ class MoodLogData(BaseLogData): mood_type: Optional[str] = None mood_reason_ids: Optional[list[int]] = None mood_quality: Optional[int] = None + weather_temp: Optional[float] = None + weather_description: Optional[str] = None @classmethod def override_fields(cls) -> dict: diff --git a/vrobbler/apps/moods/utils.py b/vrobbler/apps/moods/utils.py new file mode 100644 index 0000000..c763527 --- /dev/null +++ b/vrobbler/apps/moods/utils.py @@ -0,0 +1,80 @@ +import logging +import xml.etree.ElementTree as ET +from typing import Optional +from urllib.parse import urlencode + +import requests + +logger = logging.getLogger(__name__) + +NWS_URL = "https://forecast.weather.gov/MapClick.php" + + +def fetch_current_weather(lat: float, lon: float) -> Optional[dict]: + """Fetch current weather from NWS DWML feed for the given lat/lon. + + Returns dict with 'temp' and 'description' keys, or None on failure. + """ + params = { + "lat": lat, + "lon": lon, + "unit": 0, + "lg": "english", + "FcstType": "dwml", + } + url = f"{NWS_URL}?{urlencode(params)}" + + try: + resp = requests.get(url, timeout=10) + resp.raise_for_status() + except requests.RequestException as e: + logger.warning("Failed to fetch NWS weather data: %s", e) + return None + + try: + root = ET.fromstring(resp.content) + except ET.ParseError as e: + logger.warning("Failed to parse NWS XML: %s", e) + return None + + # Find current observations data block + data_blocks = root.findall("data") + obs_block = None + for block in data_blocks: + if block.get("type") == "current observations": + obs_block = block + break + + if obs_block is None: + logger.warning("No current observations block in NWS response") + return None + + params_el = obs_block.find("parameters") + if params_el is None: + return None + + # Temperature (apparent / feels-like) + temp = None + for temp_el in params_el.findall("temperature"): + if temp_el.get("type") == "apparent": + val_el = temp_el.find("value") + if val_el is not None and val_el.text: + try: + temp = int(round(float(val_el.text))) + except (ValueError, TypeError): + pass + break + + # Weather description + description = None + weather_el = params_el.find("weather") + if weather_el is not None: + cond_el = weather_el.find("weather-conditions") + if cond_el is not None: + description = cond_el.get("weather-summary") + + if temp is None and description is None: + logger.warning("No weather data found in NWS response") + return None + + return {"temp": temp, "description": description} diff --git a/vrobbler/apps/moods/views.py b/vrobbler/apps/moods/views.py index d7a5a2d..7ecb5f3 100644 --- a/vrobbler/apps/moods/views.py +++ b/vrobbler/apps/moods/views.py @@ -5,6 +5,7 @@ from django.shortcuts import redirect, render from django.urls import reverse from django.utils import timezone from moods.models import Mood, MoodLogData, MoodQuality, MoodReason +from moods.utils import fetch_current_weather from scrobbles.models import Scrobble logger = logging.getLogger(__name__) @@ -66,6 +67,25 @@ def checkin(request): mood_reason_ids=[int(reason_id)] if reason_id else [], ) + # Attempt to add current weather from most recent location + last_loc = ( + Scrobble.objects.filter( + user=request.user, + media_type=Scrobble.MediaType.GEO_LOCATION, + geo_location__isnull=False, + ) + .order_by("-timestamp") + .first() + ) + if last_loc and last_loc.geo_location: + weather = fetch_current_weather( + last_loc.geo_location.lat, + last_loc.geo_location.lon, + ) + if weather: + log_data.weather_temp = weather["temp"] + log_data.weather_description = weather["description"] + scrobble = Scrobble.objects.create( user=request.user, mood=mood,