Merge pull request #62 from rosskouk/feature-scrobbling
- Bumped versions of Actions modules in use - Fix incorrect field name preventing playing Albums by Artist] - Added multi threading to all intents that enqueue songs, skill responds much faster and combats issues with timeouts - Resolves #61 - Added scrobbling functionality, tracks will now be scrobbled if you have enabled the feature on your Navidrome instance. This both scrobbles the track and updates the play count and last played date on Navidrome - Resolves #52 - Resolves #33 - Fixed comments and whitespace
This commit is contained in:
6
.github/workflows/build_sphinx_docs.yml
vendored
6
.github/workflows/build_sphinx_docs.yml
vendored
@@ -12,13 +12,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# https://github.com/marketplace/actions/checkout
|
# https://github.com/marketplace/actions/checkout
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
# https://github.com/marketplace/actions/setup-python
|
# https://github.com/marketplace/actions/setup-python
|
||||||
- name: Install Python
|
- name: Install Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.13'
|
||||||
|
|
||||||
- name: Setup Python environment
|
- name: Setup Python environment
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
154
skill/app.py
154
skill/app.py
@@ -1,6 +1,7 @@
|
|||||||
|
from datetime import datetime
|
||||||
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
|
||||||
@@ -42,7 +43,7 @@ logger.addHandler(handler)
|
|||||||
#
|
#
|
||||||
|
|
||||||
logger.info('AskNavidrome 0.6!')
|
logger.info('AskNavidrome 0.6!')
|
||||||
logger.debug('Getting configutration from the environment...')
|
logger.debug('Getting configuration from the environment...')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if 'NAVI_SKILL_ID' in os.environ:
|
if 'NAVI_SKILL_ID' in os.environ:
|
||||||
@@ -174,7 +175,7 @@ if 'NAVI_DEBUG' in os.environ:
|
|||||||
logger.setLevel(logging.WARNING)
|
logger.setLevel(logging.WARNING)
|
||||||
logger.warning('Log level set to WARNING')
|
logger.warning('Log level set to WARNING')
|
||||||
|
|
||||||
# Create a sharable queue than can be updated by multiple threads to enable larger playlists
|
# Create a shareable queue than can be updated by multiple threads to enable larger playlists
|
||||||
# to be returned in the back ground avoiding the Amazon 8 second timeout
|
# to be returned in the back ground avoiding the Amazon 8 second timeout
|
||||||
BaseManager.register('MediaQueue', queue.MediaQueue)
|
BaseManager.register('MediaQueue', queue.MediaQueue)
|
||||||
manager = BaseManager()
|
manager = BaseManager()
|
||||||
@@ -183,7 +184,7 @@ play_queue = manager.MediaQueue()
|
|||||||
logger.debug('MediaQueue object created...')
|
logger.debug('MediaQueue object created...')
|
||||||
|
|
||||||
# Variable to store the additional thread used to populate large playlists
|
# Variable to store the additional thread used to populate large playlists
|
||||||
# this is used to avoid concurency issues if there is an attempt to load multiple playlists
|
# this is used to avoid concurrency issues if there is an attempt to load multiple playlists
|
||||||
# at the same time.
|
# at the same time.
|
||||||
backgroundProcess = None
|
backgroundProcess = None
|
||||||
|
|
||||||
@@ -294,8 +295,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 +323,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 +350,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')
|
||||||
@@ -362,7 +381,7 @@ class NaviSonicPlayAlbumByArtist(AbstractRequestHandler):
|
|||||||
|
|
||||||
# Search the list of dictionaries for the requested album
|
# Search the list of dictionaries for the requested album
|
||||||
# Strings are all converted to lower case to minimise matching errors
|
# 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()]
|
result = [album_result for album_result in artist_album_lookup if album_result.get('name').lower() == album.value.lower()]
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
text = f"I couldn't find an album called {album.value} by {artist.value} in the collection."
|
text = f"I couldn't find an album called {album.value} by {artist.value} in the collection."
|
||||||
@@ -371,9 +390,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 +420,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)
|
||||||
@@ -506,6 +534,7 @@ class NaviSonicPlayPlaylist(AbstractRequestHandler):
|
|||||||
controller.enqueue_songs(connection, play_queue, [song_id_list[0], song_id_list[1]]) # When generating the playlist return the first two tracks.
|
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 = 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
|
backgroundProcess.start() # Start the additional thread
|
||||||
|
|
||||||
speech = 'Playing playlist ' + str(playlist.value)
|
speech = 'Playing playlist ' + str(playlist.value)
|
||||||
logger.info(speech)
|
logger.info(speech)
|
||||||
card = {'title': 'AskNavidrome',
|
card = {'title': 'AskNavidrome',
|
||||||
@@ -522,17 +551,24 @@ def queueWorkerThread(connection, play_queue, song_id_list):
|
|||||||
logger.debug('Finished playlist processing!')
|
logger.debug('Finished playlist processing!')
|
||||||
|
|
||||||
class NaviSonicPlayMusicByGenre(AbstractRequestHandler):
|
class NaviSonicPlayMusicByGenre(AbstractRequestHandler):
|
||||||
""" Play songs from the given genere
|
""" Play songs from the given genre
|
||||||
|
|
||||||
50 tracks from the given genere are shuffled and played
|
50 tracks from the given genre are shuffled and played
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def can_handle(self, handler_input: HandlerInput) -> bool:
|
def can_handle(self, handler_input: HandlerInput) -> bool:
|
||||||
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 +583,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 +609,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 +629,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 +655,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 +675,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 +721,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 +745,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 +765,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 +809,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
|
||||||
@@ -777,6 +847,11 @@ class PlaybackFinishedHandler(AbstractRequestHandler):
|
|||||||
|
|
||||||
def handle(self, handler_input: HandlerInput) -> Response:
|
def handle(self, handler_input: HandlerInput) -> Response:
|
||||||
logger.debug('In PlaybackFinishedHandler')
|
logger.debug('In PlaybackFinishedHandler')
|
||||||
|
|
||||||
|
# Generate a timestamp in milliseconds for scrobbling
|
||||||
|
timestamp_ms = datetime.now().timestamp()
|
||||||
|
current_track = play_queue.get_current_track()
|
||||||
|
connection.scrobble(current_track.id, timestamp_ms)
|
||||||
play_queue.get_next_track()
|
play_queue.get_next_track()
|
||||||
|
|
||||||
return handler_input.response_builder.response
|
return handler_input.response_builder.response
|
||||||
@@ -813,14 +888,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()
|
||||||
@@ -853,7 +930,7 @@ class PreviousPlaybackHandler(AbstractRequestHandler):
|
|||||||
|
|
||||||
def handle(self, handler_input: HandlerInput) -> Response:
|
def handle(self, handler_input: HandlerInput) -> Response:
|
||||||
logger.debug('In PreviousPlaybackHandler')
|
logger.debug('In PreviousPlaybackHandler')
|
||||||
track_details = play_queue.get_prevous_track()
|
track_details = play_queue.get_previous_track()
|
||||||
|
|
||||||
# Set the offset to 0 as we are skipping we want to start at the beginning
|
# Set the offset to 0 as we are skipping we want to start at the beginning
|
||||||
track_details.offset = 0
|
track_details.offset = 0
|
||||||
@@ -873,7 +950,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,31 +1100,37 @@ 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():
|
||||||
"""View the contents of play_queue.history
|
"""View the contents of play_queue.history
|
||||||
|
|
||||||
Creates a tabulated page contining the contents of the play_queue.history deque.
|
Creates a tabulated page containing 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():
|
||||||
"""View the contents of play_queue.buffer
|
"""View the contents of play_queue.buffer
|
||||||
|
|
||||||
Creates a tabulated page contining the contents of the play_queue.buffer deque.
|
Creates a tabulated page containing 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
|
||||||
|
|
||||||
@@ -130,7 +188,7 @@ class MediaQueue:
|
|||||||
|
|
||||||
return self.current_track
|
return self.current_track
|
||||||
|
|
||||||
def get_prevous_track(self) -> Track:
|
def get_previous_track(self) -> Track:
|
||||||
"""Get the previous track
|
"""Get the previous track
|
||||||
|
|
||||||
Get the last track added to the history deque and
|
Get the last track added to the history deque and
|
||||||
@@ -140,7 +198,7 @@ class MediaQueue:
|
|||||||
:rtype: Track
|
:rtype: Track
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.logger.debug('In get_prevous_track()')
|
self.logger.debug('In get_previous_track()')
|
||||||
|
|
||||||
# Return the current track to the queue
|
# Return the current track to the queue
|
||||||
self.queue.appendleft(self.current_track)
|
self.queue.appendleft(self.current_track)
|
||||||
@@ -200,7 +258,7 @@ class MediaQueue:
|
|||||||
return len(self.history)
|
return len(self.history)
|
||||||
|
|
||||||
def sync(self) -> None:
|
def sync(self) -> None:
|
||||||
"""Syncronise the buffer with the queue
|
"""Synchronise the buffer with the queue
|
||||||
|
|
||||||
Overwrite the buffer with the current queue.
|
Overwrite the buffer with the current queue.
|
||||||
This is useful when pausing or stopping to ensure
|
This is useful when pausing or stopping to ensure
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class SubsonicConnection:
|
|||||||
:param str server_url: The URL of the Subsonic API compatible media server
|
:param str server_url: The URL of the Subsonic API compatible media server
|
||||||
:param str user: Username to authenticate against the API
|
:param str user: Username to authenticate against the API
|
||||||
:param str passwd: Password to authenticate against the API
|
:param str passwd: Password to authenticate against the API
|
||||||
:param int port: Port the Subsonic compatibe server is listening on
|
:param int port: Port the Subsonic compatible server is listening on
|
||||||
:param str api_location: Path to the API, this is appended to server_url
|
:param str api_location: Path to the API, this is appended to server_url
|
||||||
:param str api_version: The version of the Subsonic API that is in use
|
:param str api_version: The version of the Subsonic API that is in use
|
||||||
:return: None
|
:return: None
|
||||||
@@ -64,6 +64,19 @@ class SubsonicConnection:
|
|||||||
|
|
||||||
return self.conn.ping()
|
return self.conn.ping()
|
||||||
|
|
||||||
|
def scrobble(self, track_id: str, time: int) -> None:
|
||||||
|
"""Scrobble the given track
|
||||||
|
|
||||||
|
:param str track_id: The ID of the track to scrobble
|
||||||
|
:param int time: UNIX timestamp of track play time
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.logger.debug('In function scrobble()')
|
||||||
|
|
||||||
|
self.conn.scrobble(track_id, True, time)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def search_playlist(self, term: str) -> Union[str, None]:
|
def search_playlist(self, term: str) -> Union[str, None]:
|
||||||
"""Search the media server for the given playlist
|
"""Search the media server for the given playlist
|
||||||
|
|
||||||
@@ -271,7 +284,7 @@ class SubsonicConnection:
|
|||||||
def build_song_list_from_genre(self, genre: str, count: int) -> Union[list, None]:
|
def build_song_list_from_genre(self, genre: str, count: int) -> Union[list, None]:
|
||||||
"""Build a shuffled list songs of songs from the given genre.
|
"""Build a shuffled list songs of songs from the given genre.
|
||||||
|
|
||||||
:param str genre: The genre, acceptible values are with the getGenres Subsonic API call.
|
:param str genre: The genre, acceptable values are with the getGenres Subsonic API call.
|
||||||
:param int count: The number of songs to return
|
:param int count: The number of songs to return
|
||||||
:return: A list of song IDs or None if no tracks are found.
|
:return: A list of song IDs or None if no tracks are found.
|
||||||
:rtype: list | None
|
:rtype: list | None
|
||||||
@@ -279,7 +292,7 @@ class SubsonicConnection:
|
|||||||
|
|
||||||
self.logger.debug('In function build_song_list_from_genre()')
|
self.logger.debug('In function build_song_list_from_genre()')
|
||||||
|
|
||||||
# Note the use of title() to captalise the first letter of each word in the genre
|
# Note the use of title() to capitalise the first letter of each word in the genre
|
||||||
# without this the genres do not match the strings returned by the API.
|
# without this the genres do not match the strings returned by the API.
|
||||||
self.logger.debug(f'Searching for {genre.title()} music')
|
self.logger.debug(f'Searching for {genre.title()} music')
|
||||||
songs_from_genre = self.conn.getSongsByGenre(genre.title(), count).get('songsByGenre').get('song')
|
songs_from_genre = self.conn.getSongsByGenre(genre.title(), count).get('songsByGenre').get('song')
|
||||||
|
|||||||
Reference in New Issue
Block a user