-
-
Notifications
You must be signed in to change notification settings - Fork 198
Add Pandora provider #2503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
OzGav
wants to merge
20
commits into
dev
Choose a base branch
from
pandora
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add Pandora provider #2503
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
6bafb6b
Initial Commit
OzGav ffa7662
Try and fix streaming
OzGav 5b445e8
more drafting
OzGav 51fd492
Initial Commit
OzGav 0783f5f
Try and fix streaming
OzGav bf76031
more drafting
OzGav 80d1b90
Merge branch 'pandora' of https://github.com/music-assistant/server i…
OzGav 5973e37
More drafting
OzGav 89eec6e
Merge remote-tracking branch 'origin/dev' into pandora
OzGav d2f76eb
final drafting
OzGav 82f5476
Final tweaks
OzGav 369e708
Increase number of URLs retrieved
OzGav 1f4696a
Adjust number of fragments
OzGav 8c26414
fix constants
OzGav a934851
PR review comment
OzGav bb9aef8
implement dynamic URL
OzGav 2bf9013
Update music_assistant/providers/pandora/provider.py
OzGav 8cafc0b
Potential fix for code scanning alert no. 34: Information exposure th…
OzGav 419d154
interim code
OzGav acf2cf3
Update metadata
OzGav File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| """Pandora music provider support for Music Assistant.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| from music_assistant_models.config_entries import ConfigEntry, ConfigValueType | ||
| from music_assistant_models.enums import ConfigEntryType, ProviderFeature | ||
| from music_assistant_models.errors import SetupFailedError | ||
|
|
||
| from music_assistant.constants import CONF_PASSWORD, CONF_USERNAME | ||
|
|
||
| from .provider import PandoraProvider | ||
|
|
||
| if TYPE_CHECKING: | ||
| from music_assistant_models.config_entries import ProviderConfig | ||
| from music_assistant_models.provider import ProviderManifest | ||
|
|
||
| from music_assistant import MusicAssistant | ||
| from music_assistant.models import ProviderInstanceType | ||
|
|
||
| # Supported Features - Pandora is primarily a radio service | ||
| SUPPORTED_FEATURES = { | ||
| ProviderFeature.BROWSE, | ||
| ProviderFeature.LIBRARY_RADIOS, | ||
| } | ||
|
|
||
|
|
||
| async def setup( | ||
| mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig | ||
| ) -> ProviderInstanceType: | ||
| """Initialize provider instance with given configuration.""" | ||
| username = config.get_value(CONF_USERNAME) | ||
| password = config.get_value(CONF_PASSWORD) | ||
|
|
||
| # Type-safe validation | ||
| if ( | ||
| not username | ||
| or not password | ||
| or not isinstance(username, str) | ||
| or not isinstance(password, str) | ||
| or not username.strip() | ||
| or not password.strip() | ||
| ): | ||
| raise SetupFailedError("Username and password are required") | ||
|
|
||
| return PandoraProvider(mass, manifest, config, SUPPORTED_FEATURES) | ||
|
|
||
|
|
||
| async def get_config_entries( | ||
| mass: MusicAssistant, | ||
| instance_id: str | None = None, | ||
| action: str | None = None, | ||
| values: dict[str, ConfigValueType] | None = None, | ||
| ) -> tuple[ConfigEntry, ...]: | ||
| """Return configuration entries for this provider.""" | ||
| # ruff: noqa: ARG001 | ||
| return ( | ||
| ConfigEntry( | ||
| key=CONF_USERNAME, | ||
| type=ConfigEntryType.STRING, | ||
| label="Username", | ||
| description="Your Pandora username or email address", | ||
| required=True, | ||
| ), | ||
| ConfigEntry( | ||
| key=CONF_PASSWORD, | ||
| type=ConfigEntryType.SECURE_STRING, | ||
| label="Password", | ||
| description="Your Pandora password", | ||
| required=True, | ||
| ), | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| """Constants for the Pandora provider.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| # API Endpoints | ||
| API_BASE_URL = "https://www.pandora.com/api/v1" | ||
| LOGIN_ENDPOINT = f"{API_BASE_URL}/auth/login" | ||
| STATIONS_ENDPOINT = f"{API_BASE_URL}/station/getStations" | ||
| PLAYLIST_FRAGMENT_ENDPOINT = f"{API_BASE_URL}/playlist/getFragment" | ||
|
|
||
| # Error Codes from Pandora API | ||
| PANDORA_ERROR_CODES = { | ||
| 0: "INVALID_REQUEST", | ||
| 1: "INVALID_PARTNER", | ||
| 2: "LISTENER_NOT_AUTHORIZED", | ||
| 3: "USER_NOT_AUTHORIZED", | ||
| 4: "STATION_DOES_NOT_EXIST", | ||
| 5: "TRACK_NOT_FOUND", | ||
| 9: "PANDORA_NOT_AVAILABLE", | ||
| 10: "SYSTEM_NOT_AVAILABLE", | ||
| 11: "CALL_NOT_ALLOWED", | ||
| 12: "INVALID_USERNAME", | ||
| 13: "INVALID_PASSWORD", | ||
| 14: "DEVICE_NOT_FOUND", | ||
| 15: "PARTNER_NOT_AUTHORIZED", | ||
| 1000: "READ_ONLY_MODE", | ||
| 1001: "INVALID_AUTH_TOKEN", | ||
| 1002: "INVALID_LOGIN", | ||
| 1003: "LISTENER_NOT_AUTHORIZED", | ||
| 1004: "USER_ALREADY_EXISTS", | ||
| 1005: "DEVICE_ALREADY_ASSOCIATED_TO_ACCOUNT", | ||
| 1006: "UPGRADE_DEVICE_MODEL_INVALID", | ||
| 1009: "DEVICE_MODEL_INVALID", | ||
| 1010: "INVALID_SPONSOR", | ||
| 1018: "EXPLICIT_PIN_INCORRECT", | ||
| 1020: "EXPLICIT_PIN_MALFORMED", | ||
| 1023: "DEVICE_DISABLED", | ||
| 1024: "DAILY_TRIAL_LIMIT_REACHED", | ||
| 1025: "INVALID_SPONSOR_USERNAME", | ||
| 1026: "SPONSOR_CANNOT_SKIP_ADS", | ||
| 1027: "INSUFFICIENT_CONNECTIVITY", | ||
| 1034: "GEOLOCATION_REQUIRED", | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| """Helper utilities for the Pandora provider.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import secrets | ||
| from typing import Any | ||
|
|
||
| import aiohttp | ||
| from music_assistant_models.errors import ( | ||
| LoginFailed, | ||
| MediaNotFoundError, | ||
| ProviderUnavailableError, | ||
| ResourceTemporarilyUnavailable, | ||
| ) | ||
|
|
||
| from .constants import PANDORA_ERROR_CODES | ||
|
|
||
|
|
||
| def generate_csrf_token() -> str: | ||
| """Generate a random CSRF token.""" | ||
| return secrets.token_hex(16) | ||
|
|
||
|
|
||
| def handle_pandora_error(response_data: dict[str, Any]) -> None: | ||
| """Handle Pandora API error responses. | ||
|
|
||
| Maps Pandora API error codes to appropriate Music Assistant exceptions. | ||
|
|
||
| Raises: | ||
| LoginFailed: For authentication errors | ||
| MediaNotFoundError: For missing stations/tracks | ||
| ResourceTemporarilyUnavailable: For service availability issues | ||
| ProviderUnavailableError: For other API errors | ||
| """ | ||
| if response_data.get("errorCode") is not None: | ||
| error_code = response_data["errorCode"] | ||
| error_string = response_data.get("errorString", "UNKNOWN_ERROR") | ||
| message = response_data.get("message", "An unknown error occurred") | ||
|
|
||
| # Map specific error codes to Music Assistant exceptions | ||
| if error_code in (12, 13, 1002): # Invalid username/password/login | ||
| raise LoginFailed(f"Login failed: {message}") | ||
|
|
||
| if error_code in (4, 5): # Station/track not found | ||
| raise MediaNotFoundError(f"Media not found: {message}") | ||
|
|
||
| if error_code in (9, 10): # Service unavailable | ||
| raise ResourceTemporarilyUnavailable(f"Service unavailable: {message}") | ||
|
|
||
| if error_code in (1001, 1003): # Auth token issues | ||
| raise LoginFailed(f"Authentication error: {message}") | ||
|
|
||
| # Get error description from our mapping | ||
| error_desc = PANDORA_ERROR_CODES.get(error_code, error_string) | ||
| raise ProviderUnavailableError(f"Pandora API error {error_code} ({error_desc}): {message}") | ||
|
|
||
|
|
||
| async def get_csrf_token(session: aiohttp.ClientSession) -> str: | ||
| """Get CSRF token from Pandora website. | ||
|
|
||
| Attempts to retrieve CSRF token from Pandora cookies. | ||
|
|
||
| Args: | ||
| session: aiohttp client session | ||
|
|
||
| Returns: | ||
| CSRF token string | ||
|
|
||
| Raises: | ||
| ResourceTemporarilyUnavailable: If network request fails or no token available | ||
| """ | ||
| try: | ||
| async with session.head("https://www.pandora.com/") as response: | ||
| if "csrftoken" in response.cookies: | ||
| return str(response.cookies["csrftoken"].value) | ||
| except aiohttp.ClientError as e: | ||
| raise ResourceTemporarilyUnavailable(f"Failed to get CSRF token from Pandora: {e}") from e | ||
|
|
||
| # No token found - service may be unavailable | ||
| raise ResourceTemporarilyUnavailable( | ||
| "Pandora did not provide a CSRF token. Service may be unavailable." | ||
| ) | ||
|
|
||
|
|
||
| def create_auth_headers(csrf_token: str, auth_token: str | None = None) -> dict[str, str]: | ||
| """Create authentication headers for Pandora API requests. | ||
|
|
||
| Args: | ||
| csrf_token: CSRF token for request validation | ||
| auth_token: Optional authentication token for authenticated requests | ||
|
|
||
| Returns: | ||
| Dictionary of HTTP headers | ||
| """ | ||
| headers = { | ||
| "Content-Type": "application/json;charset=utf-8", | ||
| "X-CsrfToken": csrf_token, | ||
| "Cookie": f"csrftoken={csrf_token}", | ||
| "User-Agent": "Music Assistant Pandora Provider/1.0", | ||
| } | ||
|
|
||
| if auth_token: | ||
| headers["X-AuthToken"] = auth_token | ||
|
|
||
| return headers |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "domain": "pandora", | ||
| "name": "Pandora", | ||
| "description": "Pandora is a music and podcast streaming service that creates personalized radio stations based on your favorite songs and artists.", | ||
| "documentation": "https://music-assistant.io/music-providers/pandora/", | ||
| "type": "music", | ||
| "requirements": [], | ||
| "codeowners": "@ozgav", | ||
| "multi_instance": false, | ||
| "stage": "beta" | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.