Compare commits
10 Commits
0dde614fb9
...
2ee1abd813
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ee1abd813 | |||
|
|
0461264f16 | ||
|
|
4cf9480f41 | ||
|
|
8edd3296c2 | ||
|
|
da6e0ff317 | ||
|
|
a8b1087c80 | ||
|
|
67e5c7e20e | ||
|
|
b33209f7df | ||
|
|
da418c55f8 | ||
|
|
1db8ff9b16 |
15
.github/workflows/build_image.yml
vendored
15
.github/workflows/build_image.yml
vendored
@@ -18,10 +18,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.11.1
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -29,14 +35,15 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
uses: docker/metadata-action@v5.8.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
uses: docker/build-push-action@v6.18.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64, linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
@@ -6,21 +6,30 @@
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Current File",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true
|
||||
},
|
||||
{
|
||||
"name": "Python: Flask",
|
||||
"type": "python",
|
||||
"name": "Python: Flask - AskNavidrome",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "flask",
|
||||
"env": {
|
||||
"FLASK_APP": "skill/app.py",
|
||||
"FLASK_ENV": "development",
|
||||
"FLASK_DEBUG": "0",
|
||||
"NAVI_SKILL_ID": "<skill_id>",
|
||||
"NAVI_SONG_COUNT": "50",
|
||||
"NAVI_URL": "https://<url>",
|
||||
"NAVI_USER": "<username>",
|
||||
"NAVI_PASS": "<password>",
|
||||
"NAVI_PORT": "443",
|
||||
"NAVI_API_PATH": "/rest",
|
||||
"NAVI_API_VER": "1.16.1",
|
||||
"NAVI_DEBUG": "3"
|
||||
},
|
||||
"args": [
|
||||
"run",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM alpine:3.15.0 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
|
||||
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
|
||||
|
||||
@@ -15,8 +15,9 @@ WORKDIR /opt/asknavidrome
|
||||
RUN source ../env/bin/activate && pip --no-cache-dir install wheel && pip --no-cache-dir install -r skill/requirements-docker.txt
|
||||
|
||||
|
||||
FROM alpine:3.15.0
|
||||
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
|
||||
|
||||
|
||||
91
alexa.json
91
alexa.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"interactionModel": {
|
||||
"languageModel": {
|
||||
"invocationName": "navisonic",
|
||||
"invocationName": "cafofo music",
|
||||
"intents": [
|
||||
{
|
||||
"name": "AMAZON.CancelIntent",
|
||||
@@ -40,8 +40,8 @@
|
||||
}
|
||||
],
|
||||
"samples": [
|
||||
"play songs by {artist}",
|
||||
"play music by {artist}"
|
||||
"tocar músicas da {artist}",
|
||||
"tocar músicas do {artist}"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -65,8 +65,12 @@
|
||||
}
|
||||
],
|
||||
"samples": [
|
||||
"Play the album {album}",
|
||||
"Play the album {album} by {artist}"
|
||||
"Tocar o album {album}",
|
||||
"Tocar o album {album} do {artist}",
|
||||
"Tocar o album {album} da {artist}",
|
||||
"Tocar o disco {album}",
|
||||
"Tocar o disco {album} do {artist}",
|
||||
"Tocar o disco {album} da {artist}"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -78,49 +82,42 @@
|
||||
}
|
||||
],
|
||||
"samples": [
|
||||
"Play the {playlist} playlist",
|
||||
"Start the {playlist} playlist"
|
||||
"Tocar a playlist {playlist}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NaviSonicSongDetails",
|
||||
"slots": [],
|
||||
"samples": [
|
||||
"What is playing",
|
||||
"Who is singing",
|
||||
"Who's singing",
|
||||
"What album is this song on",
|
||||
"What album is this on",
|
||||
"Which album",
|
||||
"Which album is this song from",
|
||||
"Which artist is this",
|
||||
"What band is this",
|
||||
"Who sings this song",
|
||||
"Who sings this",
|
||||
"What song is this",
|
||||
"What's playing"
|
||||
"O que está tocando",
|
||||
"Quem está tocando",
|
||||
"Quem está cantando",
|
||||
"Qual o album dessa música",
|
||||
"Que banda é essa",
|
||||
"Que musica é essa",
|
||||
"Que banda é essa"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NaviSonicStarSong",
|
||||
"slots": [],
|
||||
"samples": [
|
||||
"Add this song to my favourites",
|
||||
"Favourite this song",
|
||||
"Like this song",
|
||||
"Add this song to my liked songs",
|
||||
"Star this song"
|
||||
"Adicionar música aos favoritos",
|
||||
"Adicionar aos favoritos",
|
||||
"Favoritar essa musica",
|
||||
"Lembrar dessa música",
|
||||
"Eu gosto dessa música"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NaviSonicUnstarSong",
|
||||
"slots": [],
|
||||
"samples": [
|
||||
"Remove this song from favourites",
|
||||
"I don't like this song",
|
||||
"Remove the star from this song",
|
||||
"Delete this song from my favourites",
|
||||
"Unstar this song"
|
||||
"Tirar a música dos favoritos",
|
||||
"Tirar dos favoritos",
|
||||
"Desfavoritar a música",
|
||||
"Esquecer essa música",
|
||||
"Eu não gosto dessa música"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -136,21 +133,18 @@
|
||||
}
|
||||
],
|
||||
"samples": [
|
||||
"Play the song {song} by the band {artist}",
|
||||
"Play {song} by the band {artist}",
|
||||
"Play {song} by {artist}",
|
||||
"Play the song {song} by the artist {artist}"
|
||||
"Tocar a música {song} da banda {artist}",
|
||||
"Tocar {song} da banda {artist}",
|
||||
"Tocar {song} da {artist}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NaviSonicPlayFavouriteSongs",
|
||||
"slots": [],
|
||||
"samples": [
|
||||
"Play my starred tracks",
|
||||
"Play starred tracks",
|
||||
"Play starred songs",
|
||||
"Play my starred songs",
|
||||
"Play my favourite songs"
|
||||
"Tocar favoritos",
|
||||
"Tocar meus favoritos",
|
||||
"Tocar minhas músicas favoritas"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -162,29 +156,26 @@
|
||||
}
|
||||
],
|
||||
"samples": [
|
||||
"Play {genre} songs",
|
||||
"Play {genre} music"
|
||||
"Tocar {genre}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NaviSonicPlayMusicRandom",
|
||||
"slots": [],
|
||||
"samples": [
|
||||
"Play a selection of music",
|
||||
"Play a mix of tracks",
|
||||
"Play a mix of songs",
|
||||
"Play random music",
|
||||
"Play random songs"
|
||||
"Tocar seleção aleatória",
|
||||
"Tocar qualquer coisa",
|
||||
"Tocar mix",
|
||||
"Tocar músicas aleatórias"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NaviSonicRandomiseQueue",
|
||||
"slots": [],
|
||||
"samples": [
|
||||
"randomise the queue",
|
||||
"randomise",
|
||||
"shuffle",
|
||||
"shuffle the queue"
|
||||
"shuffle na lista",
|
||||
"mistura tudo"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -212,4 +203,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
docs/_static/documentation_options.js
vendored
2
docs/_static/documentation_options.js
vendored
@@ -1,5 +1,5 @@
|
||||
const DOCUMENTATION_OPTIONS = {
|
||||
VERSION: '0.8',
|
||||
VERSION: '0.9',
|
||||
LANGUAGE: 'en',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Index — AskNavidrome 0.8 documentation</title>
|
||||
<title>Index — AskNavidrome 0.9 documentation</title>
|
||||
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<link rel="preload" as="script" href="_static/scripts/pydata-sphinx-theme.js?digest=dfe6caa3a7d634c4db9b" />
|
||||
<script src="_static/vendor/fontawesome/6.5.2/js/all.min.js?digest=dfe6caa3a7d634c4db9b"></script>
|
||||
|
||||
<script src="_static/documentation_options.js?v=85e8db4b"></script>
|
||||
<script src="_static/documentation_options.js?v=3e145956"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/sphinx-book-theme.js?v=887ef09a"></script>
|
||||
@@ -133,7 +133,7 @@
|
||||
|
||||
|
||||
|
||||
<p class="title logo__title">AskNavidrome 0.8 documentation</p>
|
||||
<p class="title logo__title">AskNavidrome 0.9 documentation</p>
|
||||
|
||||
</a></div>
|
||||
<div class="sidebar-primary-item">
|
||||
@@ -612,7 +612,7 @@ document.write(`
|
||||
</li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="index.html#app.queueWorkerThread">queueWorkerThread() (in module app)</a>
|
||||
<li><a href="index.html#app.queue_worker_thread">queue_worker_thread() (in module app)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
</tr></table>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>AskNavidrome Alexa Skill Documentation — AskNavidrome 0.8 documentation</title>
|
||||
<title>AskNavidrome Alexa Skill Documentation — AskNavidrome 0.9 documentation</title>
|
||||
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<link rel="preload" as="script" href="_static/scripts/pydata-sphinx-theme.js?digest=dfe6caa3a7d634c4db9b" />
|
||||
<script src="_static/vendor/fontawesome/6.5.2/js/all.min.js?digest=dfe6caa3a7d634c4db9b"></script>
|
||||
|
||||
<script src="_static/documentation_options.js?v=85e8db4b"></script>
|
||||
<script src="_static/documentation_options.js?v=3e145956"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/sphinx-book-theme.js?v=887ef09a"></script>
|
||||
@@ -134,7 +134,7 @@
|
||||
|
||||
|
||||
|
||||
<p class="title logo__title">AskNavidrome 0.8 documentation</p>
|
||||
<p class="title logo__title">AskNavidrome 0.9 documentation</p>
|
||||
|
||||
</a></div>
|
||||
<div class="sidebar-primary-item">
|
||||
@@ -480,7 +480,7 @@ document.write(`
|
||||
<li class="toc-h5 nav-item toc-entry"><a class="reference internal nav-link" href="#app.SystemExceptionHandler.handle"><code class="docutils literal notranslate"><span class="pre">SystemExceptionHandler.handle()</span></code></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toc-h4 nav-item toc-entry"><a class="reference internal nav-link" href="#app.queueWorkerThread"><code class="docutils literal notranslate"><span class="pre">queueWorkerThread()</span></code></a></li>
|
||||
<li class="toc-h4 nav-item toc-entry"><a class="reference internal nav-link" href="#app.queue_worker_thread"><code class="docutils literal notranslate"><span class="pre">queue_worker_thread()</span></code></a></li>
|
||||
<li class="toc-h4 nav-item toc-entry"><a class="reference internal nav-link" href="#app.sanitise_speech_output"><code class="docutils literal notranslate"><span class="pre">sanitise_speech_output()</span></code></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -1196,8 +1196,8 @@ and setting it to <em>DNS Only</em>.</p>
|
||||
<p><strong>Functions:</strong></p>
|
||||
<div class="pst-scrollable-table-container"><table class="autosummary longtable table">
|
||||
<tbody>
|
||||
<tr class="row-odd"><td><p><a class="reference internal" href="#app.queueWorkerThread" title="app.queueWorkerThread"><code class="xref py py-obj docutils literal notranslate"><span class="pre">queueWorkerThread</span></code></a>(connection, play_queue, ...)</p></td>
|
||||
<td><p></p></td>
|
||||
<tr class="row-odd"><td><p><a class="reference internal" href="#app.queue_worker_thread" title="app.queue_worker_thread"><code class="xref py py-obj docutils literal notranslate"><span class="pre">queue_worker_thread</span></code></a>(connection, play_queue, ...)</p></td>
|
||||
<td><p>Media queue worker</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p><a class="reference internal" href="#app.sanitise_speech_output" title="app.sanitise_speech_output"><code class="xref py py-obj docutils literal notranslate"><span class="pre">sanitise_speech_output</span></code></a>(speech_string)</p></td>
|
||||
<td><p>Sanitise speech output inline with the SSML standard</p></td>
|
||||
@@ -2787,9 +2787,21 @@ during dispatch.</p>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="py function">
|
||||
<dt class="sig sig-object py" id="app.queueWorkerThread">
|
||||
<span class="sig-prename descclassname"><span class="pre">app.</span></span><span class="sig-name descname"><span class="pre">queueWorkerThread</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">connection</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">play_queue</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">song_id_list</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#app.queueWorkerThread" title="Link to this definition">#</a></dt>
|
||||
<dd></dd></dl>
|
||||
<dt class="sig sig-object py" id="app.queue_worker_thread">
|
||||
<span class="sig-prename descclassname"><span class="pre">app.</span></span><span class="sig-name descname"><span class="pre">queue_worker_thread</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">connection</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">object</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">play_queue</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">object</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">song_id_list</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">list</span></span></em><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">→</span> <span class="sig-return-typehint"><span class="pre">None</span></span></span><a class="headerlink" href="#app.queue_worker_thread" title="Link to this definition">#</a></dt>
|
||||
<dd><p>Media queue worker</p>
|
||||
<p>This function allows media queues to be populated in the background enabling multithreading
|
||||
and increasing skill response times.</p>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>connection</strong> (<em>object</em>) – A SubSonic API connection object</p></li>
|
||||
<li><p><strong>play_queue</strong> (<em>object</em>) – A MediaQueue object</p></li>
|
||||
<li><p><strong>song_id_list</strong> (<em>list</em>) – A list containing Navidrome song IDs</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="py function">
|
||||
<dt class="sig sig-object py" id="app.sanitise_speech_output">
|
||||
@@ -3090,7 +3102,7 @@ request early to queue the next track while maintaining the playlist</p>
|
||||
<span class="sig-name descname"><span class="pre">get_current_track</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">→</span> <span class="sig-return-typehint"><a class="reference internal" href="#asknavidrome.track.Track" title="asknavidrome.track.Track"><span class="pre">Track</span></a></span></span><a class="headerlink" href="#asknavidrome.media_queue.MediaQueue.get_current_track" title="Link to this definition">#</a></dt>
|
||||
<dd><p>Method to return current_track attribute</p>
|
||||
<p>Added to allow access to the current_track object while using BaseManager
|
||||
for multi threading, as BaseManager does not allow access to class
|
||||
for multi threading, as BaseManager does not allow access to class
|
||||
attributes / properties</p>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Returns<span class="colon">:</span></dt>
|
||||
@@ -3181,7 +3193,7 @@ add it to the front of the play queue</p>
|
||||
<span class="sig-name descname"><span class="pre">set_current_track_offset</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">offset</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">int</span></span></em><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">→</span> <span class="sig-return-typehint"><span class="pre">None</span></span></span><a class="headerlink" href="#asknavidrome.media_queue.MediaQueue.set_current_track_offset" title="Link to this definition">#</a></dt>
|
||||
<dd><p>Method to set the offset of the current track in milliseconds</p>
|
||||
<p>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
|
||||
when resuming a paused track to ensure the track isn’t played from
|
||||
the beginning again.</p>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
|
||||
@@ -3842,7 +3854,7 @@ is working</p>
|
||||
<li class="toc-h5 nav-item toc-entry"><a class="reference internal nav-link" href="#app.SystemExceptionHandler.handle"><code class="docutils literal notranslate"><span class="pre">SystemExceptionHandler.handle()</span></code></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toc-h4 nav-item toc-entry"><a class="reference internal nav-link" href="#app.queueWorkerThread"><code class="docutils literal notranslate"><span class="pre">queueWorkerThread()</span></code></a></li>
|
||||
<li class="toc-h4 nav-item toc-entry"><a class="reference internal nav-link" href="#app.queue_worker_thread"><code class="docutils literal notranslate"><span class="pre">queue_worker_thread()</span></code></a></li>
|
||||
<li class="toc-h4 nav-item toc-entry"><a class="reference internal nav-link" href="#app.sanitise_speech_output"><code class="docutils literal notranslate"><span class="pre">sanitise_speech_output()</span></code></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
BIN
docs/objects.inv
BIN
docs/objects.inv
Binary file not shown.
@@ -7,7 +7,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Python Module Index — AskNavidrome 0.8 documentation</title>
|
||||
<title>Python Module Index — AskNavidrome 0.9 documentation</title>
|
||||
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<link rel="preload" as="script" href="_static/scripts/pydata-sphinx-theme.js?digest=dfe6caa3a7d634c4db9b" />
|
||||
<script src="_static/vendor/fontawesome/6.5.2/js/all.min.js?digest=dfe6caa3a7d634c4db9b"></script>
|
||||
|
||||
<script src="_static/documentation_options.js?v=85e8db4b"></script>
|
||||
<script src="_static/documentation_options.js?v=3e145956"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/sphinx-book-theme.js?v=887ef09a"></script>
|
||||
@@ -136,7 +136,7 @@
|
||||
|
||||
|
||||
|
||||
<p class="title logo__title">AskNavidrome 0.8 documentation</p>
|
||||
<p class="title logo__title">AskNavidrome 0.9 documentation</p>
|
||||
|
||||
</a></div>
|
||||
<div class="sidebar-primary-item">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Search - AskNavidrome 0.8 documentation</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Search - AskNavidrome 0.9 documentation</title>
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<link rel="preload" as="script" href="_static/scripts/pydata-sphinx-theme.js?digest=dfe6caa3a7d634c4db9b" />
|
||||
<script src="_static/vendor/fontawesome/6.5.2/js/all.min.js?digest=dfe6caa3a7d634c4db9b"></script>
|
||||
|
||||
<script src="_static/documentation_options.js?v=85e8db4b"></script>
|
||||
<script src="_static/documentation_options.js?v=3e145956"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/sphinx-book-theme.js?v=887ef09a"></script>
|
||||
@@ -135,7 +135,7 @@
|
||||
|
||||
|
||||
|
||||
<p class="title logo__title">AskNavidrome 0.8 documentation</p>
|
||||
<p class="title logo__title">AskNavidrome 0.9 documentation</p>
|
||||
|
||||
</a></div>
|
||||
<div class="sidebar-primary-item">
|
||||
|
||||
File diff suppressed because one or more lines are too long
109
skill/app.py
109
skill/app.py
@@ -42,7 +42,7 @@ logger.addHandler(handler)
|
||||
# Get service configuration
|
||||
#
|
||||
|
||||
logger.info('AskNavidrome 0.6!')
|
||||
logger.info('AskNavidrome 0.9!')
|
||||
logger.debug('Getting configuration from the environment...')
|
||||
|
||||
try:
|
||||
@@ -222,7 +222,7 @@ class LaunchRequestHandler(AbstractRequestHandler):
|
||||
logger.debug('In LaunchRequestHandler')
|
||||
|
||||
connection.ping()
|
||||
speech = sanitise_speech_output('Ready!')
|
||||
speech = sanitise_speech_output('Simbora!')
|
||||
|
||||
handler_input.response_builder.speak(speech).ask(speech)
|
||||
return handler_input.response_builder.response
|
||||
@@ -300,7 +300,7 @@ class NaviSonicPlayMusicByArtist(AbstractRequestHandler):
|
||||
|
||||
# Check if a background process is already running, if it is then terminate the process
|
||||
# in favour of the new process.
|
||||
if backgroundProcess != None:
|
||||
if backgroundProcess is not None:
|
||||
backgroundProcess.terminate()
|
||||
backgroundProcess.join()
|
||||
|
||||
@@ -311,7 +311,8 @@ class NaviSonicPlayMusicByArtist(AbstractRequestHandler):
|
||||
artist_lookup = connection.search_artist(artist.value)
|
||||
|
||||
if artist_lookup is None:
|
||||
text = sanitise_speech_output(f"I couldn't find the artist {artist.value} in the collection.")
|
||||
#text = sanitise_speech_output(f"I couldn't find the artist {artist.value} in the collection.")
|
||||
text = sanitise_speech_output(f"Não achei o artista {artist.value} na nossa coleção.")
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
@@ -325,10 +326,11 @@ class NaviSonicPlayMusicByArtist(AbstractRequestHandler):
|
||||
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=queueWorkerThread, args=(connection, play_queue, song_id_list[2:])) # Create a thread to enqueue the remaining 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'Playing music by: {artist.value}')
|
||||
#speech = sanitise_speech_output(f'Playing music by: {artist.value}')
|
||||
speech = sanitise_speech_output(f'Tocando músicas de: {artist.value}')
|
||||
logger.info(speech)
|
||||
|
||||
card = {'title': 'AskNavidrome',
|
||||
@@ -355,7 +357,7 @@ class NaviSonicPlayAlbumByArtist(AbstractRequestHandler):
|
||||
|
||||
# Check if a background process is already running, if it is then terminate the process
|
||||
# in favour of the new process.
|
||||
if backgroundProcess != None:
|
||||
if backgroundProcess is not None:
|
||||
backgroundProcess.terminate()
|
||||
backgroundProcess.join()
|
||||
|
||||
@@ -371,7 +373,8 @@ class NaviSonicPlayAlbumByArtist(AbstractRequestHandler):
|
||||
artist_lookup = connection.search_artist(artist.value)
|
||||
|
||||
if artist_lookup is None:
|
||||
text = sanitise_speech_output(f"I couldn't find the artist {artist.value} in the collection.")
|
||||
#text = sanitise_speech_output(f"I couldn't find the artist {artist.value} in the collection.")
|
||||
text = sanitise_speech_output(f"Não achei o artista {artist.value} na nossa coleção.")
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
@@ -384,7 +387,8 @@ class NaviSonicPlayAlbumByArtist(AbstractRequestHandler):
|
||||
result = [album_result for album_result in artist_album_lookup if album_result.get('name').lower() == album.value.lower()]
|
||||
|
||||
if not result:
|
||||
text = sanitise_speech_output(f"I couldn't find an album called {album.value} by {artist.value} in the collection.")
|
||||
#text = sanitise_speech_output(f"I couldn't find an album called {album.value} by {artist.value} in the collection.")
|
||||
text = sanitise_speech_output(f"Não achei na nossa coleção um disco chamado {album.value} de {artist.value}.")
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
@@ -395,10 +399,11 @@ class NaviSonicPlayAlbumByArtist(AbstractRequestHandler):
|
||||
|
||||
# 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 = 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'Playing {album.value} by: {artist.value}')
|
||||
#speech = sanitise_speech_output(f'Playing {album.value} by: {artist.value}')
|
||||
speech = sanitise_speech_output(f'Tocando o álbum {album.value} de: {artist.value}')
|
||||
logger.info(speech)
|
||||
card = {'title': 'AskNavidrome',
|
||||
'text': speech
|
||||
@@ -414,7 +419,8 @@ class NaviSonicPlayAlbumByArtist(AbstractRequestHandler):
|
||||
result = connection.search_album(album.value)
|
||||
|
||||
if result is None:
|
||||
text = sanitise_speech_output(f"I couldn't find the album {album.value} in the collection.")
|
||||
#text = sanitise_speech_output(f"I couldn't find the album {album.value} in the collection.")
|
||||
text = sanitise_speech_output(f"Não achei o disco {album.value} na nossa coleção.")
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
@@ -425,11 +431,10 @@ class NaviSonicPlayAlbumByArtist(AbstractRequestHandler):
|
||||
|
||||
# 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 = 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'Playing {album.value}')
|
||||
speech = sanitise_speech_output(f'Tocando o disco {album.value}')
|
||||
logger.info(speech)
|
||||
card = {'title': 'AskNavidrome',
|
||||
'text': speech
|
||||
@@ -462,7 +467,8 @@ class NaviSonicPlaySongByArtist(AbstractRequestHandler):
|
||||
artist_lookup = connection.search_artist(artist.value)
|
||||
|
||||
if artist_lookup is None:
|
||||
text = sanitise_speech_output(f"I couldn't find the artist {artist.value} in the collection.")
|
||||
#text = sanitise_speech_output(f"I couldn't find the artist {artist.value} in the collection.")
|
||||
text = sanitise_speech_output(f"Não achei o artista {artist.value} na nossa coleção.")
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
@@ -477,7 +483,8 @@ class NaviSonicPlaySongByArtist(AbstractRequestHandler):
|
||||
song_dets = [item.get('id') for item in song_list if item.get('artistId') == artist_id]
|
||||
|
||||
if not song_dets:
|
||||
text = sanitise_speech_output(f"I couldn't find a song called {song.value} by {artist.value} in the collection.")
|
||||
#text = sanitise_speech_output(f"I couldn't find a song called {song.value} by {artist.value} in the collection.")
|
||||
text = sanitise_speech_output(f"Não achei uma música chamada {song.value} de {artist.value} na nossa coleção.")
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
@@ -485,7 +492,7 @@ class NaviSonicPlaySongByArtist(AbstractRequestHandler):
|
||||
play_queue.clear()
|
||||
controller.enqueue_songs(connection, play_queue, song_dets)
|
||||
|
||||
speech = sanitise_speech_output(f'Playing {song.value} by {artist.value}')
|
||||
speech = sanitise_speech_output(f'Tocando {song.value} de {artist.value}')
|
||||
logger.info(speech)
|
||||
card = {'title': 'AskNavidrome',
|
||||
'text': speech
|
||||
@@ -510,7 +517,7 @@ class NaviSonicPlayPlaylist(AbstractRequestHandler):
|
||||
|
||||
# Check if a background process is already running, if it is then terminate the process
|
||||
# in favour of the new process.
|
||||
if backgroundProcess != None:
|
||||
if backgroundProcess is not None:
|
||||
backgroundProcess.terminate()
|
||||
backgroundProcess.join()
|
||||
|
||||
@@ -521,7 +528,7 @@ class NaviSonicPlayPlaylist(AbstractRequestHandler):
|
||||
playlist_id = connection.search_playlist(playlist.value)
|
||||
|
||||
if playlist_id is None:
|
||||
text = sanitise_speech_output("I couldn't find the playlist " + str(playlist.value) + ' in the collection.')
|
||||
text = sanitise_speech_output("Não achei a playlist " + str(playlist.value) + ' na nossa coleção.')
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
@@ -532,10 +539,10 @@ class NaviSonicPlayPlaylist(AbstractRequestHandler):
|
||||
|
||||
# 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 = 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('Playing playlist ' + str(playlist.value))
|
||||
speech = sanitise_speech_output('Tocando a playlist ' + str(playlist.value))
|
||||
logger.info(speech)
|
||||
card = {'title': 'AskNavidrome',
|
||||
'text': speech
|
||||
@@ -544,11 +551,6 @@ class NaviSonicPlayPlaylist(AbstractRequestHandler):
|
||||
|
||||
return controller.start_playback('play', speech, card, track_details, handler_input)
|
||||
|
||||
def queueWorkerThread(connection, play_queue, song_id_list):
|
||||
logger.debug('In playlist processing thread!')
|
||||
controller.enqueue_songs(connection, play_queue, song_id_list)
|
||||
play_queue.sync()
|
||||
logger.debug('Finished playlist processing!')
|
||||
|
||||
class NaviSonicPlayMusicByGenre(AbstractRequestHandler):
|
||||
""" Play songs from the given genre
|
||||
@@ -565,7 +567,7 @@ class NaviSonicPlayMusicByGenre(AbstractRequestHandler):
|
||||
|
||||
# Check if a background process is already running, if it is then terminate the process
|
||||
# in favour of the new process.
|
||||
if backgroundProcess != None:
|
||||
if backgroundProcess is not None:
|
||||
backgroundProcess.terminate()
|
||||
backgroundProcess.join()
|
||||
|
||||
@@ -575,7 +577,7 @@ class NaviSonicPlayMusicByGenre(AbstractRequestHandler):
|
||||
song_id_list = connection.build_song_list_from_genre(genre.value, min_song_count)
|
||||
|
||||
if song_id_list is None:
|
||||
text = sanitise_speech_output(f"I couldn't find any {genre.value} songs in the collection.")
|
||||
text = sanitise_speech_output(f"Não achei nada do estilo {genre.value} na nossa coleção.")
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
@@ -586,10 +588,10 @@ class NaviSonicPlayMusicByGenre(AbstractRequestHandler):
|
||||
|
||||
# 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 = 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'Playing {genre.value} music')
|
||||
speech = sanitise_speech_output(f'Tocando músicas do estilo {genre.value}')
|
||||
logger.info(speech)
|
||||
card = {'title': 'AskNavidrome',
|
||||
'text': speech
|
||||
@@ -614,14 +616,15 @@ class NaviSonicPlayMusicRandom(AbstractRequestHandler):
|
||||
|
||||
# Check if a background process is already running, if it is then terminate the process
|
||||
# in favour of the new process.
|
||||
if backgroundProcess != None:
|
||||
if backgroundProcess is not None:
|
||||
backgroundProcess.terminate()
|
||||
backgroundProcess.join()
|
||||
|
||||
song_id_list = connection.build_random_song_list(min_song_count)
|
||||
|
||||
if song_id_list is None:
|
||||
text = sanitise_speech_output("I couldn't find any songs in the collection.")
|
||||
#text = sanitise_speech_output("I couldn't find any songs in the collection.")
|
||||
text = sanitise_speech_output("Não achei nenhuma música na nossa coleção. Estranho.")
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
@@ -632,10 +635,10 @@ class NaviSonicPlayMusicRandom(AbstractRequestHandler):
|
||||
|
||||
# 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 = 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('Playing random music')
|
||||
speech = sanitise_speech_output('Tocando qualquer coisa aleatória')
|
||||
logger.info(speech)
|
||||
card = {'title': 'AskNavidrome',
|
||||
'text': speech
|
||||
@@ -660,14 +663,14 @@ class NaviSonicPlayFavouriteSongs(AbstractRequestHandler):
|
||||
|
||||
# Check if a background process is already running, if it is then terminate the process
|
||||
# in favour of the new process.
|
||||
if backgroundProcess != None:
|
||||
if backgroundProcess is not None:
|
||||
backgroundProcess.terminate()
|
||||
backgroundProcess.join()
|
||||
|
||||
song_id_list = connection.build_song_list_from_favourites()
|
||||
|
||||
if song_id_list is None:
|
||||
text = sanitise_speech_output("You don't have any favourite songs in the collection.")
|
||||
text = sanitise_speech_output("Você não tem nada nos favoritos.")
|
||||
handler_input.response_builder.speak(text).ask(text)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
@@ -678,10 +681,10 @@ class NaviSonicPlayFavouriteSongs(AbstractRequestHandler):
|
||||
|
||||
# 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 = 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('Playing your favourite tracks.')
|
||||
speech = sanitise_speech_output('Tocando suas músicas favoritas.')
|
||||
logger.info(speech)
|
||||
card = {'title': 'AskNavidrome',
|
||||
'text': speech
|
||||
@@ -993,7 +996,8 @@ class SystemExceptionHandler(AbstractExceptionHandler):
|
||||
if get_request_type(handler_input) == 'IntentRequest':
|
||||
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!!")
|
||||
handler_input.response_builder.speak(speech).ask(speech)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
@@ -1019,7 +1023,8 @@ class GeneralExceptionHandler(AbstractExceptionHandler):
|
||||
if get_request_type(handler_input) == 'IntentRequest':
|
||||
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!!")
|
||||
handler_input.response_builder.speak(speech).ask(speech)
|
||||
|
||||
return handler_input.response_builder.response
|
||||
@@ -1053,6 +1058,7 @@ class LoggingResponseInterceptor(AbstractResponseInterceptor):
|
||||
# Functions
|
||||
#
|
||||
|
||||
|
||||
def sanitise_speech_output(speech_string: str) -> str:
|
||||
"""Sanitise speech output inline with the SSML standard
|
||||
|
||||
@@ -1084,6 +1090,27 @@ def sanitise_speech_output(speech_string: str) -> str:
|
||||
|
||||
return speech_string
|
||||
|
||||
|
||||
def queue_worker_thread(connection: object, play_queue: object, song_id_list: list) -> None:
|
||||
"""Media queue worker
|
||||
|
||||
This function allows media queues to be populated in the background enabling multithreading
|
||||
and increasing skill response times.
|
||||
|
||||
:param connection: A SubSonic API connection object
|
||||
:type connection: object
|
||||
:param play_queue: A MediaQueue object
|
||||
:type play_queue: object
|
||||
:param song_id_list: A list containing Navidrome song IDs
|
||||
:type song_id_list: list
|
||||
"""
|
||||
|
||||
logger.debug('In playlist processing thread!')
|
||||
controller.enqueue_songs(connection, play_queue, song_id_list)
|
||||
play_queue.sync()
|
||||
logger.debug('Finished playlist processing!')
|
||||
|
||||
|
||||
# Register Intent Handlers
|
||||
sb.add_request_handler(LaunchRequestHandler())
|
||||
sb.add_request_handler(CheckAudioInterfaceHandler())
|
||||
|
||||
@@ -44,19 +44,19 @@ class MediaQueue:
|
||||
"""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
|
||||
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
|
||||
when resuming a paused track to ensure the track isn't played from
|
||||
the beginning again.
|
||||
|
||||
:param offset: The track offset in milliseconds
|
||||
@@ -75,7 +75,7 @@ class MediaQueue:
|
||||
"""
|
||||
|
||||
return self.queue
|
||||
|
||||
|
||||
def get_buffer(self) -> deque:
|
||||
"""Get the buffer
|
||||
|
||||
@@ -86,7 +86,7 @@ class MediaQueue:
|
||||
"""
|
||||
|
||||
return self.buffer
|
||||
|
||||
|
||||
def get_history(self) -> deque:
|
||||
"""Get history
|
||||
|
||||
@@ -97,7 +97,7 @@ class MediaQueue:
|
||||
"""
|
||||
|
||||
return self.history
|
||||
|
||||
|
||||
def add_track(self, track: Track) -> None:
|
||||
"""Add tracks to the queue
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class SubsonicConnection:
|
||||
self.logger.error('Failed to connect to Navidrome')
|
||||
|
||||
return self.conn.ping()
|
||||
|
||||
|
||||
def scrobble(self, track_id: str, time: int) -> None:
|
||||
"""Scrobble the given track
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ copyright = '2025, Ross Stewart'
|
||||
author = 'Ross Stewart'
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = '0.8'
|
||||
release = '0.9'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user