From 0742cc45e6860efe9d863013942f85c678b7d117 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 15 Mar 2026 18:49:51 -0700 Subject: [PATCH] Add hemisphere setting for seasonal playlists on Discover page Southern hemisphere users now see correct seasons (e.g. March = Autumn, December = Summer). Holidays (Halloween, Christmas, Valentine's) stay calendar-fixed regardless of hemisphere. - Hemisphere dropdown in Discovery Pool Settings - GET/POST /api/discovery/hemisphere endpoints - Season detection offsets months by 6 for southern hemisphere - Stored in metadata table, defaults to northern --- core/seasonal_discovery.py | 65 ++++++++++++++++++++++++++++++++++---- web_server.py | 43 +++++++++++++++++++++++++ webui/index.html | 13 ++++++++ webui/static/script.js | 23 ++++++++++++++ 4 files changed, 137 insertions(+), 7 deletions(-) diff --git a/core/seasonal_discovery.py b/core/seasonal_discovery.py index 96821fa1..d0bfa029 100644 --- a/core/seasonal_discovery.py +++ b/core/seasonal_discovery.py @@ -176,20 +176,63 @@ class SeasonalDiscoveryService: except Exception as e: logger.error(f"Error creating seasonal database schema: {e}") + def _get_effective_month(self, hemisphere: str = None) -> int: + """Get the effective month, adjusted for hemisphere setting. + + Southern hemisphere offsets by 6 months so seasons align correctly + (e.g., December in southern hemisphere maps to June = summer → winter). + Holiday months (Halloween, Christmas, Valentine's) are NOT offset. + """ + month = datetime.now().month + if hemisphere is None: + hemisphere = self._get_hemisphere() + if hemisphere == 'southern': + # Offset by 6 months for seasonal content + return ((month - 1 + 6) % 12) + 1 + return month + + def _get_hemisphere(self) -> str: + """Get configured hemisphere from database metadata.""" + try: + with self.database._get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT value FROM metadata WHERE key = 'hemisphere'") + row = cursor.fetchone() + if row: + val = row[0] if isinstance(row, tuple) else row['value'] + if val in ('northern', 'southern'): + return val + except Exception: + pass + return 'northern' + def get_current_season(self) -> Optional[str]: """ Detect current season based on current month. + Respects hemisphere setting — southern hemisphere offsets seasonal + months by 6, but holidays stay calendar-fixed. Returns: Season key (e.g., 'halloween', 'christmas') or None """ - current_month = datetime.now().month + real_month = datetime.now().month + hemisphere = self._get_hemisphere() + effective_month = self._get_effective_month(hemisphere) + + # Holidays that are calendar-fixed (not season-dependent) + _HOLIDAY_KEYS = {'halloween', 'christmas', 'valentines'} # Check each season to find active ones active_seasons = [] for season_key, config in SEASONAL_CONFIG.items(): - if current_month in config['active_months']: - active_seasons.append(season_key) + if hemisphere == 'southern' and season_key in _HOLIDAY_KEYS: + # Holidays use real calendar month + if real_month in config['active_months']: + active_seasons.append(season_key) + else: + # Seasons use effective (offset) month + if effective_month in config['active_months']: + active_seasons.append(season_key) if not active_seasons: return None @@ -205,13 +248,21 @@ class SeasonalDiscoveryService: return active_seasons[0] if active_seasons else None def get_all_active_seasons(self) -> List[str]: - """Get all seasons active in current month (for displaying multiple sections)""" - current_month = datetime.now().month + """Get all seasons active in current month (hemisphere-aware)""" + real_month = datetime.now().month + hemisphere = self._get_hemisphere() + effective_month = self._get_effective_month(hemisphere) + + _HOLIDAY_KEYS = {'halloween', 'christmas', 'valentines'} active_seasons = [] for season_key, config in SEASONAL_CONFIG.items(): - if current_month in config['active_months']: - active_seasons.append(season_key) + if hemisphere == 'southern' and season_key in _HOLIDAY_KEYS: + if real_month in config['active_months']: + active_seasons.append(season_key) + else: + if effective_month in config['active_months']: + active_seasons.append(season_key) return active_seasons diff --git a/web_server.py b/web_server.py index ea043a37..22e185cf 100644 --- a/web_server.py +++ b/web_server.py @@ -18124,6 +18124,49 @@ def set_discovery_lookback_period(): print(f"Error setting discovery lookback period: {e}") return jsonify({"error": str(e)}), 500 +@app.route('/api/discovery/hemisphere', methods=['GET']) +def get_hemisphere(): + """Get the hemisphere setting for seasonal content.""" + try: + db = get_database() + with db._get_connection() as conn: + cursor = conn.cursor() + cursor.execute("SELECT value FROM metadata WHERE key = 'hemisphere'") + row = cursor.fetchone() + value = 'northern' + if row: + val = row[0] if isinstance(row, tuple) else row['value'] + if val in ('northern', 'southern'): + value = val + return jsonify({"hemisphere": value}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route('/api/discovery/hemisphere', methods=['POST']) +def set_hemisphere(): + """Set the hemisphere for seasonal content (northern or southern).""" + try: + data = request.get_json() + hemisphere = data.get('hemisphere', '').lower() + if hemisphere not in ('northern', 'southern'): + return jsonify({"error": "Must be 'northern' or 'southern'"}), 400 + + db = get_database() + with db._get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT OR REPLACE INTO metadata (key, value, updated_at) + VALUES ('hemisphere', ?, CURRENT_TIMESTAMP) + """, (hemisphere,)) + conn.commit() + + logger.info("Hemisphere set to: %s", hemisphere) + return jsonify({"success": True, "hemisphere": hemisphere}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + @app.route('/api/wishlist/tracks', methods=['GET']) def get_wishlist_tracks(): """ diff --git a/webui/index.html b/webui/index.html index 31584740..32fd7526 100644 --- a/webui/index.html +++ b/webui/index.html @@ -4347,6 +4347,19 @@ Once scanned, artists are kept updated with new releases only. + +
+ + +
+ Adjusts seasonal playlists on the Discover page to match your region. + Southern hemisphere flips seasons (e.g. December = Summer, June = Winter). + Holidays like Christmas and Halloween stay calendar-fixed. +
+
diff --git a/webui/static/script.js b/webui/static/script.js index d589a08d..9db71f63 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -4916,6 +4916,17 @@ async function loadSettingsData() { console.error('Error loading discovery lookback period:', error); } + // Load Hemisphere setting + try { + const hemiResponse = await fetch('/api/discovery/hemisphere'); + const hemiData = await hemiResponse.json(); + if (hemiData.hemisphere) { + document.getElementById('discovery-hemisphere').value = hemiData.hemisphere; + } + } catch (error) { + console.error('Error loading hemisphere setting:', error); + } + // Load current log level try { const logLevelResponse = await fetch('/api/settings/log-level'); @@ -5725,6 +5736,18 @@ async function saveSettings(quiet = false) { lookbackSaved = false; } + // Save hemisphere setting + try { + const hemisphere = document.getElementById('discovery-hemisphere').value; + await fetch('/api/discovery/hemisphere', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ hemisphere }) + }); + } catch (error) { + console.error('Error saving hemisphere setting:', error); + } + if (result.success && qualityProfileSaved && lookbackSaved) { showToast(quiet ? 'Settings auto-saved' : 'Settings saved successfully', 'success'); setTimeout(updateServiceStatus, 1000);