feat(scrobble): send Subsonic scrobble on playback start (now-playing)
This commit is contained in:
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
FROM python:3.11-alpine
|
||||||
|
|
||||||
|
# Ensure deterministic, unbuffered logs
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt requirements.txt
|
||||||
|
|
||||||
|
RUN apk add --no-cache build-base ffmpeg libffi libffi-dev libsodium libsodium-dev \
|
||||||
|
&& pip3 install --no-cache-dir -r requirements.txt \
|
||||||
|
&& apk del build-base libffi-dev libsodium-dev
|
||||||
|
|
||||||
|
RUN adduser -D -h /app app \
|
||||||
|
&& chown -R app:app /app
|
||||||
|
|
||||||
|
COPY --chown=app:app . .
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
# Set stop signal to SIGTERM to ensure clean shutdown
|
||||||
|
STOPSIGNAL SIGTERM
|
||||||
|
|
||||||
|
CMD ["python3", "/app/discodrome.py"]
|
||||||
@@ -7,7 +7,7 @@ import data
|
|||||||
import ui
|
import ui
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from subsonic import Song, APIError, get_random_songs, get_similar_songs, stream
|
from subsonic import Song, APIError, get_random_songs, get_similar_songs, stream, scrobble
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -152,6 +152,11 @@ class Player():
|
|||||||
|
|
||||||
voice_client.play(audio_src, after=lambda e: loop.create_task(playback_finished(e)))
|
voice_client.play(audio_src, after=lambda e: loop.create_task(playback_finished(e)))
|
||||||
logger.info(f"Started playing: {song.title} by {song.artist}")
|
logger.info(f"Started playing: {song.title} by {song.artist}")
|
||||||
|
# Fire-and-forget scrobble (now-playing ping with submission=False)
|
||||||
|
try:
|
||||||
|
asyncio.create_task(scrobble(song.song_id, submission=False))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to schedule scrobble for {song.title}: {e}")
|
||||||
return # Success, exit the function
|
return # Success, exit the function
|
||||||
except discord.ClientException as e:
|
except discord.ClientException as e:
|
||||||
logger.error(f"Discord client exception while playing audio (attempt {attempt+1}): {e}")
|
logger.error(f"Discord client exception while playing audio (attempt {attempt+1}): {e}")
|
||||||
|
|||||||
35
subsonic.py
35
subsonic.py
@@ -571,3 +571,38 @@ async def stream(stream_id: str):
|
|||||||
logger.error("Failed to stream song: %s", await response.text())
|
logger.error("Failed to stream song: %s", await response.text())
|
||||||
return None
|
return None
|
||||||
return str(response.url)
|
return str(response.url)
|
||||||
|
|
||||||
|
async def scrobble(song_id: str, *, submission: bool=True, timestamp: int=None) -> bool:
|
||||||
|
''' Submit a scrobble event for a song to the Subsonic/Navidrome API.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- song_id: The Subsonic song id to scrobble
|
||||||
|
- submission: Whether this is a final submission (True) or a now-playing ping (False)
|
||||||
|
- timestamp: Optional Unix epoch seconds for when the song was played
|
||||||
|
'''
|
||||||
|
|
||||||
|
scrobble_params: dict[str, any] = {
|
||||||
|
"id": song_id,
|
||||||
|
"submission": "true" if submission else "false",
|
||||||
|
}
|
||||||
|
if timestamp is not None:
|
||||||
|
scrobble_params["time"] = str(timestamp)
|
||||||
|
|
||||||
|
params = SUBSONIC_REQUEST_PARAMS | scrobble_params
|
||||||
|
|
||||||
|
try:
|
||||||
|
session = await get_session()
|
||||||
|
async with await session.get(f"{env.SUBSONIC_SERVER}/rest/scrobble.view", params=params) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
data = await response.json()
|
||||||
|
if await check_subsonic_error(data):
|
||||||
|
logger.warning("Scrobble returned a Subsonic error for song %s", song_id)
|
||||||
|
return False
|
||||||
|
logger.debug("Scrobble response: %s", data)
|
||||||
|
return True
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
logger.error("HTTP error during scrobble for song %s: %s", song_id, e)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Unexpected error during scrobble for song %s: %s", song_id, e)
|
||||||
|
return False
|
||||||
|
|||||||
Reference in New Issue
Block a user