user-editable hifi instances
This commit is contained in:
parent
69e909e687
commit
6ae1cb471e
5 changed files with 285 additions and 8 deletions
|
|
@ -91,14 +91,12 @@ class HiFiClient:
|
|||
self.download_path = Path(download_path)
|
||||
self.download_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# API instance management
|
||||
self._instances = list(DEFAULT_INSTANCES)
|
||||
if base_url:
|
||||
# User-provided instance gets top priority
|
||||
self._instances.insert(0, base_url.rstrip('/'))
|
||||
# API instance management — loaded from database
|
||||
self._instances = []
|
||||
self._instance_lock = threading.Lock()
|
||||
self._load_instances_from_db()
|
||||
|
||||
self._current_instance = self._instances[0] if self._instances else None
|
||||
self._instance_lock = threading.Lock()
|
||||
|
||||
# HTTP session with retry-friendly settings
|
||||
self.session = http_requests.Session()
|
||||
|
|
@ -126,6 +124,33 @@ class HiFiClient:
|
|||
"""Set a callback function to check for system shutdown."""
|
||||
self.shutdown_check = check_callable
|
||||
|
||||
def _load_instances_from_db(self):
|
||||
"""Load instances from the database, seeding defaults if empty."""
|
||||
try:
|
||||
from database.music_database import get_database
|
||||
db = get_database()
|
||||
db.seed_hifi_instances(DEFAULT_INSTANCES)
|
||||
rows = db.get_hifi_instances()
|
||||
urls = [r['url'] for r in rows if r['enabled']]
|
||||
if urls:
|
||||
self._instances = urls
|
||||
else:
|
||||
self._instances = list(DEFAULT_INSTANCES)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load HiFi instances from DB, using defaults: {e}")
|
||||
self._instances = list(DEFAULT_INSTANCES)
|
||||
|
||||
def reload_instances(self):
|
||||
"""Reload instances from the database (called after settings change)."""
|
||||
with self._instance_lock:
|
||||
old_current = self._current_instance
|
||||
self._load_instances_from_db()
|
||||
self._current_instance = self._instances[0] if self._instances else None
|
||||
if self._current_instance != old_current:
|
||||
logger.info(f"HiFi instances reloaded, active: {self._current_instance}")
|
||||
else:
|
||||
logger.info("HiFi instances reloaded")
|
||||
|
||||
# ===================== Instance Management =====================
|
||||
|
||||
def _get_instance(self) -> Optional[str]:
|
||||
|
|
|
|||
|
|
@ -723,6 +723,17 @@ class MusicDatabase:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
# HiFi API instances table
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS hifi_instances (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
url TEXT NOT NULL UNIQUE,
|
||||
priority INTEGER NOT NULL DEFAULT 0,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
logger.info("Database initialized successfully")
|
||||
|
||||
|
|
@ -11594,6 +11605,88 @@ class MusicDatabase:
|
|||
logger.error(f"Error getting issue counts: {e}")
|
||||
return {'open': 0, 'in_progress': 0, 'resolved': 0, 'dismissed': 0, 'total': 0}
|
||||
|
||||
# ===================== HiFi Instances =====================
|
||||
|
||||
def get_hifi_instances(self) -> List[Dict[str, Any]]:
|
||||
"""Get all enabled HiFi instances ordered by priority."""
|
||||
try:
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT url, priority, enabled FROM hifi_instances WHERE enabled = 1 ORDER BY priority ASC, id ASC")
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting HiFi instances: {e}")
|
||||
return []
|
||||
|
||||
def get_all_hifi_instances(self) -> List[Dict[str, Any]]:
|
||||
"""Get all HiFi instances (including disabled) ordered by priority."""
|
||||
try:
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT url, priority, enabled FROM hifi_instances ORDER BY priority ASC, id ASC")
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting all HiFi instances: {e}")
|
||||
return []
|
||||
|
||||
def add_hifi_instance(self, url: str, priority: int = 0) -> bool:
|
||||
"""Add a new HiFi instance."""
|
||||
try:
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"INSERT OR IGNORE INTO hifi_instances (url, priority, enabled) VALUES (?, ?, 1)",
|
||||
(url, priority)
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding HiFi instance: {e}")
|
||||
return False
|
||||
|
||||
def remove_hifi_instance(self, url: str) -> bool:
|
||||
"""Remove a HiFi instance."""
|
||||
try:
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM hifi_instances WHERE url = ?", (url,))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing HiFi instance: {e}")
|
||||
return False
|
||||
|
||||
def reorder_hifi_instances(self, urls: List[str]) -> bool:
|
||||
"""Update priorities based on the given URL order."""
|
||||
try:
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
for i, url in enumerate(urls):
|
||||
cursor.execute("UPDATE hifi_instances SET priority = ? WHERE url = ?", (i, url))
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error reordering HiFi instances: {e}")
|
||||
return False
|
||||
|
||||
def seed_hifi_instances(self, default_urls: List[str]) -> None:
|
||||
"""Insert default instances if the table is empty."""
|
||||
try:
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) as cnt FROM hifi_instances")
|
||||
count = cursor.fetchone()['cnt']
|
||||
if count == 0:
|
||||
for i, url in enumerate(default_urls):
|
||||
cursor.execute(
|
||||
"INSERT OR IGNORE INTO hifi_instances (url, priority, enabled) VALUES (?, ?, 1)",
|
||||
(url, i)
|
||||
)
|
||||
conn.commit()
|
||||
logger.info(f"Seeded {len(default_urls)} default HiFi instances")
|
||||
except Exception as e:
|
||||
logger.error(f"Error seeding HiFi instances: {e}")
|
||||
|
||||
# Thread-safe singleton pattern for database access
|
||||
_database_instances: Dict[int, MusicDatabase] = {} # Thread ID -> Database instance
|
||||
_database_lock = threading.Lock()
|
||||
|
|
|
|||
|
|
@ -24700,6 +24700,89 @@ def hifi_instances():
|
|||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/hifi/instances', methods=['POST'])
|
||||
def hifi_add_instance():
|
||||
"""Add a new HiFi API instance."""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
url = data.get('url', '').strip().rstrip('/')
|
||||
if not url:
|
||||
return jsonify({'success': False, 'error': 'URL is required'}), 400
|
||||
if not url.startswith(('http://', 'https://')):
|
||||
return jsonify({'success': False, 'error': 'URL must start with http:// or https://'}), 400
|
||||
from database.music_database import get_database
|
||||
db = get_database()
|
||||
# Get current count to assign next priority
|
||||
existing = db.get_all_hifi_instances()
|
||||
priority = len(existing)
|
||||
added = db.add_hifi_instance(url, priority)
|
||||
if not added:
|
||||
return jsonify({'success': False, 'error': 'Instance already exists'}), 400
|
||||
# Reload the client
|
||||
if soulseek_client and hasattr(soulseek_client, 'hifi') and soulseek_client.hifi:
|
||||
soulseek_client.hifi.reload_instances()
|
||||
return jsonify({'success': True, 'url': url})
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding HiFi instance: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/hifi/instances/<path:url>', methods=['DELETE'])
|
||||
def hifi_remove_instance(url):
|
||||
"""Remove a HiFi API instance."""
|
||||
try:
|
||||
url = url.strip().rstrip('/')
|
||||
if not url:
|
||||
return jsonify({'success': False, 'error': 'URL is required'}), 400
|
||||
from database.music_database import get_database
|
||||
db = get_database()
|
||||
removed = db.remove_hifi_instance(url)
|
||||
if not removed:
|
||||
return jsonify({'success': False, 'error': 'Instance not found'}), 404
|
||||
# Reload the client
|
||||
if soulseek_client and hasattr(soulseek_client, 'hifi') and soulseek_client.hifi:
|
||||
soulseek_client.hifi.reload_instances()
|
||||
return jsonify({'success': True, 'url': url})
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing HiFi instance: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/hifi/instances/reorder', methods=['POST'])
|
||||
def hifi_reorder_instances():
|
||||
"""Reorder HiFi API instances."""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
urls = data.get('urls', [])
|
||||
if not urls:
|
||||
return jsonify({'success': False, 'error': 'URL list is required'}), 400
|
||||
from database.music_database import get_database
|
||||
db = get_database()
|
||||
db.reorder_hifi_instances(urls)
|
||||
# Reload the client
|
||||
if soulseek_client and hasattr(soulseek_client, 'hifi') and soulseek_client.hifi:
|
||||
soulseek_client.hifi.reload_instances()
|
||||
return jsonify({'success': True})
|
||||
except Exception as e:
|
||||
logger.error(f"Error reordering HiFi instances: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/hifi/instances/list', methods=['GET'])
|
||||
def hifi_list_instances():
|
||||
"""Get editable list of HiFi API instances."""
|
||||
try:
|
||||
from database.music_database import get_database
|
||||
from core.hifi_client import DEFAULT_INSTANCES
|
||||
db = get_database()
|
||||
db.seed_hifi_instances(DEFAULT_INSTANCES)
|
||||
instances = db.get_all_hifi_instances()
|
||||
return jsonify({'instances': instances})
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing HiFi instances: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# DEEZER DOWNLOAD ENDPOINTS
|
||||
# ===================================================================
|
||||
|
|
|
|||
|
|
@ -4622,8 +4622,13 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<label>API Instances:</label>
|
||||
<div id="hifi-instances-list" style="margin-bottom: 8px;"></div>
|
||||
<div style="display:flex;gap:8px;margin-bottom:8px;">
|
||||
<input type="url" id="hifi-new-instance" class="form-input" placeholder="https://example.com" style="flex:1;">
|
||||
<button class="test-button" onclick="addHiFiInstance()">Add</button>
|
||||
</div>
|
||||
<button class="test-button" onclick="checkHiFiInstances()" id="hifi-instances-check-btn" style="margin-bottom: 8px;">Check All Instances</button>
|
||||
<div id="hifi-instances-panel" style="display: none;"></div>
|
||||
<div id="hifi-instances-status-panel" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -797,6 +797,7 @@ async function loadSettingsData() {
|
|||
document.getElementById('qobuz-allow-fallback').checked = settings.qobuz?.allow_fallback !== false;
|
||||
document.getElementById('hifi-download-quality').value = settings.hifi_download?.quality || 'lossless';
|
||||
document.getElementById('hifi-allow-fallback').checked = settings.hifi_download?.allow_fallback !== false;
|
||||
loadHiFiInstances();
|
||||
document.getElementById('deezer-download-quality').value = settings.deezer_download?.quality || 'flac';
|
||||
document.getElementById('deezer-allow-fallback').checked = settings.deezer_download?.allow_fallback !== false;
|
||||
document.getElementById('deezer-download-arl').value = settings.deezer_download?.arl || '';
|
||||
|
|
@ -3277,8 +3278,78 @@ async function testLidarrConnection() {
|
|||
}
|
||||
}
|
||||
|
||||
async function loadHiFiInstances() {
|
||||
const listEl = document.getElementById('hifi-instances-list');
|
||||
if (!listEl) return;
|
||||
try {
|
||||
const resp = await fetch('/api/hifi/instances/list');
|
||||
const data = await resp.json();
|
||||
if (!data.instances || data.instances.length === 0) {
|
||||
listEl.innerHTML = '<div style="color: rgba(255,255,255,0.4); font-size: 0.85em;">No instances configured.</div>';
|
||||
return;
|
||||
}
|
||||
listEl.innerHTML = data.instances.map((inst, i) => {
|
||||
const enabledClass = inst.enabled ? '' : 'opacity:0.4;';
|
||||
const checkHtml = inst.enabled
|
||||
? `<span style="color:#4caf50;cursor:pointer;" onclick="toggleHiFiInstance('${escapeHtml(inst.url)}')" title="Click to disable">✔</span>`
|
||||
: `<span style="color:#666;cursor:pointer;" onclick="toggleHiFiInstance('${escapeHtml(inst.url)}')" title="Click to enable">✘</span>`;
|
||||
return `<div style="display:flex;align-items:center;gap:8px;padding:4px 0;font-size:0.82em;${enabledClass}">
|
||||
<span style="color:rgba(255,255,255,0.4);cursor:default;user-select:none;">☰</span>
|
||||
<span style="flex:1;color:rgba(255,255,255,0.7);font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(inst.url)}</span>
|
||||
${checkHtml}
|
||||
<span style="color:#f44336;cursor:pointer;font-size:0.9em;" onclick="removeHiFiInstance('${escapeHtml(inst.url)}')" title="Remove instance">✖</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
listEl.innerHTML = `<div style="color:#f44336;font-size:0.85em;">Error loading instances: ${escapeHtml(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function addHiFiInstance() {
|
||||
const input = document.getElementById('hifi-new-instance');
|
||||
if (!input) return;
|
||||
const url = input.value.trim();
|
||||
if (!url) return;
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
alert('URL must start with http:// or https://');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch('/api/hifi/instances', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
input.value = '';
|
||||
loadHiFiInstances();
|
||||
} else {
|
||||
alert(data.error || 'Failed to add instance');
|
||||
}
|
||||
} catch (e) {
|
||||
alert(`Error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeHiFiInstance(url) {
|
||||
try {
|
||||
const resp = await fetch(`/api/hifi/instances/${encodeURIComponent(url)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
loadHiFiInstances();
|
||||
} else {
|
||||
alert(data.error || 'Failed to remove instance');
|
||||
}
|
||||
} catch (e) {
|
||||
alert(`Error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkHiFiInstances() {
|
||||
const panel = document.getElementById('hifi-instances-panel');
|
||||
const panel = document.getElementById('hifi-instances-status-panel');
|
||||
const btn = document.getElementById('hifi-instances-check-btn');
|
||||
if (!panel) return;
|
||||
panel.style.display = 'block';
|
||||
|
|
|
|||
Loading…
Reference in a new issue