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
This commit is contained in:
Broque Thomas 2026-03-15 18:49:51 -07:00
parent 2003e58358
commit 0742cc45e6
4 changed files with 137 additions and 7 deletions

View file

@ -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

View file

@ -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():
"""

View file

@ -4347,6 +4347,19 @@
Once scanned, artists are kept updated with new releases only.
</div>
</div>
<div class="form-group">
<label>Hemisphere:</label>
<select id="discovery-hemisphere" class="form-select">
<option value="northern">Northern Hemisphere</option>
<option value="southern">Southern Hemisphere</option>
</select>
<div class="setting-help-text">
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.
</div>
</div>
</div>
</div>

View file

@ -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);