feat: Add Navidrome log monitor for rapid star/unstar detection
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
This commit is contained in:
92
main.py
92
main.py
@@ -4,7 +4,93 @@ logging.basicConfig(
|
||||
format='[%(asctime)s] %(levelname)s %(module)s/%(funcName)s - %(message)s',
|
||||
level=logging.DEBUG if DEBUG else logging.INFO)
|
||||
|
||||
import requests
|
||||
import asyncio
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
def main():
|
||||
pass
|
||||
import aiodocker
|
||||
|
||||
NAVIDROME_CONTAINER = os.environ.get('NAVIDROME_CONTAINER', '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<ts>[^"]+)".*msg="Changing starred" ids="\[`(?P<id>[^`]+)`\]".*starred=(?P<starred>true|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.")
|
||||
|
||||
Reference in New Issue
Block a user