Persist a hard lockout to EPGSource custom_properties when SD returns 4100 MAX_LINEUP_CHANGES_REACHED. SD lineup change counters reset at 00:00Z (midnight UTC) per SD's documented behavior — error 4100 states "lineup changes for today" and all SD rate counters reset
(self, source)
| 197 | source.save(update_fields=['custom_properties']) |
| 198 | |
| 199 | def _save_sd_lockout(self, source): |
| 200 | """ |
| 201 | Persist a hard lockout to EPGSource custom_properties when SD returns |
| 202 | 4100 MAX_LINEUP_CHANGES_REACHED. SD lineup change counters reset at |
| 203 | 00:00Z (midnight UTC) per SD's documented behavior — error 4100 states |
| 204 | "lineup changes for today" and all SD rate counters reset at midnight UTC. |
| 205 | Lockout clears automatically when the next midnight UTC passes. |
| 206 | """ |
| 207 | from django.utils import timezone |
| 208 | from datetime import datetime, timedelta, timezone as dt_timezone |
| 209 | |
| 210 | now = timezone.now() |
| 211 | # Calculate next midnight UTC — SD resets at 00:00Z not on a rolling window |
| 212 | tomorrow = (now + timedelta(days=1)).replace( |
| 213 | hour=0, minute=0, second=0, microsecond=0, |
| 214 | tzinfo=dt_timezone.utc |
| 215 | ) |
| 216 | reset_at = tomorrow |
| 217 | |
| 218 | cp = source.custom_properties or {} |
| 219 | cp['sd_changes_remaining'] = 0 |
| 220 | cp['sd_changes_reset_at'] = reset_at.isoformat() |
| 221 | source.custom_properties = cp |
| 222 | source.save(update_fields=['custom_properties']) |
| 223 | logger.warning( |
| 224 | f"SD source {source.id}: daily add limit reached (4100). " |
| 225 | f"Lockout set until {reset_at.isoformat()}." |
| 226 | ) |
| 227 | |
| 228 | def _fetch_sd_countries(self): |
| 229 | """Fetch the SD country list (token not required; User-Agent is).""" |