Initial commit - Navi.FM v3
This commit is contained in:
+160
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user