diff --git a/core/debug_info.py b/core/debug_info.py new file mode 100644 index 00000000..4ad83f1d --- /dev/null +++ b/core/debug_info.py @@ -0,0 +1,377 @@ +"""Debug info endpoint — lifted from web_server.py. + +The function bodies are byte-identical to the originals. Module-level +shims for ``spotify_client`` and ``tidal_client`` (proxies that resolve +through the metadata registry / runtime client registry) plus injected +state dicts and helpers let the bodies resolve their original names +without modification. +""" +import logging +import os +import platform +from pathlib import Path + +from flask import jsonify, request + +from config.settings import config_manager +from core.metadata.registry import get_spotify_client + +logger = logging.getLogger(__name__) + + +class _SpotifyClientProxy: + """Resolves the global Spotify client lazily through core.metadata.registry.""" + + def __getattr__(self, name): + client = get_spotify_client() + if client is None: + raise AttributeError(name) + return getattr(client, name) + + def __bool__(self): + return get_spotify_client() is not None + + +class _TidalClientProxy: + """Resolves the global Tidal client lazily via an injected getter so a + Tidal re-auth that rebinds web_server.tidal_client is visible here.""" + + def __getattr__(self, name): + if _get_tidal_client is None: + raise AttributeError(name) + client = _get_tidal_client() + if client is None: + raise AttributeError(name) + return getattr(client, name) + + def __bool__(self): + if _get_tidal_client is None: + return False + return _get_tidal_client() is not None + + +spotify_client = _SpotifyClientProxy() +tidal_client = _TidalClientProxy() +_get_tidal_client = None # injected via init() + + +# Injected at runtime via init(). +SOULSYNC_VERSION = None +_DIRECT_RUN = None +_status_cache = None +qobuz_enrichment_worker = None +download_batches = None +sync_states = None +youtube_playlist_states = None +tidal_discovery_states = None +soulseek_client = None +_log_path = None +_log_dir = None +app = None +get_database = None + + +def init( + soulsync_version, + direct_run, + status_cache, + qobuz_worker, + download_batches_dict, + sync_states_dict, + youtube_playlist_states_dict, + tidal_discovery_states_dict, + soulseek_client_obj, + log_path, + log_dir, + flask_app, + get_database_fn, + tidal_client_getter, +): + """Bind shared state/helpers from web_server.""" + global SOULSYNC_VERSION, _DIRECT_RUN, _status_cache, qobuz_enrichment_worker + global download_batches, sync_states, youtube_playlist_states + global tidal_discovery_states, soulseek_client, _log_path, _log_dir + global app, get_database, _get_tidal_client + SOULSYNC_VERSION = soulsync_version + _DIRECT_RUN = direct_run + _status_cache = status_cache + qobuz_enrichment_worker = qobuz_worker + download_batches = download_batches_dict + sync_states = sync_states_dict + youtube_playlist_states = youtube_playlist_states_dict + tidal_discovery_states = tidal_discovery_states_dict + soulseek_client = soulseek_client_obj + _log_path = log_path + _log_dir = log_dir + app = flask_app + get_database = get_database_fn + _get_tidal_client = tidal_client_getter + + +def _safe_check(fn, default=False): + """Safely evaluate a check function, returning default on any error.""" + try: + return fn() + except Exception: + return default + + +def get_debug_info(): + """Collect system diagnostics for troubleshooting support requests.""" + import sys + import psutil + import time + from datetime import timedelta + + log_lines = request.args.get('lines', 20, type=int) + log_lines = max(10, min(log_lines, 500)) + log_source = request.args.get('log', 'app') + + info = {} + + # App info + info['version'] = SOULSYNC_VERSION + info['os'] = f"{platform.system()} {platform.release()}" + info['python'] = sys.version.split()[0] + info['docker'] = os.path.exists('/.dockerenv') + info['runner'] = 'gunicorn' if not _DIRECT_RUN else 'direct (python web_server.py)' + + # ffmpeg version + try: + import subprocess + result = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True, timeout=5) + first_line = result.stdout.split('\n')[0] if result.stdout else '' + # e.g. "ffmpeg version 6.1.1 Copyright ..." + info['ffmpeg'] = first_line.split('Copyright')[0].replace('ffmpeg version', '').strip() if first_line else 'installed (version unknown)' + except FileNotFoundError: + info['ffmpeg'] = 'NOT INSTALLED' + except Exception: + info['ffmpeg'] = 'unknown' + + # Uptime + start_time = getattr(app, 'start_time', time.time()) + uptime_seconds = time.time() - start_time + info['uptime'] = str(timedelta(seconds=int(uptime_seconds))) + + # Paths + download_path = config_manager.get('soulseek.download_path', './downloads') + transfer_folder = config_manager.get('soulseek.transfer_path', './Transfer') + staging_folder = config_manager.get('import.staging_path', '') + info['paths'] = { + 'download_path': download_path, + 'download_path_exists': os.path.isdir(download_path) if download_path else False, + 'download_path_writable': os.access(download_path, os.W_OK) if download_path and os.path.isdir(download_path) else False, + 'transfer_folder': transfer_folder, + 'transfer_folder_exists': os.path.isdir(transfer_folder) if transfer_folder else False, + 'transfer_folder_writable': os.access(transfer_folder, os.W_OK) if transfer_folder and os.path.isdir(transfer_folder) else False, + 'staging_folder': staging_folder, + 'staging_folder_exists': os.path.isdir(staging_folder) if staging_folder else False, + } + # Music library paths (Settings > Library) + music_paths = config_manager.get('library.music_paths', []) + if isinstance(music_paths, list) and music_paths: + info['paths']['music_library_paths'] = [] + for p in music_paths: + if p and isinstance(p, str): + info['paths']['music_library_paths'].append({ + 'path': p, + 'exists': os.path.isdir(p), + }) + # Music videos directory + music_videos_path = config_manager.get('library.music_videos_path', '') + if music_videos_path: + info['paths']['music_videos_path'] = music_videos_path + info['paths']['music_videos_path_exists'] = os.path.isdir(music_videos_path) + + # Services from status cache + spotify_cache = _status_cache.get('spotify', {}) + media_server_cache = _status_cache.get('media_server', {}) + soulseek_cache = _status_cache.get('soulseek', {}) + info['services'] = { + 'music_source': spotify_cache.get('source', 'unknown'), + 'spotify_connected': spotify_cache.get('connected', False), + 'spotify_rate_limited': spotify_cache.get('rate_limited', False), + 'media_server_type': media_server_cache.get('type', 'none'), + 'media_server_connected': media_server_cache.get('connected', False), + 'soulseek_connected': soulseek_cache.get('connected', False), + 'download_source': config_manager.get('download_source.mode', 'hybrid'), + 'tidal_connected': _safe_check(lambda: bool(tidal_client and tidal_client.is_authenticated())), + 'qobuz_connected': _safe_check(lambda: bool(qobuz_enrichment_worker and qobuz_enrichment_worker.client and qobuz_enrichment_worker.client.is_authenticated())), + } + + # Enrichment workers + workers = {} + worker_names = ['musicbrainz', 'audiodb', 'deezer', 'spotify', 'itunes', 'lastfm', 'genius', 'discogs', 'tidal', 'qobuz'] + for name in worker_names: + paused_key = f'{name}_enrichment_paused' + workers[name] = 'paused' if config_manager.get(paused_key, False) else 'active' + info['enrichment_workers'] = workers + + # Library stats — use same method as dashboard (filters by active server) + try: + db = get_database() + lib_stats = db.get_database_info_for_server() + info['library'] = { + 'artists': lib_stats.get('artists', 0), + 'albums': lib_stats.get('albums', 0), + 'tracks': lib_stats.get('tracks', 0), + } + except Exception: + info['library'] = {'artists': 0, 'albums': 0, 'tracks': 0} + + # Watchlist count + try: + db = get_database() + info['watchlist_count'] = db.get_watchlist_count() + except Exception: + info['watchlist_count'] = 0 + + # Wishlist pending count + try: + db = get_database() + info['wishlist_count'] = db.get_wishlist_count() + except Exception: + info['wishlist_count'] = 0 + + # Automation count + try: + db = get_database() + automations = db.get_automations() + info['automations'] = { + 'total': len(automations), + 'enabled': len([a for a in automations if a.get('enabled', False)]), + } + except Exception: + info['automations'] = {'total': 0, 'enabled': 0} + + # Active downloads & syncs (use list() snapshots to avoid RuntimeError from concurrent mutation) + try: + active_downloads = len([bid for bid, bd in list(download_batches.items()) if bd.get('phase') == 'downloading']) + except Exception: + active_downloads = 0 + active_syncs = 0 + try: + for _pid, ss in list(sync_states.items()): + if ss.get('status') == 'syncing': + active_syncs += 1 + for _uh, st in list(youtube_playlist_states.items()): + if st.get('phase') == 'syncing': + active_syncs += 1 + for _pid, st in list(tidal_discovery_states.items()): + if st.get('phase') == 'syncing': + active_syncs += 1 + except Exception: + pass + info['active_downloads'] = active_downloads + info['active_syncs'] = active_syncs + + # Config settings relevant to troubleshooting + source_mode = config_manager.get('download_source.mode', 'hybrid') + info['config'] = { + 'source_mode': source_mode, + 'quality_profile': config_manager.get('download_source.quality_profile', 'default'), + 'organization_template': config_manager.get('organization.folder_template', ''), + 'post_processing_enabled': config_manager.get('post_processing.enabled', True), + 'acoustid_enabled': bool(config_manager.get('acoustid.api_key', '')), + 'auto_scan_enabled': config_manager.get('watchlist.auto_scan', False), + 'm3u_export_enabled': config_manager.get('m3u.enabled', False), + 'log_level': config_manager.get('logging.level', 'INFO'), + 'primary_metadata_source': config_manager.get('metadata.fallback_source', 'deezer'), + 'lossy_copy_enabled': config_manager.get('post_processing.lossy_copy.enabled', False), + 'lossy_copy_format': config_manager.get('post_processing.lossy_copy.format', 'mp3'), + 'lossy_copy_bitrate': config_manager.get('post_processing.lossy_copy.bitrate', 320), + 'allow_duplicate_tracks': config_manager.get('library.allow_duplicate_tracks', False), + 'replace_lower_quality': config_manager.get('import.replace_lower_quality', False), + 'auto_import_enabled': config_manager.get('import.auto_import_enabled', False), + } + # Hybrid source priority order + if source_mode == 'hybrid': + info['config']['hybrid_sources'] = config_manager.get('download_source.hybrid_order', []) + # Discogs connection status + info['services']['discogs_connected'] = bool(config_manager.get('discogs.token', '')) + + # Download client init failures + info['download_client_failures'] = [] + if soulseek_client and hasattr(soulseek_client, '_init_failures'): + info['download_client_failures'] = soulseek_client._init_failures + elif not soulseek_client: + info['download_client_failures'] = ['ALL (orchestrator failed to initialize)'] + + # API rate monitor — current calls/min, 24h totals, peaks, rate limit events + try: + from core.api_call_tracker import api_call_tracker + rates = api_call_tracker.get_all_rates() + info['api_rates'] = rates + # Rich 24h debug summary with peaks, totals, per-endpoint breakdown, events + info['api_debug_summary'] = api_call_tracker.get_debug_summary() + # Spotify rate limit details + if spotify_client: + rl_info = spotify_client.get_rate_limit_info() + if rl_info: + info['spotify_rate_limit'] = { + 'active': True, + 'remaining_seconds': rl_info.get('remaining_seconds', 0), + 'retry_after': rl_info.get('retry_after', 0), + 'endpoint': rl_info.get('endpoint', ''), + 'expires_at': rl_info.get('expires_at', ''), + } + else: + info['spotify_rate_limit'] = {'active': False} + except Exception: + info['api_rates'] = {} + info['api_debug_summary'] = {} + info['spotify_rate_limit'] = {'active': False} + + # Database size + db_path = os.path.join('database', 'music_library.db') + if os.path.exists(db_path): + db_size_mb = os.path.getsize(db_path) / (1024 * 1024) + info['database_size'] = f"{db_size_mb:.1f} MB" + else: + info['database_size'] = 'not found' + + # Memory & CPU + process = psutil.Process(os.getpid()) + mem = process.memory_info() + info['memory_usage'] = f"{mem.rss / (1024 * 1024):.0f} MB" + info['system_memory'] = f"{psutil.virtual_memory().percent}%" + try: + info['cpu_percent'] = f"{process.cpu_percent(interval=0.1):.1f}%" + except Exception: + info['cpu_percent'] = 'unknown' + info['thread_count'] = process.num_threads() + + # Log lines + log_map = { + 'app': Path(_log_path), + 'acoustid': _log_dir / 'acoustid.log', + 'post_processing': _log_dir / 'post_processing.log', + 'source_reuse': _log_dir / 'source_reuse.log', + } + log_path = log_map.get(log_source, log_map['app']) + info['log_source'] = log_source + info['log_lines_requested'] = log_lines + info['recent_logs'] = [] + if os.path.exists(log_path): + try: + with open(log_path, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + info['recent_logs'] = [line.rstrip() for line in lines[-log_lines:]] + except Exception: + info['recent_logs'] = ['(could not read log file)'] + + # Available log files + info['available_logs'] = [] + logs_dir = 'logs' + if os.path.isdir(logs_dir): + for fname in sorted(os.listdir(logs_dir)): + if fname.endswith('.log'): + fpath = os.path.join(logs_dir, fname) + size_kb = os.path.getsize(fpath) / 1024 + info['available_logs'].append({ + 'name': fname.replace('.log', ''), + 'file': fname, + 'size': f"{size_kb:.0f} KB" if size_kb < 1024 else f"{size_kb/1024:.1f} MB", + }) + + return jsonify(info) diff --git a/web_server.py b/web_server.py index 456a39d8..4814169c 100644 --- a/web_server.py +++ b/web_server.py @@ -3972,274 +3972,16 @@ def get_system_stats(): return jsonify({'error': str(e)}), 500 -def _safe_check(fn, default=False): - """Safely evaluate a check function, returning default on any error.""" - try: - return fn() - except Exception: - return default +from core.debug_info import ( + _safe_check, + get_debug_info as _debug_info_get, + init as _init_debug_info, +) @app.route('/api/debug-info') def get_debug_info(): - """Collect system diagnostics for troubleshooting support requests.""" - import sys - import psutil - import time - from datetime import timedelta - - log_lines = request.args.get('lines', 20, type=int) - log_lines = max(10, min(log_lines, 500)) - log_source = request.args.get('log', 'app') - - info = {} - - # App info - info['version'] = SOULSYNC_VERSION - info['os'] = f"{platform.system()} {platform.release()}" - info['python'] = sys.version.split()[0] - info['docker'] = os.path.exists('/.dockerenv') - info['runner'] = 'gunicorn' if not _DIRECT_RUN else 'direct (python web_server.py)' - - # ffmpeg version - try: - import subprocess - result = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True, timeout=5) - first_line = result.stdout.split('\n')[0] if result.stdout else '' - # e.g. "ffmpeg version 6.1.1 Copyright ..." - info['ffmpeg'] = first_line.split('Copyright')[0].replace('ffmpeg version', '').strip() if first_line else 'installed (version unknown)' - except FileNotFoundError: - info['ffmpeg'] = 'NOT INSTALLED' - except Exception: - info['ffmpeg'] = 'unknown' - - # Uptime - start_time = getattr(app, 'start_time', time.time()) - uptime_seconds = time.time() - start_time - info['uptime'] = str(timedelta(seconds=int(uptime_seconds))) - - # Paths - download_path = config_manager.get('soulseek.download_path', './downloads') - transfer_folder = config_manager.get('soulseek.transfer_path', './Transfer') - staging_folder = config_manager.get('import.staging_path', '') - info['paths'] = { - 'download_path': download_path, - 'download_path_exists': os.path.isdir(download_path) if download_path else False, - 'download_path_writable': os.access(download_path, os.W_OK) if download_path and os.path.isdir(download_path) else False, - 'transfer_folder': transfer_folder, - 'transfer_folder_exists': os.path.isdir(transfer_folder) if transfer_folder else False, - 'transfer_folder_writable': os.access(transfer_folder, os.W_OK) if transfer_folder and os.path.isdir(transfer_folder) else False, - 'staging_folder': staging_folder, - 'staging_folder_exists': os.path.isdir(staging_folder) if staging_folder else False, - } - # Music library paths (Settings > Library) - music_paths = config_manager.get('library.music_paths', []) - if isinstance(music_paths, list) and music_paths: - info['paths']['music_library_paths'] = [] - for p in music_paths: - if p and isinstance(p, str): - info['paths']['music_library_paths'].append({ - 'path': p, - 'exists': os.path.isdir(p), - }) - # Music videos directory - music_videos_path = config_manager.get('library.music_videos_path', '') - if music_videos_path: - info['paths']['music_videos_path'] = music_videos_path - info['paths']['music_videos_path_exists'] = os.path.isdir(music_videos_path) - - # Services from status cache - spotify_cache = _status_cache.get('spotify', {}) - media_server_cache = _status_cache.get('media_server', {}) - soulseek_cache = _status_cache.get('soulseek', {}) - info['services'] = { - 'music_source': spotify_cache.get('source', 'unknown'), - 'spotify_connected': spotify_cache.get('connected', False), - 'spotify_rate_limited': spotify_cache.get('rate_limited', False), - 'media_server_type': media_server_cache.get('type', 'none'), - 'media_server_connected': media_server_cache.get('connected', False), - 'soulseek_connected': soulseek_cache.get('connected', False), - 'download_source': config_manager.get('download_source.mode', 'hybrid'), - 'tidal_connected': _safe_check(lambda: bool(tidal_client and tidal_client.is_authenticated())), - 'qobuz_connected': _safe_check(lambda: bool(qobuz_enrichment_worker and qobuz_enrichment_worker.client and qobuz_enrichment_worker.client.is_authenticated())), - } - - # Enrichment workers - workers = {} - worker_names = ['musicbrainz', 'audiodb', 'deezer', 'spotify', 'itunes', 'lastfm', 'genius', 'discogs', 'tidal', 'qobuz'] - for name in worker_names: - paused_key = f'{name}_enrichment_paused' - workers[name] = 'paused' if config_manager.get(paused_key, False) else 'active' - info['enrichment_workers'] = workers - - # Library stats — use same method as dashboard (filters by active server) - try: - db = get_database() - lib_stats = db.get_database_info_for_server() - info['library'] = { - 'artists': lib_stats.get('artists', 0), - 'albums': lib_stats.get('albums', 0), - 'tracks': lib_stats.get('tracks', 0), - } - except Exception: - info['library'] = {'artists': 0, 'albums': 0, 'tracks': 0} - - # Watchlist count - try: - db = get_database() - info['watchlist_count'] = db.get_watchlist_count() - except Exception: - info['watchlist_count'] = 0 - - # Wishlist pending count - try: - db = get_database() - info['wishlist_count'] = db.get_wishlist_count() - except Exception: - info['wishlist_count'] = 0 - - # Automation count - try: - db = get_database() - automations = db.get_automations() - info['automations'] = { - 'total': len(automations), - 'enabled': len([a for a in automations if a.get('enabled', False)]), - } - except Exception: - info['automations'] = {'total': 0, 'enabled': 0} - - # Active downloads & syncs (use list() snapshots to avoid RuntimeError from concurrent mutation) - try: - active_downloads = len([bid for bid, bd in list(download_batches.items()) if bd.get('phase') == 'downloading']) - except Exception: - active_downloads = 0 - active_syncs = 0 - try: - for _pid, ss in list(sync_states.items()): - if ss.get('status') == 'syncing': - active_syncs += 1 - for _uh, st in list(youtube_playlist_states.items()): - if st.get('phase') == 'syncing': - active_syncs += 1 - for _pid, st in list(tidal_discovery_states.items()): - if st.get('phase') == 'syncing': - active_syncs += 1 - except Exception: - pass - info['active_downloads'] = active_downloads - info['active_syncs'] = active_syncs - - # Config settings relevant to troubleshooting - source_mode = config_manager.get('download_source.mode', 'hybrid') - info['config'] = { - 'source_mode': source_mode, - 'quality_profile': config_manager.get('download_source.quality_profile', 'default'), - 'organization_template': config_manager.get('organization.folder_template', ''), - 'post_processing_enabled': config_manager.get('post_processing.enabled', True), - 'acoustid_enabled': bool(config_manager.get('acoustid.api_key', '')), - 'auto_scan_enabled': config_manager.get('watchlist.auto_scan', False), - 'm3u_export_enabled': config_manager.get('m3u.enabled', False), - 'log_level': config_manager.get('logging.level', 'INFO'), - 'primary_metadata_source': config_manager.get('metadata.fallback_source', 'deezer'), - 'lossy_copy_enabled': config_manager.get('post_processing.lossy_copy.enabled', False), - 'lossy_copy_format': config_manager.get('post_processing.lossy_copy.format', 'mp3'), - 'lossy_copy_bitrate': config_manager.get('post_processing.lossy_copy.bitrate', 320), - 'allow_duplicate_tracks': config_manager.get('library.allow_duplicate_tracks', False), - 'replace_lower_quality': config_manager.get('import.replace_lower_quality', False), - 'auto_import_enabled': config_manager.get('import.auto_import_enabled', False), - } - # Hybrid source priority order - if source_mode == 'hybrid': - info['config']['hybrid_sources'] = config_manager.get('download_source.hybrid_order', []) - # Discogs connection status - info['services']['discogs_connected'] = bool(config_manager.get('discogs.token', '')) - - # Download client init failures - info['download_client_failures'] = [] - if soulseek_client and hasattr(soulseek_client, '_init_failures'): - info['download_client_failures'] = soulseek_client._init_failures - elif not soulseek_client: - info['download_client_failures'] = ['ALL (orchestrator failed to initialize)'] - - # API rate monitor — current calls/min, 24h totals, peaks, rate limit events - try: - from core.api_call_tracker import api_call_tracker - rates = api_call_tracker.get_all_rates() - info['api_rates'] = rates - # Rich 24h debug summary with peaks, totals, per-endpoint breakdown, events - info['api_debug_summary'] = api_call_tracker.get_debug_summary() - # Spotify rate limit details - if spotify_client: - rl_info = spotify_client.get_rate_limit_info() - if rl_info: - info['spotify_rate_limit'] = { - 'active': True, - 'remaining_seconds': rl_info.get('remaining_seconds', 0), - 'retry_after': rl_info.get('retry_after', 0), - 'endpoint': rl_info.get('endpoint', ''), - 'expires_at': rl_info.get('expires_at', ''), - } - else: - info['spotify_rate_limit'] = {'active': False} - except Exception: - info['api_rates'] = {} - info['api_debug_summary'] = {} - info['spotify_rate_limit'] = {'active': False} - - # Database size - db_path = os.path.join('database', 'music_library.db') - if os.path.exists(db_path): - db_size_mb = os.path.getsize(db_path) / (1024 * 1024) - info['database_size'] = f"{db_size_mb:.1f} MB" - else: - info['database_size'] = 'not found' - - # Memory & CPU - process = psutil.Process(os.getpid()) - mem = process.memory_info() - info['memory_usage'] = f"{mem.rss / (1024 * 1024):.0f} MB" - info['system_memory'] = f"{psutil.virtual_memory().percent}%" - try: - info['cpu_percent'] = f"{process.cpu_percent(interval=0.1):.1f}%" - except Exception: - info['cpu_percent'] = 'unknown' - info['thread_count'] = process.num_threads() - - # Log lines - log_map = { - 'app': Path(_log_path), - 'acoustid': _log_dir / 'acoustid.log', - 'post_processing': _log_dir / 'post_processing.log', - 'source_reuse': _log_dir / 'source_reuse.log', - } - log_path = log_map.get(log_source, log_map['app']) - info['log_source'] = log_source - info['log_lines_requested'] = log_lines - info['recent_logs'] = [] - if os.path.exists(log_path): - try: - with open(log_path, 'r', encoding='utf-8', errors='replace') as f: - lines = f.readlines() - info['recent_logs'] = [line.rstrip() for line in lines[-log_lines:]] - except Exception: - info['recent_logs'] = ['(could not read log file)'] - - # Available log files - info['available_logs'] = [] - logs_dir = 'logs' - if os.path.isdir(logs_dir): - for fname in sorted(os.listdir(logs_dir)): - if fname.endswith('.log'): - fpath = os.path.join(logs_dir, fname) - size_kb = os.path.getsize(fpath) / 1024 - info['available_logs'].append({ - 'name': fname.replace('.log', ''), - 'file': fname, - 'size': f"{size_kb:.0f} KB" if size_kb < 1024 else f"{size_kb/1024:.1f} MB", - }) - - return jsonify(info) + return _debug_info_get() @app.route('/api/activity/feed') @@ -34192,6 +33934,23 @@ _init_connection_test( docker_resolve_url_fn=docker_resolve_url, docker_resolve_path_fn=docker_resolve_path, ) + +_init_debug_info( + soulsync_version=SOULSYNC_VERSION, + direct_run=_DIRECT_RUN, + status_cache=_status_cache, + qobuz_worker=qobuz_enrichment_worker, + download_batches_dict=download_batches, + sync_states_dict=sync_states, + youtube_playlist_states_dict=youtube_playlist_states, + tidal_discovery_states_dict=tidal_discovery_states, + soulseek_client_obj=soulseek_client, + log_path=_log_path, + log_dir=_log_dir, + flask_app=app, + get_database_fn=get_database, + tidal_client_getter=lambda: tidal_client, +) metadata_registry.register_profile_spotify_credentials_provider( lambda profile_id: get_database().get_profile_spotify(profile_id) )