diff --git a/news_crawler/ollama_client.py b/news_crawler/ollama_client.py
index d86672d..f8b962c 100644
--- a/news_crawler/ollama_client.py
+++ b/news_crawler/ollama_client.py
@@ -112,6 +112,9 @@ class OllamaClient:
'duration': time.time() - start_time
}
+ # Clean markdown formatting from summary
+ summary = self._clean_markdown(summary)
+
summary_word_count = len(summary.split())
return {
@@ -303,6 +306,35 @@ German headline:
return translation
+ def _clean_markdown(self, text):
+ """Remove markdown formatting from text"""
+ import re
+
+ # Remove markdown headers (##, ###, etc.)
+ text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE)
+
+ # Remove bold/italic markers (**text**, *text*, __text__, _text_)
+ text = re.sub(r'\*\*([^\*]+)\*\*', r'\1', text)
+ text = re.sub(r'__([^_]+)__', r'\1', text)
+ text = re.sub(r'\*([^\*]+)\*', r'\1', text)
+ text = re.sub(r'_([^_]+)_', r'\1', text)
+
+ # Remove markdown links [text](url) -> text
+ text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', text)
+
+ # Remove inline code `text`
+ text = re.sub(r'`([^`]+)`', r'\1', text)
+
+ # Remove bullet points and list markers
+ text = re.sub(r'^\s*[-*+]\s+', '', text, flags=re.MULTILINE)
+ text = re.sub(r'^\s*\d+\.\s+', '', text, flags=re.MULTILINE)
+
+ # Clean up extra whitespace
+ text = re.sub(r'\n\s*\n', '\n\n', text)
+ text = text.strip()
+
+ return text
+
def _build_summarization_prompt(self, content, max_words):
"""Build prompt for article summarization"""
# Truncate content if too long (keep first 5000 words)
@@ -319,6 +351,8 @@ Write in the clear, engaging, and authoritative style of New York Times Magazine
- Focus on what matters to readers
- Even if the source is in German or another language, write your summary entirely in English
+IMPORTANT: Write in plain text only. Do NOT use markdown formatting (no ##, **, *, bullets, etc.). Just write natural prose.
+
Article:
{content}
diff --git a/news_sender/newsletter_template.html b/news_sender/newsletter_template.html
index 4d88939..e8a72b6 100644
--- a/news_sender/newsletter_template.html
+++ b/news_sender/newsletter_template.html
@@ -40,6 +40,39 @@
Here's what's happening in Munich today. We've summarized {{ article_count }} stories using AI so you can stay informed in under 5 minutes.
+
+ {% if weather and weather.success %}
+
+
+
+
+
+
+ |
+
+ TODAY'S WEATHER
+
+
+ {{ weather.icon }} {{ weather.temperature }}°C
+
+
+ {{ weather.condition }}
+
+ |
+
+
+ High: {{ weather.high }}°C
+
+
+ Low: {{ weather.low }}°C
+
+ |
+
+
+ |
+
+
+ {% endif %}
@@ -256,6 +289,36 @@
+
+
+
+
+ 📋 TL;DR - Quick Summary
+
+
+ Here's everything in one sentence each:
+
+
+ {% set all_articles = (trending_articles or []) + (other_articles or []) %}
+ {% for article in all_articles %}
+
+ {{ loop.index }}.
+
+ {{ article.title_en if article.title_en else article.title }} —
+ {{ article.summary.split('.')[0] }}.
+
+
+ {% endfor %}
+ |
+
+
+
+
+ |
+
+ |
+
+
|
diff --git a/news_sender/requirements.txt b/news_sender/requirements.txt
index 14d0870..576daf3 100644
--- a/news_sender/requirements.txt
+++ b/news_sender/requirements.txt
@@ -4,3 +4,4 @@ Jinja2==3.1.2
beautifulsoup4==4.12.2
schedule==1.2.0
pytz==2023.3
+requests==2.31.0
diff --git a/news_sender/sender_service.py b/news_sender/sender_service.py
index 8211896..46b99c4 100644
--- a/news_sender/sender_service.py
+++ b/news_sender/sender_service.py
@@ -242,6 +242,10 @@ def render_newsletter_html(articles, tracking_enabled=False, pixel_tracking_id=N
trending_articles = articles[:3] if len(articles) >= 3 else articles
other_articles = articles[3:] if len(articles) > 3 else []
+ # Get weather data
+ from weather_service import get_munich_weather
+ weather = get_munich_weather()
+
# Prepare template data
now = datetime.now()
template_data = {
@@ -252,7 +256,8 @@ def render_newsletter_html(articles, tracking_enabled=False, pixel_tracking_id=N
'other_articles': other_articles,
'unsubscribe_link': f'{Config.WEBSITE_URL}/unsubscribe',
'website_link': Config.WEBSITE_URL,
- 'tracking_enabled': tracking_enabled
+ 'tracking_enabled': tracking_enabled,
+ 'weather': weather
}
# Render HTML
diff --git a/news_sender/weather_service.py b/news_sender/weather_service.py
new file mode 100644
index 0000000..999e087
--- /dev/null
+++ b/news_sender/weather_service.py
@@ -0,0 +1,130 @@
+"""
+Weather service for Munich using Open-Meteo API (free, no API key needed)
+"""
+import requests
+from datetime import datetime
+
+
+def get_munich_weather():
+ """
+ Get current weather for Munich, Germany
+
+ Returns:
+ {
+ 'success': bool,
+ 'temperature': float, # Celsius
+ 'condition': str, # e.g., "Partly cloudy"
+ 'icon': str, # Weather emoji
+ 'high': float, # Today's high
+ 'low': float, # Today's low
+ 'error': str or None
+ }
+ """
+ try:
+ # Munich coordinates
+ lat = 48.1351
+ lon = 11.5820
+
+ # Open-Meteo API (free, no key needed)
+ url = f"https://api.open-meteo.com/v1/forecast"
+ params = {
+ 'latitude': lat,
+ 'longitude': lon,
+ 'current': 'temperature_2m,weather_code',
+ 'daily': 'temperature_2m_max,temperature_2m_min',
+ 'timezone': 'Europe/Berlin',
+ 'forecast_days': 1
+ }
+
+ response = requests.get(url, params=params, timeout=10)
+ response.raise_for_status()
+ data = response.json()
+
+ # Extract data
+ current = data.get('current', {})
+ daily = data.get('daily', {})
+
+ temperature = current.get('temperature_2m')
+ weather_code = current.get('weather_code', 0)
+ high = daily.get('temperature_2m_max', [None])[0]
+ low = daily.get('temperature_2m_min', [None])[0]
+
+ # Map weather code to condition and emoji
+ condition, icon = _get_weather_description(weather_code)
+
+ return {
+ 'success': True,
+ 'temperature': round(temperature, 1) if temperature else None,
+ 'condition': condition,
+ 'icon': icon,
+ 'high': round(high, 1) if high else None,
+ 'low': round(low, 1) if low else None,
+ 'error': None
+ }
+
+ except requests.exceptions.Timeout:
+ return {
+ 'success': False,
+ 'temperature': None,
+ 'condition': 'Unknown',
+ 'icon': '🌡️',
+ 'high': None,
+ 'low': None,
+ 'error': 'Weather service timeout'
+ }
+ except Exception as e:
+ return {
+ 'success': False,
+ 'temperature': None,
+ 'condition': 'Unknown',
+ 'icon': '🌡️',
+ 'high': None,
+ 'low': None,
+ 'error': str(e)
+ }
+
+
+def _get_weather_description(code):
+ """
+ Map WMO weather code to description and emoji
+ https://open-meteo.com/en/docs
+ """
+ weather_map = {
+ 0: ('Clear sky', '☀️'),
+ 1: ('Mainly clear', '🌤️'),
+ 2: ('Partly cloudy', '⛅'),
+ 3: ('Overcast', '☁️'),
+ 45: ('Foggy', '🌫️'),
+ 48: ('Foggy', '🌫️'),
+ 51: ('Light drizzle', '🌦️'),
+ 53: ('Drizzle', '🌦️'),
+ 55: ('Heavy drizzle', '🌧️'),
+ 61: ('Light rain', '🌧️'),
+ 63: ('Rain', '🌧️'),
+ 65: ('Heavy rain', '⛈️'),
+ 71: ('Light snow', '🌨️'),
+ 73: ('Snow', '❄️'),
+ 75: ('Heavy snow', '❄️'),
+ 77: ('Snow grains', '🌨️'),
+ 80: ('Light showers', '🌦️'),
+ 81: ('Showers', '🌧️'),
+ 82: ('Heavy showers', '⛈️'),
+ 85: ('Light snow showers', '🌨️'),
+ 86: ('Snow showers', '❄️'),
+ 95: ('Thunderstorm', '⛈️'),
+ 96: ('Thunderstorm with hail', '⛈️'),
+ 99: ('Thunderstorm with hail', '⛈️'),
+ }
+
+ return weather_map.get(code, ('Unknown', '🌡️'))
+
+
+if __name__ == '__main__':
+ # Test
+ weather = get_munich_weather()
+ print(f"Success: {weather['success']}")
+ print(f"Temperature: {weather['temperature']}°C")
+ print(f"Condition: {weather['icon']} {weather['condition']}")
+ print(f"High/Low: {weather['high']}°C / {weather['low']}°C")
+ if weather['error']:
+ print(f"Error: {weather['error']}")
|