|
20 | 20 | DEFAULT_SLOWDOWN_AFTER = 50 # Number of requests before introducing slowdown |
21 | 21 |
|
22 | 22 |
|
| 23 | +class PlaybackError(Exception): |
| 24 | + """Custom exception for playback-related errors that shouldn't crash the program""" |
| 25 | + pass |
| 26 | + |
| 27 | + |
23 | 28 | class RateLimiter: |
24 | 29 | """Simple token-bucket rate limiter to avoid server-side throttling.""" |
25 | 30 | def __init__(self, qps: float): |
@@ -209,26 +214,30 @@ def _request_with_retry(self, method: str, url: str, **kwargs): |
209 | 214 |
|
210 | 215 | return response |
211 | 216 |
|
212 | | - def get_streams(self, media_id: str) -> Dict: |
213 | | - """Get available streams for media_id.""" |
| 217 | + def get_streams(self, media_id: str) -> Optional[Dict]: |
| 218 | + """ |
| 219 | + Get available streams for media_id. |
| 220 | + """ |
214 | 221 | response = self._request_with_retry( |
215 | 222 | 'GET', |
216 | 223 | f'{BASE_URL}/playback/v3/{media_id}/web/chrome/play', |
217 | 224 | params={'locale': self.locale} |
218 | 225 | ) |
219 | 226 |
|
220 | 227 | if response.status_code == 403: |
221 | | - raise Exception("Playback Rejected: Subscription does not have access to this content") |
| 228 | + logging.warning(f"Access denied for media {media_id}: Subscription required") |
| 229 | + return None |
222 | 230 |
|
223 | 231 | if response.status_code == 420: |
224 | | - raise Exception("TOO_MANY_ACTIVE_STREAMS. Wait a few minutes and try again.") |
| 232 | + raise PlaybackError("TOO_MANY_ACTIVE_STREAMS. Wait a few minutes and try again.") |
225 | 233 |
|
226 | 234 | response.raise_for_status() |
227 | 235 |
|
228 | 236 | data = response.json() |
229 | 237 |
|
230 | 238 | if data.get('error') == 'Playback is Rejected': |
231 | | - raise Exception("Playback Rejected: Premium required") |
| 239 | + logging.warning(f"Playback rejected for media {media_id}: Premium required") |
| 240 | + return None |
232 | 241 |
|
233 | 242 | return data |
234 | 243 |
|
@@ -270,18 +279,19 @@ def _find_token_anywhere(obj) -> Optional[str]: |
270 | 279 | return None |
271 | 280 |
|
272 | 281 |
|
273 | | -def get_playback_session(client: CrunchyrollClient, url_id: str) -> Tuple[str, Dict, List[Dict], Optional[str], Optional[str]]: |
| 282 | +def get_playback_session(client: CrunchyrollClient, url_id: str) -> Optional[Tuple[str, Dict, List[Dict], Optional[str], Optional[str]]]: |
274 | 283 | """ |
275 | 284 | Return the playback session details. |
276 | 285 | |
277 | 286 | Returns: |
278 | | - - mpd_url: str |
279 | | - - headers: Dict |
280 | | - - subtitles: List[Dict] with metadata (kind, closed_caption, etc.) |
281 | | - - token: Optional[str] for cleanup |
282 | | - - audio_locale: Optional[str] current audio language |
| 287 | + Tuple with (mpd_url, headers, subtitles, token, audio_locale) or None if access denied |
283 | 288 | """ |
284 | 289 | data = client.get_streams(url_id) |
| 290 | + |
| 291 | + # If get_streams returns None, it means access was denied (403) |
| 292 | + if data is None: |
| 293 | + return None |
| 294 | + |
285 | 295 | url = data.get('url') |
286 | 296 | audio_locale_current = data.get('audio_locale') or data.get('audio', {}).get('locale') |
287 | 297 |
|
|
0 commit comments