Compare commits

...

6 Commits

Author SHA1 Message Date
0da71b7398 refactor: Remove aiodocker, use aiohttp for Docker REST API
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-07 14:18:54 -07:00
45c38a8496 fix: Configure aiodocker client with long timeout for log stream
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-07 14:16:55 -07:00
e89b578931 fix: Configure aiodocker with custom session to disable log timeouts
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-07 14:14:59 -07:00
81bdc71a44 fix: Revert custom aiohttp session and set aiodocker log timeout
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-07 13:06:56 -07:00
a411cb2b5e fix: Prevent aiodocker log stream from timing out
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-07 12:43:03 -07:00
811a9228c0 fix: Log full stack trace for unexpected errors
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-07 12:34:33 -07:00

90
main.py
View File

@@ -7,7 +7,6 @@ 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 aiohttp import aiohttp
import random import random
import string import string
@@ -152,48 +151,73 @@ async def main():
""" """
Monitors Navidrome container logs for rapid star/unstar events. Monitors Navidrome container logs for rapid star/unstar events.
""" """
docker = None session = None
try: try:
docker = aiodocker.Docker() connector = aiohttp.UnixConnector(path="/var/run/docker.sock")
container = await docker.containers.get(settings.NAVIDROME_CONTAINER) # Disable all timeouts for the long-polling log stream
timeout = aiohttp.ClientTimeout(total=None, sock_read=None)
session = aiohttp.ClientSession(connector=connector, timeout=timeout)
logging.info(f"Monitoring logs for container '{settings.NAVIDROME_CONTAINER}'...") container_name = settings.NAVIDROME_CONTAINER
logs = container.log(
stdout=True,
stderr=True,
follow=True,
since=datetime.now(timezone.utc).timestamp()
)
async for line in logs: # First, check if the container exists.
parsed = parse_log_line(line) inspect_url = f"http://localhost/containers/{container_name}/json"
if not parsed: async with session.get(inspect_url) as response:
continue if response.status == 404:
logging.error(f"Container '{container_name}' not found.")
return
response.raise_for_status()
timestamp, song_id, is_starred = parsed logging.info(f"Monitoring logs for container '{container_name}'...")
since = int(datetime.now(timezone.utc).timestamp())
params = {
'stdout': 'true',
'stderr': 'true',
'follow': 'true',
'since': str(since),
}
logs_url = f"http://localhost/containers/{container_name}/logs"
async with session.get(logs_url, params=params) as response:
response.raise_for_status()
while True:
line_bytes = await response.content.readline()
if not line_bytes:
break
# Docker's log stream is multiplexed. The first 8 bytes are a header.
# We strip it to get the raw log line.
if len(line_bytes) > 8:
line = line_bytes[8:].decode('utf-8', errors='replace').strip()
parsed = parse_log_line(line)
if not parsed:
continue
if is_starred: timestamp, song_id, is_starred = parsed
starred_songs[song_id] = timestamp
elif song_id in starred_songs: # is_starred is False (unstarred)
time_diff = timestamp - starred_songs[song_id]
if time_diff <= STAR_UNSTAR_WINDOW:
logging.info(
f"Song {song_id} was starred and then unstarred within {STAR_UNSTAR_WINDOW.seconds} seconds."
)
await handle_star_unstar_event(song_id)
# Remove song from tracking after it has been unstarred
del starred_songs[song_id]
except aiodocker.exceptions.DockerError as e: if is_starred:
starred_songs[song_id] = timestamp
elif song_id in starred_songs: # is_starred is False (unstarred)
time_diff = timestamp - starred_songs[song_id]
if time_diff <= STAR_UNSTAR_WINDOW:
logging.info(
f"Song {song_id} was starred and then unstarred within {STAR_UNSTAR_WINDOW.seconds} seconds."
)
await handle_star_unstar_event(song_id)
# Remove song from tracking after it has been unstarred
del starred_songs[song_id]
except aiohttp.ClientResponseError as e:
if e.status == 404: if e.status == 404:
logging.error(f"Container '{settings.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 with Docker API: {e}")
except Exception as e: except aiohttp.ClientError as e:
logging.error(f"An unexpected error occurred: {e}") # For other client errors like connection issues
logging.error(f"Error connecting to Docker: {e}")
except Exception:
logging.exception("An unexpected error occurred")
finally: finally:
if docker: if session:
await docker.close() await session.close()
if __name__ == "__main__": if __name__ == "__main__":