- Added multi threading to all intents that enqueue songs, skill responds much faster and combats issues with timeouts
- Resolves #61
This commit is contained in:
129
skill/app.py
129
skill/app.py
@@ -1,6 +1,6 @@
|
|||||||
from flask import Flask, render_template
|
from flask import Flask, render_template
|
||||||
import logging
|
import logging
|
||||||
from multiprocessing import Process, Manager
|
from multiprocessing import Process
|
||||||
from multiprocessing.managers import BaseManager
|
from multiprocessing.managers import BaseManager
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
@@ -294,8 +294,15 @@ class NaviSonicPlayMusicByArtist(AbstractRequestHandler):
|
|||||||
return is_intent_name('NaviSonicPlayMusicByArtist')(handler_input)
|
return is_intent_name('NaviSonicPlayMusicByArtist')(handler_input)
|
||||||
|
|
||||||
def handle(self, handler_input: HandlerInput) -> Response:
|
def handle(self, handler_input: HandlerInput) -> Response:
|
||||||
|
global backgroundProcess
|
||||||
logger.debug('In NaviSonicPlayMusicByArtist')
|
logger.debug('In NaviSonicPlayMusicByArtist')
|
||||||
|
|
||||||
|
# Check if a background process is already running, if it is then terminate the process
|
||||||
|
# in favour of the new process.
|
||||||
|
if backgroundProcess != None:
|
||||||
|
backgroundProcess.terminate()
|
||||||
|
backgroundProcess.join()
|
||||||
|
|
||||||
# Get the requested artist
|
# Get the requested artist
|
||||||
artist = get_slot_value_v2(handler_input, 'artist')
|
artist = get_slot_value_v2(handler_input, 'artist')
|
||||||
|
|
||||||
@@ -315,7 +322,11 @@ class NaviSonicPlayMusicByArtist(AbstractRequestHandler):
|
|||||||
# Build a list of songs to play
|
# Build a list of songs to play
|
||||||
song_id_list = connection.build_song_list_from_albums(artist_album_lookup, min_song_count)
|
song_id_list = connection.build_song_list_from_albums(artist_album_lookup, min_song_count)
|
||||||
play_queue.clear()
|
play_queue.clear()
|
||||||
controller.enqueue_songs(connection, play_queue, song_id_list)
|
|
||||||
|
controller.enqueue_songs(connection, play_queue, [song_id_list[0], song_id_list[1]]) # When generating the playlist return the first two tracks.
|
||||||
|
backgroundProcess = Process(target=queueWorkerThread, args=(connection, play_queue, song_id_list[2:])) # Create a thread to enqueue the remaining tracks
|
||||||
|
backgroundProcess.start() # Start the additional thread
|
||||||
|
|
||||||
speech = f'Playing music by: {artist.value}'
|
speech = f'Playing music by: {artist.value}'
|
||||||
logger.info(speech)
|
logger.info(speech)
|
||||||
|
|
||||||
@@ -338,8 +349,15 @@ class NaviSonicPlayAlbumByArtist(AbstractRequestHandler):
|
|||||||
return is_intent_name('NaviSonicPlayAlbumByArtist')(handler_input)
|
return is_intent_name('NaviSonicPlayAlbumByArtist')(handler_input)
|
||||||
|
|
||||||
def handle(self, handler_input: HandlerInput) -> Response:
|
def handle(self, handler_input: HandlerInput) -> Response:
|
||||||
|
global backgroundProcess
|
||||||
logger.debug('In NaviSonicPlayAlbumByArtist')
|
logger.debug('In NaviSonicPlayAlbumByArtist')
|
||||||
|
|
||||||
|
# Check if a background process is already running, if it is then terminate the process
|
||||||
|
# in favour of the new process.
|
||||||
|
if backgroundProcess != None:
|
||||||
|
backgroundProcess.terminate()
|
||||||
|
backgroundProcess.join()
|
||||||
|
|
||||||
# Get variables from intent
|
# Get variables from intent
|
||||||
artist = get_slot_value_v2(handler_input, 'artist')
|
artist = get_slot_value_v2(handler_input, 'artist')
|
||||||
album = get_slot_value_v2(handler_input, 'album')
|
album = get_slot_value_v2(handler_input, 'album')
|
||||||
@@ -371,9 +389,13 @@ class NaviSonicPlayAlbumByArtist(AbstractRequestHandler):
|
|||||||
return handler_input.response_builder.response
|
return handler_input.response_builder.response
|
||||||
|
|
||||||
# At this point we have found an album that matches
|
# At this point we have found an album that matches
|
||||||
songs = connection.build_song_list_from_albums(result, -1)
|
song_id_list = connection.build_song_list_from_albums(result, -1)
|
||||||
play_queue.clear()
|
play_queue.clear()
|
||||||
controller.enqueue_songs(connection, play_queue, songs)
|
|
||||||
|
# Work around the Amazon / Alexa 8 second timeout.
|
||||||
|
controller.enqueue_songs(connection, play_queue, [song_id_list[0], song_id_list[1]]) # When generating the playlist return the first two tracks.
|
||||||
|
backgroundProcess = Process(target=queueWorkerThread, args=(connection, play_queue, song_id_list[2:])) # Create a thread to enqueue the remaining tracks
|
||||||
|
backgroundProcess.start() # Start the additional thread
|
||||||
|
|
||||||
speech = f'Playing {album.value} by: {artist.value}'
|
speech = f'Playing {album.value} by: {artist.value}'
|
||||||
logger.info(speech)
|
logger.info(speech)
|
||||||
@@ -397,9 +419,14 @@ class NaviSonicPlayAlbumByArtist(AbstractRequestHandler):
|
|||||||
return handler_input.response_builder.response
|
return handler_input.response_builder.response
|
||||||
|
|
||||||
else:
|
else:
|
||||||
songs = connection.build_song_list_from_albums(result, -1)
|
song_id_list = connection.build_song_list_from_albums(result, -1)
|
||||||
play_queue.clear()
|
play_queue.clear()
|
||||||
controller.enqueue_songs(connection, play_queue, songs)
|
|
||||||
|
# Work around the Amazon / Alexa 8 second timeout.
|
||||||
|
controller.enqueue_songs(connection, play_queue, [song_id_list[0], song_id_list[1]]) # When generating the playlist return the first two tracks.
|
||||||
|
backgroundProcess = Process(target=queueWorkerThread, args=(connection, play_queue, song_id_list[2:])) # Create a thread to enqueue the remaining tracks
|
||||||
|
backgroundProcess.start() # Start the additional thread
|
||||||
|
|
||||||
|
|
||||||
speech = f'Playing {album.value}'
|
speech = f'Playing {album.value}'
|
||||||
logger.info(speech)
|
logger.info(speech)
|
||||||
@@ -531,8 +558,15 @@ class NaviSonicPlayMusicByGenre(AbstractRequestHandler):
|
|||||||
return is_intent_name('NaviSonicPlayMusicByGenre')(handler_input)
|
return is_intent_name('NaviSonicPlayMusicByGenre')(handler_input)
|
||||||
|
|
||||||
def handle(self, handler_input: HandlerInput) -> Response:
|
def handle(self, handler_input: HandlerInput) -> Response:
|
||||||
|
global backgroundProcess
|
||||||
logger.debug('In NaviSonicPlayMusicByGenre')
|
logger.debug('In NaviSonicPlayMusicByGenre')
|
||||||
|
|
||||||
|
# Check if a background process is already running, if it is then terminate the process
|
||||||
|
# in favour of the new process.
|
||||||
|
if backgroundProcess != None:
|
||||||
|
backgroundProcess.terminate()
|
||||||
|
backgroundProcess.join()
|
||||||
|
|
||||||
# Get the requested genre
|
# Get the requested genre
|
||||||
genre = get_slot_value_v2(handler_input, 'genre')
|
genre = get_slot_value_v2(handler_input, 'genre')
|
||||||
|
|
||||||
@@ -547,7 +581,11 @@ class NaviSonicPlayMusicByGenre(AbstractRequestHandler):
|
|||||||
else:
|
else:
|
||||||
random.shuffle(song_id_list)
|
random.shuffle(song_id_list)
|
||||||
play_queue.clear()
|
play_queue.clear()
|
||||||
controller.enqueue_songs(connection, play_queue, song_id_list)
|
|
||||||
|
# Work around the Amazon / Alexa 8 second timeout.
|
||||||
|
controller.enqueue_songs(connection, play_queue, [song_id_list[0], song_id_list[1]]) # When generating the playlist return the first two tracks.
|
||||||
|
backgroundProcess = Process(target=queueWorkerThread, args=(connection, play_queue, song_id_list[2:])) # Create a thread to enqueue the remaining tracks
|
||||||
|
backgroundProcess.start() # Start the additional thread
|
||||||
|
|
||||||
speech = f'Playing {genre.value} music'
|
speech = f'Playing {genre.value} music'
|
||||||
logger.info(speech)
|
logger.info(speech)
|
||||||
@@ -569,8 +607,15 @@ class NaviSonicPlayMusicRandom(AbstractRequestHandler):
|
|||||||
return is_intent_name('NaviSonicPlayMusicRandom')(handler_input)
|
return is_intent_name('NaviSonicPlayMusicRandom')(handler_input)
|
||||||
|
|
||||||
def handle(self, handler_input: HandlerInput) -> Response:
|
def handle(self, handler_input: HandlerInput) -> Response:
|
||||||
|
global backgroundProcess
|
||||||
logger.debug('In NaviSonicPlayMusicRandom')
|
logger.debug('In NaviSonicPlayMusicRandom')
|
||||||
|
|
||||||
|
# Check if a background process is already running, if it is then terminate the process
|
||||||
|
# in favour of the new process.
|
||||||
|
if backgroundProcess != None:
|
||||||
|
backgroundProcess.terminate()
|
||||||
|
backgroundProcess.join()
|
||||||
|
|
||||||
song_id_list = connection.build_random_song_list(min_song_count)
|
song_id_list = connection.build_random_song_list(min_song_count)
|
||||||
|
|
||||||
if song_id_list is None:
|
if song_id_list is None:
|
||||||
@@ -582,7 +627,11 @@ class NaviSonicPlayMusicRandom(AbstractRequestHandler):
|
|||||||
else:
|
else:
|
||||||
random.shuffle(song_id_list)
|
random.shuffle(song_id_list)
|
||||||
play_queue.clear()
|
play_queue.clear()
|
||||||
controller.enqueue_songs(connection, play_queue, song_id_list)
|
|
||||||
|
# Work around the Amazon / Alexa 8 second timeout.
|
||||||
|
controller.enqueue_songs(connection, play_queue, [song_id_list[0], song_id_list[1]]) # When generating the playlist return the first two tracks.
|
||||||
|
backgroundProcess = Process(target=queueWorkerThread, args=(connection, play_queue, song_id_list[2:])) # Create a thread to enqueue the remaining tracks
|
||||||
|
backgroundProcess.start() # Start the additional thread
|
||||||
|
|
||||||
speech = 'Playing random music'
|
speech = 'Playing random music'
|
||||||
logger.info(speech)
|
logger.info(speech)
|
||||||
@@ -604,8 +653,15 @@ class NaviSonicPlayFavouriteSongs(AbstractRequestHandler):
|
|||||||
return is_intent_name('NaviSonicPlayFavouriteSongs')(handler_input)
|
return is_intent_name('NaviSonicPlayFavouriteSongs')(handler_input)
|
||||||
|
|
||||||
def handle(self, handler_input: HandlerInput) -> Response:
|
def handle(self, handler_input: HandlerInput) -> Response:
|
||||||
|
global backgroundProcess
|
||||||
logger.debug('In NaviSonicPlayFavouriteSongs')
|
logger.debug('In NaviSonicPlayFavouriteSongs')
|
||||||
|
|
||||||
|
# Check if a background process is already running, if it is then terminate the process
|
||||||
|
# in favour of the new process.
|
||||||
|
if backgroundProcess != None:
|
||||||
|
backgroundProcess.terminate()
|
||||||
|
backgroundProcess.join()
|
||||||
|
|
||||||
song_id_list = connection.build_song_list_from_favourites()
|
song_id_list = connection.build_song_list_from_favourites()
|
||||||
|
|
||||||
if song_id_list is None:
|
if song_id_list is None:
|
||||||
@@ -617,7 +673,11 @@ class NaviSonicPlayFavouriteSongs(AbstractRequestHandler):
|
|||||||
else:
|
else:
|
||||||
random.shuffle(song_id_list)
|
random.shuffle(song_id_list)
|
||||||
play_queue.clear()
|
play_queue.clear()
|
||||||
controller.enqueue_songs(connection, play_queue, song_id_list)
|
|
||||||
|
# Work around the Amazon / Alexa 8 second timeout.
|
||||||
|
controller.enqueue_songs(connection, play_queue, [song_id_list[0], song_id_list[1]]) # When generating the playlist return the first two tracks.
|
||||||
|
backgroundProcess = Process(target=queueWorkerThread, args=(connection, play_queue, song_id_list[2:])) # Create a thread to enqueue the remaining tracks
|
||||||
|
backgroundProcess.start() # Start the additional thread
|
||||||
|
|
||||||
speech = 'Playing your favourite tracks.'
|
speech = 'Playing your favourite tracks.'
|
||||||
logger.info(speech)
|
logger.info(speech)
|
||||||
@@ -659,9 +719,11 @@ class NaviSonicSongDetails(AbstractRequestHandler):
|
|||||||
def handle(self, handler_input: HandlerInput) -> Response:
|
def handle(self, handler_input: HandlerInput) -> Response:
|
||||||
logger.debug('In NaviSonicSongDetails Handler')
|
logger.debug('In NaviSonicSongDetails Handler')
|
||||||
|
|
||||||
title = play_queue.current_track.title
|
current_track = play_queue.get_current_track()
|
||||||
artist = play_queue.current_track.artist
|
|
||||||
album = play_queue.current_track.album
|
title = current_track.title
|
||||||
|
artist = current_track.artist
|
||||||
|
album = current_track.album
|
||||||
|
|
||||||
text = f'This is {title} by {artist}, from the album {album}'
|
text = f'This is {title} by {artist}, from the album {album}'
|
||||||
handler_input.response_builder.speak(text)
|
handler_input.response_builder.speak(text)
|
||||||
@@ -681,7 +743,9 @@ class NaviSonicStarSong(AbstractRequestHandler):
|
|||||||
def handle(self, handler_input: HandlerInput) -> Response:
|
def handle(self, handler_input: HandlerInput) -> Response:
|
||||||
logger.debug('In NaviSonicStarSong Handler')
|
logger.debug('In NaviSonicStarSong Handler')
|
||||||
|
|
||||||
song_id = play_queue.current_track.id
|
current_track = play_queue.get_current_track()
|
||||||
|
|
||||||
|
song_id = current_track.id
|
||||||
connection.star_entry(song_id, 'song')
|
connection.star_entry(song_id, 'song')
|
||||||
|
|
||||||
return handler_input.response_builder.response
|
return handler_input.response_builder.response
|
||||||
@@ -699,7 +763,9 @@ class NaviSonicUnstarSong(AbstractRequestHandler):
|
|||||||
def handle(self, handler_input: HandlerInput) -> Response:
|
def handle(self, handler_input: HandlerInput) -> Response:
|
||||||
logger.debug('In NaviSonicUnstarSong Handler')
|
logger.debug('In NaviSonicUnstarSong Handler')
|
||||||
|
|
||||||
song_id = play_queue.current_track.id
|
current_track = play_queue.get_current_track()
|
||||||
|
|
||||||
|
song_id = current_track.id
|
||||||
connection.star_entry(song_id, 'song')
|
connection.star_entry(song_id, 'song')
|
||||||
connection.unstar_entry(song_id, 'song')
|
connection.unstar_entry(song_id, 'song')
|
||||||
|
|
||||||
@@ -741,8 +807,10 @@ class PlaybackStoppedHandler(AbstractRequestHandler):
|
|||||||
logger.debug('In PlaybackStoppedHandler')
|
logger.debug('In PlaybackStoppedHandler')
|
||||||
|
|
||||||
# store the current offset for later resumption
|
# store the current offset for later resumption
|
||||||
play_queue.current_track.offset = handler_input.request_envelope.request.offset_in_milliseconds
|
play_queue.set_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}')
|
|
||||||
|
current_track = play_queue.get_current_track()
|
||||||
|
logger.debug(f'Stored track offset of: {current_track.offset} ms for {current_track.title}')
|
||||||
logger.info('Playback stopped')
|
logger.info('Playback stopped')
|
||||||
|
|
||||||
return handler_input.response_builder.response
|
return handler_input.response_builder.response
|
||||||
@@ -813,14 +881,16 @@ class ResumePlaybackHandler(AbstractRequestHandler):
|
|||||||
def handle(self, handler_input: HandlerInput) -> Response:
|
def handle(self, handler_input: HandlerInput) -> Response:
|
||||||
logger.debug('In ResumePlaybackHandler')
|
logger.debug('In ResumePlaybackHandler')
|
||||||
|
|
||||||
if play_queue.current_track.offset > 0:
|
current_track = play_queue.get_current_track()
|
||||||
|
|
||||||
|
if current_track.offset > 0:
|
||||||
# There is a paused track, continue
|
# There is a paused track, continue
|
||||||
logger.info('Resuming ' + str(play_queue.current_track.title))
|
logger.info('Resuming ' + str(current_track.title))
|
||||||
logger.info('Offset ' + str(play_queue.current_track.offset))
|
logger.info('Offset ' + str(current_track.offset))
|
||||||
|
|
||||||
return controller.start_playback('play', None, None, play_queue.current_track, handler_input)
|
return controller.start_playback('play', None, None, current_track, handler_input)
|
||||||
|
|
||||||
elif play_queue.get_queue_count() > 0 and play_queue.current_track.offset == 0:
|
elif play_queue.get_queue_count() > 0 and current_track.offset == 0:
|
||||||
# No paused tracks but tracks in queue
|
# No paused tracks but tracks in queue
|
||||||
logger.info('Resuming - There was no paused track, getting next track from queue')
|
logger.info('Resuming - There was no paused track, getting next track from queue')
|
||||||
track_details = play_queue.get_next_track()
|
track_details = play_queue.get_next_track()
|
||||||
@@ -873,7 +943,8 @@ class PlaybackFailedEventHandler(AbstractRequestHandler):
|
|||||||
def handle(self, handler_input: HandlerInput) -> Response:
|
def handle(self, handler_input: HandlerInput) -> Response:
|
||||||
logger.debug('In PlaybackFailedHandler')
|
logger.debug('In PlaybackFailedHandler')
|
||||||
|
|
||||||
song_id = play_queue.current_track.id
|
current_track = play_queue.get_current_track()
|
||||||
|
song_id = current_track.id
|
||||||
|
|
||||||
# Log failure and track ID
|
# Log failure and track ID
|
||||||
logger.error(f'Playback Failed: {handler_input.request_envelope.request.error}')
|
logger.error(f'Playback Failed: {handler_input.request_envelope.request.error}')
|
||||||
@@ -1022,11 +1093,13 @@ if navidrome_log_level == 3:
|
|||||||
def view_queue():
|
def view_queue():
|
||||||
"""View the contents of play_queue.queue
|
"""View the contents of play_queue.queue
|
||||||
|
|
||||||
Creates a tabulated page contining the contents of the play_queue.queue deque.
|
Creates a tabulated page containing the contents of the play_queue.queue deque.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
current_track = play_queue.get_current_track()
|
||||||
|
|
||||||
return render_template('table.html', title='AskNavidrome - Queued Tracks',
|
return render_template('table.html', title='AskNavidrome - Queued Tracks',
|
||||||
tracks=play_queue.queue, current=play_queue.current_track)
|
tracks=play_queue.get_current_queue(), current=current_track)
|
||||||
|
|
||||||
@app.route('/history')
|
@app.route('/history')
|
||||||
def view_history():
|
def view_history():
|
||||||
@@ -1035,8 +1108,10 @@ if navidrome_log_level == 3:
|
|||||||
Creates a tabulated page contining the contents of the play_queue.history deque.
|
Creates a tabulated page contining the contents of the play_queue.history deque.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
current_track = play_queue.get_current_track()
|
||||||
|
|
||||||
return render_template('table.html', title='AskNavidrome - Track History',
|
return render_template('table.html', title='AskNavidrome - Track History',
|
||||||
tracks=play_queue.history, current=play_queue.current_track)
|
tracks=play_queue.get_history(), current=current_track)
|
||||||
|
|
||||||
@app.route('/buffer')
|
@app.route('/buffer')
|
||||||
def view_buffer():
|
def view_buffer():
|
||||||
@@ -1045,8 +1120,10 @@ if navidrome_log_level == 3:
|
|||||||
Creates a tabulated page contining the contents of the play_queue.buffer deque.
|
Creates a tabulated page contining the contents of the play_queue.buffer deque.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
current_track = play_queue.get_current_track()
|
||||||
|
|
||||||
return render_template('table.html', title='AskNavidrome - Buffered Tracks',
|
return render_template('table.html', title='AskNavidrome - Buffered Tracks',
|
||||||
tracks=play_queue.buffer, current=play_queue.current_track)
|
tracks=play_queue.get_buffer(), current=current_track)
|
||||||
|
|
||||||
|
|
||||||
# Run web app by default when file is executed.
|
# Run web app by default when file is executed.
|
||||||
|
|||||||
@@ -40,6 +40,64 @@ class MediaQueue:
|
|||||||
self.current_track: Track = Track()
|
self.current_track: Track = Track()
|
||||||
"""Property to hold the current track object"""
|
"""Property to hold the current track object"""
|
||||||
|
|
||||||
|
def get_current_track(self) -> Track:
|
||||||
|
"""Method to return current_track attribute
|
||||||
|
|
||||||
|
Added to allow access to the current_track object while using BaseManager
|
||||||
|
for multi threading, as BaseManager does not allow access to class
|
||||||
|
attributes / properties
|
||||||
|
|
||||||
|
:return: A Track object representing the current playing audio track
|
||||||
|
:rtype: Track
|
||||||
|
"""
|
||||||
|
return self.current_track
|
||||||
|
|
||||||
|
def set_current_track_offset(self, offset: int) -> None:
|
||||||
|
"""Method to set the offset of the current track in milliseconds
|
||||||
|
|
||||||
|
Set the offset for the current track in milliseconds. This is used
|
||||||
|
when resuming a paused track to ensure the track isn't played from
|
||||||
|
the beginning again.
|
||||||
|
|
||||||
|
:param offset: The track offset in milliseconds
|
||||||
|
:type offset: int
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.current_track.offset = offset
|
||||||
|
|
||||||
|
def get_current_queue(self) -> deque:
|
||||||
|
"""Get the current queue
|
||||||
|
|
||||||
|
Returns a deque containing the current queue of music to be played
|
||||||
|
|
||||||
|
:return: The current queue
|
||||||
|
:rtype: deque
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.queue
|
||||||
|
|
||||||
|
def get_buffer(self) -> deque:
|
||||||
|
"""Get the buffer
|
||||||
|
|
||||||
|
Returns a deque containing the current buffer
|
||||||
|
|
||||||
|
:return: The current buffer
|
||||||
|
:rtype: deque
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.buffer
|
||||||
|
|
||||||
|
def get_history(self) -> deque:
|
||||||
|
"""Get history
|
||||||
|
|
||||||
|
Returns a deque of tracks that have already been played
|
||||||
|
|
||||||
|
:return: A deque container tracks that have already been played
|
||||||
|
:rtype: deque
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.history
|
||||||
|
|
||||||
def add_track(self, track: Track) -> None:
|
def add_track(self, track: Track) -> None:
|
||||||
"""Add tracks to the queue
|
"""Add tracks to the queue
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user