Move to GitHub
This commit is contained in:
968
skill/app.py
Executable file
968
skill/app.py
Executable file
@@ -0,0 +1,968 @@
|
||||
from flask import Flask, render_template
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
|
||||
from ask_sdk_core.skill_builder import SkillBuilder
|
||||
from ask_sdk_core.dispatch_components import AbstractRequestHandler, AbstractRequestInterceptor, AbstractResponseInterceptor
|
||||
from ask_sdk_core.utils import is_request_type, is_intent_name, get_slot_value_v2, get_intent_name, get_request_type
|
||||
from ask_sdk_core.handler_input import HandlerInput
|
||||
from ask_sdk_model import Response
|
||||
from ask_sdk_core.dispatch_components import AbstractExceptionHandler
|
||||
from flask_ask_sdk.skill_adapter import SkillAdapter
|
||||
|
||||
import asknavidrome.subsonic_api as api
|
||||
import asknavidrome.media_queue as queue
|
||||
import asknavidrome.controller as controller
|
||||
|
||||
# Create web service
|
||||
app = Flask(__name__)
|
||||
|
||||
# Create skill object
|
||||
sb = SkillBuilder()
|
||||
|
||||
# Setup Logging
|
||||
logger = logging.getLogger() # Create logger
|
||||
level = logging.getLevelName('DEBUG')
|
||||
logger.setLevel(level) # Set logger log level
|
||||
|
||||
log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setLevel(level)
|
||||
handler.setFormatter(log_formatter)
|
||||
|
||||
logger.addHandler(handler)
|
||||
|
||||
#
|
||||
# Get service configuration
|
||||
#
|
||||
|
||||
logger.debug('Getting configutration from the environment...')
|
||||
|
||||
try:
|
||||
if 'NAVI_SKILL_ID' in os.environ:
|
||||
# Set skill ID, this is available on the Alexa Developer Console
|
||||
# if this is not set the web service will respond to any skill.
|
||||
sb.skill_id = os.getenv('NAVI_SKILL_ID')
|
||||
|
||||
logger.info(f'Skill ID set to: {sb.skill_id}')
|
||||
|
||||
else:
|
||||
raise NameError
|
||||
except NameError as err:
|
||||
logger.error(f'The Alexa skill ID was not found! {err}')
|
||||
raise
|
||||
|
||||
try:
|
||||
if 'NAVI_SONG_COUNT' in os.environ:
|
||||
min_song_count = os.getenv('NAVI_SONG_COUNT')
|
||||
|
||||
logger.info(f'Minimum song count is set to: {min_song_count}')
|
||||
|
||||
else:
|
||||
raise NameError
|
||||
except NameError as err:
|
||||
logger.error(f'The minimum song count was not found! {err}')
|
||||
raise
|
||||
|
||||
try:
|
||||
if 'NAVI_URL' in os.environ:
|
||||
navidrome_url = os.getenv('NAVI_URL')
|
||||
|
||||
logger.info(f'The URL for Navidrome is set to: {navidrome_url}')
|
||||
|
||||
else:
|
||||
raise NameError
|
||||
except NameError as err:
|
||||
logger.error(f'The URL of the Navidrome server was not found! {err}')
|
||||
raise
|
||||
|
||||
try:
|
||||
if 'NAVI_USER' in os.environ:
|
||||
navidrome_user = os.getenv('NAVI_USER')
|
||||
|
||||
logger.info(f'The Navidrome user name is set to: {navidrome_user}')
|
||||
|
||||
else:
|
||||
raise NameError
|
||||
except NameError as err:
|
||||
logger.error(f'The Navidrome user name was not found! {err}')
|
||||
raise
|
||||
|
||||
try:
|
||||
if 'NAVI_PASS' in os.environ:
|
||||
navidrome_passwd = os.getenv('NAVI_PASS')
|
||||
|
||||
logger.info('The Navidrome password is set')
|
||||
|
||||
else:
|
||||
raise NameError
|
||||
except NameError as err:
|
||||
logger.error(f'The Navidrome password was not found! {err}')
|
||||
raise
|
||||
|
||||
try:
|
||||
if 'NAVI_PORT' in os.environ:
|
||||
navidrome_port = os.getenv('NAVI_PORT')
|
||||
|
||||
logger.info(f'The Navidrome port is set to: {navidrome_port}')
|
||||
|
||||
else:
|
||||
raise NameError
|
||||
except NameError as err:
|
||||
logger.error(f'The Navidrome port was not found! {err}')
|
||||
raise
|
||||
|
||||
try:
|
||||
if 'NAVI_API_PATH' in os.environ:
|
||||
navidrome_api_location = os.getenv('NAVI_API_PATH')
|
||||
|
||||
logger.info(f'The Navidrome API path is set to: {navidrome_api_location}')
|
||||
|
||||
else:
|
||||
raise NameError
|
||||
except NameError as err:
|
||||
logger.error(f'The Navidrome API path was not found! {err}')
|
||||
raise
|
||||
|
||||
try:
|
||||
if 'NAVI_API_VER' in os.environ:
|
||||
navidrome_api_version = os.getenv('NAVI_API_VER')
|
||||
|
||||
logger.info(f'The Navidrome API version is set to: {navidrome_api_version}')
|
||||
|
||||
else:
|
||||
raise NameError
|
||||
except NameError as err:
|
||||
logger.error(f'The Navidrome API version was not found! {err}')
|
||||
raise
|
||||
|
||||
logger.debug('Configuration has been successfully loaded')
|
||||
|
||||
# Create a queue
|
||||
play_queue = queue.MediaQueue()
|
||||
logger.debug('MediaQueue object created...')
|
||||
|
||||
# Connect to Navidrome
|
||||
connection = api.SubsonicConnection(navidrome_url,
|
||||
navidrome_user,
|
||||
navidrome_passwd,
|
||||
navidrome_port,
|
||||
navidrome_api_location,
|
||||
navidrome_api_version)
|
||||
|
||||
try:
|
||||
connection.ping()
|
||||
|
||||
except:
|
||||
raise RuntimeError('Could not connect to SubSonic API!')
|
||||
|
||||
logger.info('AskNavidrome Web Service is ready to start!')
|
||||
|
||||
|
||||
#
|
||||
# Handler Classes
|
||||
#
|
||||
|
||||
class LaunchRequestHandler(AbstractRequestHandler):
|
||||
"""Handle LaunchRequest and NavigateHomeIntent"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return (
|
||||
is_request_type('LaunchRequest')(handler_input) or
|
||||
is_intent_name('AMAZON.NavigateHomeIntent')(handler_input)
|
||||
)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In LaunchRequestHandler')
|
||||
|
||||
connection.ping()
|
||||
speech = 'Ready!'
|
||||
|
||||
handler_input.response_builder.speak(speech).ask(speech)
|
||||
return handler_input.response_builder.response
|
||||
|
||||
|
||||
class CheckAudioInterfaceHandler(AbstractRequestHandler):
|
||||
"""Check if device supports audio play.
|
||||
|
||||
This can be used as the first handler to be checked, before invoking
|
||||
other handlers, thus making the skill respond to unsupported devices
|
||||
without doing much processing.
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
if handler_input.request_envelope.context.system.device:
|
||||
# Since skill events won't have device information
|
||||
return handler_input.request_envelope.context.system.device.supported_interfaces.audio_player is None
|
||||
else:
|
||||
return False
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In CheckAudioInterfaceHandler')
|
||||
|
||||
_ = handler_input.attributes_manager.request_attributes['_']
|
||||
handler_input.response_builder.speak('This device is not supported').set_should_end_session(True)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
|
||||
class SkillEventHandler(AbstractRequestHandler):
|
||||
"""Close session for skill events or when session ends.
|
||||
|
||||
Handler to handle session end or skill events (SkillEnabled,
|
||||
SkillDisabled etc.)
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return (handler_input.request_envelope.request.object_type.startswith(
|
||||
'AlexaSkillEvent') or
|
||||
is_request_type('SessionEndedRequest')(handler_input))
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In SkillEventHandler')
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
|
||||
class HelpHandler(AbstractRequestHandler):
|
||||
"""Handle HelpIntent"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_intent_name('AMAZON.HelpIntent')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In HelpHandler')
|
||||
|
||||
text = 'AskNavidrome lets you interact with media servers that offer a Subsonic compatible A.P.I.'
|
||||
handler_input.response_builder.speak(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
|
||||
class NaviSonicPlayMusicByArtist(AbstractRequestHandler):
|
||||
"""Handle NaviSonicPlayMusicByArtist
|
||||
|
||||
Play a selection of songs for the given artist
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_intent_name('NaviSonicPlayMusicByArtist')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In NaviSonicPlayMusicByArtist')
|
||||
|
||||
# Get the requested artist
|
||||
artist = get_slot_value_v2(handler_input, 'artist')
|
||||
|
||||
# Search for an artist
|
||||
artist_lookup = connection.search_artist(artist.value)
|
||||
|
||||
if artist_lookup is None:
|
||||
text = f"I couldn't find the artist {artist.value} in the collection."
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
else:
|
||||
# Get a list of albums by the artist
|
||||
artist_album_lookup = connection.albums_by_artist(artist_lookup[0].get('id'))
|
||||
|
||||
# Build a list of songs to play
|
||||
song_id_list = connection.build_song_list_from_albums(artist_album_lookup, min_song_count)
|
||||
play_queue.clear()
|
||||
controller.enqueue_songs(connection, play_queue, song_id_list)
|
||||
speech = f'Playing music by: {artist.value}'
|
||||
logger.info(speech)
|
||||
|
||||
card = {'title': 'AskNavidrome',
|
||||
'text': speech
|
||||
}
|
||||
|
||||
play_queue.shuffle()
|
||||
track_details = play_queue.get_next_track()
|
||||
return controller.start_playback('play', speech, card, track_details, handler_input)
|
||||
|
||||
|
||||
class NaviSonicPlayAlbumByArtist(AbstractRequestHandler):
|
||||
"""Handle NaviSonicPlayAlbumByArtist
|
||||
|
||||
Play a given album by a given artist
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_intent_name('NaviSonicPlayAlbumByArtist')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In NaviSonicPlayAlbumByArtist')
|
||||
|
||||
# Get variables from intent
|
||||
artist = get_slot_value_v2(handler_input, 'artist')
|
||||
album = get_slot_value_v2(handler_input, 'album')
|
||||
|
||||
if artist is not None and album is not None:
|
||||
# Play album by artist method
|
||||
logger.debug(f'Searching for the album {album.value} by {artist.value}')
|
||||
|
||||
# Search for an artist
|
||||
artist_lookup = connection.search_artist(artist.value)
|
||||
|
||||
if artist_lookup is None:
|
||||
text = f"I couldn't find the artist {artist.value} in the collection."
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
else:
|
||||
artist_album_lookup = connection.albums_by_artist(artist_lookup[0].get('id'))
|
||||
|
||||
# Search the list of dictionaries for the requested album
|
||||
# Strings are all converted to lower case to minimise matching errors
|
||||
result = [album_result for album_result in artist_album_lookup if album_result.get('title').lower() == album.value.lower()]
|
||||
|
||||
if not result:
|
||||
text = f"I couldn't find an album called {album.value} by {artist.value} in the collection."
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
# At this point we have found an album that matches
|
||||
songs = connection.build_song_list_from_albums(result, -1)
|
||||
play_queue.clear()
|
||||
controller.enqueue_songs(connection, play_queue, songs)
|
||||
|
||||
speech = f'Playing {album.value} by: {artist.value}'
|
||||
logger.info(speech)
|
||||
card = {'title': 'AskNavidrome',
|
||||
'text': speech
|
||||
}
|
||||
track_details = play_queue.get_next_track()
|
||||
|
||||
return controller.start_playback('play', speech, card, track_details, handler_input)
|
||||
|
||||
elif artist is None and album:
|
||||
# Play album method
|
||||
logger.debug(f'Searching for the album {album.value}')
|
||||
|
||||
result = connection.search_album(album.value)
|
||||
|
||||
if result is None:
|
||||
text = f"I couldn't find the album {album.value} in the collection."
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
else:
|
||||
songs = connection.build_song_list_from_albums(result, -1)
|
||||
play_queue.clear()
|
||||
controller.enqueue_songs(connection, play_queue, songs)
|
||||
|
||||
speech = f'Playing {album.value}'
|
||||
logger.info(speech)
|
||||
card = {'title': 'AskNavidrome',
|
||||
'text': speech
|
||||
}
|
||||
track_details = play_queue.get_next_track()
|
||||
|
||||
return controller.start_playback('play', speech, card, track_details, handler_input)
|
||||
|
||||
|
||||
class NaviSonicPlaySongByArtist(AbstractRequestHandler):
|
||||
"""Handle the NaviSonicPlaySongByArtist intent
|
||||
|
||||
Play the given song by the given artist if it exists in the
|
||||
collection.
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_intent_name('NaviSonicPlaySongByArtist')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In NaviSonicPlaySongByArtist')
|
||||
|
||||
# Get variables from intent
|
||||
artist = get_slot_value_v2(handler_input, 'artist')
|
||||
song = get_slot_value_v2(handler_input, 'song')
|
||||
|
||||
logger.debug(f'Searching for the song {song.value} by {artist.value}')
|
||||
|
||||
# Search for the artist
|
||||
artist_lookup = connection.search_artist(artist.value)
|
||||
|
||||
if artist_lookup is None:
|
||||
text = f"I couldn't find the artist {artist.value} in the collection."
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
else:
|
||||
artist_id = artist_lookup[0].get('id')
|
||||
|
||||
# Search for song
|
||||
song_list = connection.search_song(song.value)
|
||||
|
||||
# Search for song by given artist.
|
||||
song_dets = [item.get('id') for item in song_list if item.get('artistId') == artist_id]
|
||||
|
||||
if not song_dets:
|
||||
text = f"I couldn't find a song called {song.value} by {artist.value} in the collection."
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
play_queue.clear()
|
||||
controller.enqueue_songs(connection, play_queue, song_dets)
|
||||
|
||||
speech = f'Playing {song.value} by {artist.value}'
|
||||
logger.info(speech)
|
||||
card = {'title': 'AskNavidrome',
|
||||
'text': speech
|
||||
}
|
||||
track_details = play_queue.get_next_track()
|
||||
|
||||
return controller.start_playback('play', speech, card, track_details, handler_input)
|
||||
|
||||
|
||||
class NaviSonicPlayPlaylist(AbstractRequestHandler):
|
||||
"""Handle NaviSonicPlayPlaylist
|
||||
|
||||
Play the given playlist
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_intent_name('NaviSonicPlayPlaylist')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In NaviSonicPlayPlaylist')
|
||||
|
||||
# Get the requested playlist
|
||||
playlist = get_slot_value_v2(handler_input, 'playlist')
|
||||
|
||||
# Search for a playlist
|
||||
playlist_id = connection.search_playlist(playlist.value)
|
||||
|
||||
if playlist_id is None:
|
||||
text = "I couldn't find the playlist " + str(playlist.value) + ' in the collection.'
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
else:
|
||||
song_id_list = connection.build_song_list_from_playlist(playlist_id)
|
||||
play_queue.clear()
|
||||
controller.enqueue_songs(connection, play_queue, song_id_list)
|
||||
|
||||
speech = 'Playing playlist ' + str(playlist.value)
|
||||
logger.info(speech)
|
||||
card = {'title': 'AskNavidrome',
|
||||
'text': speech
|
||||
}
|
||||
track_details = play_queue.get_next_track()
|
||||
|
||||
return controller.start_playback('play', speech, card, track_details, handler_input)
|
||||
|
||||
|
||||
class NaviSonicPlayMusicByGenre(AbstractRequestHandler):
|
||||
""" Play songs from the given genere
|
||||
|
||||
50 tracks from the given genere are shuffled and played
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_intent_name('NaviSonicPlayMusicByGenre')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In NaviSonicPlayMusicByGenre')
|
||||
|
||||
# Get the requested genre
|
||||
genre = get_slot_value_v2(handler_input, 'genre')
|
||||
|
||||
song_id_list = connection.build_song_list_from_genre(genre.value, min_song_count)
|
||||
|
||||
if song_id_list is None:
|
||||
text = f"I couldn't find any {genre.value} songs in the collection."
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
else:
|
||||
random.shuffle(song_id_list)
|
||||
play_queue.clear()
|
||||
controller.enqueue_songs(connection, play_queue, song_id_list)
|
||||
|
||||
speech = f'Playing {genre.value} music'
|
||||
logger.info(speech)
|
||||
card = {'title': 'AskNavidrome',
|
||||
'text': speech
|
||||
}
|
||||
track_details = play_queue.get_next_track()
|
||||
|
||||
return controller.start_playback('play', speech, card, track_details, handler_input)
|
||||
|
||||
|
||||
class NaviSonicPlayMusicRandom(AbstractRequestHandler):
|
||||
"""Handle the NaviSonicPlayMusicRandom intent
|
||||
|
||||
Play a random selection of music.
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_intent_name('NaviSonicPlayMusicRandom')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In NaviSonicPlayMusicRandom')
|
||||
|
||||
song_id_list = connection.build_random_song_list(min_song_count)
|
||||
|
||||
if song_id_list is None:
|
||||
text = "I couldn't find any songs in the collection."
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
else:
|
||||
random.shuffle(song_id_list)
|
||||
play_queue.clear()
|
||||
controller.enqueue_songs(connection, play_queue, song_id_list)
|
||||
|
||||
speech = 'Playing random music'
|
||||
logger.info(speech)
|
||||
card = {'title': 'AskNavidrome',
|
||||
'text': speech
|
||||
}
|
||||
track_details = play_queue.get_next_track()
|
||||
|
||||
return controller.start_playback('play', speech, card, track_details, handler_input)
|
||||
|
||||
|
||||
class NaviSonicPlayFavouriteSongs(AbstractRequestHandler):
|
||||
"""Handle the NaviSonicPlayFavouriteSongs intent
|
||||
|
||||
Play all starred / liked songs, songs are automatically shuffled.
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_intent_name('NaviSonicPlayFavouriteSongs')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In NaviSonicPlayFavouriteSongs')
|
||||
|
||||
song_id_list = connection.build_song_list_from_favourites()
|
||||
|
||||
if song_id_list is None:
|
||||
text = "You don't have any favourite songs in the collection."
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
else:
|
||||
random.shuffle(song_id_list)
|
||||
play_queue.clear()
|
||||
controller.enqueue_songs(connection, play_queue, song_id_list)
|
||||
|
||||
speech = 'Playing your favourite tracks.'
|
||||
logger.info(speech)
|
||||
card = {'title': 'AskNavidrome',
|
||||
'text': speech
|
||||
}
|
||||
track_details = play_queue.get_next_track()
|
||||
|
||||
return controller.start_playback('play', speech, card, track_details, handler_input)
|
||||
|
||||
|
||||
class NaviSonicSongDetails(AbstractRequestHandler):
|
||||
"""Handle NaviSonicSongDetails Intent
|
||||
|
||||
Returns information on the track that is currently playing
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_intent_name('NaviSonicSongDetails')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In NaviSonicSongDetails Handler')
|
||||
|
||||
title = play_queue.current_track.title
|
||||
artist = play_queue.current_track.artist
|
||||
album = play_queue.current_track.album
|
||||
|
||||
text = f'This is {title} by {artist}, from the album {album}'
|
||||
handler_input.response_builder.speak(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
|
||||
class NaviSonicStarSong(AbstractRequestHandler):
|
||||
"""Handle NaviSonicStarSong Intent
|
||||
|
||||
Star / favourite the current song
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_intent_name('NaviSonicStarSong')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In NaviSonicStarSong Handler')
|
||||
|
||||
song_id = play_queue.current_track.id
|
||||
connection.star_entry(song_id, 'song')
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
|
||||
class NaviSonicUnstarSong(AbstractRequestHandler):
|
||||
"""Handle NaviSonicUnstarSong Intent
|
||||
|
||||
Star / favourite the current song
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_intent_name('NaviSonicUnstarSong')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In NaviSonicUnstarSong Handler')
|
||||
|
||||
song_id = play_queue.current_track.id
|
||||
connection.star_entry(song_id, 'song')
|
||||
connection.unstar_entry(song_id, 'song')
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
|
||||
#
|
||||
# AudioPlayer Handlers
|
||||
#
|
||||
|
||||
|
||||
class PlaybackStartedHandler(AbstractRequestHandler):
|
||||
"""AudioPlayer.PlaybackStarted Directive received.
|
||||
|
||||
Confirming that the requested audio file began playing.
|
||||
Do not send any specific response.
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_request_type('AudioPlayer.PlaybackStarted')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In PlaybackStartedHandler')
|
||||
logger.info('Playback started')
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
|
||||
class PlaybackStoppedHandler(AbstractRequestHandler):
|
||||
"""AudioPlayer.PlaybackStopped Directive received.
|
||||
|
||||
Confirming that the requested audio file stopped playing.
|
||||
Do not send any specific response.
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_request_type('AudioPlayer.PlaybackStopped')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In PlaybackStoppedHandler')
|
||||
|
||||
# store the current offset for later resumption
|
||||
play_queue.current_track.offset = handler_input.request_envelope.request.offset_in_milliseconds
|
||||
logger.debug(f'Stored track offset of: {play_queue.current_track.offset} ms for {play_queue.current_track.title}')
|
||||
logger.info('Playback stopped')
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
|
||||
class PlaybackNearlyFinishedHandler(AbstractRequestHandler):
|
||||
"""AudioPlayer.PlaybackNearlyFinished Directive received.
|
||||
|
||||
Replacing queue with the URL again. This should not happen on live streams.
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_request_type('AudioPlayer.PlaybackNearlyFinished')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In PlaybackNearlyFinishedHandler')
|
||||
logger.info('Queuing next track...')
|
||||
track_details = play_queue.enqueue_next_track()
|
||||
|
||||
return controller.start_playback('continue', None, None, track_details, handler_input)
|
||||
|
||||
|
||||
class PlaybackFinishedHandler(AbstractRequestHandler):
|
||||
"""AudioPlayer.PlaybackFinished Directive received.
|
||||
|
||||
Confirming that the requested audio file completed playing.
|
||||
Do not send any specific response.
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_request_type('AudioPlayer.PlaybackFinished')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In PlaybackFinishedHandler')
|
||||
play_queue.get_next_track()
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
|
||||
class PausePlaybackHandler(AbstractRequestHandler):
|
||||
"""Handler for stopping audio.
|
||||
|
||||
Handles Stop, Cancel and Pause Intents and PauseCommandIssued event.
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return (is_intent_name('AMAZON.StopIntent')(handler_input) or
|
||||
is_intent_name('AMAZON.CancelIntent')(handler_input) or
|
||||
is_intent_name('AMAZON.PauseIntent')(handler_input))
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In PausePlaybackHandler')
|
||||
play_queue.sync()
|
||||
|
||||
return controller.stop(handler_input)
|
||||
|
||||
|
||||
class ResumePlaybackHandler(AbstractRequestHandler):
|
||||
"""Handler for resuming audio on different events.
|
||||
|
||||
Handles PlayAudio Intent, Resume Intent.
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return (is_intent_name('AMAZON.ResumeIntent')(handler_input) or
|
||||
is_intent_name('PlayAudio')(handler_input))
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In ResumePlaybackHandler')
|
||||
|
||||
if play_queue.current_track.offset > 0:
|
||||
# There is a paused track, continue
|
||||
logger.info('Resuming ' + str(play_queue.current_track.title))
|
||||
logger.info('Offset ' + str(play_queue.current_track.offset))
|
||||
|
||||
return controller.start_playback('play', None, None, play_queue.current_track, handler_input)
|
||||
|
||||
elif play_queue.get_queue_count() > 0 and play_queue.current_track.offset == 0:
|
||||
# No paused tracks but tracks in queue
|
||||
logger.info('Resuming - There was no paused track, getting next track from queue')
|
||||
track_details = play_queue.get_next_track()
|
||||
|
||||
return controller.start_playback('play', None, None, track_details, handler_input)
|
||||
|
||||
|
||||
class NextPlaybackHandler(AbstractRequestHandler):
|
||||
"""Handle NextIntent"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_intent_name('AMAZON.NextIntent')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In NextPlaybackHandler')
|
||||
|
||||
track_details = play_queue.get_next_track()
|
||||
|
||||
# Set the offset to 0 as we are skipping we want to start at the beginning
|
||||
track_details.offset = 0
|
||||
|
||||
return controller.start_playback('play', None, None, track_details, handler_input)
|
||||
|
||||
|
||||
class PreviousPlaybackHandler(AbstractRequestHandler):
|
||||
"""Handle PreviousIntent"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_intent_name('AMAZON.PreviousIntent')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In PreviousPlaybackHandler')
|
||||
track_details = play_queue.get_prevous_track()
|
||||
|
||||
# Set the offset to 0 as we are skipping we want to start at the beginning
|
||||
track_details.offset = 0
|
||||
|
||||
return controller.start_playback('play', None, None, track_details, handler_input)
|
||||
|
||||
|
||||
class PlaybackFailedEventHandler(AbstractRequestHandler):
|
||||
"""AudioPlayer.PlaybackFailed Directive received.
|
||||
|
||||
Logging the error and restarting playing with no output speech.
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||
return is_request_type('AudioPlayer.PlaybackFailed')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput) -> Response:
|
||||
logger.debug('In PlaybackFailedHandler')
|
||||
logger.error(f'Playback Failed: {handler_input.request_envelope.request.error}')
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
|
||||
#
|
||||
# Exception Handers
|
||||
#
|
||||
|
||||
|
||||
class SystemExceptionHandler(AbstractExceptionHandler):
|
||||
"""Handle System.ExceptionEncountered
|
||||
|
||||
Handles exceptions and prints error information
|
||||
in the log
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput, exception: Exception) -> bool:
|
||||
return is_request_type('System.ExceptionEncountered')(handler_input)
|
||||
|
||||
def handle(self, handler_input: HandlerInput, exception: Exception) -> Response:
|
||||
logger.debug('In SystemExceptionHandler')
|
||||
|
||||
# Log the exception
|
||||
logger.error(f'System Exception: {exception}')
|
||||
logger.error(f'Request Type Was: {get_request_type(handler_input)}')
|
||||
error = handler_input.request_envelope.request.to_dict()
|
||||
logger.error(f"Details: {error.get('error').get('message')}")
|
||||
|
||||
if get_request_type(handler_input) == 'IntentRequest':
|
||||
logger.error(f'Intent Name Was: {get_intent_name(handler_input)}')
|
||||
|
||||
speech = "Sorry, I didn't get that. Can you please say it again!!"
|
||||
handler_input.response_builder.speak(speech).ask(speech)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
|
||||
class GeneralExceptionHandler(AbstractExceptionHandler):
|
||||
"""Handle general exceptions
|
||||
|
||||
Handles exceptions and prints error information
|
||||
in the log
|
||||
"""
|
||||
|
||||
def can_handle(self, handler_input: HandlerInput, exception: Exception) -> bool:
|
||||
return True
|
||||
|
||||
def handle(self, handler_input: HandlerInput, exception: Exception) -> Response:
|
||||
logger.debug('In GeneralExceptionHandler')
|
||||
|
||||
# Log the exception
|
||||
logger.error(f'General Exception: {exception}')
|
||||
logger.error(f'Request Type Was: {get_request_type(handler_input)}')
|
||||
|
||||
if get_request_type(handler_input) == 'IntentRequest':
|
||||
logger.error(f'Intent Name Was: {get_intent_name(handler_input)}')
|
||||
|
||||
speech = "Sorry, I didn't get that. Can you please say it again!!"
|
||||
handler_input.response_builder.speak(speech).ask(speech)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
|
||||
|
||||
#
|
||||
# Request Interceptors
|
||||
#
|
||||
|
||||
|
||||
class LoggingRequestInterceptor(AbstractRequestInterceptor):
|
||||
"""Intercept all requests
|
||||
|
||||
Intercepts all requests sent to the skill and prints them in the log
|
||||
"""
|
||||
|
||||
def process(self, handler_input: HandlerInput):
|
||||
logger.debug(f'Request received: {handler_input.request_envelope.request}')
|
||||
|
||||
|
||||
class LoggingResponseInterceptor(AbstractResponseInterceptor):
|
||||
"""Intercept all responses
|
||||
|
||||
Intercepts all responses sent from the skill and prints them in the log
|
||||
"""
|
||||
|
||||
def process(self, handler_input: HandlerInput, response: Response):
|
||||
logger.debug(f'Response sent: {response}')
|
||||
|
||||
|
||||
# Register Intent Handlers
|
||||
sb.add_request_handler(LaunchRequestHandler())
|
||||
sb.add_request_handler(CheckAudioInterfaceHandler())
|
||||
sb.add_request_handler(SkillEventHandler())
|
||||
sb.add_request_handler(HelpHandler())
|
||||
sb.add_request_handler(NaviSonicPlayMusicByArtist())
|
||||
sb.add_request_handler(NaviSonicPlayAlbumByArtist())
|
||||
sb.add_request_handler(NaviSonicPlaySongByArtist())
|
||||
sb.add_request_handler(NaviSonicPlayPlaylist())
|
||||
sb.add_request_handler(NaviSonicPlayFavouriteSongs())
|
||||
sb.add_request_handler(NaviSonicPlayMusicByGenre())
|
||||
sb.add_request_handler(NaviSonicPlayMusicRandom())
|
||||
sb.add_request_handler(NaviSonicSongDetails())
|
||||
sb.add_request_handler(NaviSonicStarSong())
|
||||
sb.add_request_handler(NaviSonicUnstarSong())
|
||||
|
||||
# Register AutoPlayer Handlers
|
||||
sb.add_request_handler(PlaybackStartedHandler())
|
||||
sb.add_request_handler(PlaybackStoppedHandler())
|
||||
sb.add_request_handler(PlaybackNearlyFinishedHandler())
|
||||
sb.add_request_handler(PlaybackFinishedHandler())
|
||||
sb.add_request_handler(PausePlaybackHandler())
|
||||
sb.add_request_handler(NextPlaybackHandler())
|
||||
sb.add_request_handler(PreviousPlaybackHandler())
|
||||
sb.add_request_handler(ResumePlaybackHandler())
|
||||
sb.add_request_handler(PlaybackFailedEventHandler())
|
||||
|
||||
|
||||
# Register Exception Handlers
|
||||
sb.add_exception_handler(SystemExceptionHandler())
|
||||
sb.add_exception_handler(GeneralExceptionHandler())
|
||||
|
||||
# Register Interceptors (log all requests)
|
||||
# sb.add_global_request_interceptor(LoggingRequestInterceptor())
|
||||
# sb.add_global_response_interceptor(LoggingResponseInterceptor())
|
||||
|
||||
sa = SkillAdapter(skill=sb.create(), skill_id='test', app=app)
|
||||
sa.register(app=app, route='/')
|
||||
|
||||
# Enable queue and history diagnostics
|
||||
if 'NAVI_DEBUG' in os.environ:
|
||||
logger.warning('AskNavidrome debugging has been enabled, this should only be used when testing!')
|
||||
logger.warning('The /buffer, /queue and /history http endpoints are available publicly!')
|
||||
|
||||
@app.route('/queue')
|
||||
def view_queue():
|
||||
"""View the contents of play_queue.queue
|
||||
|
||||
Creates a tabulated page contining the contents of the play_queue.queue deque.
|
||||
"""
|
||||
|
||||
return render_template('table.html', title='AskNavidrome - Queued Tracks',
|
||||
tracks=play_queue.queue, current=play_queue.current_track)
|
||||
|
||||
@app.route('/history')
|
||||
def view_history():
|
||||
"""View the contents of play_queue.history
|
||||
|
||||
Creates a tabulated page contining the contents of the play_queue.history deque.
|
||||
"""
|
||||
|
||||
return render_template('table.html', title='AskNavidrome - Track History',
|
||||
tracks=play_queue.history, current=play_queue.current_track)
|
||||
|
||||
@app.route('/buffer')
|
||||
def view_buffer():
|
||||
"""View the contents of play_queue.buffer
|
||||
|
||||
Creates a tabulated page contining the contents of the play_queue.buffer deque.
|
||||
"""
|
||||
|
||||
return render_template('table.html', title='AskNavidrome - Buffered Tracks',
|
||||
tracks=play_queue.buffer, current=play_queue.current_track)
|
||||
|
||||
|
||||
# Run web app by default when file is executed.
|
||||
if __name__ == '__main__':
|
||||
# Start the web service
|
||||
app.run(host='0.0.0.0')
|
||||
Reference in New Issue
Block a user