import os, logging DEBUG = os.environ.get('DEBUG') logging.basicConfig( format='[%(asctime)s] %(levelname)s %(module)s/%(funcName)s - %(message)s', level=logging.DEBUG if DEBUG else logging.INFO) import asyncio import re from datetime import datetime, timedelta, timezone import aiodocker NAVIDROME_CONTAINER = os.environ.get('NAVIDROME_CONTAINER', 'navidrome-navidrome-1') STAR_UNSTAR_WINDOW = timedelta(seconds=4) # A dictionary to store the timestamp of when a song was starred. # {song_id: timestamp} starred_songs = {} LOG_PATTERN = re.compile( r'time="(?P[^"]+)".*msg="Changing starred" ids="\[`(?P[^`]+)`\]".*starred=(?Ptrue|false)' ) def parse_log_line(line): """ Parses a log line to find song star/unstar events. Returns a tuple of (timestamp, song_id, is_starred) or None if not a match. """ match = LOG_PATTERN.search(line) if not match: return None data = match.groupdict() ts_str = data['ts'] song_id = data['id'] is_starred = data['starred'] == 'true' # fromisoformat doesn't like Z, so we replace it with +00:00 timezone info. timestamp = datetime.fromisoformat(ts_str.replace('Z', '+00:00')) return timestamp, song_id, is_starred async def main(): """ Monitors Navidrome container logs for rapid star/unstar events. """ docker = None try: docker = aiodocker.Docker() container = await docker.containers.get(NAVIDROME_CONTAINER) logging.info(f"Monitoring logs for container '{NAVIDROME_CONTAINER}'...") logs = container.log( stdout=True, stderr=True, follow=True, since=datetime.now(timezone.utc).timestamp() ) async for line in logs: parsed = parse_log_line(line) if not parsed: continue timestamp, song_id, is_starred = parsed 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." ) # Remove song from tracking after it has been unstarred del starred_songs[song_id] except aiodocker.exceptions.DockerError as e: if e.status == 404: logging.error(f"Container '{NAVIDROME_CONTAINER}' not found.") else: logging.error(f"Error connecting to Docker or getting container: {e}") except Exception as e: logging.error(f"An unexpected error occurred: {e}") finally: if docker: await docker.close() if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: logging.info("Exiting.")