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 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__)
|
||||
|
||||
@@ -152,6 +152,11 @@ class Player():
|
||||
|
||||
voice_client.play(audio_src, after=lambda e: loop.create_task(playback_finished(e)))
|
||||
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
|
||||
except discord.ClientException as 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())
|
||||
return None
|
||||
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