Initial commit - Navi.FM v3

This commit is contained in:
Collin Kasbergen
2026-07-22 13:23:22 +02:00
commit badafc91e3
55 changed files with 6384 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
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()