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:
|
except Exception as e:
|
||||||
logger.error(f"Error creating seasonal database schema: {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]:
|
def get_current_season(self) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Detect current season based on current month.
|
Detect current season based on current month.
|
||||||
|
Respects hemisphere setting — southern hemisphere offsets seasonal
|
||||||
|
months by 6, but holidays stay calendar-fixed.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Season key (e.g., 'halloween', 'christmas') or None
|
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
|
# Check each season to find active ones
|
||||||
active_seasons = []
|
active_seasons = []
|
||||||
for season_key, config in SEASONAL_CONFIG.items():
|
for season_key, config in SEASONAL_CONFIG.items():
|
||||||
if current_month in config['active_months']:
|
if hemisphere == 'southern' and season_key in _HOLIDAY_KEYS:
|
||||||
active_seasons.append(season_key)
|
# 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:
|
if not active_seasons:
|
||||||
return None
|
return None
|
||||||
|
|
@ -205,13 +248,21 @@ class SeasonalDiscoveryService:
|
||||||
return active_seasons[0] if active_seasons else None
|
return active_seasons[0] if active_seasons else None
|
||||||
|
|
||||||
def get_all_active_seasons(self) -> List[str]:
|
def get_all_active_seasons(self) -> List[str]:
|
||||||
"""Get all seasons active in current month (for displaying multiple sections)"""
|
"""Get all seasons active in current month (hemisphere-aware)"""
|
||||||
current_month = datetime.now().month
|
real_month = datetime.now().month
|
||||||
|
hemisphere = self._get_hemisphere()
|
||||||
|
effective_month = self._get_effective_month(hemisphere)
|
||||||
|
|
||||||
|
_HOLIDAY_KEYS = {'halloween', 'christmas', 'valentines'}
|
||||||
|
|
||||||
active_seasons = []
|
active_seasons = []
|
||||||
for season_key, config in SEASONAL_CONFIG.items():
|
for season_key, config in SEASONAL_CONFIG.items():
|
||||||
if current_month in config['active_months']:
|
if hemisphere == 'southern' and season_key in _HOLIDAY_KEYS:
|
||||||
active_seasons.append(season_key)
|
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
|
return active_seasons
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18124,6 +18124,49 @@ def set_discovery_lookback_period():
|
||||||
print(f"Error setting discovery lookback period: {e}")
|
print(f"Error setting discovery lookback period: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
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'])
|
@app.route('/api/wishlist/tracks', methods=['GET'])
|
||||||
def get_wishlist_tracks():
|
def get_wishlist_tracks():
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -4347,6 +4347,19 @@
|
||||||
Once scanned, artists are kept updated with new releases only.
|
Once scanned, artists are kept updated with new releases only.
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4916,6 +4916,17 @@ async function loadSettingsData() {
|
||||||
console.error('Error loading discovery lookback period:', error);
|
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
|
// Load current log level
|
||||||
try {
|
try {
|
||||||
const logLevelResponse = await fetch('/api/settings/log-level');
|
const logLevelResponse = await fetch('/api/settings/log-level');
|
||||||
|
|
@ -5725,6 +5736,18 @@ async function saveSettings(quiet = false) {
|
||||||
lookbackSaved = 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) {
|
if (result.success && qualityProfileSaved && lookbackSaved) {
|
||||||
showToast(quiet ? 'Settings auto-saved' : 'Settings saved successfully', 'success');
|
showToast(quiet ? 'Settings auto-saved' : 'Settings saved successfully', 'success');
|
||||||
setTimeout(updateServiceStatus, 1000);
|
setTimeout(updateServiceStatus, 1000);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue