Compare commits

...

20 Commits

Author SHA1 Message Date
nunks 87920bb2c0 dockerfile buscando da branch modificada 2026-05-31 18:23:43 -03:00
nunks bf73f99907 Tentando conciliar as novidades com as minhas alteracoes 2026-05-31 17:46:32 -03:00
Ross Stewart e0b6be24c1 Bump version number
Documentation Generator / Build Sphinx documentation (push) Has been cancelled
2026-03-06 15:49:51 +00:00
Ross Stewart cd3575d514 Documentation update 2026-03-06 15:15:09 +00:00
rosskouk 77e8546baa Merge pull request #73 from rosskouk/feature-enhanced-debugging
Added enhanced debugging to ping() method
2026-03-06 15:14:33 +00:00
rosskouk 122d328de4 Merge pull request #66 from f-longobardi/main
Enable display of song metadata
Thanks to @f-longobardi
2026-03-06 15:13:01 +00:00
Ross Stewart f3846a2311 Added enhanced debugging to ping() method 2026-03-06 14:27:29 +00:00
nunks 496ff9bb52 Adicionando skills para tocar mais musicas do artista/album que esta sendo tocado no momento
Documentation Generator / Build Sphinx documentation (push) Has been cancelled
2025-11-15 18:24:35 -03:00
nunks b27b77c572 Adicionando skills para tocar mais musicas do artista/album que esta sendo tocado no momento
Documentation Generator / Build Sphinx documentation (push) Has been cancelled
2025-11-15 18:08:49 -03:00
nunks e0b8cb71e4 Adicionando skills para tocar mais musicas do artista/album que esta sendo tocado no momento
Documentation Generator / Build Sphinx documentation (push) Has been cancelled
2025-11-15 17:56:11 -03:00
nunks a702b1befd Adicionando skills para tocar mais musicas do artista/album que esta sendo tocado no momento
Documentation Generator / Build Sphinx documentation (push) Has been cancelled
2025-11-15 17:50:11 -03:00
nunks d748ee2602 Adicionando skills para tocar mais musicas do artista/album que esta sendo tocado no momento
Documentation Generator / Build Sphinx documentation (push) Has been cancelled
2025-11-15 17:38:56 -03:00
nunks 609109dd17 Adicionando skills para tocar mais musicas do artista/album que esta sendo tocado no momento
Documentation Generator / Build Sphinx documentation (push) Has been cancelled
2025-11-15 17:15:49 -03:00
nunks c44b22d486 melhorando as falas da alexa
Documentation Generator / Build Sphinx documentation (push) Has been cancelled
2025-11-08 21:39:53 -03:00
nunks 4a36f0561b dockerfile mais enxuto
Documentation Generator / Build Sphinx documentation (push) Has been cancelled
2025-11-08 20:57:13 -03:00
nunks 4e9d03e774 testando alterar dockerfile
Documentation Generator / Build Sphinx documentation (push) Has been cancelled
2025-11-08 20:44:27 -03:00
nunks 46e9203677 testando alterar dockerfile
Documentation Generator / Build Sphinx documentation (push) Has been cancelled
2025-11-08 20:39:17 -03:00
nunks aa4d19aafd testando alterar dockerfile
Documentation Generator / Build Sphinx documentation (push) Has been cancelled
2025-11-08 13:03:04 -03:00
Francesco 0cf1c52743 also for continued play 2025-10-25 16:31:15 +02:00
Francesco fcce6a8d11 add song metadata, respond to button requests in media player 2025-10-25 14:56:38 +02:00
7 changed files with 168 additions and 40 deletions
+9 -20
View File
@@ -1,34 +1,23 @@
FROM alpine:3.22.1 AS build FROM alpine:3.22.1 AS build
LABEL maintainer="Ross Stewart <rosskouk@gmail.com>"
LABEL org.opencontainers.image.source=https://github.com/rosskouk/asknavidrome
RUN apk add python3 py3-pip git build-base python3-dev libffi-dev openssl-dev RUN apk add python3 py3-pip git build-base python3-dev libffi-dev openssl-dev
WORKDIR /opt WORKDIR /opt
RUN pwd
RUN python3 -m venv env RUN python3 -m venv env
RUN ls -la
RUN git clone https://github.com/rosskouk/asknavidrome.git RUN git clone -b nunks https://g.nunks.org/nunks/asknavidrome-cafofo.git
RUN ls -la asknavidrome-cafofo
WORKDIR /opt/asknavidrome WORKDIR /opt/asknavidrome-cafofo
RUN pwd
RUN ls -la
RUN source ../env/bin/activate && pip --no-cache-dir install wheel && pip --no-cache-dir install -r skill/requirements-docker.txt RUN source ../env/bin/activate && pip --no-cache-dir install wheel && pip --no-cache-dir install -r skill/requirements-docker.txt
FROM alpine:3.22.1 FROM alpine:3.22.1
LABEL maintainer="Ross Stewart <rosskouk@gmail.com>"
LABEL org.opencontainers.image.source=https://github.com/rosskouk/asknavidrome
RUN apk add python3 RUN apk add python3
COPY --from=build /opt/env /opt/env COPY --from=build /opt/env /opt/env
COPY --from=build /opt/asknavidrome/skill /opt/asknavidrome/ COPY --from=build /opt/asknavidrome-cafofo/skill /opt/asknavidrome/
WORKDIR /opt/asknavidrome WORKDIR /opt/asknavidrome
# Activate Python Virtual Environment # Activate Python Virtual Environment
ENV PATH="/opt/env/bin:$PATH" ENV PATH="/opt/env/bin:$PATH"
EXPOSE 5000 EXPOSE 5000
ENTRYPOINT ["python3", "app.py"]
ENTRYPOINT ["python3", "app.py"]
+17
View File
@@ -73,6 +73,14 @@
"Tocar o disco {album} da {artist}" "Tocar o disco {album} da {artist}"
] ]
}, },
{
"name": "NaviSonicPlayMusicByCurrentAlbum",
"slots": [],
"samples": [
"Tocar mais músicas desse album",
"Tocar mais músicas desse disco"
]
},
{ {
"name": "NaviSonicPlayPlaylist", "name": "NaviSonicPlayPlaylist",
"slots": [ "slots": [
@@ -138,6 +146,15 @@
"Tocar {song} da {artist}" "Tocar {song} da {artist}"
] ]
}, },
{
"name": "NaviSonicPlayMusicByCurrentArtist",
"slots": [],
"samples": [
"Tocar mais músicas desse artista",
"Tocar mais músicas desse grupo",
"Tocar mais músicas dessa banda"
]
},
{ {
"name": "NaviSonicPlayFavouriteSongs", "name": "NaviSonicPlayFavouriteSongs",
"slots": [], "slots": [],
+3 -1
View File
@@ -3491,7 +3491,9 @@ embedded in the URI</p>
<span class="sig-name descname"><span class="pre">ping</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">&#x2192;</span> <span class="sig-return-typehint"><span class="pre">bool</span></span></span><a class="headerlink" href="#asknavidrome.subsonic_api.SubsonicConnection.ping" title="Link to this definition">#</a></dt> <span class="sig-name descname"><span class="pre">ping</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">&#x2192;</span> <span class="sig-return-typehint"><span class="pre">bool</span></span></span><a class="headerlink" href="#asknavidrome.subsonic_api.SubsonicConnection.ping" title="Link to this definition">#</a></dt>
<dd><p>Ping a Subsonic API server</p> <dd><p>Ping a Subsonic API server</p>
<p>Verify the connection to a Subsonic compatible API server <p>Verify the connection to a Subsonic compatible API server
is working</p> is working. Enhanced logging has been added to examine the
HTTP response returned from the server to assist with troubleshooting
connection issues.</p>
<dl class="field-list simple"> <dl class="field-list simple">
<dt class="field-odd">Returns<span class="colon">:</span></dt> <dt class="field-odd">Returns<span class="colon">:</span></dt>
<dd class="field-odd"><p>True if the connection works, False if it does not</p> <dd class="field-odd"><p>True if the connection works, False if it does not</p>
+1 -1
View File
File diff suppressed because one or more lines are too long
+102 -6
View File
@@ -42,7 +42,7 @@ logger.addHandler(handler)
# Get service configuration # Get service configuration
# #
logger.info('AskNavidrome 0.9!') logger.info('AskNavidrome 0.10!')
logger.debug('Getting configuration from the environment...') logger.debug('Getting configuration from the environment...')
try: try:
@@ -341,6 +341,97 @@ class NaviSonicPlayMusicByArtist(AbstractRequestHandler):
track_details = play_queue.get_next_track() track_details = play_queue.get_next_track()
return controller.start_playback('play', speech, card, track_details, handler_input) return controller.start_playback('play', speech, card, track_details, handler_input)
def get_current_song_details():
current_track = play_queue.get_current_track()
logger.debug(current_track)
return {
"title": current_track.title,
"artist": current_track.artist,
"album": current_track.album,
"_sanitized": {
"title": sanitise_speech_output(current_track.title),
"artist": sanitise_speech_output(current_track.artist),
"album": sanitise_speech_output(current_track.album)
}
}
class NaviSonicPlayMusicByCurrentArtist(AbstractRequestHandler):
"""Handle NaviSonicPlayMusicByCurrentArtist Intent
Plays more music of the current playing Artist
"""
def can_handle(self, handler_input: HandlerInput) -> bool:
return is_intent_name('NaviSonicPlayMusicByCurrentArtist')(handler_input)
def handle(self, handler_input: HandlerInput) -> Response:
global backgroundProcess
logger.debug('In NaviSonicPlayMusicByCurrentArtist Handler (custom)')
if backgroundProcess is not None:
backgroundProcess.terminate()
backgroundProcess.join()
t = get_current_song_details() #current "T"rack
artist_lookup = connection.search_artist(t["artist"])
artist_album_lookup = connection.albums_by_artist(artist_lookup[0].get('id'))
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[0], song_id_list[1]]) # When generating the playlist return the first two tracks.
backgroundProcess = Process(target=queue_worker_thread, args=(connection, play_queue, song_id_list[2:])) # Create a thread to enqueue the remaining tracks
backgroundProcess.start() # Start the additional thread
speech = sanitise_speech_output(f'Tocando mais músicas de {t["artist"]}')
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 NaviSonicPlayMusicByCurrentAlbum(AbstractRequestHandler):
"""Handle NaviSonicPlayMusicByCurrentAlbum Intent
Plays more music of the current playing Album
"""
def can_handle(self, handler_input: HandlerInput) -> bool:
return is_intent_name('NaviSonicPlayMusicByCurrentAlbum')(handler_input)
def handle(self, handler_input: HandlerInput) -> Response:
global backgroundProcess
logger.debug('In NaviSonicPlayMusicByCurrentAlbum Handler (custom)')
if backgroundProcess is not None:
backgroundProcess.terminate()
backgroundProcess.join()
t = get_current_song_details() #current "T"rack
artist_lookup = connection.search_artist(t["artist"])
artist_album_lookup = connection.albums_by_artist(artist_lookup[0].get('id'))
result = [album_result for album_result in artist_album_lookup if album_result.get('name').lower() == t["album"].lower()]
song_id_list = connection.build_song_list_from_albums(result, -1)
play_queue.clear()
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=queue_worker_thread, args=(connection, play_queue, song_id_list[2:])) # Create a thread to enqueue the remaining tracks
backgroundProcess.start() # Start the additional thread
speech = sanitise_speech_output(f'Tocando mais músicas do album {t["album"]}, de {t["artist"]}')
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): class NaviSonicPlayAlbumByArtist(AbstractRequestHandler):
"""Handle NaviSonicPlayAlbumByArtist """Handle NaviSonicPlayAlbumByArtist
@@ -670,7 +761,7 @@ class NaviSonicPlayFavouriteSongs(AbstractRequestHandler):
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:
text = sanitise_speech_output("Você não tem nada nos favoritos.") text = sanitise_speech_output("Não tenho nada nos favoritos.")
handler_input.response_builder.speak(text).ask(text) handler_input.response_builder.speak(text).ask(text)
return handler_input.response_builder.response return handler_input.response_builder.response
@@ -730,7 +821,8 @@ class NaviSonicSongDetails(AbstractRequestHandler):
artist = sanitise_speech_output(current_track.artist) artist = sanitise_speech_output(current_track.artist)
album = sanitise_speech_output(current_track.album) album = sanitise_speech_output(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}'
text = f'Essa é {title} de {artist}, do album {album}'
handler_input.response_builder.speak(text) handler_input.response_builder.speak(text)
return handler_input.response_builder.response return handler_input.response_builder.response
@@ -912,7 +1004,8 @@ class NextPlaybackHandler(AbstractRequestHandler):
"""Handle NextIntent""" """Handle NextIntent"""
def can_handle(self, handler_input: HandlerInput) -> bool: def can_handle(self, handler_input: HandlerInput) -> bool:
return is_intent_name('AMAZON.NextIntent')(handler_input) return (is_intent_name('AMAZON.NextIntent')(handler_input) or
is_request_type('PlaybackController.NextCommandIssued')(handler_input))
def handle(self, handler_input: HandlerInput) -> Response: def handle(self, handler_input: HandlerInput) -> Response:
logger.debug('In NextPlaybackHandler') logger.debug('In NextPlaybackHandler')
@@ -929,7 +1022,8 @@ class PreviousPlaybackHandler(AbstractRequestHandler):
"""Handle PreviousIntent""" """Handle PreviousIntent"""
def can_handle(self, handler_input: HandlerInput) -> bool: def can_handle(self, handler_input: HandlerInput) -> bool:
return is_intent_name('AMAZON.PreviousIntent')(handler_input) return (is_intent_name('AMAZON.PreviousIntent')(handler_input) or
is_request_type('PlaybackController.PreviousCommandIssued')(handler_input))
def handle(self, handler_input: HandlerInput) -> Response: def handle(self, handler_input: HandlerInput) -> Response:
logger.debug('In PreviousPlaybackHandler') logger.debug('In PreviousPlaybackHandler')
@@ -997,7 +1091,7 @@ class SystemExceptionHandler(AbstractExceptionHandler):
logger.error(f'Intent Name Was: {get_intent_name(handler_input)}') logger.error(f'Intent Name Was: {get_intent_name(handler_input)}')
#speech = sanitise_speech_output("Sorry, I didn't get that. Can you please say it again!!") #speech = sanitise_speech_output("Sorry, I didn't get that. Can you please say it again!!")
speech = sanitise_speech_output("Foi mal, não entendi. Pode repetir!!") speech = sanitise_speech_output("Foi mal, não entendi.")
handler_input.response_builder.speak(speech).ask(speech) handler_input.response_builder.speak(speech).ask(speech)
return handler_input.response_builder.response return handler_input.response_builder.response
@@ -1123,6 +1217,8 @@ sb.add_request_handler(NaviSonicPlayPlaylist())
sb.add_request_handler(NaviSonicPlayFavouriteSongs()) sb.add_request_handler(NaviSonicPlayFavouriteSongs())
sb.add_request_handler(NaviSonicPlayMusicByGenre()) sb.add_request_handler(NaviSonicPlayMusicByGenre())
sb.add_request_handler(NaviSonicPlayMusicRandom()) sb.add_request_handler(NaviSonicPlayMusicRandom())
sb.add_request_handler(NaviSonicPlayMusicByCurrentArtist())
sb.add_request_handler(NaviSonicPlayMusicByCurrentAlbum())
sb.add_request_handler(NaviSonicRandomiseQueue()) sb.add_request_handler(NaviSonicRandomiseQueue())
sb.add_request_handler(NaviSonicSongDetails()) sb.add_request_handler(NaviSonicSongDetails())
sb.add_request_handler(NaviSonicStarSong()) sb.add_request_handler(NaviSonicStarSong())
Regular → Executable
+15 -3
View File
@@ -42,7 +42,19 @@ def start_playback(mode: str, text: str, card_data: dict, track_details: Track,
:return: Amazon Alexa Response class :return: Amazon Alexa Response class
:rtype: Response :rtype: Response
""" """
metadata = AudioItemMetadata(
title=track_details.title,
subtitle=track_details.artist,
art=display.Image(
content_description=track_details.title,
sources=[
display.ImageInstance(
url='https://github.com/navidrome/navidrome/raw/master/resources/logo-192x192.png'
)
]
)
)
if mode == 'play': if mode == 'play':
# Starting playback # Starting playback
logger.debug('In start_playback() - play mode') logger.debug('In start_playback() - play mode')
@@ -67,7 +79,7 @@ def start_playback(mode: str, text: str, card_data: dict, track_details: Track,
url=track_details.uri, url=track_details.uri,
offset_in_milliseconds=track_details.offset, offset_in_milliseconds=track_details.offset,
expected_previous_token=None), expected_previous_token=None),
metadata=add_screen_background(card_data) if card_data else None metadata=metadata
) )
) )
).set_should_end_session(True) ).set_should_end_session(True)
@@ -95,7 +107,7 @@ def start_playback(mode: str, text: str, card_data: dict, track_details: Track,
# if the Previous intent is used # if the Previous intent is used
offset_in_milliseconds=0, offset_in_milliseconds=0,
expected_previous_token=track_details.previous_id), expected_previous_token=track_details.previous_id),
metadata=None metadata=metadata
) )
) )
).set_should_end_session(True) ).set_should_end_session(True)
+21 -9
View File
@@ -40,29 +40,41 @@ class SubsonicConnection:
self.api_version, self.api_version,
False) False)
self.logger.debug('Connected to Navidrome') self.logger.debug('Connecting to Navidrome.....')
def ping(self) -> bool: def ping(self) -> bool:
"""Ping a Subsonic API server """Ping a Subsonic API server
Verify the connection to a Subsonic compatible API server Verify the connection to a Subsonic compatible API server
is working is working. Enhanced logging has been added to examine the
HTTP response returned from the server to assist with troubleshooting
connection issues.
:return: True if the connection works, False if it does not :return: True if the connection works, False if it does not
:rtype: bool :rtype: bool
""" """
self.logger.debug('In function ping()') self.logger.debug('In function ping()')
status = self.conn.ping() http_request = self.conn._getRequest('ping.view')
try:
http_request_result = self.conn._doInfoReq(http_request)
except Exception as e:
self.logger.error('Failed to connect to Navidrome: %s', e, exc_info=True)
return False
if status: self.logger.debug('ping() response from server: %s', http_request_result)
# Success status = http_request_result.get('status')
if status == 'ok':
self.logger.info('Successfully connected to Navidrome') self.logger.info('Successfully connected to Navidrome')
return True
elif status == 'failed':
err = http_request_result.get('error', {})
self.logger.error('Failed to connect to Navidrome: code=%s msg=%s',
err.get('code'), err.get('message'))
return False
else: else:
# Fail self.logger.error('Unexpected error when connecting to Navidrome: %r', status)
self.logger.error('Failed to connect to Navidrome') return False
return self.conn.ping()
def scrobble(self, track_id: str, time: int) -> None: def scrobble(self, track_id: str, time: int) -> None:
"""Scrobble the given track """Scrobble the given track