Compare commits

...

4 Commits

Author SHA1 Message Date
55cc7d3310 Add settings example 2026-02-07 11:52:40 -07:00
b534efd93b refactor: Get NAVIDROME_CONTAINER from settings module
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-07 11:52:40 -07:00
264756686b feat: Implement Navidrome setRating API for starred/unstarred songs
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-07 11:52:40 -07:00
bc41f33cde feat: Implement navidrome_set_rating function for Subsonic API 2026-02-07 11:52:40 -07:00
2 changed files with 68 additions and 5 deletions

62
main.py
View File

@@ -7,10 +7,14 @@ logging.basicConfig(
import asyncio import asyncio
import re import re
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import aiodocker import aiodocker
import aiohttp
import random
import string
import hashlib
import settings
NAVIDROME_CONTAINER = os.environ.get('NAVIDROME_CONTAINER', 'navidrome-navidrome-1')
STAR_UNSTAR_WINDOW = timedelta(seconds=4) STAR_UNSTAR_WINDOW = timedelta(seconds=4)
# A dictionary to store the timestamp of when a song was starred. # A dictionary to store the timestamp of when a song was starred.
@@ -42,6 +46,53 @@ def parse_log_line(line):
return timestamp, song_id, is_starred return timestamp, song_id, is_starred
async def navidrome_set_rating(song_id):
navidrome_url = settings.NAVIDROME_URL
username = settings.NAVIDROME_USER
if not all([navidrome_url, username]):
logging.error("NAVIDROME_URL and NAVIDROME_USER must be set in settings.py.")
return
salt = settings.SUBSONIC_SALT
token = settings.SUBSONIC_TOKEN
if not all([salt, token]):
password = settings.NAVIDROME_PASSWORD
if not password:
logging.error("Either (SUBSONIC_SALT and SUBSONIC_TOKEN) or NAVIDROME_PASSWORD must be set in settings.py.")
return
# Subsonic API requires a salt and a token (md5(password + salt))
salt = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10))
token = hashlib.md5((password + salt).encode('utf-8')).hexdigest()
params = {
'u': username,
't': token,
's': salt,
'v': '1.16.1',
'c': 'heart-monitor',
'f': 'json',
'id': song_id,
'rating': 1,
}
api_url = f"{navidrome_url.rstrip('/')}/rest/setRating"
try:
async with aiohttp.ClientSession() as session:
async with session.get(api_url, params=params) as response:
response.raise_for_status()
data = await response.json()
if data.get('subsonic-response', {}).get('status') == 'ok':
logging.info(f"Successfully set rating for song {song_id} to 1.")
else:
logging.error(f"Failed to set rating for song {song_id}. Response: {data}")
except aiohttp.ClientError as e:
logging.error(f"Error calling Navidrome API for song {song_id}: {e}")
async def main(): async def main():
""" """
Monitors Navidrome container logs for rapid star/unstar events. Monitors Navidrome container logs for rapid star/unstar events.
@@ -49,9 +100,9 @@ async def main():
docker = None docker = None
try: try:
docker = aiodocker.Docker() docker = aiodocker.Docker()
container = await docker.containers.get(NAVIDROME_CONTAINER) container = await docker.containers.get(settings.NAVIDROME_CONTAINER)
logging.info(f"Monitoring logs for container '{NAVIDROME_CONTAINER}'...") logging.info(f"Monitoring logs for container '{settings.NAVIDROME_CONTAINER}'...")
logs = container.log( logs = container.log(
stdout=True, stdout=True,
stderr=True, stderr=True,
@@ -74,12 +125,13 @@ async def main():
logging.info( logging.info(
f"Song {song_id} was starred and then unstarred within {STAR_UNSTAR_WINDOW.seconds} seconds." f"Song {song_id} was starred and then unstarred within {STAR_UNSTAR_WINDOW.seconds} seconds."
) )
await navidrome_set_rating(song_id)
# Remove song from tracking after it has been unstarred # Remove song from tracking after it has been unstarred
del starred_songs[song_id] del starred_songs[song_id]
except aiodocker.exceptions.DockerError as e: except aiodocker.exceptions.DockerError as e:
if e.status == 404: if e.status == 404:
logging.error(f"Container '{NAVIDROME_CONTAINER}' not found.") logging.error(f"Container '{settings.NAVIDROME_CONTAINER}' not found.")
else: else:
logging.error(f"Error connecting to Docker or getting container: {e}") logging.error(f"Error connecting to Docker or getting container: {e}")
except Exception as e: except Exception as e:

11
settings.py.example Normal file
View File

@@ -0,0 +1,11 @@
NAVIDROME_CONTAINER = 'navidrome-navidrome-1'
NAVIDROME_URL = 'https://navidrome.example.com'
NAVIDROME_USER = ''
# Set this one:
NAVIDROME_PASSWORD = ''
# or these two:
SUBSONIC_SALT = ''
SUBSONIC_TOKEN = ''