import time import os import threading import subprocess from database import SessionLocal, Track from navidrome import navidrome_client from analyzer import analyze_audio from queue_manager import get_active_profile, set_active_profile import json import datetime def sync_tracks_loop(): while True: try: if not navidrome_client.url: time.sleep(10) continue print("Syncing tracks from Navidrome...") songs = navidrome_client.get_random_songs(count=200) db = SessionLocal() added = 0 for song in songs: # Check if song exists existing = db.query(Track).filter(Track.id == song["id"]).first() if not existing: new_track = Track( id=song["id"], title=song.get("title", "Unknown"), artist=song.get("artist", "Unknown"), album=song.get("album", "Unknown"), duration=song.get("duration", 0), path=song.get("path", "") ) db.add(new_track) added += 1 if added > 0: db.commit() db.close() print(f"Sync complete. Added {added} new tracks.") except Exception as e: print(f"Error syncing tracks: {e}") # Run every hour time.sleep(3600) def analyze_tracks_loop(): while True: try: if not navidrome_client.url: time.sleep(10) continue db = SessionLocal() # Find an un-analyzed track track = db.query(Track).filter(Track.analyzed == False).first() if track: print(f"Analyzing track: {track.artist} - {track.title}") temp_path = f"/tmp/{track.id}.audio" # Download if navidrome_client.download_song(track.id, temp_path): # Analyze metadata = analyze_audio(temp_path) if metadata: track.bpm = metadata["bpm"] track.key = metadata["key"] track.energy = metadata["energy"] track.spectral_centroid = metadata["spectral_centroid"] track.onset_density = metadata["onset_density"] track.lufs = metadata["lufs"] track.analyzed = True db.commit() print("Analysis successful.") else: # Mark as analyzed anyway so we don't get stuck in a loop, or maybe add an error flag track.analyzed = True db.commit() print("Analysis failed, marked as analyzed to skip.") # Cleanup if os.path.exists(temp_path): os.remove(temp_path) else: print("Download failed.") else: # No tracks to analyze, sleep for a while time.sleep(60) db.close() except Exception as e: print(f"Error in analysis loop: {e}") time.sleep(60) def scheduler_loop(): """ Checks the auto_pilot_schedule every minute and changes the profile if it matches the current time. """ while True: try: # Wait until the start of the next minute now = datetime.datetime.now() sleep_seconds = 60 - now.second time.sleep(sleep_seconds) # Now it's a new minute now = datetime.datetime.now() current_time_str = now.strftime("%H:%M") db = SessionLocal() from database import get_setting schedule_str = get_setting(db, "auto_pilot_schedule", "[]") db.close() schedule = json.loads(schedule_str) for rule in schedule: if rule.get("time") == current_time_str: target_profile = rule.get("profile") if target_profile: print(f"[Scheduler] Time is {current_time_str}. Switching profile to: {target_profile}") set_active_profile(target_profile) except Exception as e: print(f"Error in scheduler loop: {e}") time.sleep(60) def startup_manager_loop(): """ Waits until at least 10 tracks have been analyzed before starting Liquidsoap. This prevents Liquidsoap from rapidly requesting /next and queuing the exact same track repeatedly when the database is empty or only has 1 track during initial setup. """ while True: try: db = SessionLocal() count = db.query(Track).filter(Track.analyzed == True).count() db.close() if count >= 10: print(f"[Startup Manager] {count} tracks analyzed. Starting Liquidsoap...") # Run supervisorctl to start liquidsoap subprocess.run(["supervisorctl", "start", "liquidsoap"], check=False) break else: print(f"[Startup Manager] Waiting for initial analysis... ({count}/10 tracks analyzed)") time.sleep(10) except Exception as e: print(f"Error in startup manager: {e}") time.sleep(10) def start_workers(): t1 = threading.Thread(target=sync_tracks_loop, daemon=True) t2 = threading.Thread(target=analyze_tracks_loop, daemon=True) t3 = threading.Thread(target=scheduler_loop, daemon=True) t4 = threading.Thread(target=startup_manager_loop, daemon=True) t1.start() t2.start() t3.start() t4.start()