# --- Pydantic Models for Alexa.Media.Playback v1.0 --- from typing import List, Optional, Union, Literal, Dict from pydantic import BaseModel, Field # --- Base Models & Common Components --- class BasePlaybackModel(BaseModel): """Base model for common configurations.""" class Config: extra = "ignore" # validate_by_name = True # Pydantic V2 equivalent for validate_by_name validate_by_name = ( True # Keep for compatibility if needed, but validate_by_name is V2 ) class User(BasePlaybackModel): id: str access_token: Optional[str] = Field(None, alias="accessToken") class Location(BasePlaybackModel): originating_locale: Optional[str] = Field(None, alias="originatingLocale") class RequestContext(BasePlaybackModel): user: Optional[User] = Field(default=None) location: Optional[Location] = Field(default=None) class Filter(BasePlaybackModel): explicit_language_allowed: Optional[bool] = Field( None, alias="explicitLanguageAllowed" ) # Add other potential filters if needed class MediaReferenceValue(BasePlaybackModel): id: str # Often the item/track ID queue_id: Optional[str] = Field( None, alias="queueId" ) # ID of the queue this item belongs to content_id: Optional[str] = Field( None, alias="contentId" ) # Original contentId requested that resulted in this queue class MediaReference(BasePlaybackModel): """Reference to a media item, often the currently playing one.""" namespace: Literal["Alexa.Audio.PlayQueue"] name: Literal["item"] value: MediaReferenceValue class PlaybackRepeat(BasePlaybackModel): status: Literal["ON", "OFF"] class PlaybackModes(BasePlaybackModel): shuffle: Optional[bool] = Field(default=False) loop: Optional[bool] = Field(default=False) # Loop the entire queue repeat: PlaybackRepeat = Field( default_factory=lambda: PlaybackRepeat(status="OFF") ) # Repeat the single item class ContentFormat(BasePlaybackModel): """Describes audio formats supported by an endpoint player.""" type: Literal["AUDIO"] content_type_id: str = Field( ..., alias="contentTypeId" ) # Skill-defined ID representing a specific codec/bitrate/etc. class PlayerProperties(BasePlaybackModel): supported_content_formats: Optional[List[ContentFormat]] = Field( None, alias="supportedContentFormats" ) class Player(BasePlaybackModel): """Represents a playback capability on an endpoint.""" playback_method_type: Literal["ALEXA_AUDIO_PLAYER_QUEUE"] = Field( ..., alias="playbackMethodType" ) id: str # Usually "AudioPlayer" properties: Optional[PlayerProperties] = Field(default=None) class Endpoint(BasePlaybackModel): """Represents a target device for playback.""" id: str = Field(..., alias="endpointId") # Use alias='id' if JSON uses "id" players: Optional[List[Player]] = Field(default=None) # --- Models for Initiate Response --- class Control(BasePlaybackModel): """Represents a playback control (button/state).""" type: Literal["TOGGLE", "CYCLE", "COMMAND", "ADJUST"] name: Literal[ "SHUFFLE", "LOOP", "REPEAT", "NEXT", "PREVIOUS", "SEEK_POSITION" ] # Add other commands if applicable enabled: bool selected: Optional[bool] = Field(default=None) # For TOGGLE value: Optional[PlaybackRepeat] = Field( default=None ) # For CYCLE (e.g., REPEAT status) class RuleFeedback(BasePlaybackModel): type: Literal["PREFERENCE"] enabled: bool class Rule(BasePlaybackModel): """Defines rules for the queue or item.""" # Queue-level rules feedback: Optional[RuleFeedback] = Field(default=None) # Item-level rules feedback_enabled: Optional[bool] = Field(default=None, alias="feedbackEnabled") class Speech(BasePlaybackModel): type: Literal["PLAIN_TEXT"] text: str class Name(BasePlaybackModel): speech: Speech display: str class ArtSource(BasePlaybackModel): url: str size: Optional[Literal["X_SMALL", "SMALL", "MEDIUM", "LARGE", "X_LARGE"]] = Field( default=None ) width_pixels: Optional[int] = Field(None, alias="widthPixels") height_pixels: Optional[int] = Field(None, alias="heightPixels") class Art(BasePlaybackModel): sources: List[ArtSource] class BaseMetadata(BasePlaybackModel): """Common metadata fields.""" type: Literal[ "TRACK", "PODCAST_EPISODE", "RADIO_STATION" ] # Add other types as needed name: Name art: Optional[Art] = Field(default=None) background: Optional[Art] = Field( default=None, description="Optional background image for display devices." ) # Add other common fields like artistName, albumName, etc. class TrackMetadata(BaseMetadata): type: Literal["TRACK"] = "TRACK" # Add track-specific fields: artistName, albumName, etc. class PodcastEpisodeMetadata(BaseMetadata): type: Literal["PODCAST_EPISODE"] = "PODCAST_EPISODE" # Add podcast-specific fields: seriesName, episodeNumber, etc. class RadioStationMetadata(BaseMetadata): type: Literal["RADIO_STATION"] = "RADIO_STATION" # Add radio-specific fields: callSign, frequency, etc. # Union of possible metadata types Metadata = Union[ TrackMetadata, PodcastEpisodeMetadata, RadioStationMetadata ] # Add other types to Union class Stream(BasePlaybackModel): id: str # Your internal identifier for this stream instance uri: str # The actual HTTPS URL for the audio stream offset_in_milliseconds: int = Field(..., alias="offsetInMilliseconds") valid_until: Optional[str] = Field( None, alias="validUntil", description="ISO 8601 timestamp string when the URI expires.", ) content_format: Optional[ContentFormat] = Field( None, alias="contentFormat", description="Included if premium audio/specific format is used.", ) # Add DRM info if needed class Feedback(BasePlaybackModel): type: Literal["PREFERENCE"] value: Literal[ "POSITIVE", "NEGATIVE", "NEUTRAL" ] # Or just POSITIVE/NEGATIVE? Check docs. class PlaybackInfo(BasePlaybackModel): """Placeholder for playback info (often just 'DEFAULT').""" type: Literal["DEFAULT"] # Expand if other types exist class Transcript(BasePlaybackModel): """Represents synchronized lyrics or captions.""" id: str # Your internal identifier for this transcript instance uri: str # HTTPS URL for the transcript file (e.g., WebVTT) format: Literal["WEBVTT"] # Format of the transcript file headers: Optional[List[Dict[str, str]]] class PlayQueueItem(BasePlaybackModel): """Represents a single item in the play queue (used in Initiate.Response).""" id: str # Unique ID for this specific item instance in the queue playback_info: PlaybackInfo = Field( default_factory=lambda: PlaybackInfo(type="DEFAULT"), alias="playbackInfo" ) metadata: Metadata duration_in_milliseconds: Optional[int] = Field( None, alias="durationInMilliseconds" ) controls: Optional[List[Control]] = Field(default=None) rules: Optional[Rule] = Field(default=None) stream: Stream feedback: Optional[Feedback] = Field(default=None) transcript: Optional[Transcript] = Field( default=None, description="Optional synchronized lyrics." ) class PlaybackMethod(BasePlaybackModel): """Contains the queue details needed by Alexa to start playback.""" type: Literal["ALEXA_AUDIO_PLAYER_QUEUE"] id: str # The unique identifier for this play queue instance controls: Optional[List[Control]] = Field( default=None ) # Queue-level controls (SHUFFLE, LOOP, REPEAT) rules: Optional[Rule] = Field( default=None ) # Queue-level rules (Feedback preference) first_item: PlayQueueItem = Field( ..., alias="firstItem", description="The first track/episode to play." ) # --- Request Models --- class InitiateRequestPayload(BasePlaybackModel): request_context: Optional[RequestContext] = Field( default=None, alias="requestContext" ) filters: Optional[Filter] = Field(default=None) content_id: str = Field( ..., alias="contentId", description="The identifier for the content user wants to play (from GetPlayableContent).", ) current_item_reference: Optional[MediaReference] = Field( default=None, alias="currentItemReference", description="Present if content is already playing on the target device.", ) playback_modes: PlaybackModes = Field( default_factory=PlaybackModes, alias="playbackModes" ) playback_position: Optional[Literal["RESUME"]] = Field( None, alias="playbackPosition", description="Podcasts only: Indicates user asked to resume.", ) endpoints: Optional[List[Endpoint]] = Field( default=None, description="Premium Audio only: Target device capabilities." ) class ReinitiateRequestPayload(BasePlaybackModel): request_context: RequestContext = Field(..., alias="requestContext") playback_snapshot: "PlaybackSnapshot" = Field( ..., alias="playbackSnapshot", description="State of the playback being moved." ) endpoints: List[Endpoint] = Field(..., description="Target device(s) capabilities.") class PlaybackSnapshot(BasePlaybackModel): """Snapshot of the playback state to be moved in Reinitiate.""" media_reference: MediaReference = Field( ..., alias="mediaReference", description="Reference to the item playing on the origin device.", ) offset_in_milliseconds: int = Field( ..., alias="offsetInMilliseconds", description="Playback position when paused on origin device.", ) endpoint: Endpoint = Field(..., description="Capabilities of the *origin* device.") # Note: Docs imply playbackModes and filters might also be in snapshot, add if observed # --- Top Level Request Structure --- class PlaybackHeader(BasePlaybackModel): namespace: Literal["Alexa.Media.Playback"] name: Literal["Initiate", "Reinitiate"] message_id: str = Field(..., alias="messageId") payload_version: Literal["1.0"] = Field(..., alias="payloadVersion") class InitiateRequest(BasePlaybackModel): header: PlaybackHeader payload: InitiateRequestPayload class ReinitiateRequest(BasePlaybackModel): header: PlaybackHeader payload: ReinitiateRequestPayload # --- Response Models --- class InitiateResponsePayload(BasePlaybackModel): playback_method: PlaybackMethod = Field(..., alias="playbackMethod") class ReinitiateResponsePayload(BasePlaybackModel): action_type: Literal["CONFIRM_QUEUE", "REPLACE_STREAM"] = Field( ..., alias="actionType", description="Indicates if modification is needed." ) # Conditionally required based on actionType - Pydantic doesn't enforce this easily, check in code stream: Optional[Stream] = Field( default=None, description="Required if actionType is REPLACE_STREAM." ) playback_method: Optional[PlaybackMethod] = Field( default=None, alias="playbackMethod", description="Required if actionType requires full queue replacement (less common). If CONFIRM_QUEUE, often can return just actionType.", ) # Documentation shows playbackMethod being returned even for CONFIRM_QUEUE sometimes, include as optional. class PlaybackResponseHeader(BasePlaybackModel): namespace: Literal["Alexa.Media.Playback"] name: Literal["Initiate.Response", "Reinitiate.Response"] message_id: str = Field( ..., alias="messageId" ) # Should correlate with request messageId payload_version: Literal["1.0"] = Field(..., alias="payloadVersion") class InitiateResponse(BasePlaybackModel): header: PlaybackResponseHeader payload: InitiateResponsePayload class ReinitiateResponse(BasePlaybackModel): header: PlaybackResponseHeader payload: ReinitiateResponsePayload # --- Error Response Models (Simplified) --- class ErrorResponsePayload(BasePlaybackModel): type: str # e.g., "INTERNAL_ERROR", "INVALID_REQUEST", "USER_CONTEXT_MISMATCH" message: str class ErrorResponseHeader(BasePlaybackModel): namespace: Literal["Alexa.Media", "Alexa"] # Namespace varies slightly for errors name: Literal["ErrorResponse"] message_id: str = Field(..., alias="messageId") payload_version: Literal["1.0", "3"] # Version can vary class AlexaErrorResponse(BasePlaybackModel): header: ErrorResponseHeader payload: ErrorResponsePayload