2 Commits
2 changed files with 25 additions and 18 deletions

No files matched your search

+23 -17
View File
@@ -1,4 +1,6 @@
import os import os
import sys
import random
import time import time
import json import json
import sqlite3 import sqlite3
@@ -57,6 +59,10 @@ AI_PLAYLIST_DEFINITIONS = [
} }
] ]
jitter = random.randint(0, 300)
print(f"Jitter active: sleeping for {jitter} seconds...", flush=True)
time.sleep(jitter)
# Initialize Gemini Client # Initialize Gemini Client
gemini_client = genai.Client(api_key=GEMINI_API_KEY) gemini_client = genai.Client(api_key=GEMINI_API_KEY)
@@ -94,7 +100,7 @@ def init_db():
conn.close() conn.close()
def sync_library_to_sqlite(): def sync_library_to_sqlite():
print("Fetching full track catalog from Gonic API...") print("Fetching full track catalog from Gonic API...", flush=True)
auth = get_auth_params() auth = get_auth_params()
res = requests.get(f"{GONIC_URL}/getArtists.view", params=auth).json() res = requests.get(f"{GONIC_URL}/getArtists.view", params=auth).json()
@@ -128,7 +134,7 @@ def sync_library_to_sqlite():
cursor.executemany("INSERT OR REPLACE INTO tracks VALUES (?, ?, ?, ?, ?, ?)", all_tracks) cursor.executemany("INSERT OR REPLACE INTO tracks VALUES (?, ?, ?, ?, ?, ?)", all_tracks)
conn.commit() conn.commit()
conn.close() conn.close()
print(f"Synced {len(all_tracks)} tracks to local SQLite index.") print(f"Synced {len(all_tracks)} tracks to local SQLite index.", flush=True)
def check_library_changed(): def check_library_changed():
auth = get_auth_params() auth = get_auth_params()
@@ -215,7 +221,7 @@ def ask_gemini_for_tracks(prompt, candidates):
except Exception as e: except Exception as e:
if "503" in str(e) or "UNAVAILABLE" in str(e): if "503" in str(e) or "UNAVAILABLE" in str(e):
if attempt < max_retries - 1: if attempt < max_retries - 1:
print(f"Gemini capacity spike (503). Retrying in {delay}s (Attempt {attempt + 1}/{max_retries})...") print(f"Gemini capacity spike (503). Retrying in {delay}s (Attempt {attempt + 1}/{max_retries})...", flush=True)
time.sleep(delay) time.sleep(delay)
delay *= 2 delay *= 2
continue continue
@@ -227,7 +233,7 @@ def ask_gemini_for_tracks(prompt, candidates):
try: try:
return json.loads(response.text) return json.loads(response.text)
except Exception as e: except Exception as e:
print(f"Error parsing Gemini response: {e}\nRaw output: {response.text}") print(f"Error parsing Gemini response: {e}\nRaw output: {response.text}", flush=True)
return [] return []
def ensure_and_process_ai_playlists(library_has_changed): def ensure_and_process_ai_playlists(library_has_changed):
@@ -251,7 +257,7 @@ def ensure_and_process_ai_playlists(library_has_changed):
# 1. Create playlist automatically if it doesn't exist for this user account # 1. Create playlist automatically if it doesn't exist for this user account
if not playlist_id: if not playlist_id:
print(f"Playlist '{name}' not found for user. Creating it...") print(f"Playlist '{name}' not found for user. Creating it...", flush=True)
requests.get(f"{GONIC_URL}/createPlaylist.view", params={ requests.get(f"{GONIC_URL}/createPlaylist.view", params={
**auth, **auth,
"name": name, "name": name,
@@ -266,7 +272,7 @@ def ensure_and_process_ai_playlists(library_has_changed):
break break
if not playlist_id: if not playlist_id:
print(f"Failed to create or locate playlist '{name}'. Skipping.") print(f"Failed to create or locate playlist '{name}'. Skipping.", flush=True)
continue continue
# 2. Fetch current playlist contents from Gonic # 2. Fetch current playlist contents from Gonic
@@ -287,19 +293,19 @@ def ensure_and_process_ai_playlists(library_has_changed):
library_refresh_due = library_has_changed and (now - last_curated) > 86400 library_refresh_due = library_has_changed and (now - last_curated) > 86400
if not (is_empty or cooldown_expired or library_refresh_due): if not (is_empty or cooldown_expired or library_refresh_due):
print(f"Playlist '{name}' is up to date (Last curated {(now - last_curated) // 3600}h ago). Skipping Gemini.") print(f"Playlist '{name}' is up to date (Last curated {(now - last_curated) // 3600}h ago). Skipping Gemini.", flush=True)
continue continue
reason = "Empty/New" if is_empty else ("Periodic refresh" if cooldown_expired else "Library update") reason = "Empty/New" if is_empty else ("Periodic refresh" if cooldown_expired else "Library update")
print(f"Processing AI Playlist ({reason}): '{name}' with prompt: '{prompt}'") print(f"Processing AI Playlist ({reason}): '{name}' with prompt: '{prompt}'", flush=True)
candidates = query_candidate_tracks(prompt) candidates = query_candidate_tracks(prompt)
if not candidates: if not candidates:
print("No matching candidate tracks found in local index.") print("No matching candidate tracks found in local index.", flush=True)
continue continue
selected_ids = ask_gemini_for_tracks(prompt, candidates) selected_ids = ask_gemini_for_tracks(prompt, candidates)
print(f"Gemini selected {len(selected_ids)} tracks.") print(f"Gemini selected {len(selected_ids)} tracks.", flush=True)
if selected_ids: if selected_ids:
existing_song_ids = {str(song.get("id")) for song in existing_entries} existing_song_ids = {str(song.get("id")) for song in existing_entries}
@@ -318,11 +324,11 @@ def ensure_and_process_ai_playlists(library_has_changed):
update_res = requests.get(f"{GONIC_URL}/updatePlaylist.view", params=query_list).json() update_res = requests.get(f"{GONIC_URL}/updatePlaylist.view", params=query_list).json()
if update_res.get("subsonic-response", {}).get("status") == "ok": if update_res.get("subsonic-response", {}).get("status") == "ok":
print(f"Successfully added {len(tracks_to_add)} new tracks to '{name}'!") print(f"Successfully added {len(tracks_to_add)} new tracks to '{name}'!", flush=True)
else: else:
print(f"Failed to update playlist: {update_res}") print(f"Failed to update playlist: {update_res}")
else: else:
print(f"Playlist '{name}' has no new unique tracks to add from this selection.") print(f"Playlist '{name}' has no new unique tracks to add from this selection.", flush=True)
# Update last curated timestamp in SQLite state table # Update last curated timestamp in SQLite state table
cursor.execute("INSERT OR REPLACE INTO playlist_state (name, last_curated) VALUES (?, ?)", (name, now)) cursor.execute("INSERT OR REPLACE INTO playlist_state (name, last_curated) VALUES (?, ?)", (name, now))
@@ -332,23 +338,23 @@ def ensure_and_process_ai_playlists(library_has_changed):
def main(): def main():
init_db() init_db()
print("AI Playlist Sync one-shot execution started.") print("AI Playlist Sync one-shot execution started.", flush=True)
try: try:
# 1. Sync library catalog only if changes are detected or local DB is missing # 1. Sync library catalog only if changes are detected or local DB is missing
lib_changed = check_library_changed() lib_changed = check_library_changed()
if lib_changed or not os.path.exists(DB_PATH): if lib_changed or not os.path.exists(DB_PATH):
print("Library changes detected (or missing local index). Updating local SQLite index...") print("Library changes detected (or missing local index). Updating local SQLite index...", flush=True)
sync_library_to_sqlite() sync_library_to_sqlite()
else: else:
print("Library catalog unchanged. Using existing local SQLite index.") print("Library catalog unchanged. Using existing local SQLite index.", flush=True)
# 2. Check and process playlists using the time-based curation cooldown matrix # 2. Check and process playlists using the time-based curation cooldown matrix
ensure_and_process_ai_playlists(lib_changed) ensure_and_process_ai_playlists(lib_changed)
print("AI Playlist Sync execution completed successfully.") print("AI Playlist Sync execution completed successfully.", flush=True)
except Exception as e: except Exception as e:
print(f"Error during execution: {e}") print(f"Error during execution: {e}", flush=True)
raise e raise e
if __name__ == "__main__": if __name__ == "__main__":
+2 -1
View File
@@ -1,2 +1,3 @@
requests requests
google-genai google-genai
dotenv