├── .github └── PULL_REQUEST_TEMPLATE.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MultiStream ├── .ask │ └── config ├── README.md ├── lambda │ └── py │ │ ├── alexa │ │ ├── __init__.py │ │ ├── data.py │ │ └── util.py │ │ ├── lambda_function.py │ │ └── requirements.txt ├── models │ ├── en-GB.json │ └── en-US.json └── skill.json ├── NOTICE ├── README.md └── SingleStream ├── .ask └── config ├── README.md ├── lambda └── py │ ├── alexa │ ├── __init__.py │ ├── data.py │ └── util.py │ ├── lambda_function.py │ ├── locales │ ├── data.pot │ ├── es-ES │ │ └── LC_MESSAGES │ │ │ ├── data.mo │ │ │ └── data.po │ ├── fr-FR │ │ └── LC_MESSAGES │ │ │ ├── data.mo │ │ │ └── data.po │ └── it-IT │ │ └── LC_MESSAGES │ │ ├── data.mo │ │ └── data.po │ └── requirements.txt ├── models ├── en-AU.json ├── en-CA.json ├── en-GB.json ├── en-IN.json ├── en-US.json ├── es-ES.json ├── es-MX.json ├── fr-FR.json └── it-IT.json └── skill.json /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/alexa/skill-sample-python-audio-player/issues), or [recently closed](https://github.com/alexa/skill-sample-python-audio-player/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/alexa/skill-sample-python-audio-player/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/alexa/skill-sample-python-audio-player/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Amazon Software License 1.0 2 | 3 | This Amazon Software License ("License") governs your use, reproduction, and 4 | distribution of the accompanying software as specified below. 5 | 6 | 1. Definitions 7 | 8 | "Licensor" means any person or entity that distributes its Work. 9 | 10 | "Software" means the original work of authorship made available under this 11 | License. 12 | 13 | "Work" means the Software and any additions to or derivative works of the 14 | Software that are made available under this License. 15 | 16 | The terms "reproduce," "reproduction," "derivative works," and 17 | "distribution" have the meaning as provided under U.S. copyright law; 18 | provided, however, that for the purposes of this License, derivative works 19 | shall not include works that remain separable from, or merely link (or bind 20 | by name) to the interfaces of, the Work. 21 | 22 | Works, including the Software, are "made available" under this License by 23 | including in or with the Work either (a) a copyright notice referencing the 24 | applicability of this License to the Work, or (b) a copy of this License. 25 | 26 | 2. License Grants 27 | 28 | 2.1 Copyright Grant. Subject to the terms and conditions of this License, 29 | each Licensor grants to you a perpetual, worldwide, non-exclusive, 30 | royalty-free, copyright license to reproduce, prepare derivative works of, 31 | publicly display, publicly perform, sublicense and distribute its Work and 32 | any resulting derivative works in any form. 33 | 34 | 2.2 Patent Grant. Subject to the terms and conditions of this License, each 35 | Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free 36 | patent license to make, have made, use, sell, offer for sale, import, and 37 | otherwise transfer its Work, in whole or in part. The foregoing license 38 | applies only to the patent claims licensable by Licensor that would be 39 | infringed by Licensor's Work (or portion thereof) individually and 40 | excluding any combinations with any other materials or technology. 41 | 42 | 3. Limitations 43 | 44 | 3.1 Redistribution. You may reproduce or distribute the Work only if 45 | (a) you do so under this License, (b) you include a complete copy of this 46 | License with your distribution, and (c) you retain without modification 47 | any copyright, patent, trademark, or attribution notices that are present 48 | in the Work. 49 | 50 | 3.2 Derivative Works. You may specify that additional or different terms 51 | apply to the use, reproduction, and distribution of your derivative works 52 | of the Work ("Your Terms") only if (a) Your Terms provide that the use 53 | limitation in Section 3.3 applies to your derivative works, and (b) you 54 | identify the specific derivative works that are subject to Your Terms. 55 | Notwithstanding Your Terms, this License (including the redistribution 56 | requirements in Section 3.1) will continue to apply to the Work itself. 57 | 58 | 3.3 Use Limitation. The Work and any derivative works thereof only may be 59 | used or intended for use with the web services, computing platforms or 60 | applications provided by Amazon.com, Inc. or its affiliates, including 61 | Amazon Web Services, Inc. 62 | 63 | 3.4 Patent Claims. If you bring or threaten to bring a patent claim against 64 | any Licensor (including any claim, cross-claim or counterclaim in a 65 | lawsuit) to enforce any patents that you allege are infringed by any Work, 66 | then your rights under this License from such Licensor (including the 67 | grants in Sections 2.1 and 2.2) will terminate immediately. 68 | 69 | 3.5 Trademarks. This License does not grant any rights to use any 70 | Licensor's or its affiliates' names, logos, or trademarks, except as 71 | necessary to reproduce the notices described in this License. 72 | 73 | 3.6 Termination. If you violate any term of this License, then your rights 74 | under this License (including the grants in Sections 2.1 and 2.2) will 75 | terminate immediately. 76 | 77 | 4. Disclaimer of Warranty. 78 | 79 | THE WORK IS PROVIDED "AS IS" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 80 | EITHER EXPRESS OR IMPLIED, INCLUDING WARRANTIES OR CONDITIONS OF 81 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR 82 | NON-INFRINGEMENT. YOU BEAR THE RISK OF UNDERTAKING ANY ACTIVITIES UNDER 83 | THIS LICENSE. SOME STATES' CONSUMER LAWS DO NOT ALLOW EXCLUSION OF AN 84 | IMPLIED WARRANTY, SO THIS DISCLAIMER MAY NOT APPLY TO YOU. 85 | 86 | 5. Limitation of Liability. 87 | 88 | EXCEPT AS PROHIBITED BY APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL 89 | THEORY, WHETHER IN TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE 90 | SHALL ANY LICENSOR BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, 91 | INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR 92 | RELATED TO THIS LICENSE, THE USE OR INABILITY TO USE THE WORK (INCLUDING 93 | BUT NOT LIMITED TO LOSS OF GOODWILL, BUSINESS INTERRUPTION, LOST PROFITS 94 | OR DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY OTHER COMM ERCIAL DAMAGES 95 | OR LOSSES), EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF 96 | SUCH DAMAGES. 97 | -------------------------------------------------------------------------------- /MultiStream/.ask/config: -------------------------------------------------------------------------------- 1 | { 2 | "deploy_settings": { 3 | "default": { 4 | "skill_id": "amzn1.ask.skill.e5c21f3d-32a7-49b1-b341-d09b8f650fbe", 5 | "was_cloned": false, 6 | "resources": { 7 | "manifest": { 8 | "eTag": "1a9a9cec03b0583af5828d1db0d11ff9" 9 | }, 10 | "interactionModel": { 11 | "en-US": { 12 | "eTag": "8a62193c7f6432661809bc883985beb2" 13 | }, 14 | "en-GB": { 15 | "eTag": "28bc1baebe415480fb571e1ac3d1c1c1" 16 | } 17 | } 18 | }, 19 | "merge": { 20 | "manifest": { 21 | "apis": { 22 | "custom": { 23 | "endpoint": { 24 | "uri": "ask-custom-multistream-audioplayer-default" 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MultiStream/README.md: -------------------------------------------------------------------------------- 1 | # Multi Stream Audio Player Sample Skill (using ASK Python SDK) 2 | 3 | The Alexa Skills Kit now allows developers to build skills that play long-form audio content on Alexa devices. This sample project demonstrates how to use the new interfaces for triggering playback of audio and handling audio player input events. 4 | 5 | ## How to Run the Sample 6 | 7 | You will need to comply to the prerequisites below and to change a few configuration files before creating the skill and upload the lambda code. 8 | 9 | ### Pre-requisites 10 | 11 | 0. This sample uses the [ASK Python SDK](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/) packages for developing the Alexa skill. 12 | 13 | - If you already have the ASK Python SDK installed, then install the dependencies in the ``lambda/py/requirements.txt`` 14 | using ``pip``. 15 | - If you are starting fresh, follow the [Setting up the ASK SDK](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/GETTING_STARTED.html) 16 | documentation, to get the ASK Python SDK installed on your machine. We recommend you to use the 17 | [virtualenv approach](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/GETTING_STARTED.html#option-1-set-up-the-sdk-in-a-virtual-environment). 18 | Please run the following command in your virtualenv, to install the dependencies, before working on the skill code. 19 | 20 | `` 21 | pip install -r lambda/py/requirements.txt 22 | `` 23 | 24 | 1. You need an [AWS account](https://aws.amazon.com) and an [Amazon developer account](https://developer.amazon.com) to create an Alexa Skill. 25 | 26 | 27 | ### Code changes before deploying 28 | 29 | 1. ```./skill.json``` 30 | 31 | Change the skill name, example phrase, icons, testing instructions etc ... 32 | 33 | Remember than most information is locale-specific and must be changed for each locale (en-GB and en-US) 34 | 35 | Please refer to https://developer.amazon.com/docs/smapi/skill-manifest.html for details about manifest values. 36 | 37 | 2. ```./lambda/py/alexa/data.py``` 38 | 39 | Modify each value in the ```data.py``` file to provide your skill with the correct runtime values for ``AUDIO_DATA``, different responses by Alexa, DynamoDB table name. 40 | 41 | To learn more about Alexa App cards, see https://developer.amazon.com/docs/custom-skills/include-a-card-in-your-skills-response.html 42 | 43 | 3. ```./models/*.json``` 44 | 45 | Change the model definition to replace the invocation name (it defaults to "audio player") and the sample phrases for each intent. 46 | 47 | Repeat the operation for each locale you are planning to support. 48 | 49 | 4. DynamoDB table 50 | 51 | The dynamodb table is used to store the playback settings information of the user. If the table does not exist, the persistence code will create the table at the first invocation of the skill. 52 | 53 | You can manually create the DynamoDB table with the following command: 54 | 55 | ```bash 56 | aws dynamodb create-table --table-name Audio-Player-Multi-Stream --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 57 | ``` 58 | 59 | To minimize latency, we recommend to create the DynamoDB table in the same region as the Lambda function. 60 | 61 | When using DynamoDB, you also must ensure your Lambda function [execution role](http://docs.aws.amazon.com/lambda/latest/dg/intro-permission-model.html) will have permissions to read and write to the DynamoDB table. Be sure [to add the following policy](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_manage_modify.html) to the Lambda function [execution role](http://docs.aws.amazon.com/lambda/latest/dg/intro-permission-model.html): 62 | 63 | ```json 64 | { 65 | "Version": "2012-10-17", 66 | "Statement": [ 67 | { 68 | "Sid": "sid123", 69 | "Effect": "Allow", 70 | "Action": [ 71 | "dynamodb:PutItem", 72 | "dynamodb:GetItem", 73 | "dynamodb:UpdateItem" 74 | ], 75 | "Resource": "arn:aws:dynamodb:us-east-1:YOUR_ACCOUNT_ID:table/Audio-Player-Multi-Stream" 76 | } 77 | ] 78 | } 79 | ``` 80 | 81 | ### Deployment 82 | 83 | For AWS Lambda to correctly execute the skill code, we need to zip the skill code along 84 | with all dependencies and upload it. Follow the steps mentioned [here](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/DEVELOPING_YOUR_FIRST_SKILL.html#preparing-your-code-for-aws-lambda) 85 | to get your skill code ready for uploading to AWS Lambda console. 86 | 87 | ## On Device Tests 88 | 89 | To invoke the skill from your device, you need to login to the Alexa Developer Console, and enable the "Test" switch on your skill. 90 | 91 | See https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html#step-4-test-your-skill for more testing instructions. 92 | 93 | Then, just say : 94 | 95 | ```text 96 | Alexa, open audio player. 97 | ``` 98 | 99 | ## How it Works 100 | 101 | Alexa Skills Kit now includes a set of output directives and input events that allow you to control the playback of audio files or streams. There are a few important concepts to get familiar with: 102 | 103 | * **AudioPlayer directives** are used by your skill to start and stop audio playback from content hosted at a publicly accessible secure URL. You send AudioPlayer directives in response to the intents you've configured for your skill, or new events you'll receive when a user controls their device with a dedicated controller (see PlaybackController events below). 104 | * **PlaybackController events** are sent to your skill when a user selects play/next/prev/pause on dedicated hardware controls on the Alexa device, such as on the Amazon Tap or the Voice Remote for Amazon Echo and Echo Dot. Your skill receives these events if your skill is currently controlling audio on the device (i.e., you were the last to send an AudioPlayer directive). 105 | * **AudioPlayer events** are sent to your skill at key changes in the status of audio playback, such as when audio has begun playing, been stopped or has finished. You can use them to track what's currently playing or queue up more content. Unlike intents, when you receive an AudioPlayer event, you may only respond with appropriate AudioPlayer directives to control playback. 106 | 107 | You can learn more about the new [AudioPlayer interface](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/custom-audioplayer-interface-reference) and [PlaybackController interface](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/custom-playbackcontroller-interface-reference). 108 | 109 | ## Cleanup 110 | 111 | If you were deploying this skill just for learning purposes or for testing, do not forget to clean your AWS account to avoid recurring charges for your DynamoDB table. 112 | 113 | - delete the lambda function 114 | - delete the IAM execution role 115 | - delete the DynamoDB table (Audio-Player-Multi-Stream) 116 | -------------------------------------------------------------------------------- /MultiStream/lambda/py/alexa/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-audio-player/7a6e3c07dbd08e5a9085a5b999acac4e38a59fa5/MultiStream/lambda/py/alexa/__init__.py -------------------------------------------------------------------------------- /MultiStream/lambda/py/alexa/data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | WELCOME_MSG = "Welcome to the Alexa Dev Chat Podcast. You can say, play the audio to begin the podcast." 4 | WELCOME_REPROMPT_MSG = "You can say, play the audio, to begin." 5 | WELCOME_PLAYBACK_MSG = "You were listening to {}. Would you like to resume?" 6 | WELCOME_PLAYBACK_REPROMPT_MSG = "You can say yes to resume or no to play from the top" 7 | DEVICE_NOT_SUPPORTED = "Sorry, this skill is not supported on this device" 8 | LOOP_ON_MSG = "Loop turned on." 9 | LOOP_OFF_MSG = "Loop turned off." 10 | HELP_MSG = WELCOME_MSG 11 | HELP_PLAYBACK_MSG = WELCOME_PLAYBACK_MSG 12 | HELP_DURING_PLAY_MSG = "You are listening to the Alexa Dev Chat Podcast. You can say, Next or Previous to navigate through the playlist. At any time, you can say Pause to pause the audio and Resume to resume." 13 | STOP_MSG = "Goodbye." 14 | EXCEPTION_MSG = "Sorry, this is not a valid command. Please say help, to hear what you can say." 15 | PLAYBACK_PLAY = "This is {}" 16 | PLAYBACK_PLAY_CARD = "Playing {}" 17 | PLAYBACK_NEXT_END = "You have reached the end of the playlist" 18 | PLAYBACK_PREVIOUS_END = "You have reached the start of the playlist" 19 | 20 | DYNAMODB_TABLE_NAME = "Audio-Player-Multi-Stream" 21 | 22 | AUDIO_DATA = [ 23 | { 24 | "title": "Episode 22", 25 | "url": "https://feeds.soundcloud.com/stream/459953355-user-652822799-episode-022-getting-started-with-alexa-for-business.mp3", 26 | }, 27 | { 28 | "title": "Episode 23", 29 | "url": "https://feeds.soundcloud.com/stream/476469807-user-652822799-episode-023-voicefirst-in-2018-where-are-we-now.mp3", 30 | }, 31 | { 32 | "title": "Episode 24", 33 | "url": "https://feeds.soundcloud.com/stream/496340574-user-652822799-episode-024-the-voice-generation-will-include-all-generations.mp3", 34 | } 35 | ] -------------------------------------------------------------------------------- /MultiStream/lambda/py/alexa/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import random 4 | from typing import List, Dict 5 | from ask_sdk_model import IntentRequest, Response 6 | from ask_sdk_model.ui import SimpleCard 7 | from ask_sdk_model.interfaces.audioplayer import ( 8 | PlayDirective, PlayBehavior, AudioItem, Stream, StopDirective) 9 | from ask_sdk_core.handler_input import HandlerInput 10 | from . import data 11 | 12 | 13 | def get_playback_info(handler_input): 14 | # type: (HandlerInput) -> Dict 15 | persistence_attr = handler_input.attributes_manager.persistent_attributes 16 | return persistence_attr.get('playback_info') 17 | 18 | 19 | def can_throw_card(handler_input): 20 | # type: (HandlerInput) -> bool 21 | playback_info = get_playback_info(handler_input) 22 | if (isinstance(handler_input.request_envelope.request, IntentRequest) 23 | and playback_info.get('playback_index_changed')): 24 | playback_info['playback_index_changed'] = False 25 | return True 26 | else: 27 | return False 28 | 29 | 30 | def get_token(handler_input): 31 | """Extracting token received in the request.""" 32 | # type: (HandlerInput) -> str 33 | return handler_input.request_envelope.request.token 34 | 35 | 36 | def get_index(handler_input): 37 | """Extracting index from the token received in the request.""" 38 | # type: (HandlerInput) -> int 39 | token = int(get_token(handler_input)) 40 | persistent_attr = handler_input.attributes_manager.persistent_attributes 41 | 42 | return persistent_attr.get("playback_info").get("play_order").index(token) 43 | 44 | 45 | def get_offset_in_ms(handler_input): 46 | """Extracting offset in milliseconds received in the request""" 47 | # type: (HandlerInput) -> int 48 | return handler_input.request_envelope.request.offset_in_milliseconds 49 | 50 | 51 | def shuffle_order(): 52 | # type: () -> List 53 | podcast_indices = [l for l in range(0, len(data.AUDIO_DATA))] 54 | random.shuffle(podcast_indices) 55 | return podcast_indices 56 | 57 | 58 | class Controller: 59 | """Audioplayer and Playback Controller.""" 60 | @staticmethod 61 | def play(handler_input, is_playback=False): 62 | # type: (HandlerInput) -> Response 63 | playback_info = get_playback_info(handler_input) 64 | response_builder = handler_input.response_builder 65 | 66 | play_order = playback_info.get("play_order") 67 | offset_in_ms = playback_info.get("offset_in_ms") 68 | index = playback_info.get("index") 69 | 70 | play_behavior = PlayBehavior.REPLACE_ALL 71 | podcast = data.AUDIO_DATA[play_order[index]] 72 | token = play_order[index] 73 | playback_info['next_stream_enqueued'] = False 74 | 75 | response_builder.add_directive( 76 | PlayDirective( 77 | play_behavior=play_behavior, 78 | audio_item=AudioItem( 79 | stream=Stream( 80 | token=token, 81 | url=podcast.get("url"), 82 | offset_in_milliseconds=offset_in_ms, 83 | expected_previous_token=None), 84 | metadata=None)) 85 | ).set_should_end_session(True) 86 | 87 | if not is_playback: 88 | # Add card and response only for events not triggered by 89 | # Playback Controller 90 | handler_input.response_builder.speak( 91 | data.PLAYBACK_PLAY.format(podcast.get("title"))) 92 | 93 | if can_throw_card(handler_input): 94 | response_builder.set_card(SimpleCard( 95 | title=data.PLAYBACK_PLAY_CARD.format( 96 | podcast.get("title")), 97 | content=data.PLAYBACK_PLAY_CARD.format( 98 | podcast.get("title")))) 99 | 100 | return response_builder.response 101 | 102 | @staticmethod 103 | def stop(handler_input): 104 | # type: (HandlerInput) -> Response 105 | handler_input.response_builder.add_directive(StopDirective()) 106 | return handler_input.response_builder.response 107 | 108 | @staticmethod 109 | def play_next(handler_input, is_playback=False): 110 | # type: (HandlerInput) -> Response 111 | persistent_attr = handler_input.attributes_manager.persistent_attributes 112 | 113 | playback_info = persistent_attr.get("playback_info") 114 | playback_setting = persistent_attr.get("playback_setting") 115 | next_index = (playback_info.get("index") + 1) % len(data.AUDIO_DATA) 116 | 117 | if next_index == 0 and not playback_setting.get("loop"): 118 | if not is_playback: 119 | handler_input.response_builder.speak(data.PLAYBACK_NEXT_END) 120 | 121 | return handler_input.response_builder.add_directive( 122 | StopDirective()).response 123 | 124 | playback_info["index"] = next_index 125 | playback_info["offset_in_ms"] = 0 126 | playback_info["playback_index_changed"] = True 127 | 128 | return Controller.play(handler_input, is_playback) 129 | 130 | @staticmethod 131 | def play_previous(handler_input, is_playback=False): 132 | # type: (HandlerInput) -> Response 133 | persistent_attr = handler_input.attributes_manager.persistent_attributes 134 | 135 | playback_info = persistent_attr.get("playback_info") 136 | playback_setting = persistent_attr.get("playback_setting") 137 | prev_index = playback_info.get("index") - 1 138 | 139 | if prev_index == -1: 140 | if playback_setting.get("loop"): 141 | prev_index += len(data.AUDIO_DATA) 142 | else: 143 | if not is_playback: 144 | handler_input.response_builder.speak( 145 | data.PLAYBACK_PREVIOUS_END) 146 | 147 | return handler_input.response_builder.add_directive( 148 | StopDirective()).response 149 | 150 | playback_info["index"] = prev_index 151 | playback_info["offset_in_ms"] = 0 152 | playback_info["playback_index_changed"] = True 153 | 154 | return Controller.play(handler_input, is_playback) 155 | -------------------------------------------------------------------------------- /MultiStream/lambda/py/lambda_function.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | from ask_sdk.standard import StandardSkillBuilder 5 | from ask_sdk_core.dispatch_components import ( 6 | AbstractRequestHandler, AbstractExceptionHandler, 7 | AbstractRequestInterceptor, AbstractResponseInterceptor) 8 | from ask_sdk_core.utils import is_request_type, is_intent_name 9 | from ask_sdk_core.handler_input import HandlerInput 10 | from ask_sdk_model import Response 11 | from ask_sdk_model.interfaces.audioplayer import ( 12 | PlayDirective, PlayBehavior, AudioItem, Stream) 13 | 14 | from alexa import data, util 15 | 16 | logger = logging.getLogger(__name__) 17 | logger.setLevel(logging.DEBUG) 18 | 19 | # ######################### INTENT HANDLERS ######################### 20 | # This section contains handlers for the built-in intents and generic 21 | # request handlers like launch, session end, skill events etc. 22 | 23 | 24 | class CheckAudioInterfaceHandler(AbstractRequestHandler): 25 | """Check if device supports audio play. 26 | 27 | This can be used as the first handler to be checked, before invoking 28 | other handlers, thus making the skill respond to unsupported devices 29 | without doing much processing. 30 | """ 31 | def can_handle(self, handler_input): 32 | # type: (HandlerInput) -> bool 33 | if handler_input.request_envelope.context.system.device: 34 | # Since skill events won't have device information 35 | return handler_input.request_envelope.context.system.device.supported_interfaces.audio_player is None 36 | else: 37 | return False 38 | 39 | def handle(self, handler_input): 40 | # type: (HandlerInput) -> Response 41 | logger.info("In CheckAudioInterfaceHandler") 42 | handler_input.response_builder.speak( 43 | data.DEVICE_NOT_SUPPORTED).set_should_end_session(True) 44 | return handler_input.response_builder.response 45 | 46 | 47 | class LaunchRequestHandler(AbstractRequestHandler): 48 | """Launch radio for skill launch or PlayAudio intent.""" 49 | def can_handle(self, handler_input): 50 | # type: (HandlerInput) -> bool 51 | return is_request_type("LaunchRequest")(handler_input) 52 | 53 | def handle(self, handler_input): 54 | # type: (HandlerInput) -> Response 55 | logger.info("In LaunchRequestHandler") 56 | 57 | playback_info = util.get_playback_info(handler_input) 58 | 59 | if not playback_info.get('has_previous_playback_session'): 60 | message = data.WELCOME_MSG 61 | reprompt = data.WELCOME_REPROMPT_MSG 62 | else: 63 | playback_info['in_playback_session'] = False 64 | message = data.WELCOME_PLAYBACK_MSG.format( 65 | data.AUDIO_DATA[ 66 | playback_info.get("play_order")[ 67 | playback_info.get("index")]].get("title")) 68 | reprompt = data.WELCOME_PLAYBACK_REPROMPT_MSG 69 | 70 | return handler_input.response_builder.speak(message).ask( 71 | reprompt).response 72 | 73 | 74 | class StartPlaybackHandler(AbstractRequestHandler): 75 | """Handler for Playing audio on different events. 76 | 77 | Handles PlayAudio Intent, Resume Intent. 78 | """ 79 | def can_handle(self, handler_input): 80 | # type: (HandlerInput) -> bool 81 | return (is_intent_name("AMAZON.ResumeIntent")(handler_input) 82 | or is_intent_name("PlayAudio")(handler_input)) 83 | 84 | def handle(self, handler_input): 85 | # type: (HandlerInput) -> Response 86 | logger.info("In StartPlaybackHandler") 87 | return util.Controller.play(handler_input) 88 | 89 | 90 | class NextPlaybackHandler(AbstractRequestHandler): 91 | """Handler for Playing next audio on different events. 92 | 93 | Handles Next Intent and NextCommandIssued event. 94 | """ 95 | def can_handle(self, handler_input): 96 | # type: (HandlerInput) -> bool 97 | playback_info = util.get_playback_info(handler_input) 98 | 99 | return (playback_info.get("in_playback_session") 100 | and is_intent_name("AMAZON.NextIntent")(handler_input)) 101 | 102 | def handle(self, handler_input): 103 | # type: (HandlerInput) -> Response 104 | logger.info("In NextPlaybackHandler") 105 | return util.Controller.play_next(handler_input) 106 | 107 | 108 | class PreviousPlaybackHandler(AbstractRequestHandler): 109 | """Handler for Playing previous audio on different events. 110 | 111 | Handles Previous Intent and PreviousCommandIssued event. 112 | """ 113 | def can_handle(self, handler_input): 114 | # type: (HandlerInput) -> bool 115 | playback_info = util.get_playback_info(handler_input) 116 | 117 | return (playback_info.get("in_playback_session") 118 | and is_intent_name("AMAZON.PreviousIntent")(handler_input)) 119 | 120 | def handle(self, handler_input): 121 | # type: (HandlerInput) -> Response 122 | logger.info("In PreviousPlaybackHandler") 123 | return util.Controller.play_previous(handler_input) 124 | 125 | 126 | class PausePlaybackHandler(AbstractRequestHandler): 127 | """Handler for stopping audio. 128 | 129 | Handles Stop, Cancel and Pause Intents and PauseCommandIssued event. 130 | """ 131 | def can_handle(self, handler_input): 132 | # type: (HandlerInput) -> bool 133 | playback_info = util.get_playback_info(handler_input) 134 | 135 | return (playback_info.get("in_playback_session") 136 | and (is_intent_name("AMAZON.StopIntent")(handler_input) 137 | or is_intent_name("AMAZON.CancelIntent")(handler_input) 138 | or is_intent_name("AMAZON.PauseIntent")(handler_input))) 139 | 140 | def handle(self, handler_input): 141 | # type: (HandlerInput) -> Response 142 | logger.info("PausePlaybackHandler") 143 | return util.Controller.stop(handler_input) 144 | 145 | 146 | class LoopOnHandler(AbstractRequestHandler): 147 | """Handler for setting the audio loop on.""" 148 | def can_handle(self, handler_input): 149 | # type: (HandlerInput) -> bool 150 | playback_info = util.get_playback_info(handler_input) 151 | 152 | return (playback_info.get("in_playback_session") 153 | and is_intent_name("AMAZON.LoopOnIntent")(handler_input)) 154 | 155 | def handle(self, handler_input): 156 | # type: (HandlerInput) -> Response 157 | logger.info("In LoopOnHandler") 158 | persistent_attr = handler_input.attributes_manager.persistent_attributes 159 | playback_setting = persistent_attr.get("playback_setting") 160 | playback_setting["loop"] = True 161 | 162 | return handler_input.response_builder.speak(data.LOOP_ON_MSG).response 163 | 164 | 165 | class LoopOffHandler(AbstractRequestHandler): 166 | """Handler for setting the audio loop off.""" 167 | def can_handle(self, handler_input): 168 | # type: (HandlerInput) -> bool 169 | playback_info = util.get_playback_info(handler_input) 170 | 171 | return (playback_info.get("in_playback_session") 172 | and is_intent_name("AMAZON.LoopOffIntent")(handler_input)) 173 | 174 | def handle(self, handler_input): 175 | # type: (HandlerInput) -> Response 176 | logger.info("In LoopOffHandler") 177 | persistent_attr = handler_input.attributes_manager.persistent_attributes 178 | playback_setting = persistent_attr.get("playback_setting") 179 | playback_setting["loop"] = False 180 | 181 | return handler_input.response_builder.speak( 182 | data.LOOP_OFF_MSG).response 183 | 184 | 185 | class ShuffleOnHandler(AbstractRequestHandler): 186 | """Handler for setting the audio shuffle on.""" 187 | def can_handle(self, handler_input): 188 | # type: (HandlerInput) -> bool 189 | playback_info = util.get_playback_info(handler_input) 190 | 191 | return (playback_info.get("in_playback_session") 192 | and is_intent_name("AMAZON.ShuffleOnIntent")( 193 | handler_input)) 194 | 195 | def handle(self, handler_input): 196 | # type: (HandlerInput) -> Response 197 | logger.info("In ShuffleOnHandler") 198 | persistent_attr = handler_input.attributes_manager.persistent_attributes 199 | playback_setting = persistent_attr.get("playback_setting") 200 | playback_info = persistent_attr.get("playback_info") 201 | 202 | playback_setting["shuffle"] = True 203 | playback_info["play_order"] = util.shuffle_order() 204 | playback_info["index"] = 0 205 | playback_info["offset_in_ms"] = 0 206 | playback_info["playback_index_changed"] = True 207 | return util.Controller.play(handler_input) 208 | 209 | 210 | class ShuffleOffHandler(AbstractRequestHandler): 211 | """Handler for setting the audio shuffle off.""" 212 | def can_handle(self, handler_input): 213 | # type: (HandlerInput) -> bool 214 | playback_info = util.get_playback_info(handler_input) 215 | 216 | return (playback_info.get("in_playback_session") 217 | and is_intent_name("AMAZON.ShuffleOffIntent")( 218 | handler_input)) 219 | 220 | def handle(self, handler_input): 221 | # type: (HandlerInput) -> Response 222 | logger.info("In ShuffleOffHandler") 223 | persistent_attr = handler_input.attributes_manager.persistent_attributes 224 | playback_setting = persistent_attr.get("playback_setting") 225 | playback_info = persistent_attr.get("playback_info") 226 | 227 | playback_setting["shuffle"] = False 228 | playback_info["index"] = playback_info["play_order"][ 229 | playback_info["index"]] 230 | playback_info["play_order"] = [l for l in range( 231 | 0, len(data.AUDIO_DATA))] 232 | return util.Controller.play(handler_input) 233 | 234 | 235 | class StartOverHandler(AbstractRequestHandler): 236 | """Handler for start over.""" 237 | def can_handle(self, handler_input): 238 | # type: (HandlerInput) -> bool 239 | playback_info = util.get_playback_info(handler_input) 240 | 241 | return (playback_info.get("in_playback_session") 242 | and is_intent_name("AMAZON.StartOverIntent")( 243 | handler_input)) 244 | 245 | def handle(self, handler_input): 246 | # type: (HandlerInput) -> Response 247 | logger.info("In StartOverHandler") 248 | playback_info = util.get_playback_info(handler_input) 249 | playback_info["offset_in_ms"] = 0 250 | 251 | return util.Controller.play(handler_input) 252 | 253 | 254 | class YesHandler(AbstractRequestHandler): 255 | """Handler for Yes intent when audio is not playing.""" 256 | def can_handle(self, handler_input): 257 | # type: (HandlerInput) -> bool 258 | playback_info = util.get_playback_info(handler_input) 259 | 260 | return (not playback_info.get("in_playback_session") 261 | and is_intent_name("AMAZON.YesIntent")( 262 | handler_input)) 263 | 264 | def handle(self, handler_input): 265 | # type: (HandlerInput) -> Response 266 | logger.info("In YesHandler") 267 | return util.Controller.play(handler_input) 268 | 269 | 270 | class NoHandler(AbstractRequestHandler): 271 | """Handler for No intent when audio is not playing.""" 272 | def can_handle(self, handler_input): 273 | # type: (HandlerInput) -> bool 274 | playback_info = util.get_playback_info(handler_input) 275 | 276 | return (not playback_info.get("in_playback_session") 277 | and is_intent_name("AMAZON.NoIntent")( 278 | handler_input)) 279 | 280 | def handle(self, handler_input): 281 | # type: (HandlerInput) -> Response 282 | logger.info("In NoHandler") 283 | playback_info = util.get_playback_info(handler_input) 284 | 285 | playback_info["index"] = 0 286 | playback_info["offset_in_ms"] = 0 287 | playback_info["playback_index_changed"] = True 288 | playback_info["has_previous_playback_session"] = False 289 | 290 | return util.Controller.play(handler_input) 291 | 292 | 293 | class CancelOrStopIntentHandler(AbstractRequestHandler): 294 | """Handler for cancel, stop intents when not playing an audio.""" 295 | def can_handle(self, handler_input): 296 | # type: (HandlerInput) -> bool 297 | playback_info = util.get_playback_info(handler_input) 298 | 299 | return (not playback_info.get("in_playback_session") 300 | and (is_intent_name("AMAZON.CancelIntent")(handler_input) 301 | or is_intent_name("AMAZON.StopIntent")(handler_input))) 302 | 303 | def handle(self, handler_input): 304 | # type: (HandlerInput) -> Response 305 | logger.info("In CancelOrStopIntentHandler") 306 | return handler_input.response_builder.speak(data.STOP_MSG).response 307 | 308 | 309 | class SessionEndedRequestHandler(AbstractRequestHandler): 310 | """Handler for session end.""" 311 | def can_handle(self, handler_input): 312 | # type: (HandlerInput) -> bool 313 | return is_request_type("SessionEndedRequest")(handler_input) 314 | 315 | def handle(self, handler_input): 316 | # type: (HandlerInput) -> Response 317 | logger.info("In SessionEndedRequestHandler") 318 | logger.info("Session ended with reason: {}".format( 319 | handler_input.request_envelope.request.reason)) 320 | return handler_input.response_builder.response 321 | 322 | 323 | class HelpIntentHandler(AbstractRequestHandler): 324 | """Handler for providing help information to user.""" 325 | def can_handle(self, handler_input): 326 | # type: (HandlerInput) -> bool 327 | return is_intent_name("AMAZON.HelpIntent")(handler_input) 328 | 329 | def handle(self, handler_input): 330 | # type: (HandlerInput) -> Response 331 | logger.info("In HelpIntentHandler") 332 | 333 | playback_info = util.get_playback_info(handler_input) 334 | 335 | if not playback_info.get('has_previous_playback_session'): 336 | message = data.HELP_MSG 337 | elif not playback_info.get('in_playback_session'): 338 | message = data.HELP_PLAYBACK_MSG 339 | else: 340 | message = data.HELP_DURING_PLAY_MSG 341 | 342 | return handler_input.response_builder.speak(message).ask( 343 | message).response 344 | 345 | 346 | class FallbackIntentHandler(AbstractRequestHandler): 347 | """Handler for fallback intent, for unmatched utterances. 348 | 349 | 2018-July-12: AMAZON.FallbackIntent is currently available in all 350 | English locales. This handler will not be triggered except in that 351 | locale, so it can be safely deployed for any locale. More info 352 | on the fallback intent can be found here: 353 | https://developer.amazon.com/docs/custom-skills/standard-built-in-intents.html#fallback 354 | """ 355 | def can_handle(self, handler_input): 356 | # type: (HandlerInput) -> bool 357 | return is_intent_name("AMAZON.FallbackIntent")(handler_input) 358 | 359 | def handle(self, handler_input): 360 | # type: (HandlerInput) -> Response 361 | logger.info("In FallbackIntentHandler") 362 | 363 | handler_input.response_builder.speak( 364 | data.EXCEPTION_MSG).ask(data.EXCEPTION_MSG) 365 | return handler_input.response_builder.response 366 | 367 | 368 | # ########## AUDIOPLAYER INTERFACE HANDLERS ######################### 369 | # This section contains handlers related to Audioplayer interface 370 | 371 | class PlaybackStartedEventHandler(AbstractRequestHandler): 372 | """AudioPlayer.PlaybackStarted Directive received. 373 | 374 | Confirming that the requested audio file began playing. 375 | Do not send any specific response. 376 | """ 377 | def can_handle(self, handler_input): 378 | # type: (HandlerInput) -> bool 379 | return is_request_type("AudioPlayer.PlaybackStarted")(handler_input) 380 | 381 | def handle(self, handler_input): 382 | # type: (HandlerInput) -> Response 383 | logger.info("In PlaybackStartedHandler") 384 | 385 | playback_info = util.get_playback_info(handler_input) 386 | 387 | playback_info["token"] = util.get_token(handler_input) 388 | playback_info["index"] = util.get_index(handler_input) 389 | playback_info["in_playback_session"] = True 390 | playback_info["has_previous_playback_session"] = True 391 | 392 | return handler_input.response_builder.response 393 | 394 | class PlaybackFinishedEventHandler(AbstractRequestHandler): 395 | """AudioPlayer.PlaybackFinished Directive received. 396 | 397 | Confirming that the requested audio file completed playing. 398 | Do not send any specific response. 399 | """ 400 | def can_handle(self, handler_input): 401 | # type: (HandlerInput) -> bool 402 | return is_request_type("AudioPlayer.PlaybackFinished")(handler_input) 403 | 404 | def handle(self, handler_input): 405 | # type: (HandlerInput) -> Response 406 | logger.info("In PlaybackFinishedHandler") 407 | 408 | playback_info = util.get_playback_info(handler_input) 409 | 410 | playback_info["in_playback_session"] = False 411 | playback_info["has_previous_playback_session"] = False 412 | playback_info["next_stream_enqueued"] = False 413 | 414 | return handler_input.response_builder.response 415 | 416 | 417 | class PlaybackStoppedEventHandler(AbstractRequestHandler): 418 | """AudioPlayer.PlaybackStopped Directive received. 419 | 420 | Confirming that the requested audio file stopped playing. 421 | Do not send any specific response. 422 | """ 423 | def can_handle(self, handler_input): 424 | # type: (HandlerInput) -> bool 425 | return is_request_type("AudioPlayer.PlaybackStopped")(handler_input) 426 | 427 | def handle(self, handler_input): 428 | # type: (HandlerInput) -> Response 429 | logger.info("In PlaybackStoppedHandler") 430 | 431 | playback_info = util.get_playback_info(handler_input) 432 | 433 | playback_info["token"] = util.get_token(handler_input) 434 | playback_info["index"] = util.get_index(handler_input) 435 | playback_info["offset_in_ms"] = util.get_offset_in_ms( 436 | handler_input) 437 | 438 | return handler_input.response_builder.response 439 | 440 | 441 | class PlaybackNearlyFinishedEventHandler(AbstractRequestHandler): 442 | """AudioPlayer.PlaybackNearlyFinished Directive received. 443 | 444 | Replacing queue with the URL again. This should not happen on live streams. 445 | """ 446 | def can_handle(self, handler_input): 447 | # type: (HandlerInput) -> bool 448 | return is_request_type("AudioPlayer.PlaybackNearlyFinished")(handler_input) 449 | 450 | def handle(self, handler_input): 451 | # type: (HandlerInput) -> Response 452 | logger.info("In PlaybackNearlyFinishedHandler") 453 | 454 | persistent_attr = handler_input.attributes_manager.persistent_attributes 455 | playback_info = persistent_attr.get("playback_info") 456 | playback_setting = persistent_attr.get("playback_setting") 457 | 458 | if playback_info.get("next_stream_enqueued"): 459 | return handler_input.response_builder.response 460 | 461 | enqueue_index = (playback_info.get("index") + 1) % len(data.AUDIO_DATA) 462 | if enqueue_index == 0 and not playback_setting.get("loop"): 463 | return handler_input.response_builder.response 464 | 465 | playback_info["next_stream_enqueued"] = True 466 | enqueue_token = playback_info.get("play_order")[enqueue_index] 467 | play_behavior = PlayBehavior.ENQUEUE 468 | podcast = data.AUDIO_DATA[enqueue_token] 469 | expected_previous_token = playback_info.get("token") 470 | offset_in_ms = 0 471 | 472 | handler_input.response_builder.add_directive( 473 | PlayDirective( 474 | play_behavior=play_behavior, 475 | audio_item=AudioItem( 476 | stream=Stream( 477 | token=enqueue_token, 478 | url=podcast.get("url"), 479 | offset_in_milliseconds=offset_in_ms, 480 | expected_previous_token=expected_previous_token), 481 | metadata=None))) 482 | 483 | return handler_input.response_builder.response 484 | 485 | 486 | class PlaybackFailedEventHandler(AbstractRequestHandler): 487 | """AudioPlayer.PlaybackFailed Directive received. 488 | 489 | Logging the error and restarting playing with no output speech. 490 | """ 491 | def can_handle(self, handler_input): 492 | # type: (HandlerInput) -> bool 493 | return is_request_type("AudioPlayer.PlaybackFailed")(handler_input) 494 | 495 | def handle(self, handler_input): 496 | # type: (HandlerInput) -> Response 497 | logger.info("In PlaybackFailedHandler") 498 | 499 | playback_info = util.get_playback_info(handler_input) 500 | playback_info["in_playback_session"] = False 501 | 502 | logger.info("Playback Failed: {}".format( 503 | handler_input.request_envelope.request.error)) 504 | 505 | return handler_input.response_builder.response 506 | 507 | 508 | class ExceptionEncounteredHandler(AbstractRequestHandler): 509 | """Handler to handle exceptions from responses sent by AudioPlayer 510 | request. 511 | """ 512 | def can_handle(self, handler_input): 513 | # type; (HandlerInput) -> bool 514 | return is_request_type("System.ExceptionEncountered")(handler_input) 515 | 516 | def handle(self, handler_input): 517 | # type: (HandlerInput) -> Response 518 | logger.info("In ExceptionEncounteredHandler") 519 | logger.info("System exception encountered: {}".format( 520 | handler_input.request_envelope.request)) 521 | return handler_input.response_builder.response 522 | 523 | # ################################################################### 524 | 525 | # ########## PLAYBACK CONTROLLER INTERFACE HANDLERS ################# 526 | # This section contains handlers related to Playback Controller interface 527 | # https://developer.amazon.com/docs/custom-skills/playback-controller-interface-reference.html#requests 528 | 529 | class PlayCommandHandler(AbstractRequestHandler): 530 | """Handler for Play command from hardware buttons or touch control. 531 | 532 | This handler handles the play command sent through hardware buttons such 533 | as remote control or the play control from Alexa-devices with a screen. 534 | """ 535 | def can_handle(self, handler_input): 536 | # type: (HandlerInput) -> bool 537 | return is_request_type( 538 | "PlaybackController.PlayCommandIssued")(handler_input) 539 | 540 | def handle(self, handler_input): 541 | # type: (HandlerInput) -> Response 542 | logger.info("In PlayCommandHandler") 543 | return util.Controller.play(handler_input, is_playback=True) 544 | 545 | 546 | class NextCommandHandler(AbstractRequestHandler): 547 | """Handler for Next command from hardware buttons or touch 548 | control. 549 | 550 | This handler handles the next command sent through hardware 551 | buttons such as remote control or the next control from 552 | Alexa-devices with a screen. 553 | """ 554 | def can_handle(self, handler_input): 555 | # type: (HandlerInput) -> bool 556 | playback_info = util.get_playback_info(handler_input) 557 | 558 | return (playback_info.get("in_playback_session") 559 | and is_request_type( 560 | "PlaybackController.NextCommandIssued")(handler_input)) 561 | 562 | def handle(self, handler_input): 563 | # type: (HandlerInput) -> Response 564 | logger.info("In NextCommandHandler") 565 | return util.Controller.play_next(handler_input, is_playback=True) 566 | 567 | 568 | class PreviousCommandHandler(AbstractRequestHandler): 569 | """Handler for Previous command from hardware buttons or touch 570 | control. 571 | 572 | This handler handles the previous command sent through hardware 573 | buttons such as remote control or the previous control from 574 | Alexa-devices with a screen. 575 | """ 576 | def can_handle(self, handler_input): 577 | # type: (HandlerInput) -> bool 578 | playback_info = util.get_playback_info(handler_input) 579 | 580 | return (playback_info.get("in_playback_session") 581 | and is_request_type( 582 | "PlaybackController.PreviousCommandIssued")(handler_input)) 583 | 584 | def handle(self, handler_input): 585 | # type: (HandlerInput) -> Response 586 | logger.info("In PreviousCommandHandler") 587 | return util.Controller.play_previous(handler_input, is_playback=True) 588 | 589 | 590 | class PauseCommandHandler(AbstractRequestHandler): 591 | """Handler for Pause command from hardware buttons or touch control. 592 | 593 | This handler handles the pause command sent through hardware 594 | buttons such as remote control or the pause control from 595 | Alexa-devices with a screen. 596 | """ 597 | def can_handle(self, handler_input): 598 | # type: (HandlerInput) -> bool 599 | playback_info = util.get_playback_info(handler_input) 600 | return (playback_info.get("in_playback_session") 601 | and is_request_type( 602 | "PlaybackController.PauseCommandIssued")(handler_input)) 603 | 604 | def handle(self, handler_input): 605 | # type: (HandlerInput) -> Response 606 | logger.info("In PauseCommandHandler") 607 | return util.Controller.stop(handler_input) 608 | 609 | # ################################################################### 610 | 611 | # ################## EXCEPTION HANDLERS ############################# 612 | class CatchAllExceptionHandler(AbstractExceptionHandler): 613 | """Catch all exception handler, log exception and 614 | respond with custom message. 615 | """ 616 | def can_handle(self, handler_input, exception): 617 | # type: (HandlerInput, Exception) -> bool 618 | return True 619 | 620 | def handle(self, handler_input, exception): 621 | # type: (HandlerInput, Exception) -> Response 622 | logger.info("In CatchAllExceptionHandler") 623 | logger.error(exception, exc_info=True) 624 | handler_input.response_builder.speak(data.EXCEPTION_MSG).ask( 625 | data.EXCEPTION_MSG) 626 | 627 | return handler_input.response_builder.response 628 | 629 | # ################################################################### 630 | 631 | # ############# REQUEST / RESPONSE INTERCEPTORS ##################### 632 | class RequestLogger(AbstractRequestInterceptor): 633 | """Log the alexa requests.""" 634 | def process(self, handler_input): 635 | # type: (HandlerInput) -> None 636 | logger.debug("Alexa Request: {}".format( 637 | handler_input.request_envelope.request)) 638 | 639 | 640 | class LoadPersistenceAttributesRequestInterceptor(AbstractRequestInterceptor): 641 | """Check if user is invoking skill for first time and initialize preset.""" 642 | def process(self, handler_input): 643 | # type: (HandlerInput) -> None 644 | persistence_attr = handler_input.attributes_manager.persistent_attributes 645 | 646 | if len(persistence_attr) == 0: 647 | # First time skill user 648 | persistence_attr["playback_setting"] = { 649 | "loop": False, 650 | "shuffle": False 651 | } 652 | 653 | persistence_attr["playback_info"] = { 654 | "play_order": [l for l in range(0, len(data.AUDIO_DATA))], 655 | "index": 0, 656 | "offset_in_ms": 0, 657 | "playback_index_changed": False, 658 | "token": None, 659 | "next_stream_enqueued": False, 660 | "in_playback_session": False, 661 | "has_previous_playback_session": False 662 | } 663 | else: 664 | # Convert decimals to integers, because of AWS SDK DynamoDB issue 665 | # https://github.com/boto/boto3/issues/369 666 | playback_info = persistence_attr.get("playback_info") 667 | playback_info["index"] = int(playback_info.get("index")) 668 | playback_info["offset_in_ms"] = int(playback_info.get( 669 | "offset_in_ms")) 670 | playback_info["play_order"] = [ 671 | int(l) for l in playback_info.get("play_order")] 672 | 673 | 674 | class ResponseLogger(AbstractResponseInterceptor): 675 | """Log the alexa responses.""" 676 | def process(self, handler_input, response): 677 | # type: (HandlerInput, Response) -> None 678 | logger.debug("Alexa Response: {}".format(response)) 679 | 680 | 681 | class SavePersistenceAttributesResponseInterceptor(AbstractResponseInterceptor): 682 | """Save persistence attributes before sending response to user.""" 683 | def process(self, handler_input, response): 684 | # type: (HandlerInput, Response) -> None 685 | handler_input.attributes_manager.save_persistent_attributes() 686 | # ################################################################### 687 | 688 | 689 | sb = StandardSkillBuilder( 690 | table_name=data.DYNAMODB_TABLE_NAME, auto_create_table=True) 691 | 692 | # ############# REGISTER HANDLERS ##################### 693 | # Request Handlers 694 | sb.add_request_handler(CheckAudioInterfaceHandler()) 695 | sb.add_request_handler(LaunchRequestHandler()) 696 | sb.add_request_handler(HelpIntentHandler()) 697 | sb.add_request_handler(ExceptionEncounteredHandler()) 698 | sb.add_request_handler(SessionEndedRequestHandler()) 699 | sb.add_request_handler(YesHandler()) 700 | sb.add_request_handler(NoHandler()) 701 | sb.add_request_handler(StartPlaybackHandler()) 702 | sb.add_request_handler(PlayCommandHandler()) 703 | sb.add_request_handler(NextPlaybackHandler()) 704 | sb.add_request_handler(NextCommandHandler()) 705 | sb.add_request_handler(PreviousPlaybackHandler()) 706 | sb.add_request_handler(PreviousCommandHandler()) 707 | sb.add_request_handler(PausePlaybackHandler()) 708 | sb.add_request_handler(PauseCommandHandler()) 709 | sb.add_request_handler(LoopOnHandler()) 710 | sb.add_request_handler(LoopOffHandler()) 711 | sb.add_request_handler(ShuffleOnHandler()) 712 | sb.add_request_handler(ShuffleOffHandler()) 713 | sb.add_request_handler(StartOverHandler()) 714 | sb.add_request_handler(CancelOrStopIntentHandler()) 715 | sb.add_request_handler(PlaybackStartedEventHandler()) 716 | sb.add_request_handler(PlaybackFinishedEventHandler()) 717 | sb.add_request_handler(PlaybackStoppedEventHandler()) 718 | sb.add_request_handler(PlaybackNearlyFinishedEventHandler()) 719 | sb.add_request_handler(PlaybackFailedEventHandler()) 720 | 721 | # Exception handlers 722 | sb.add_exception_handler(CatchAllExceptionHandler()) 723 | 724 | # Interceptors 725 | sb.add_global_request_interceptor(RequestLogger()) 726 | sb.add_global_request_interceptor(LoadPersistenceAttributesRequestInterceptor()) 727 | 728 | sb.add_global_response_interceptor(ResponseLogger()) 729 | sb.add_global_response_interceptor(SavePersistenceAttributesResponseInterceptor()) 730 | 731 | # AWS Lambda handler 732 | lambda_handler = sb.lambda_handler() 733 | -------------------------------------------------------------------------------- /MultiStream/lambda/py/requirements.txt: -------------------------------------------------------------------------------- 1 | ask-sdk 2 | -------------------------------------------------------------------------------- /MultiStream/models/en-GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "audio player", 5 | "intents": [{ 6 | "name": "AMAZON.CancelIntent", 7 | "slots": [], 8 | "samples": [] 9 | }, 10 | { 11 | "name": "AMAZON.HelpIntent", 12 | "slots": [], 13 | "samples": [] 14 | }, 15 | { 16 | "name": "AMAZON.StopIntent", 17 | "slots": [], 18 | "samples": [] 19 | }, 20 | { 21 | "name": "PlayAudio", 22 | "slots": [], 23 | "samples": [ 24 | "begin podcast", 25 | "begin the podcast", 26 | "begin playing the podcast", 27 | "start podcast", 28 | "start the podcast", 29 | "start playing the podcast", 30 | "play the podcast", 31 | "to play", 32 | "to begin podcast", 33 | "to begin the podcast", 34 | "to begin playing the podcast", 35 | "to start podcast", 36 | "to start the podcast", 37 | "to start playing the podcast", 38 | "to play the podcast" 39 | ] 40 | }, 41 | { 42 | "name": "AMAZON.PauseIntent", 43 | "slots": [], 44 | "samples": [] 45 | }, 46 | { 47 | "name": "AMAZON.ResumeIntent", 48 | "slots": [], 49 | "samples": [] 50 | }, 51 | { 52 | "name": "AMAZON.PreviousIntent", 53 | "slots": [], 54 | "samples": [] 55 | }, 56 | { 57 | "name": "AMAZON.StartOverIntent", 58 | "slots": [], 59 | "samples": [] 60 | }, 61 | { 62 | "name": "AMAZON.YesIntent", 63 | "slots": [], 64 | "samples": [] 65 | }, 66 | { 67 | "name": "AMAZON.NoIntent", 68 | "slots": [], 69 | "samples": [] 70 | }, 71 | { 72 | "name": "AMAZON.NextIntent", 73 | "slots": [], 74 | "samples": [] 75 | } 76 | ], 77 | "types": [] 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /MultiStream/models/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "audio player", 5 | "intents": [ 6 | { 7 | "name": "AMAZON.CancelIntent", 8 | "samples": [] 9 | }, 10 | { 11 | "name": "AMAZON.HelpIntent", 12 | "samples": [] 13 | }, 14 | { 15 | "name": "AMAZON.StopIntent", 16 | "samples": [] 17 | }, 18 | { 19 | "name": "PlayAudio", 20 | "slots": [], 21 | "samples": [ 22 | "begin podcast", 23 | "begin the podcast", 24 | "begin playing the podcast", 25 | "start podcast", 26 | "start the podcast", 27 | "start playing the podcast", 28 | "play the podcast", 29 | "to play", 30 | "to begin podcast", 31 | "to begin the podcast", 32 | "to begin playing the podcast", 33 | "to start podcast", 34 | "to start the podcast", 35 | "to start playing the podcast", 36 | "to play the podcast" 37 | ] 38 | }, 39 | { 40 | "name": "AMAZON.PauseIntent", 41 | "samples": [] 42 | }, 43 | { 44 | "name": "AMAZON.ResumeIntent", 45 | "samples": [] 46 | }, 47 | { 48 | "name": "AMAZON.PreviousIntent", 49 | "samples": [] 50 | }, 51 | { 52 | "name": "AMAZON.StartOverIntent", 53 | "samples": [] 54 | }, 55 | { 56 | "name": "AMAZON.YesIntent", 57 | "samples": [] 58 | }, 59 | { 60 | "name": "AMAZON.NoIntent", 61 | "samples": [] 62 | }, 63 | { 64 | "name": "AMAZON.NextIntent", 65 | "samples": [] 66 | }, 67 | { 68 | "name": "AMAZON.FallbackIntent", 69 | "samples": [] 70 | }, 71 | { 72 | "name": "AMAZON.NavigateHomeIntent", 73 | "samples": [] 74 | } 75 | ], 76 | "types": [] 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /MultiStream/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "en-US": { 6 | "summary": "Very basic podcast sample", 7 | "examplePhrases": [ 8 | "Alexa, open audio player", 9 | "Alexa, ask audio player to begin podcast", 10 | "Alexa, ask audio player to stop" 11 | ], 12 | "keywords": [ 13 | "podcast", 14 | "streaming", 15 | "example" 16 | ], 17 | "name": "Multi Stream Audio Player", 18 | "description": "", 19 | "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", 20 | "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" 21 | }, 22 | "en-GB": { 23 | "summary": "Very basic podcast sample", 24 | "examplePhrases": [ 25 | "Alexa, open audio player", 26 | "Alexa, ask audio player to begin podcast", 27 | "Alexa, ask audio player to stop" 28 | ], 29 | "keywords": [ 30 | "podcast", 31 | "streaming", 32 | "example" 33 | ], 34 | "name": "Multi Stream Audio Player", 35 | "description": "", 36 | "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", 37 | "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" 38 | } 39 | }, 40 | "isAvailableWorldwide": true, 41 | "testingInstructions": "Include your testing instruction (if any) here", 42 | "category": "STREAMING_SERVICE", 43 | "distributionCountries": [] 44 | }, 45 | "apis": { 46 | "custom": { 47 | "endpoint": { 48 | "sourceDir": "lambda/py" 49 | }, 50 | "interfaces": [ 51 | { 52 | "type": "AUDIO_PLAYER" 53 | } 54 | ] 55 | } 56 | }, 57 | "manifestVersion": "1.0", 58 | "permissions": [], 59 | "privacyAndCompliance": { 60 | "allowsPurchases": false, 61 | "isExportCompliant": true, 62 | "containsAds": false, 63 | "isChildDirected": false, 64 | "usesPersonalInfo": false 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Skill Sample - Audio Player (Python) 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Skill Sample Python Audio Player 2 | 3 | This project demonstrates the use of Alexa Audio Player for skills using ASK Python SDK. 4 | 5 | - Multiple-streams folder contains an example skill to play multiple, pre-recorded audio streams, such as a basic podcast skill. 6 | 7 | - Single-stream folder contains an example skill to play a single stream, such as a live radio skill. 8 | 9 | This code is using the [Alexa Skill Kit SDK for Python](https://github.com/alexa/alexa-skills-kit-sdk-for-python). 10 | 11 | ## License 12 | 13 | This library is licensed under the Amazon Software License. -------------------------------------------------------------------------------- /SingleStream/.ask/config: -------------------------------------------------------------------------------- 1 | { 2 | "deploy_settings": { 3 | "default": { 4 | "skill_id": "amzn1.ask.skill.3164aad7-e102-4e35-a99f-46611012a94f", 5 | "was_cloned": false, 6 | "resources": { 7 | "manifest": { 8 | "eTag": "510fe2027ac0b0a6a619272d1a54d06c" 9 | }, 10 | "interactionModel": { 11 | "it-IT": { 12 | "eTag": "86a161a5ea0ca4fa6b588cba222b37c7" 13 | }, 14 | "en-US": { 15 | "eTag": "03678dc7bbef2f2fc4103ce8cec83b5f" 16 | }, 17 | "en-CA": { 18 | "eTag": "1f12b362510ff65ef93723526ca9335b" 19 | }, 20 | "en-IN": { 21 | "eTag": "03678dc7bbef2f2fc4103ce8cec83b5f" 22 | }, 23 | "en-AU": { 24 | "eTag": "d21de96898cf9ea3ba2507e4296941dd" 25 | }, 26 | "es-ES": { 27 | "eTag": "7400ec1a2ebc4c3e6f444cbb29e65318" 28 | }, 29 | "fr-FR": { 30 | "eTag": "49d15e4bae04319d3f7b3452c0725d09" 31 | }, 32 | "en-GB": { 33 | "eTag": "cc497df5095e40681ee84fab3ba4155f" 34 | }, 35 | "es-MX": { 36 | "eTag": "7400ec1a2ebc4c3e6f444cbb29e65318" 37 | } 38 | } 39 | }, 40 | "merge": { 41 | "manifest": { 42 | "apis": { 43 | "custom": { 44 | "endpoint": { 45 | "uri": "ask-custom-singlestream-audioplayer-default" 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SingleStream/README.md: -------------------------------------------------------------------------------- 1 | # Single Stream Audio Player Sample Skill (using ASK Python SDK) 2 | 3 | This skill demonstrates how to create a single stream audio skill. Single stream skills are typically used by radio stations to provide a convenient and quick access to their live stream. 4 | 5 | User interface is limited to Play and Stop use cases. 6 | 7 | ## Usage 8 | 9 | ```text 10 | Alexa, play my radio 11 | 12 | Alexa, stop 13 | ``` 14 | 15 | ## Installation 16 | 17 | You will need to comply to the prerequisites below and to change a few configuration files before creating the skill and upload the lambda code. 18 | 19 | ### Pre-requisites 20 | 21 | 0. This sample uses the [ASK Python SDK](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/) packages for developing the Alexa skill. 22 | 23 | - If you already have the ASK Python SDK installed, then install the dependencies in the ``lambda/py/requirements.txt`` 24 | using ``pip``. 25 | - If you are starting fresh, follow the [Setting up the ASK SDK](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/GETTING_STARTED.html) 26 | documentation, to get the ASK Python SDK installed on your machine. We recommend you to use the 27 | [virtualenv approach](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/GETTING_STARTED.html#option-1-set-up-the-sdk-in-a-virtual-environment). 28 | Please run the following command in your virtualenv, to install the dependencies, before working on the skill code. 29 | 30 | `` 31 | pip install -r lambda/py/requirements.txt 32 | `` 33 | 34 | 1. You need an [AWS account](https://aws.amazon.com) and an [Amazon developer account](https://developer.amazon.com) to create an Alexa Skill. 35 | 36 | 37 | ### Code changes before deploying 38 | 39 | 1. ```./skill.json``` 40 | 41 | Change the skill name, example phrase, icons, testing instructions etc ... 42 | 43 | Remember than most information is locale-specific and must be changed for each locale (en-GB, en-US etc.) 44 | 45 | Please refer to https://developer.amazon.com/docs/smapi/skill-manifest.html for details about manifest values. 46 | 47 | 2. ```./lambda/py/alexa/data.py``` 48 | 49 | - Locale specific card data, radio URL, jingle URL has to be modified with correct runtime values. 50 | ```start_jingle``` is an optional property defining a Jingle to be played before the live stream. 51 | Be sure to modify the value for each language supported by your skill. 52 | 53 | - When playing a jingle before your stream, you can choose the name of the database table where the "last played" 54 | information will be stored. If the table does not exist, the persistence code will create the table at the first 55 | invocation of the skill. You can manually create the DynamoDB table with the following command: 56 | 57 | ```bash 58 | aws dynamodb create-table --table-name my_radio --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 59 | ``` 60 | 61 | To minimize latency, we recommend to create the DynamoDB table in the same region as the Lambda function. 62 | 63 | When using DynamoDB, you also must ensure your Lambda function [execution role](http://docs.aws.amazon.com/lambda/latest/dg/intro-permission-model.html) will have permissions to read and write to the DynamoDB table. Be sure [to add the following policy](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_manage_modify.html) to the Lambda function [execution role](http://docs.aws.amazon.com/lambda/latest/dg/intro-permission-model.html): 64 | 65 | ```json 66 | { 67 | "Version": "2012-10-17", 68 | "Statement": [ 69 | { 70 | "Sid": "sid123", 71 | "Effect": "Allow", 72 | "Action": [ 73 | "dynamodb:PutItem", 74 | "dynamodb:GetItem", 75 | "dynamodb:UpdateItem" 76 | ], 77 | "Resource": "arn:aws:dynamodb:us-east-1:YOUR_ACCOUNT_ID:table/my_radio" 78 | } 79 | ] 80 | } 81 | ``` 82 | 83 | 3. Localization 84 | 85 | We use ``babel`` and python standard i18n ``gettext`` libraries, to create locale specific alexa responses. 86 | This [localization guide](https://github.com/alexa/skill-sample-python-fact/blob/master/instructions/localization.md) 87 | explains in brief on how to localize your alexa responses. We have already localized this skill sample for multiple 88 | locales. If you want to make changes to the strings, follow these steps: 89 | 90 | - The base strings (eg: the strings wrapped in ``_()``) are present in ``./lambda/py/alexa/data.py`` module. 91 | - The message catalog ``data.pot`` is present in ``./lambda/py/alexa/locales`` directory. 92 | - The language specific translations (eg: ``data.po``) are present in ``./lambda/py/alexa/locales`` directory 93 | as subfolders. The corresponding ``mo`` byte code files are also present in the same subfolder. 94 | - The localization interceptor has already been registered to the skill and can be checked in the 95 | ``lambda/py/lambda_function.py`` module. 96 | - If you want to make any changes in the base strings, remember to generate the message catalog and the locale specific 97 | translations as mentioned in **Step 2** and **Step 3** of the guide. 98 | - If you only want to change the translations, generate the ``mo`` files for the translated strings, following 99 | the **Step 3** of the guide. 100 | 101 | 4. ```./models/*.json``` 102 | 103 | Change the model definition to replace the invocation name (it defaults to "my radio") and the sample phrases for each intent. 104 | 105 | Repeat the operation for each locale you are planning to support. 106 | 107 | 108 | ### Deployment 109 | 110 | For AWS Lambda to correctly execute the skill code, we need to zip the skill code along 111 | with all dependencies and upload it. Follow the steps mentioned [here](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/DEVELOPING_YOUR_FIRST_SKILL.html#preparing-your-code-for-aws-lambda) 112 | 113 | ## On Device Tests 114 | 115 | To invoke the skill from your device, you need to login to the Alexa Developer Console, and enable the "Test" switch on your skill. 116 | 117 | See https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html#step-4-test-your-skill for more testing instructions. 118 | 119 | Then, just say : 120 | 121 | ```text 122 | Alexa, open my radio. 123 | ``` 124 | 125 | 126 | 127 | ## How it Works 128 | 129 | Alexa Skills Kit now includes a set of output directives and input events that allow you to control the playback of audio files or streams. There are a few important concepts to get familiar with: 130 | 131 | * **AudioPlayer directives** are used by your skill to start and stop audio playback from content hosted at a publicly accessible secure URL. You send AudioPlayer directives in response to the intents you've configured for your skill, or new events you'll receive when a user controls their device with a dedicated controller (see PlaybackController events below). 132 | * **PlaybackController events** are sent to your skill when a user selects play/next/prev/pause on dedicated hardware controls on the Alexa device, such as on the Amazon Tap or the Voice Remote for Amazon Echo and Echo Dot. Your skill receives these events if your skill is currently controlling audio on the device (i.e., you were the last to send an AudioPlayer directive). 133 | * **AudioPlayer events** are sent to your skill at key changes in the status of audio playback, such as when audio has begun playing, been stopped or has finished. You can use them to track what's currently playing or queue up more content. Unlike intents, when you receive an AudioPlayer event, you may only respond with appropriate AudioPlayer directives to control playback. 134 | 135 | You can learn more about the new [AudioPlayer interface](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/custom-audioplayer-interface-reference) and [PlaybackController interface](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/custom-playbackcontroller-interface-reference). 136 | 137 | ## Cleanup 138 | 139 | If you were deploying this skill just for learning purposes or for testing, do not forget to clean your AWS account to avoid recurring charges for your DynamoDB table. 140 | 141 | - delete the lambda function 142 | - delete the IAM execution role 143 | - delete the DynamoDB table 144 | -------------------------------------------------------------------------------- /SingleStream/lambda/py/alexa/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-audio-player/7a6e3c07dbd08e5a9085a5b999acac4e38a59fa5/SingleStream/lambda/py/alexa/__init__.py -------------------------------------------------------------------------------- /SingleStream/lambda/py/alexa/data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import gettext 3 | 4 | _ = gettext.gettext 5 | 6 | WELCOME_MSG = _("Welcome to {}") 7 | HELP_MSG = _("Welcome to {}. You can play, stop, resume listening. How can I help you ?") 8 | UNHANDLED_MSG = _("Sorry, I could not understand what you've just said.") 9 | CANNOT_SKIP_MSG = _("This is radio, you have to wait for previous or next track to play.") 10 | RESUME_MSG = _("Resuming {}") 11 | NOT_POSSIBLE_MSG = _("This is radio, you can not do that. You can ask me to stop or pause to stop listening.") 12 | STOP_MSG = _("Goodbye.") 13 | DEVICE_NOT_SUPPORTED = _("Sorry, this skill is not supported on this device") 14 | 15 | TEST = _("test english") 16 | TEST_PARAMS = _("test with parameters {} and {}") 17 | 18 | jingle = { 19 | "db_table": "my_radio", 20 | "play_once_every": 1000*60*60*24 # 24 hours 21 | } 22 | 23 | en = { 24 | "card": { 25 | "title": 'My Radio', 26 | "text": 'Less bla bla bla, more la la la', 27 | "large_image_url": 'https://alexademo.ninja/skills/logo-512.png', 28 | "small_image_url": 'https://alexademo.ninja/skills/logo-108.png' 29 | }, 30 | "url": 'https://audio1.maxi80.com', 31 | "start_jingle": 'https://s3-eu-west-1.amazonaws.com/alexa.maxi80.com/assets/jingle.m4a' 32 | } 33 | 34 | fr = { 35 | "card": { 36 | "title": 'My Radio', 37 | "text": 'Moins de bla bla bla, plus de la la la', 38 | "large_image_url": 'https://alexademo.ninja/skills/logo-512.png', 39 | "small_image_url": 'https://alexademo.ninja/skills/logo-108.png' 40 | }, 41 | "url": 'https://audio1.maxi80.com', 42 | "start_jingle": 'https://s3-eu-west-1.amazonaws.com/alexa.maxi80.com/assets/jingle.m4a' 43 | } 44 | 45 | it = { 46 | "card": { 47 | "title": 'La Mia Radio', 48 | "text": 'Meno parlare, più musica', 49 | "large_image_url": 'https://alexademo.ninja/skills/logo-512.png', 50 | "small_image_url": 'https://alexademo.ninja/skills/logo-108.png' 51 | }, 52 | "url": 'https://audio1.maxi80.com', 53 | "start_jingle": 'https://s3-eu-west-1.amazonaws.com/alexa.maxi80.com/assets/jingle.m4a' 54 | } 55 | 56 | es = { 57 | "card": { 58 | "title": 'Mi Radio', 59 | "text": 'Menos conversación, más música', 60 | "large_image_url": 'https://alexademo.ninja/skills/logo-512.png', 61 | "small_image_url": 'https://alexademo.ninja/skills/logo-108.png' 62 | }, 63 | "url": 'https://audio1.maxi80.com', 64 | "start_jingle": 'https://s3-eu-west-1.amazonaws.com/alexa.maxi80.com/assets/jingle.m4a' 65 | } -------------------------------------------------------------------------------- /SingleStream/lambda/py/alexa/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | from typing import Dict, Optional 5 | from ask_sdk_model import Request, Response 6 | from ask_sdk_model.ui import StandardCard, Image 7 | from ask_sdk_model.interfaces.audioplayer import ( 8 | PlayDirective, PlayBehavior, AudioItem, Stream, AudioItemMetadata, 9 | StopDirective, ClearQueueDirective, ClearBehavior) 10 | from ask_sdk_model.interfaces import display 11 | from ask_sdk_core.response_helper import ResponseFactory 12 | from ask_sdk_core.handler_input import HandlerInput 13 | from . import data 14 | 15 | def audio_data(request): 16 | # type: (Request) -> Dict 17 | default_locale = "en-US" 18 | if request.locale is None: 19 | locale = default_locale 20 | else: 21 | locale = request.locale 22 | 23 | if locale.startswith("en"): 24 | return data.en 25 | elif locale.startswith("fr"): 26 | return data.fr 27 | elif locale.startswith("it"): 28 | return data.it 29 | elif locale.startswith("es"): 30 | return data.es 31 | else: 32 | return {} 33 | 34 | 35 | def play(url, offset, text, card_data, response_builder): 36 | """Function to play audio. 37 | 38 | Using the function to begin playing audio when: 39 | - Play Audio Intent is invoked. 40 | - Resuming audio when stopped / paused. 41 | - Next / Previous commands issues. 42 | 43 | https://developer.amazon.com/docs/custom-skills/audioplayer-interface-reference.html#play 44 | REPLACE_ALL: Immediately begin playback of the specified stream, 45 | and replace current and enqueued streams. 46 | """ 47 | # type: (str, int, str, Dict, ResponseFactory) -> Response 48 | if card_data: 49 | response_builder.set_card( 50 | StandardCard( 51 | title=card_data["title"], text=card_data["text"], 52 | image=Image( 53 | small_image_url=card_data["small_image_url"], 54 | large_image_url=card_data["large_image_url"]) 55 | ) 56 | ) 57 | 58 | # Using URL as token as they are all unique 59 | response_builder.add_directive( 60 | PlayDirective( 61 | play_behavior=PlayBehavior.REPLACE_ALL, 62 | audio_item=AudioItem( 63 | stream=Stream( 64 | token=url, 65 | url=url, 66 | offset_in_milliseconds=offset, 67 | expected_previous_token=None), 68 | metadata=add_screen_background(card_data) if card_data else None 69 | ) 70 | ) 71 | ).set_should_end_session(True) 72 | 73 | if text: 74 | response_builder.speak(text) 75 | 76 | return response_builder.response 77 | 78 | def play_later(url, card_data, response_builder): 79 | """Play the stream later. 80 | 81 | https://developer.amazon.com/docs/custom-skills/audioplayer-interface-reference.html#play 82 | REPLACE_ENQUEUED: Replace all streams in the queue. This does not impact the currently playing stream. 83 | """ 84 | # type: (str, Dict, ResponseFactory) -> Response 85 | if card_data: 86 | # Using URL as token as they are all unique 87 | response_builder.add_directive( 88 | PlayDirective( 89 | play_behavior=PlayBehavior.REPLACE_ENQUEUED, 90 | audio_item=AudioItem( 91 | stream=Stream( 92 | token=url, 93 | url=url, 94 | offset_in_milliseconds=0, 95 | expected_previous_token=None), 96 | metadata=add_screen_background(card_data))) 97 | ).set_should_end_session(True) 98 | 99 | return response_builder.response 100 | 101 | def stop(text, response_builder): 102 | """Issue stop directive to stop the audio. 103 | 104 | Issuing AudioPlayer.Stop directive to stop the audio. 105 | Attributes already stored when AudioPlayer.Stopped request received. 106 | """ 107 | # type: (str, ResponseFactory) -> Response 108 | response_builder.add_directive(StopDirective()) 109 | if text: 110 | response_builder.speak(text) 111 | 112 | return response_builder.response 113 | 114 | def clear(response_builder): 115 | """Clear the queue amd stop the player.""" 116 | # type: (ResponseFactory) -> Response 117 | response_builder.add_directive(ClearQueueDirective( 118 | clear_behavior=ClearBehavior.CLEAR_ENQUEUED)) 119 | return response_builder.response 120 | 121 | def add_screen_background(card_data): 122 | # type: (Dict) -> Optional[AudioItemMetadata] 123 | if card_data: 124 | metadata = AudioItemMetadata( 125 | title=card_data["title"], 126 | subtitle=card_data["text"], 127 | art=display.Image( 128 | content_description=card_data["title"], 129 | sources=[ 130 | display.ImageInstance( 131 | url="https://alexademo.ninja/skills/logo-512.png") 132 | ] 133 | ) 134 | , background_image=display.Image( 135 | content_description=card_data["title"], 136 | sources=[ 137 | display.ImageInstance( 138 | url="https://alexademo.ninja/skills/logo-512.png") 139 | ] 140 | ) 141 | ) 142 | return metadata 143 | else: 144 | return None 145 | 146 | 147 | def should_play_jingle(handler_input): 148 | # type: (HandlerInput) -> bool 149 | will_play_jingle = False 150 | 151 | jingle_data = audio_data(handler_input.request_envelope.request) 152 | if jingle_data is None or "start_jingle" not in jingle_data: 153 | return will_play_jingle 154 | 155 | attr = handler_input.attributes_manager.persistent_attributes 156 | 157 | if attr is None or not attr.keys(): 158 | attr["last_played"] = "0001/01/01 00:00:00:000000" 159 | attr["played_count"] = 0 160 | handler_input.attributes_manager.persistent_attributes = attr 161 | 162 | last_played_epoch = attr["last_played"] 163 | now = datetime.datetime.now() 164 | format = "%Y/%m/%d %H:%M:%S:%f" 165 | delta = datetime.timedelta(milliseconds=data.jingle["play_once_every"]) 166 | 167 | # When last played is more than play_once_every milliseconds ago, play jingle 168 | 169 | will_play_jingle = (last_played_epoch == "0001/01/01 00:00:00:000000" 170 | or datetime.datetime.strptime(last_played_epoch, format) + delta < now) 171 | 172 | if will_play_jingle: 173 | attr["last_played"] = now.strftime(format) 174 | attr["played_count"] += 1 175 | handler_input.attributes_manager.save_persistent_attributes() 176 | 177 | return will_play_jingle 178 | -------------------------------------------------------------------------------- /SingleStream/lambda/py/lambda_function.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | import gettext 5 | from ask_sdk.standard import StandardSkillBuilder 6 | from ask_sdk_core.dispatch_components import ( 7 | AbstractRequestHandler, AbstractExceptionHandler, 8 | AbstractRequestInterceptor, AbstractResponseInterceptor) 9 | from ask_sdk_core.utils import is_request_type, is_intent_name 10 | from ask_sdk_core.handler_input import HandlerInput 11 | from ask_sdk_model import Response 12 | 13 | from alexa import data, util 14 | 15 | sb = StandardSkillBuilder( 16 | table_name=data.jingle["db_table"], auto_create_table=True) 17 | logger = logging.getLogger(__name__) 18 | logger.setLevel(logging.DEBUG) 19 | 20 | # ######################### INTENT HANDLERS ######################### 21 | # This section contains handlers for the built-in intents and generic 22 | # request handlers like launch, session end, skill events etc. 23 | 24 | class CheckAudioInterfaceHandler(AbstractRequestHandler): 25 | """Check if device supports audio play. 26 | 27 | This can be used as the first handler to be checked, before invoking 28 | other handlers, thus making the skill respond to unsupported devices 29 | without doing much processing. 30 | """ 31 | def can_handle(self, handler_input): 32 | # type: (HandlerInput) -> bool 33 | if handler_input.request_envelope.context.system.device: 34 | # Since skill events won't have device information 35 | return handler_input.request_envelope.context.system.device.supported_interfaces.audio_player is None 36 | else: 37 | return False 38 | 39 | def handle(self, handler_input): 40 | # type: (HandlerInput) -> Response 41 | logger.info("In CheckAudioInterfaceHandler") 42 | _ = handler_input.attributes_manager.request_attributes["_"] 43 | handler_input.response_builder.speak( 44 | _(data.DEVICE_NOT_SUPPORTED)).set_should_end_session(True) 45 | return handler_input.response_builder.response 46 | 47 | 48 | class SkillEventHandler(AbstractRequestHandler): 49 | """Close session for skill events or when session ends. 50 | 51 | Handler to handle session end or skill events (SkillEnabled, 52 | SkillDisabled etc.) 53 | """ 54 | def can_handle(self, handler_input): 55 | # type: (HandlerInput) -> bool 56 | return (handler_input.request_envelope.request.object_type.startswith( 57 | "AlexaSkillEvent") or 58 | is_request_type("SessionEndedRequest")(handler_input)) 59 | 60 | def handle(self, handler_input): 61 | # type: (HandlerInput) -> Response 62 | logger.info("In SkillEventHandler") 63 | return handler_input.response_builder.response 64 | 65 | 66 | class LaunchRequestOrPlayAudioHandler(AbstractRequestHandler): 67 | """Launch radio for skill launch or PlayAudio intent.""" 68 | def can_handle(self, handler_input): 69 | # type: (HandlerInput) -> bool 70 | return (is_request_type("LaunchRequest")(handler_input) or 71 | is_intent_name("PlayAudio")(handler_input)) 72 | 73 | def handle(self, handler_input): 74 | # type: (HandlerInput) -> Response 75 | logger.info("In LaunchRequestOrPlayAudioHandler") 76 | 77 | _ = handler_input.attributes_manager.request_attributes["_"] 78 | request = handler_input.request_envelope.request 79 | 80 | if util.audio_data(request)["start_jingle"]: 81 | if util.should_play_jingle(handler_input): 82 | return util.play(url=util.audio_data(request)["start_jingle"], 83 | offset=0, 84 | text=_(data.WELCOME_MSG).format( 85 | util.audio_data(request)["card"]["title"]), 86 | card_data=util.audio_data(request)["card"], 87 | response_builder=handler_input.response_builder) 88 | 89 | return util.play(url=util.audio_data(request)["url"], 90 | offset=0, 91 | text=_(data.WELCOME_MSG).format( 92 | util.audio_data(request)["card"]["title"]), 93 | card_data=util.audio_data(request)["card"], 94 | response_builder=handler_input.response_builder) 95 | 96 | 97 | class HelpIntentHandler(AbstractRequestHandler): 98 | """Handler for providing help information to user.""" 99 | def can_handle(self, handler_input): 100 | # type: (HandlerInput) -> bool 101 | return is_intent_name("AMAZON.HelpIntent")(handler_input) 102 | 103 | def handle(self, handler_input): 104 | # type: (HandlerInput) -> Response 105 | logger.info("In HelpIntentHandler") 106 | _ = handler_input.attributes_manager.request_attributes["_"] 107 | handler_input.response_builder.speak( 108 | _(data.HELP_MSG).format( 109 | util.audio_data( 110 | handler_input.request_envelope.request)["card"]["title"]) 111 | ).set_should_end_session(False) 112 | return handler_input.response_builder.response 113 | 114 | 115 | class UnhandledIntentHandler(AbstractRequestHandler): 116 | """Handler for fallback intent, for unmatched utterances. 117 | 118 | 2018-July-12: AMAZON.FallbackIntent is currently available in all 119 | English locales. This handler will not be triggered except in that 120 | locale, so it can be safely deployed for any locale. More info 121 | on the fallback intent can be found here: 122 | https://developer.amazon.com/docs/custom-skills/standard-built-in-intents.html#fallback 123 | """ 124 | def can_handle(self, handler_input): 125 | # type: (HandlerInput) -> bool 126 | return is_intent_name("AMAZON.FallbackIntent")(handler_input) 127 | 128 | def handle(self, handler_input): 129 | # type: (HandlerInput) -> Response 130 | logger.info("In UnhandledIntentHandler") 131 | _ = handler_input.attributes_manager.request_attributes["_"] 132 | handler_input.response_builder.speak( 133 | _(data.UNHANDLED_MSG)).set_should_end_session(True) 134 | return handler_input.response_builder.response 135 | 136 | 137 | class NextOrPreviousIntentHandler(AbstractRequestHandler): 138 | """Handler for next or previous intents.""" 139 | def can_handle(self, handler_input): 140 | # type: (HandlerInput) -> bool 141 | return (is_intent_name("AMAZON.NextIntent")(handler_input) or 142 | is_intent_name("AMAZON.PreviousIntent")(handler_input)) 143 | 144 | def handle(self, handler_input): 145 | # type: (HandlerInput) -> Response 146 | logger.info("In NextOrPreviousIntentHandler") 147 | _ = handler_input.attributes_manager.request_attributes["_"] 148 | handler_input.response_builder.speak( 149 | _(data.CANNOT_SKIP_MSG)).set_should_end_session(True) 150 | return handler_input.response_builder.response 151 | 152 | 153 | class CancelOrStopIntentHandler(AbstractRequestHandler): 154 | """Handler for cancel, stop or pause intents.""" 155 | def can_handle(self, handler_input): 156 | # type: (HandlerInput) -> bool 157 | return (is_intent_name("AMAZON.CancelIntent")(handler_input) or 158 | is_intent_name("AMAZON.StopIntent")(handler_input) or 159 | is_intent_name("AMAZON.PauseIntent")(handler_input)) 160 | 161 | def handle(self, handler_input): 162 | # type: (HandlerInput) -> Response 163 | logger.info("In CancelOrStopIntentHandler") 164 | _ = handler_input.attributes_manager.request_attributes["_"] 165 | return util.stop(_(data.STOP_MSG), handler_input.response_builder) 166 | 167 | 168 | class ResumeIntentHandler(AbstractRequestHandler): 169 | """Handler for resume intent.""" 170 | def can_handle(self, handler_input): 171 | # type: (HandlerInput) -> bool 172 | return is_intent_name("AMAZON.ResumeIntent")(handler_input) 173 | 174 | def handle(self, handler_input): 175 | # type: (HandlerInput) -> Response 176 | logger.info("In ResumeIntentHandler") 177 | request = handler_input.request_envelope.request 178 | _ = handler_input.attributes_manager.request_attributes["_"] 179 | speech = _(data.RESUME_MSG).format( 180 | util.audio_data(request)["card"]["title"]) 181 | return util.play( 182 | url=util.audio_data(request)["url"], offset=0, 183 | text=speech, card_data=util.audio_data(request)["card"], 184 | response_builder=handler_input.response_builder) 185 | 186 | 187 | class StartOverIntentHandler(AbstractRequestHandler): 188 | """Handler for start over, loop on/off, shuffle on/off intent.""" 189 | def can_handle(self, handler_input): 190 | # type: (HandlerInput) -> bool 191 | return (is_intent_name("AMAZON.StartOverIntent")(handler_input) or 192 | is_intent_name("AMAZON.LoopOnIntent")(handler_input) or 193 | is_intent_name("AMAZON.LoopOffIntent")(handler_input) or 194 | is_intent_name("AMAZON.ShuffleOnIntent")(handler_input) or 195 | is_intent_name("AMAZON.ShuffleOffIntent")(handler_input)) 196 | 197 | def handle(self, handler_input): 198 | # type: (HandlerInput) -> Response 199 | logger.info("In StartOverIntentHandler") 200 | 201 | _ = handler_input.attributes_manager.request_attributes["_"] 202 | speech = _(data.NOT_POSSIBLE_MSG) 203 | return handler_input.response_builder.speak(speech).response 204 | 205 | # ################################################################### 206 | 207 | # ########## AUDIOPLAYER INTERFACE HANDLERS ######################### 208 | # This section contains handlers related to Audioplayer interface 209 | 210 | class PlaybackStartedHandler(AbstractRequestHandler): 211 | """AudioPlayer.PlaybackStarted Directive received. 212 | 213 | Confirming that the requested audio file began playing. 214 | Do not send any specific response. 215 | """ 216 | def can_handle(self, handler_input): 217 | # type: (HandlerInput) -> bool 218 | return is_request_type("AudioPlayer.PlaybackStarted")(handler_input) 219 | 220 | def handle(self, handler_input): 221 | # type: (HandlerInput) -> Response 222 | logger.info("In PlaybackStartedHandler") 223 | logger.info("Playback started") 224 | return handler_input.response_builder.response 225 | 226 | class PlaybackFinishedHandler(AbstractRequestHandler): 227 | """AudioPlayer.PlaybackFinished Directive received. 228 | 229 | Confirming that the requested audio file completed playing. 230 | Do not send any specific response. 231 | """ 232 | def can_handle(self, handler_input): 233 | # type: (HandlerInput) -> bool 234 | return is_request_type("AudioPlayer.PlaybackFinished")(handler_input) 235 | 236 | def handle(self, handler_input): 237 | # type: (HandlerInput) -> Response 238 | logger.info("In PlaybackFinishedHandler") 239 | logger.info("Playback finished") 240 | return handler_input.response_builder.response 241 | 242 | 243 | class PlaybackStoppedHandler(AbstractRequestHandler): 244 | """AudioPlayer.PlaybackStopped Directive received. 245 | 246 | Confirming that the requested audio file stopped playing. 247 | Do not send any specific response. 248 | """ 249 | def can_handle(self, handler_input): 250 | # type: (HandlerInput) -> bool 251 | return is_request_type("AudioPlayer.PlaybackStopped")(handler_input) 252 | 253 | def handle(self, handler_input): 254 | # type: (HandlerInput) -> Response 255 | logger.info("In PlaybackStoppedHandler") 256 | logger.info("Playback stopped") 257 | return handler_input.response_builder.response 258 | 259 | 260 | class PlaybackNearlyFinishedHandler(AbstractRequestHandler): 261 | """AudioPlayer.PlaybackNearlyFinished Directive received. 262 | 263 | Replacing queue with the URL again. This should not happen on live streams. 264 | """ 265 | def can_handle(self, handler_input): 266 | # type: (HandlerInput) -> bool 267 | return is_request_type("AudioPlayer.PlaybackNearlyFinished")(handler_input) 268 | 269 | def handle(self, handler_input): 270 | # type: (HandlerInput) -> Response 271 | logger.info("In PlaybackNearlyFinishedHandler") 272 | logger.info("Playback nearly finished") 273 | request = handler_input.request_envelope.request 274 | return util.play_later( 275 | url=util.audio_data(request)["url"], 276 | card_data=util.audio_data(request)["card"], 277 | response_builder=handler_input.response_builder) 278 | 279 | 280 | class PlaybackFailedHandler(AbstractRequestHandler): 281 | """AudioPlayer.PlaybackFailed Directive received. 282 | 283 | Logging the error and restarting playing with no output speech and card. 284 | """ 285 | def can_handle(self, handler_input): 286 | # type: (HandlerInput) -> bool 287 | return is_request_type("AudioPlayer.PlaybackFailed")(handler_input) 288 | 289 | def handle(self, handler_input): 290 | # type: (HandlerInput) -> Response 291 | logger.info("In PlaybackFailedHandler") 292 | request = handler_input.request_envelope.request 293 | logger.info("Playback failed: {}".format(request.error)) 294 | return util.play( 295 | url=util.audio_data(request)["url"], offset=0, text=None, 296 | card_data=None, 297 | response_builder=handler_input.response_builder) 298 | 299 | 300 | class ExceptionEncounteredHandler(AbstractRequestHandler): 301 | """Handler to handle exceptions from responses sent by AudioPlayer 302 | request. 303 | """ 304 | def can_handle(self, handler_input): 305 | # type; (HandlerInput) -> bool 306 | return is_request_type("System.ExceptionEncountered")(handler_input) 307 | 308 | def handle(self, handler_input): 309 | # type: (HandlerInput) -> Response 310 | logger.info("\n**************** EXCEPTION *******************") 311 | logger.info(handler_input.request_envelope) 312 | return handler_input.response_builder.response 313 | 314 | # ################################################################### 315 | 316 | # ########## PLAYBACK CONTROLLER INTERFACE HANDLERS ################# 317 | # This section contains handlers related to Playback Controller interface 318 | # https://developer.amazon.com/docs/custom-skills/playback-controller-interface-reference.html#requests 319 | 320 | class PlayCommandHandler(AbstractRequestHandler): 321 | """Handler for Play command from hardware buttons or touch control. 322 | 323 | This handler handles the play command sent through hardware buttons such 324 | as remote control or the play control from Alexa-devices with a screen. 325 | """ 326 | def can_handle(self, handler_input): 327 | # type: (HandlerInput) -> bool 328 | return is_request_type( 329 | "PlaybackController.PlayCommandIssued")(handler_input) 330 | 331 | def handle(self, handler_input): 332 | # type: (HandlerInput) -> Response 333 | logger.info("In PlayCommandHandler") 334 | _ = handler_input.attributes_manager.request_attributes["_"] 335 | request = handler_input.request_envelope.request 336 | 337 | if util.audio_data(request)["start_jingle"]: 338 | if util.should_play_jingle(handler_input): 339 | return util.play(url=util.audio_data(request)["start_jingle"], 340 | offset=0, 341 | text=None, 342 | card_data=None, 343 | response_builder=handler_input.response_builder) 344 | 345 | return util.play(url=util.audio_data(request)["url"], 346 | offset=0, 347 | text=None, 348 | card_data=None, 349 | response_builder=handler_input.response_builder) 350 | 351 | 352 | class NextOrPreviousCommandHandler(AbstractRequestHandler): 353 | """Handler for Next or Previous command from hardware buttons or touch 354 | control. 355 | 356 | This handler handles the next/previous command sent through hardware 357 | buttons such as remote control or the next/previous control from 358 | Alexa-devices with a screen. 359 | """ 360 | def can_handle(self, handler_input): 361 | # type: (HandlerInput) -> bool 362 | return (is_request_type( 363 | "PlaybackController.NextCommandIssued")(handler_input) or 364 | is_request_type( 365 | "PlaybackController.PreviousCommandIssued")(handler_input)) 366 | 367 | def handle(self, handler_input): 368 | # type: (HandlerInput) -> Response 369 | logger.info("In NextOrPreviousCommandHandler") 370 | return handler_input.response_builder.response 371 | 372 | 373 | class PauseCommandHandler(AbstractRequestHandler): 374 | """Handler for Pause command from hardware buttons or touch control. 375 | 376 | This handler handles the pause command sent through hardware 377 | buttons such as remote control or the pause control from 378 | Alexa-devices with a screen. 379 | """ 380 | def can_handle(self, handler_input): 381 | # type: (HandlerInput) -> bool 382 | return is_request_type("PlaybackController.PauseCommandIssued")( 383 | handler_input) 384 | 385 | def handle(self, handler_input): 386 | # type: (HandlerInput) -> Response 387 | logger.info("In PauseCommandHandler") 388 | return util.stop(text=None, 389 | response_builder=handler_input.response_builder) 390 | 391 | # ################################################################### 392 | 393 | # ################## EXCEPTION HANDLERS ############################# 394 | class CatchAllExceptionHandler(AbstractExceptionHandler): 395 | """Catch all exception handler, log exception and 396 | respond with custom message. 397 | """ 398 | def can_handle(self, handler_input, exception): 399 | # type: (HandlerInput, Exception) -> bool 400 | return True 401 | 402 | def handle(self, handler_input, exception): 403 | # type: (HandlerInput, Exception) -> Response 404 | logger.info("In CatchAllExceptionHandler") 405 | logger.error(exception, exc_info=True) 406 | _ = handler_input.attributes_manager.request_attributes["_"] 407 | handler_input.response_builder.speak(_(data.UNHANDLED_MSG)).ask( 408 | _(data.HELP_MSG).format( 409 | util.audio_data( 410 | handler_input.request_envelope.request)["card"]["title"])) 411 | 412 | return handler_input.response_builder.response 413 | 414 | # ################################################################### 415 | 416 | # ############# REQUEST / RESPONSE INTERCEPTORS ##################### 417 | class RequestLogger(AbstractRequestInterceptor): 418 | """Log the alexa requests.""" 419 | def process(self, handler_input): 420 | # type: (HandlerInput) -> None 421 | logger.debug("Alexa Request: {}".format( 422 | handler_input.request_envelope.request)) 423 | 424 | 425 | class LocalizationInterceptor(AbstractRequestInterceptor): 426 | """Process the locale in request and load localized strings for response. 427 | 428 | This interceptors processes the locale in request, and loads the locale 429 | specific localization strings for the function `_`, that is used during 430 | responses. 431 | """ 432 | def process(self, handler_input): 433 | # type: (HandlerInput) -> None 434 | locale = getattr(handler_input.request_envelope.request, 'locale', None) 435 | logger.info("Locale is {}".format(locale)) 436 | if locale: 437 | if locale.startswith("fr"): 438 | locale_file_name = "fr-FR" 439 | elif locale.startswith("it"): 440 | locale_file_name = "it-IT" 441 | elif locale.startswith("es"): 442 | locale_file_name = "es-ES" 443 | else: 444 | locale_file_name = locale 445 | 446 | logger.info("Loading locale file: {}".format(locale_file_name)) 447 | i18n = gettext.translation( 448 | 'data', localedir='locales', languages=[locale_file_name], 449 | fallback=True) 450 | handler_input.attributes_manager.request_attributes[ 451 | "_"] = i18n.gettext 452 | else: 453 | handler_input.attributes_manager.request_attributes[ 454 | "_"] = gettext.gettext 455 | 456 | 457 | class ResponseLogger(AbstractResponseInterceptor): 458 | """Log the alexa responses.""" 459 | def process(self, handler_input, response): 460 | # type: (HandlerInput, Response) -> None 461 | logger.debug("Alexa Response: {}".format(response)) 462 | 463 | # ################################################################### 464 | 465 | 466 | # ############# REGISTER HANDLERS ##################### 467 | # Request Handlers 468 | sb.add_request_handler(CheckAudioInterfaceHandler()) 469 | sb.add_request_handler(SkillEventHandler()) 470 | sb.add_request_handler(LaunchRequestOrPlayAudioHandler()) 471 | sb.add_request_handler(PlayCommandHandler()) 472 | sb.add_request_handler(HelpIntentHandler()) 473 | sb.add_request_handler(ExceptionEncounteredHandler()) 474 | sb.add_request_handler(UnhandledIntentHandler()) 475 | sb.add_request_handler(NextOrPreviousIntentHandler()) 476 | sb.add_request_handler(NextOrPreviousCommandHandler()) 477 | sb.add_request_handler(CancelOrStopIntentHandler()) 478 | sb.add_request_handler(PauseCommandHandler()) 479 | sb.add_request_handler(ResumeIntentHandler()) 480 | sb.add_request_handler(StartOverIntentHandler()) 481 | sb.add_request_handler(PlaybackStartedHandler()) 482 | sb.add_request_handler(PlaybackFinishedHandler()) 483 | sb.add_request_handler(PlaybackStoppedHandler()) 484 | sb.add_request_handler(PlaybackNearlyFinishedHandler()) 485 | sb.add_request_handler(PlaybackFailedHandler()) 486 | 487 | # Exception handlers 488 | sb.add_exception_handler(CatchAllExceptionHandler()) 489 | 490 | # Interceptors 491 | sb.add_global_request_interceptor(RequestLogger()) 492 | sb.add_global_request_interceptor(LocalizationInterceptor()) 493 | sb.add_global_response_interceptor(ResponseLogger()) 494 | 495 | # AWS Lambda handler 496 | lambda_handler = sb.lambda_handler() 497 | -------------------------------------------------------------------------------- /SingleStream/lambda/py/locales/data.pot: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2018 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2018. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PROJECT VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2018-09-15 23:34-0700\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 2.6.0\n" 19 | 20 | #: alexa/data.py:6 21 | msgid "Welcome to {0}" 22 | msgstr "" 23 | 24 | #: alexa/data.py:7 25 | msgid "" 26 | "Welcome to {0}. You can play, stop, resume listening. How can I help you" 27 | " ?" 28 | msgstr "" 29 | 30 | #: alexa/data.py:8 31 | msgid "Sorry, I could not understand what you've just said." 32 | msgstr "" 33 | 34 | #: alexa/data.py:9 35 | msgid "This is radio, you have to wait for next track to play." 36 | msgstr "" 37 | 38 | #: alexa/data.py:10 39 | msgid "Resuming {0}" 40 | msgstr "" 41 | 42 | #: alexa/data.py:11 43 | msgid "" 44 | "This is radio, you can not do that. You can ask me to stop or pause to " 45 | "stop listening." 46 | msgstr "" 47 | 48 | #: alexa/data.py:12 49 | msgid "Goodbye." 50 | msgstr "" 51 | 52 | #: alexa/data.py:13 53 | msgid "Sorry, this skill is not supported on this device" 54 | msgstr "" 55 | 56 | #: alexa/data.py:15 57 | msgid "test english" 58 | msgstr "" 59 | 60 | #: alexa/data.py:16 61 | msgid "test with parameters {0} and {1}" 62 | msgstr "" 63 | 64 | -------------------------------------------------------------------------------- /SingleStream/lambda/py/locales/es-ES/LC_MESSAGES/data.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-audio-player/7a6e3c07dbd08e5a9085a5b999acac4e38a59fa5/SingleStream/lambda/py/locales/es-ES/LC_MESSAGES/data.mo -------------------------------------------------------------------------------- /SingleStream/lambda/py/locales/es-ES/LC_MESSAGES/data.po: -------------------------------------------------------------------------------- 1 | # Spanish (Spain) translations for PROJECT. 2 | # Copyright (C) 2018 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2018. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2018-09-15 23:34-0700\n" 11 | "PO-Revision-Date: 2018-09-15 23:37-0700\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: es_ES\n" 14 | "Language-Team: es_ES \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.6.0\n" 20 | 21 | #: alexa/data.py:6 22 | msgid "Welcome to {}" 23 | msgstr "Hola, esta es {}" 24 | 25 | #: alexa/data.py:7 26 | msgid "" 27 | "Welcome to {}. You can play, stop, resume listening. How can I help you" 28 | " ?" 29 | msgstr "" 30 | "Hola, con {}. Tu puedes reproducir, parar o volver a escuchar la radio. " 31 | "Qué quieres hacer?" 32 | 33 | #: alexa/data.py:8 34 | msgid "Sorry, I could not understand what you've just said." 35 | msgstr "Lo siento, No pude entender lo que acabas de decir." 36 | 37 | #: alexa/data.py:9 38 | msgid "This is radio, you have to wait for previous or next track to play." 39 | msgstr "Recuerda que esto es la radio, tu debes esperar la anterior o siguiente reproducción en lista." 40 | 41 | #: alexa/data.py:10 42 | msgid "Resuming {}" 43 | msgstr "reiniciando la reproduccion {}" 44 | 45 | #: alexa/data.py:11 46 | msgid "" 47 | "This is radio, you can not do that. You can ask me to stop or pause to " 48 | "stop listening." 49 | msgstr "" 50 | "Recuerda que esto es la radio, no puedo hacer eso. Sí quieres detener " 51 | " la reproducción, me puedes decir: para o pausa, ." 52 | 53 | #: alexa/data.py:12 54 | msgid "Goodbye." 55 | msgstr "Adiós." 56 | 57 | #: alexa/data.py:13 58 | msgid "Sorry, this skill is not supported on this device" 59 | msgstr "Lo sentimos, esta habilidad no es compatible con este dispositivo" 60 | 61 | #: alexa/data.py:15 62 | msgid "test english" 63 | msgstr "prueba español" 64 | 65 | #: alexa/data.py:16 66 | msgid "test with parameters {} and {}" 67 | msgstr "prueba con los parámetros {} y {)" 68 | 69 | -------------------------------------------------------------------------------- /SingleStream/lambda/py/locales/fr-FR/LC_MESSAGES/data.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-audio-player/7a6e3c07dbd08e5a9085a5b999acac4e38a59fa5/SingleStream/lambda/py/locales/fr-FR/LC_MESSAGES/data.mo -------------------------------------------------------------------------------- /SingleStream/lambda/py/locales/fr-FR/LC_MESSAGES/data.po: -------------------------------------------------------------------------------- 1 | # French (France) translations for PROJECT. 2 | # Copyright (C) 2018 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2018. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2018-09-15 23:34-0700\n" 11 | "PO-Revision-Date: 2018-09-15 23:36-0700\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: fr_FR\n" 14 | "Language-Team: fr_FR \n" 15 | "Plural-Forms: nplurals=2; plural=(n > 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.6.0\n" 20 | 21 | #: alexa/data.py:6 22 | msgid "Welcome to {}" 23 | msgstr "Bienvenue sur {}" 24 | 25 | #: alexa/data.py:7 26 | msgid "" 27 | "Welcome to {}. You can play, stop, resume listening. How can I help you" 28 | " ?" 29 | msgstr "" 30 | "Bienvenue sur {}. Vous pouvez démarrer, arrêter ou reprendre. Que " 31 | "souhaitez-vous faire ?" 32 | 33 | #: alexa/data.py:8 34 | msgid "Sorry, I could not understand what you've just said." 35 | msgstr "Désolé, je n'ai pas compris ce que vous avez dit." 36 | 37 | #: alexa/data.py:9 38 | msgid "This is radio, you have to wait for previous or next track to play." 39 | msgstr "C'est de la radio, vous devez attendre le titre précédent ou suivant." 40 | 41 | #: alexa/data.py:10 42 | msgid "Resuming {}" 43 | msgstr "Je redémarre {}" 44 | 45 | #: alexa/data.py:11 46 | msgid "" 47 | "This is radio, you can not do that. You can ask me to stop or pause to " 48 | "stop listening." 49 | msgstr "" 50 | "C'est de la radio, vous ne pouvez pas faire ca. Vous pouvez me demander " 51 | "d'arrêter ou de metre en pause pour arrêter la musique." 52 | 53 | #: alexa/data.py:12 54 | msgid "Goodbye." 55 | msgstr "au revoir !" 56 | 57 | #: alexa/data.py:13 58 | msgid "Sorry, this skill is not supported on this device" 59 | msgstr "Désolé, cette skill ne peut être utilisée sur cet appareil." 60 | 61 | #: alexa/data.py:15 62 | msgid "test english" 63 | msgstr "test français" 64 | 65 | #: alexa/data.py:16 66 | msgid "test with parameters {} and {}" 67 | msgstr "test avec paramètres {} et {}" 68 | 69 | -------------------------------------------------------------------------------- /SingleStream/lambda/py/locales/it-IT/LC_MESSAGES/data.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-python-audio-player/7a6e3c07dbd08e5a9085a5b999acac4e38a59fa5/SingleStream/lambda/py/locales/it-IT/LC_MESSAGES/data.mo -------------------------------------------------------------------------------- /SingleStream/lambda/py/locales/it-IT/LC_MESSAGES/data.po: -------------------------------------------------------------------------------- 1 | # Italian (Italy) translations for PROJECT. 2 | # Copyright (C) 2018 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2018. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2018-09-15 23:34-0700\n" 11 | "PO-Revision-Date: 2018-09-15 23:37-0700\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: it_IT\n" 14 | "Language-Team: it_IT \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.6.0\n" 20 | 21 | #: alexa/data.py:6 22 | msgid "Welcome to {}" 23 | msgstr "Benvenuto in {}" 24 | 25 | #: alexa/data.py:7 26 | msgid "" 27 | "Welcome to {}. You can play, stop, resume listening. How can I help you" 28 | " ?" 29 | msgstr "" 30 | "Benvenuto in {}. Puoi ascoltare, mettere in pausa o riprendere l'ascolto." 31 | " Come posso aiutarti?" 32 | 33 | #: alexa/data.py:8 34 | msgid "Sorry, I could not understand what you've just said." 35 | msgstr "Scusami, non ho capito quello che hai appena detto" 36 | 37 | #: alexa/data.py:9 38 | msgid "This is radio, you have to wait for previous or next track to play." 39 | msgstr "Questa è radio, devi attendere per passare al precedente o prossimo brano." 40 | 41 | #: alexa/data.py:10 42 | msgid "Resuming {}" 43 | msgstr "Riprendo {}" 44 | 45 | #: alexa/data.py:11 46 | msgid "" 47 | "This is radio, you can not do that. You can ask me to stop or pause to " 48 | "stop listening." 49 | msgstr "" 50 | "Questa è radio, non è possibile procedere. Puoi chiedermi di terminare o " 51 | "mettere in pausa l'ascolto" 52 | 53 | #: alexa/data.py:12 54 | msgid "Goodbye." 55 | msgstr "Arrivederci" 56 | 57 | #: alexa/data.py:13 58 | msgid "Sorry, this skill is not supported on this device" 59 | msgstr "Mi spiace, questa skill non è supportata da questo dispositivo" 60 | 61 | #: alexa/data.py:15 62 | msgid "test english" 63 | msgstr "test italiano" 64 | 65 | #: alexa/data.py:16 66 | msgid "test with parameters {} and {}" 67 | msgstr "testa con i parametri {} e {}" 68 | 69 | -------------------------------------------------------------------------------- /SingleStream/lambda/py/requirements.txt: -------------------------------------------------------------------------------- 1 | ask-sdk 2 | babel 3 | -------------------------------------------------------------------------------- /SingleStream/models/en-AU.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "my radio", 5 | "intents": [ 6 | { 7 | "name": "PlayAudio", 8 | "slots": [], 9 | "samples": [ 10 | "play", 11 | "start", 12 | "play my radio", 13 | "play my radio please", 14 | "start my radio", 15 | "start my radio please", 16 | "start the radio", 17 | "start the radio please", 18 | "start the audio", 19 | "start the audio please", 20 | "play the audio", 21 | "play the audio please", 22 | "start the music", 23 | "start the music please", 24 | "play the music", 25 | "play the music please" 26 | ] 27 | }, 28 | { 29 | "name": "AMAZON.PauseIntent", 30 | "samples": [] 31 | }, 32 | { 33 | "name": "AMAZON.ResumeIntent", 34 | "samples": [] 35 | }, 36 | { 37 | "name": "AMAZON.HelpIntent", 38 | "samples": [ 39 | "help me please", 40 | "help please", 41 | "what should i do", 42 | "what's next", 43 | "how can I listen to my radio", 44 | "tell me how to play", 45 | "tell me how to stop", 46 | "tell me how to resume", 47 | "how to stop" 48 | ] 49 | }, 50 | { 51 | "name": "AMAZON.StopIntent", 52 | "samples": [] 53 | }, 54 | { 55 | "name": "AMAZON.CancelIntent", 56 | "samples": [] 57 | }, 58 | { 59 | "name": "AMAZON.StartOverIntent", 60 | "samples": [] 61 | }, 62 | { 63 | "name": "AMAZON.FallbackIntent", 64 | "samples": [] 65 | }, 66 | { 67 | "name": "AMAZON.NavigateHomeIntent", 68 | "samples": [] 69 | } 70 | ], 71 | "types": [] 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /SingleStream/models/en-CA.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "my radio", 5 | "intents": [ 6 | { 7 | "name": "PlayAudio", 8 | "slots": [], 9 | "samples": [ 10 | "play", 11 | "start", 12 | "play my radio", 13 | "play my radio please", 14 | "start my radio", 15 | "start my radio please", 16 | "start the radio", 17 | "start the radio please", 18 | "start the audio", 19 | "start the audio please", 20 | "play the audio", 21 | "play the audio please", 22 | "start the music", 23 | "start the music please", 24 | "play the music", 25 | "play the music please" 26 | ] 27 | }, 28 | { 29 | "name": "AMAZON.PauseIntent", 30 | "samples": [] 31 | }, 32 | { 33 | "name": "AMAZON.ResumeIntent", 34 | "samples": [] 35 | }, 36 | { 37 | "name": "AMAZON.HelpIntent", 38 | "samples": [ 39 | "help me please", 40 | "help please", 41 | "what should i do", 42 | "what's next", 43 | "how can I listen to my radio", 44 | "tell me how to play", 45 | "tell me how to stop", 46 | "tell me how to resume", 47 | "how to stop" 48 | ] 49 | }, 50 | { 51 | "name": "AMAZON.StopIntent", 52 | "samples": [] 53 | }, 54 | { 55 | "name": "AMAZON.CancelIntent", 56 | "samples": [] 57 | }, 58 | { 59 | "name": "AMAZON.StartOverIntent", 60 | "samples": [] 61 | }, 62 | { 63 | "name": "AMAZON.NavigateHomeIntent", 64 | "samples": [] 65 | }, 66 | { 67 | "name": "AMAZON.FallbackIntent", 68 | "samples": [] 69 | } 70 | ], 71 | "types": [] 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /SingleStream/models/en-GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "my radio", 5 | "intents": [ 6 | { 7 | "name": "PlayAudio", 8 | "slots": [], 9 | "samples": [ 10 | "play", 11 | "start", 12 | "play my radio", 13 | "play my radio please", 14 | "start my radio", 15 | "start my radio please", 16 | "start the audio", 17 | "start the audio please", 18 | "play the audio", 19 | "play the audio please", 20 | "start the music", 21 | "start the music please", 22 | "play the music", 23 | "play the music please" 24 | ] 25 | }, 26 | { 27 | "name": "AMAZON.PauseIntent", 28 | "samples": [] 29 | }, 30 | { 31 | "name": "AMAZON.ResumeIntent", 32 | "samples": [] 33 | }, 34 | { 35 | "name": "AMAZON.HelpIntent", 36 | "samples": [ 37 | "help me please", 38 | "help please", 39 | "what should i do", 40 | "what's next", 41 | "how can I listen to my radio", 42 | "tell me how to play", 43 | "tell me how to stop", 44 | "tell me how to resume", 45 | "how to stop" 46 | ] 47 | }, 48 | { 49 | "name": "AMAZON.StopIntent", 50 | "samples": [] 51 | }, 52 | { 53 | "name": "AMAZON.CancelIntent", 54 | "samples": [] 55 | }, 56 | { 57 | "name": "AMAZON.StartOverIntent", 58 | "samples": [] 59 | }, 60 | { 61 | "name": "AMAZON.FallbackIntent", 62 | "samples": [] 63 | } 64 | ], 65 | "types": [] 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /SingleStream/models/en-IN.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "my radio", 5 | "intents": [ 6 | { 7 | "name": "PlayAudio", 8 | "slots": [], 9 | "samples": [ 10 | "play", 11 | "start", 12 | "play my radio", 13 | "play my radio please", 14 | "start my radio", 15 | "start my radio please", 16 | "start the radio", 17 | "start the radio please", 18 | "start the audio", 19 | "start the audio please", 20 | "play the audio", 21 | "play the audio please", 22 | "start the music", 23 | "start the music please", 24 | "play the music", 25 | "play the music please" 26 | ] 27 | }, 28 | { 29 | "name": "AMAZON.PauseIntent", 30 | "samples": [] 31 | }, 32 | { 33 | "name": "AMAZON.ResumeIntent", 34 | "samples": [] 35 | }, 36 | { 37 | "name": "AMAZON.HelpIntent", 38 | "samples": [ 39 | "help me please", 40 | "help please", 41 | "what should i do", 42 | "what's next", 43 | "how can I listen to my radio", 44 | "tell me how to play", 45 | "tell me how to stop", 46 | "tell me how to resume", 47 | "how to stop" 48 | ] 49 | }, 50 | { 51 | "name": "AMAZON.StopIntent", 52 | "samples": [] 53 | }, 54 | { 55 | "name": "AMAZON.CancelIntent", 56 | "samples": [] 57 | }, 58 | { 59 | "name": "AMAZON.StartOverIntent", 60 | "samples": [] 61 | }, 62 | { 63 | "name": "AMAZON.FallbackIntent", 64 | "samples": [] 65 | } 66 | ], 67 | "types": [] 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /SingleStream/models/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "my radio", 5 | "intents": [ 6 | { 7 | "name": "PlayAudio", 8 | "slots": [], 9 | "samples": [ 10 | "play", 11 | "start", 12 | "play my radio", 13 | "play my radio please", 14 | "start my radio", 15 | "start my radio please", 16 | "start the radio", 17 | "start the radio please", 18 | "start the audio", 19 | "start the audio please", 20 | "play the audio", 21 | "play the audio please", 22 | "start the music", 23 | "start the music please", 24 | "play the music", 25 | "play the music please" 26 | ] 27 | }, 28 | { 29 | "name": "AMAZON.PauseIntent", 30 | "samples": [] 31 | }, 32 | { 33 | "name": "AMAZON.ResumeIntent", 34 | "samples": [] 35 | }, 36 | { 37 | "name": "AMAZON.HelpIntent", 38 | "samples": [ 39 | "help me please", 40 | "help please", 41 | "what should i do", 42 | "what's next", 43 | "how can I listen to my radio", 44 | "tell me how to play", 45 | "tell me how to stop", 46 | "tell me how to resume", 47 | "how to stop" 48 | ] 49 | }, 50 | { 51 | "name": "AMAZON.StopIntent", 52 | "samples": [] 53 | }, 54 | { 55 | "name": "AMAZON.CancelIntent", 56 | "samples": [] 57 | }, 58 | { 59 | "name": "AMAZON.StartOverIntent", 60 | "samples": [] 61 | }, 62 | { 63 | "name": "AMAZON.FallbackIntent", 64 | "samples": [] 65 | } 66 | ], 67 | "types": [] 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /SingleStream/models/es-ES.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "mi radio", 5 | "intents": [ 6 | { 7 | "name": "PlayAudio", 8 | "samples": [ 9 | "jugar", 10 | "poner", 11 | "reproducir", 12 | "reproducir mi radio", 13 | "reproducir mi radio por favor", 14 | "poner mi radio", 15 | "poner mi radio por favor", 16 | "pon mi radio", 17 | "pon mi radio por favor", 18 | "pone mi radio", 19 | "pone mi radio por favor", 20 | "pon la radio", 21 | "pon la radio por favor", 22 | "pone la radio", 23 | "pone la radio por favor", 24 | "poner la radio", 25 | "poner la radio por favor", 26 | "reproducir la radio", 27 | "reproducir la radio por favor", 28 | "poner el sonido", 29 | "poner el sonido por favor", 30 | "pone el sonido", 31 | "pone el sonido por favor", 32 | "pon el sonido", 33 | "pon el sonido por favor", 34 | "poner la música", 35 | "poner la música por favor", 36 | "pon la música", 37 | "pon la música por favor", 38 | "pone la música", 39 | "pone la música por favor" 40 | ] 41 | }, 42 | { 43 | "name": "AMAZON.PauseIntent" 44 | }, 45 | { 46 | "name": "AMAZON.ResumeIntent" 47 | }, 48 | { 49 | "name": "AMAZON.HelpIntent", 50 | "samples": [ 51 | "ayudame por favor", 52 | "que debo hacer", 53 | "y luego", 54 | "como puedo escuchar mi radio", 55 | "como puedo escuchar la música", 56 | "como escuchar mi radio", 57 | "como escuchar la música", 58 | "dime cómo parar", 59 | "còmo parar", 60 | "cómo nos detenemos", 61 | "cómo so detiene" 62 | ] 63 | }, 64 | { 65 | "name": "AMAZON.StopIntent" 66 | }, 67 | { 68 | "name": "AMAZON.CancelIntent" 69 | }, 70 | { 71 | "name": "AMAZON.StartOverIntent" 72 | } 73 | ] 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /SingleStream/models/es-MX.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "mi radio", 5 | "intents": [ 6 | { 7 | "name": "PlayAudio", 8 | "samples": [ 9 | "jugar", 10 | "poner", 11 | "reproducir", 12 | "reproducir mi radio", 13 | "reproducir mi radio por favor", 14 | "poner mi radio", 15 | "poner mi radio por favor", 16 | "pon mi radio", 17 | "pon mi radio por favor", 18 | "pone mi radio", 19 | "pone mi radio por favor", 20 | "pon la radio", 21 | "pon la radio por favor", 22 | "pone la radio", 23 | "pone la radio por favor", 24 | "poner la radio", 25 | "poner la radio por favor", 26 | "reproducir la radio", 27 | "reproducir la radio por favor", 28 | "poner el sonido", 29 | "poner el sonido por favor", 30 | "pone el sonido", 31 | "pone el sonido por favor", 32 | "pon el sonido", 33 | "pon el sonido por favor", 34 | "poner la música", 35 | "poner la música por favor", 36 | "pon la música", 37 | "pon la música por favor", 38 | "pone la música", 39 | "pone la música por favor" 40 | ] 41 | }, 42 | { 43 | "name": "AMAZON.PauseIntent" 44 | }, 45 | { 46 | "name": "AMAZON.ResumeIntent" 47 | }, 48 | { 49 | "name": "AMAZON.HelpIntent", 50 | "samples": [ 51 | "ayudame por favor", 52 | "que debo hacer", 53 | "y luego", 54 | "como puedo escuchar mi radio", 55 | "como puedo escuchar la música", 56 | "como escuchar mi radio", 57 | "como escuchar la música", 58 | "dime cómo parar", 59 | "còmo parar", 60 | "cómo nos detenemos", 61 | "cómo so detiene" 62 | ] 63 | }, 64 | { 65 | "name": "AMAZON.StopIntent" 66 | }, 67 | { 68 | "name": "AMAZON.CancelIntent" 69 | }, 70 | { 71 | "name": "AMAZON.StartOverIntent" 72 | } 73 | ] 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /SingleStream/models/fr-FR.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "ma radio", 5 | "intents": [ 6 | { 7 | "name": "PlayAudio", 8 | "samples": [ 9 | "joue", 10 | "jouer", 11 | "démarre", 12 | "démarrer", 13 | "joue le direct", 14 | "jouer le direct", 15 | "joue le direct s'il te plait", 16 | "lance le direct", 17 | "lancer le direct", 18 | "lance le direct s'il te plait", 19 | "lance la radio", 20 | "lancer la radio", 21 | "lance la radio s'il te plait", 22 | "joue la radio", 23 | "jouer la radio", 24 | "joue la radio s'il te plait", 25 | "lance le son", 26 | "lancer le son", 27 | "lance le son s'il te plait", 28 | "joue le son", 29 | "jouer le son", 30 | "joue le son s'il te plait", 31 | "lance la musique", 32 | "lancer la musique", 33 | "lance la musique s'il te plait", 34 | "joue la musique", 35 | "jouer la musique", 36 | "joue la musique s'il te plait" 37 | ] 38 | }, 39 | { 40 | "name": "AMAZON.PauseIntent" 41 | }, 42 | { 43 | "name": "AMAZON.ResumeIntent" 44 | }, 45 | { 46 | "name": "AMAZON.HelpIntent", 47 | "samples": [ 48 | "aide moi s'il te plait", 49 | "que dois je faire", 50 | "et ensuite", 51 | "comment est ce que je peux écouter le direct", 52 | "comment écouter la musique", 53 | "dis moi comment arrêter", 54 | "comment on arrête", 55 | "comment arrêter", 56 | "comment ca s'arrête", 57 | "je ne comprends pas" 58 | ] 59 | }, 60 | { 61 | "name": "AMAZON.StopIntent" 62 | }, 63 | { 64 | "name": "AMAZON.CancelIntent" 65 | }, 66 | { 67 | "name": "AMAZON.StartOverIntent" 68 | } 69 | ] 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /SingleStream/models/it-IT.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "la mia radio", 5 | "intents": [ 6 | { 7 | "name": "PlayAudio", 8 | "samples": [ 9 | "giocare", 10 | "giocare per favore", 11 | "avviare la radio", 12 | "avviare la radio per favore", 13 | "metti la radio", 14 | "metti la radio per favore", 15 | "mette la radio", 16 | "mette la radio per favore", 17 | "accendi la radio", 18 | "accendi la radio per favore", 19 | "metti la mia radio", 20 | "metti la mia radio per favore", 21 | "mette la mia radio", 22 | "mette la mia radio per favore", 23 | "accendi la mia radio", 24 | "accendi la mia radio per favore", 25 | "metti la musica", 26 | "metti la musica per favore", 27 | "mette la musica", 28 | "mette la musica per favore", 29 | "accendi la musica", 30 | "accendi la musica per favore" 31 | ] 32 | }, 33 | { 34 | "name": "AMAZON.PauseIntent" 35 | }, 36 | { 37 | "name": "AMAZON.ResumeIntent" 38 | }, 39 | { 40 | "name": "AMAZON.HelpIntent", 41 | "samples": [ 42 | "come fermarsi", 43 | "come posso smettere", 44 | "aiutami", 45 | "come ascoltare la radio", 46 | "come ascoltare la mia radio", 47 | "come ascoltare la musica" 48 | ] 49 | }, 50 | { 51 | "name": "AMAZON.StopIntent", 52 | "samples" : [ 53 | "puoi mettere in pausa" 54 | ] 55 | }, 56 | { 57 | "name": "AMAZON.CancelIntent" 58 | }, 59 | { 60 | "name": "AMAZON.StartOverIntent" 61 | } 62 | ] 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /SingleStream/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "en-US": { 6 | "summary": "Listen to My Radio, less bla bla bla, more la la la.", 7 | "examplePhrases": [ 8 | "Alexa, open My Radio", 9 | "Alexa, play My Radio", 10 | "Alexa, ask My Radio to play" 11 | ], 12 | "keywords": [ 13 | "music", 14 | "streaming", 15 | "radio" 16 | ], 17 | "name": "My Radio", 18 | "description": "Listen to My Radio, with less bla bla bla, and more la la la.\n\nMy Radio provides a high quality sound 24/7 with the best music.\n\nTo start, just say \"Alexa, launch My Radio\" or \"Alexa, play my radio\" to start the radio\".\n\nAt anytime, you can stop the radio by saying \"Alexa, stop\"", 19 | "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", 20 | "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" 21 | }, 22 | "en-GB": { 23 | "summary": "Listen to My Radio, less bla bla bla, more la la la.", 24 | "examplePhrases": [ 25 | "Alexa, open My Radio", 26 | "Alexa, play My Radio", 27 | "Alexa, ask My Radio to play" 28 | ], 29 | "keywords": [ 30 | "music", 31 | "streaming", 32 | "radio" 33 | ], 34 | "name": "My Radio", 35 | "description": "Listen to My Radio, with less bla bla bla, and more la la la.\n\nMy Radio provides a high quality sound 24/7 with the best music.\n\nTo start, just say \"Alexa, launch My Radio\" or \"Alexa, play my radio\" to start the radio\".\n\nAt anytime, you can stop the radio by saying \"Alexa, stop\"", 36 | "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", 37 | "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" 38 | }, 39 | "en-IN": { 40 | "summary": "Listen to My Radio, less bla bla bla, more la la la.", 41 | "examplePhrases": [ 42 | "Alexa, open My Radio", 43 | "Alexa, play My Radio", 44 | "Alexa, ask My Radio to play" 45 | ], 46 | "keywords": [ 47 | "music", 48 | "streaming", 49 | "radio" 50 | ], 51 | "name": "My Radio", 52 | "description": "Listen to My Radio, with less bla bla bla, and more la la la.\n\nMy Radio provides a high quality sound 24/7 with the best music.\n\nTo start, just say \"Alexa, launch My Radio\" or \"Alexa, play my radio\" to start the radio\".\n\nAt anytime, you can stop the radio by saying \"Alexa, stop\"", 53 | "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", 54 | "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" 55 | }, 56 | "en-CA": { 57 | "summary": "Listen to My Radio, less bla bla bla, more la la la.", 58 | "examplePhrases": [ 59 | "Alexa, open My Radio", 60 | "Alexa, play My Radio", 61 | "Alexa, ask My Radio to play" 62 | ], 63 | "keywords": [ 64 | "music", 65 | "streaming", 66 | "radio" 67 | ], 68 | "name": "My Radio", 69 | "description": "Listen to My Radio, with less bla bla bla, and more la la la.\n\nMy Radio provides a high quality sound 24/7 with the best music.\n\nTo start, just say \"Alexa, launch My Radio\" or \"Alexa, play my radio\" to start the radio\".\n\nAt anytime, you can stop the radio by saying \"Alexa, stop\"", 70 | "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", 71 | "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" 72 | }, 73 | "en-AU": { 74 | "summary": "Listen to My Radio, less bla bla bla, more la la la.", 75 | "examplePhrases": [ 76 | "Alexa, open My Radio", 77 | "Alexa, play My Radio", 78 | "Alexa, ask My Radio to play" 79 | ], 80 | "keywords": [ 81 | "music", 82 | "streaming", 83 | "radio" 84 | ], 85 | "name": "My Radio", 86 | "description": "Listen to My Radio, with less bla bla bla, and more la la la.\n\nMy Radio provides a high quality sound 24/7 with the best music.\n\nTo start, just say \"Alexa, launch My Radio\" or \"Alexa, play my radio\" to start the radio\".\n\nAt anytime, you can stop the radio by saying \"Alexa, stop\"", 87 | "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", 88 | "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" 89 | }, 90 | "fr-FR": { 91 | "summary": "Ecoutez Ma Radio, moins de bla bla, plus de la la la.", 92 | "examplePhrases": [ 93 | "Alexa, ouvre Ma Radio", 94 | "Alexa, lance Ma Radio", 95 | "Alexa, demande à Ma Radio de démarrer" 96 | ], 97 | "keywords": [ 98 | "musique", 99 | "direct", 100 | "radio" 101 | ], 102 | "name": "Ma Radio", 103 | "description": "Ecoutez Ma Radio, moins de bla bla, plus de la la la\n\nMa Radio vous donne le meilleur son, 24h sur 24h\n\nPour démarrer, dites juste : \"Alexa, lance Ma Radio\" ou \"Alexa, démarre ma radio\"\".\n\nA n'importe quel moment, vous pouvez dire \"Alexa, stop\" pour arrêter.", 104 | "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", 105 | "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" 106 | }, 107 | "fr-CA": { 108 | "summary": "Ecoutez Ma Radio, moins de bla bla, plus de la la la.", 109 | "examplePhrases": [ 110 | "Alexa, ouvre Ma Radio", 111 | "Alexa, lance Ma Radio", 112 | "Alexa, demande à Ma Radio de démarrer" 113 | ], 114 | "keywords": [ 115 | "musique", 116 | "direct", 117 | "radio" 118 | ], 119 | "name": "Ma Radio", 120 | "description": "Ecoutez Ma Radio, moins de bla bla, plus de la la la\n\nMa Radio vous donne le meilleur son, 24h sur 24h\n\nPour démarrer, dites juste : \"Alexa, lance Ma Radio\" ou \"Alexa, démarre ma radio\"\".\n\nA n'importe quel moment, vous pouvez dire \"Alexa, stop\" pour arrêter.", 121 | "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", 122 | "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" 123 | }, 124 | "es-ES": { 125 | "summary": "Escucha Mi Radio", 126 | "examplePhrases": [ 127 | "Alexa, abre mi radio", 128 | "Alexa, pide a mi radio poner la radio", 129 | "Alexa, pide a mi radio reproducir" ], 130 | "keywords": [ 131 | "música", 132 | "reproducir radio", 133 | "radio" 134 | ], 135 | "name": "Mi Radio", 136 | "description": "Escucha Mi Radio.\n\nPara empezar, solo tienes que decir \"Alexa, abre mi radio\" o \"Alexa, pide a mi radio poner la radio\".\n\nEn cualquier momento, tu puedes parar la radio diciendo \"Alexa, para\"", 137 | "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", 138 | "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" 139 | }, 140 | "es-MX": { 141 | "summary": "Escucha Mi Radio", 142 | "examplePhrases": [ 143 | "Alexa, abre mi radio", 144 | "Alexa, pide a mi radio poner la radio", 145 | "Alexa, pide a mi radio reproducir" ], 146 | "keywords": [ 147 | "música", 148 | "reproducir radio", 149 | "radio" 150 | ], 151 | "name": "Mi Radio", 152 | "description": "Escucha Mi Radio.\n\nPara empezar, solo tienes que decir \"Alexa, abre mi radio\" o \"Alexa, pide a mi radio poner la radio\".\n\nEn cualquier momento, tu puedes parar la radio diciendo \"Alexa, para\"", 153 | "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", 154 | "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" 155 | }, 156 | "it-IT": { 157 | "summary": "Ascolta la mia radio.", 158 | "examplePhrases": [ 159 | "Alexa, apri la mia radio", 160 | "Alexa, chiedi a la mia radio di avviare la radio", 161 | "Alexa, chiedi a la mia radio di giocare" 162 | ], 163 | "keywords": [ 164 | "musica", 165 | "live", 166 | "radio" 167 | ], 168 | "name": "La Mia Radio", 169 | "description": "Ascolta La Mia Radio.\n\nPer iniziare, basta dire \"Alexa, apri La Mia Radio\" o \"Alexa, chiedi a la mia radio di avviare la radio\".\n\nIn qualsiasi momento, puoi interrompere la radio dicendo \"Alexa, basta\"", 170 | "smallIconUri": "https://alexademo.ninja/skills/logo-108.png", 171 | "largeIconUri": "https://alexademo.ninja/skills/logo-512.png" 172 | } 173 | }, 174 | "isAvailableWorldwide": true, 175 | "testingInstructions": "Include your testing instruction (if any) here", 176 | "category": "STREAMING_SERVICE", 177 | "distributionCountries": [] 178 | }, 179 | "apis": { 180 | "custom": { 181 | "endpoint": { 182 | "sourceDir": "lambda/py" 183 | }, 184 | "interfaces": [ 185 | { 186 | "type": "AUDIO_PLAYER" 187 | } 188 | ] 189 | } 190 | }, 191 | "manifestVersion": "1.0", 192 | "permissions": [], 193 | "privacyAndCompliance": { 194 | "allowsPurchases": false, 195 | "isExportCompliant": true, 196 | "containsAds": false, 197 | "isChildDirected": false, 198 | "usesPersonalInfo": false 199 | } 200 | } 201 | } 202 | --------------------------------------------------------------------------------