179 lines
6.0 KiB
Python
179 lines
6.0 KiB
Python
import os
|
|
import sqlite3
|
|
import re
|
|
from typing import Dict, List, Set, Any, Optional
|
|
|
|
def _normalize(s: Optional[str]) -> str:
|
|
if not s:
|
|
return ""
|
|
s = s.lower()
|
|
s = re.sub(r"[^\w\s]", "", s)
|
|
return " ".join(s.split())
|
|
|
|
class WaveCollector:
|
|
def __init__(self, db_path: str = "/navidrome-data/navidrome.db"):
|
|
self.db_path = db_path
|
|
# Fallback paths if running outside docker
|
|
if not os.path.exists(self.db_path):
|
|
candidates = [
|
|
"/home/ai-projects/services/music.qpid.pro/data/navidrome.db",
|
|
"../music.qpid.pro/data/navidrome.db"
|
|
]
|
|
for c in candidates:
|
|
if os.path.exists(c):
|
|
self.db_path = c
|
|
break
|
|
|
|
def _get_connection(self) -> Optional[sqlite3.Connection]:
|
|
if not os.path.exists(self.db_path):
|
|
return None
|
|
try:
|
|
return sqlite3.connect(f"file:{self.db_path}?mode=ro", uri=True)
|
|
except Exception as e:
|
|
print(f"[WaveCollector] Error connecting to Navidrome DB: {e}")
|
|
return None
|
|
|
|
def get_anchor_profile(self) -> Dict[str, Any]:
|
|
"""Collect user preference anchors: starred tracks, played tracks, top artists."""
|
|
profile: Dict[str, Any] = {
|
|
"starred": [],
|
|
"top_played": [],
|
|
"top_artists": [],
|
|
"recent_added": []
|
|
}
|
|
conn = self._get_connection()
|
|
if not conn:
|
|
return profile
|
|
|
|
try:
|
|
c = conn.cursor()
|
|
|
|
# 1. Starred tracks (high signal)
|
|
c.execute("""
|
|
SELECT mf.id, mf.artist, mf.title, mf.album, mf.genre, a.starred_at
|
|
FROM annotation a
|
|
JOIN media_file mf ON a.item_id = mf.id
|
|
WHERE a.starred = 1
|
|
ORDER BY a.starred_at DESC
|
|
LIMIT 50
|
|
""")
|
|
for row in c.fetchall():
|
|
profile["starred"].append({
|
|
"id": row[0],
|
|
"artist": row[1],
|
|
"title": row[2],
|
|
"album": row[3],
|
|
"genre": row[4],
|
|
"starred_at": row[5]
|
|
})
|
|
|
|
# 2. Top played tracks (medium-high signal)
|
|
c.execute("""
|
|
SELECT mf.id, mf.artist, mf.title, a.play_count, a.play_date
|
|
FROM annotation a
|
|
JOIN media_file mf ON a.item_id = mf.id
|
|
WHERE a.play_count > 0
|
|
ORDER BY a.play_count DESC, a.play_date DESC
|
|
LIMIT 30
|
|
""")
|
|
for row in c.fetchall():
|
|
profile["top_played"].append({
|
|
"id": row[0],
|
|
"artist": row[1],
|
|
"title": row[2],
|
|
"play_count": row[3],
|
|
"play_date": row[4]
|
|
})
|
|
|
|
# 3. Top artists in library (general library profile)
|
|
c.execute("""
|
|
SELECT artist, count(*) as cnt
|
|
FROM media_file
|
|
WHERE artist IS NOT NULL AND artist != ''
|
|
GROUP BY artist
|
|
ORDER BY cnt DESC
|
|
LIMIT 25
|
|
""")
|
|
for row in c.fetchall():
|
|
profile["top_artists"].append({
|
|
"artist": row[0],
|
|
"count": row[1]
|
|
})
|
|
|
|
# 4. Recently added tracks
|
|
c.execute("""
|
|
SELECT id, artist, title, album, created_at
|
|
FROM media_file
|
|
ORDER BY created_at DESC
|
|
LIMIT 20
|
|
""")
|
|
for row in c.fetchall():
|
|
profile["recent_added"].append({
|
|
"id": row[0],
|
|
"artist": row[1],
|
|
"title": row[2],
|
|
"album": row[3],
|
|
"created_at": row[4]
|
|
})
|
|
|
|
except Exception as e:
|
|
print(f"[WaveCollector] Profile extraction error: {e}")
|
|
finally:
|
|
conn.close()
|
|
|
|
return profile
|
|
|
|
def get_existing_track_keys(self) -> Set[str]:
|
|
"""Returns set of normalized 'artist|title' of all songs already in Navidrome library."""
|
|
keys: Set[str] = set()
|
|
conn = self._get_connection()
|
|
if not conn:
|
|
return keys
|
|
|
|
try:
|
|
c = conn.cursor()
|
|
c.execute("SELECT artist, title FROM media_file")
|
|
for r in c.fetchall():
|
|
art = _normalize(r[0])
|
|
tit = _normalize(r[1])
|
|
if art and tit:
|
|
keys.add(f"{art}|{tit}")
|
|
except Exception as e:
|
|
print(f"[WaveCollector] Error reading existing track keys: {e}")
|
|
finally:
|
|
conn.close()
|
|
|
|
return keys
|
|
|
|
def get_starred_wave_tracks(self, wave_subpath: str = "AI Recommendations/Моя Волна") -> List[Dict[str, Any]]:
|
|
"""Find tracks in Wave folder that the user has starred (ready for permanent promotion)."""
|
|
starred_wave: List[Dict[str, Any]] = []
|
|
conn = self._get_connection()
|
|
if not conn:
|
|
return starred_wave
|
|
|
|
try:
|
|
c = conn.cursor()
|
|
pattern = f"{wave_subpath}/%"
|
|
c.execute("""
|
|
SELECT mf.id, mf.path, mf.artist, mf.album, mf.title, a.starred_at
|
|
FROM annotation a
|
|
JOIN media_file mf ON a.item_id = mf.id
|
|
WHERE a.starred = 1 AND mf.path LIKE ?
|
|
""", (pattern,))
|
|
for r in c.fetchall():
|
|
starred_wave.append({
|
|
"id": r[0],
|
|
"path": r[1],
|
|
"artist": r[2],
|
|
"album": r[3],
|
|
"title": r[4],
|
|
"starred_at": r[5]
|
|
})
|
|
except Exception as e:
|
|
print(f"[WaveCollector] Error finding starred wave tracks: {e}")
|
|
finally:
|
|
conn.close()
|
|
|
|
return starred_wave
|