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:
parent
2003e58358
commit
0742cc45e6
4 changed files with 137 additions and 7 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue