diff --git a/headless.md b/headless.md
new file mode 100644
index 00000000..c352b9e5
--- /dev/null
+++ b/headless.md
@@ -0,0 +1,323 @@
+# SoulSync Web UI Development Plan
+
+## Overview
+Build a complete web replica of the SoulSync PyQt6 GUI application with identical layout, styling, and visual components. This will be a single-page application (SPA) contained in `index.html` with all pages shown/hidden via JavaScript.
+
+## Current Architecture Analysis
+
+### Main GUI Structure (PyQt6)
+- **Sidebar (240px width)**: Navigation, media player, donation widget, version info, service status
+- **Main Content Area**: QStackedWidget containing 5 pages
+- **Pages**: Dashboard, Sync, Downloads (Search), Artists, Settings
+- **Styling**: Dark theme with Spotify green accents (#1ed760), modern gradients, rounded corners
+
+### Key Components Identified
+
+#### Sidebar Components:
+1. **Header**: App name "SoulSync" with subtitle "Music Sync & Manager"
+2. **Navigation**: 5 buttons with icons
+ - π Dashboard
+ - π Sync
+ - π₯ Search (Downloads)
+ - π΅ Artists
+ - βοΈ Settings
+3. **Media Player**: Collapsible with track info, play/pause, volume control, loading animation
+4. **Crypto Donation**: Collapsible section with Ko-fi and crypto addresses
+5. **Version Info**: Clickable version button (v.0.65)
+6. **Service Status**: 3 indicators (Spotify, Media Server, Soulseek) with connection dots
+
+#### Page Components:
+1. **Dashboard**: Service stats, recent activity, quick actions, uptime tracking
+2. **Sync**: Playlist management, sync operations, progress tracking, track analysis
+3. **Downloads/Search**: Music search, download queue, media player, search filters
+4. **Artists**: Artist browsing, album management, collection overview, completion status
+5. **Settings**: Configuration forms, service setup, preferences, detection tools
+
+## Implementation Plan
+
+### Phase 1: Core Framework Setup β
+**Files to Create:**
+- `headless.md` (this document)
+- Update `webui/index.html` (single-page structure)
+- Update `webui/static/style.css` (complete styling system)
+- Update `webui/static/script.js` (page management, API integration)
+
+### Phase 2: Layout Structure
+**2.1 HTML Structure (index.html)**
+```html
+
+
+
+
...
+
...
+
...
+
...
+
...
+
+
+```
+
+**2.2 CSS Framework (style.css)**
+- Dark theme base colors (#121212, #1e1e1e, #0d1117)
+- Spotify green accent system (#1ed760, #1ed760, #1ca851)
+- Modern gradient backgrounds (qlineargradient equivalents)
+- Rounded corner design language (12px, 16px border-radius)
+- Responsive grid systems
+- Animation/transition systems (300ms ease curves)
+
+**2.3 JavaScript Page Manager (script.js)**
+- Page switching logic (show/hide with fade transitions)
+- State persistence (current page, form data)
+- API communication layer (fetch-based)
+- Component management system
+- Real-time status updates (5-second intervals)
+
+### Phase 3: Sidebar Implementation
+**3.1 Sidebar Structure**
+- Fixed 240px width sidebar
+- Gradient background: `linear-gradient(135deg, #0d1117, #121212, #0a0a0a)`
+- Border-right: `1px solid rgba(29, 185, 84, 0.1)`
+- Border-radius: `12px` (top-right, bottom-right)
+
+**3.2 Navigation System**
+- Button styling: 216px width, 52px height
+- Active state: Green gradient background + left border
+- Hover states with smooth transitions
+- Icon + text layout with proper spacing
+- Active state management with visual feedback
+
+**3.3 Media Player Component**
+- Collapsible design (85px collapsed, 145px expanded)
+- Track information display with scrolling long titles
+- Control buttons: play/pause (40px green circle), stop (32px)
+- Volume slider: horizontal with green accent
+- Loading animations: indefinite + determinate progress
+- "No track" placeholder state
+
+**3.4 Additional Sidebar Components**
+- **Crypto Donation**: Show/Hide toggle, Ko-fi + crypto addresses
+- **Version Info**: Clickable button with hover effects
+- **Service Status**: 3 indicators with colored dots (green/red)
+
+### Phase 4: Page Content Implementation
+**4.1 Dashboard Page**
+```html
+
+
+
+
+
+
+```
+
+**4.2 Sync Page**
+```html
+
+
+
+
+
+
+```
+
+**4.3 Downloads/Search Page**
+```html
+
+
+
+
+
+
+```
+
+**4.4 Artists Page**
+```html
+
+
+
+
+
+
+```
+
+**4.5 Settings Page**
+```html
+
+
+
+
+
+
+```
+
+### Phase 5: Backend Integration
+**5.1 API Endpoints (add to main.py)**
+```python
+# Page-specific data APIs
+@app.route('/api/dashboard') # Dashboard stats and activity
+@app.route('/api/sync') # Sync status and playlists
+@app.route('/api/downloads') # Download queue and history
+@app.route('/api/artists') # Artist collection and albums
+@app.route('/api/settings') # Current configuration
+
+# Action APIs
+@app.route('/api/search', methods=['POST']) # Music search
+@app.route('/api/download', methods=['POST']) # Download tracks
+@app.route('/api/sync-playlist', methods=['POST']) # Sync operations
+@app.route('/api/save-settings', methods=['POST']) # Save configuration
+@app.route('/api/test-connection', methods=['POST']) # Test service connections
+
+# Real-time APIs
+@app.route('/api/status') # Service status (existing)
+@app.route('/api/progress') # Operation progress
+@app.route('/api/activity') # Recent activity feed
+```
+
+**5.2 Data Integration**
+- Connect to existing service clients (SpotifyClient, PlexClient, etc.)
+- Reuse existing business logic from GUI pages
+- Maintain state consistency with backend services
+- Real-time updates via polling (5-second intervals)
+- Error handling and user feedback
+
+### Phase 6: Visual Polish & Testing
+**6.1 Styling Refinement**
+- Match exact colors and gradients from PyQt6
+- Perfect spacing and typography (SF Pro fonts)
+- Smooth animations and transitions (300ms standard)
+- Responsive behavior for different screen sizes
+
+**6.2 Component Testing**
+- Page switching functionality and animations
+- Form submissions and validation
+- Real-time updates and polling
+- Error handling and user feedback
+- Media player controls and state management
+
+**6.3 Integration Testing**
+- Backend API connectivity and data flow
+- State management across page switches
+- Performance optimization and caching
+- Cross-browser compatibility (Chrome, Firefox, Safari)
+
+## Color Palette & Design System
+
+### Primary Colors
+- **Background**: `#121212` (main), `#1e1e1e` (cards), `#0d1117` (sidebar)
+- **Accent**: `#1ed760` (Spotify green), `#1fdf64` (hover), `#1ca851` (pressed)
+- **Text**: `#ffffff` (primary), `rgba(255, 255, 255, 0.8)` (secondary)
+- **Borders**: `rgba(255, 255, 255, 0.05)` (subtle), `rgba(29, 185, 84, 0.1)` (accent)
+
+### Typography
+- **Headers**: SF Pro Display, 20px bold (app title)
+- **Navigation**: SF Pro Text, 12px medium
+- **Body**: SF Pro Text, 11px regular
+- **Monospace**: Courier New (crypto addresses)
+
+### Spacing System
+- **Margins**: 8px, 12px, 16px, 20px, 24px
+- **Padding**: 4px, 8px, 12px, 16px, 18px
+- **Border Radius**: 8px (small), 12px (medium), 16px (large), 20px (buttons)
+
+## Critical Safety Measures
+
+### 1. Non-Breaking Approach
+- β NO modifications to existing PyQt6 code
+- β NO changes to core business logic
+- β NO alterations to service clients or database code
+- β Only add new Flask routes and web assets
+
+### 2. Isolated Web Components
+- All web code in `webui/` directory
+- Separate CSS/JS files with no shared globals
+- Independent state management
+- No shared variables with GUI code
+
+### 3. Shared Backend Services
+- Reuse existing SpotifyClient, PlexClient, JellyfinClient, SoulseekClient
+- Call same methods as GUI application
+- Maintain identical configuration system via config_manager
+- Preserve all existing functionality and error handling
+
+### 4. State Isolation
+- Web UI maintains its own state in JavaScript
+- No persistent state shared between GUI and web
+- Configuration changes affect both modes equally
+- Independent session management
+
+## Success Criteria
+1. β Visual replica of PyQt6 interface (95%+ visual accuracy)
+2. β All 5 pages functional and navigable with smooth transitions
+3. β Sidebar components fully operational (navigation, media player, status)
+4. β Backend integration complete with all major functions working
+5. β No disruption to existing GUI app functionality
+6. β Seamless switching between `python main.py` and `python main.py --headless`
+7. β Real-time updates and responsive user interface
+8. β Proper error handling and user feedback
+
+## File Structure
+```
+webui/
+βββ index.html # Single-page application
+βββ static/
+ βββ style.css # Complete styling system
+ βββ script.js # Page management & API calls
+ βββ components.css # Reusable component styles
+ βββ animations.css # Transition and animation definitions
+```
+
+## Development Phases Timeline
+
+### Phase 1: Foundation (Current) β
+- [x] Plan document creation
+- [x] Basic file structure setup
+
+### Phase 2: Core Structure π§
+- [ ] HTML single-page structure
+- [ ] CSS foundation and color system
+- [ ] JavaScript page management
+
+### Phase 3: Sidebar Complete π
+- [ ] Navigation component
+- [ ] Media player component
+- [ ] Status indicators
+- [ ] Donation and version sections
+
+### Phase 4: Page Implementation π
+- [ ] Dashboard page
+- [ ] Sync page
+- [ ] Downloads/Search page
+- [ ] Artists page
+- [ ] Settings page
+
+### Phase 5: Backend Integration π
+- [ ] Flask API endpoints
+- [ ] Service client integration
+- [ ] Real-time data updates
+
+### Phase 6: Polish & Testing π
+- [ ] Visual accuracy review
+- [ ] Functionality testing
+- [ ] Performance optimization
+
+This plan ensures we build an exact replica of the GUI while maintaining complete safety and isolation from the existing codebase. The web interface will provide the same functionality as the desktop app but accessible through any web browser.
\ No newline at end of file
diff --git a/main.py b/main.py
index d82978b0..5588fc15 100644
--- a/main.py
+++ b/main.py
@@ -1,7 +1,7 @@
+
#!/usr/bin/env python3
import sys
-import argparse
import asyncio
import time
from pathlib import Path
@@ -405,73 +405,18 @@ class MainWindow(QMainWindow):
# Force accept the event to prevent hanging
event.accept()
-def run_web_app():
- """Run the application in headless web mode using Flask"""
- from flask import Flask, render_template, jsonify
+def main():
+ logging_config = config_manager.get_logging_config()
+ log_level = logging_config.get('level', 'INFO')
+ log_file = logging_config.get('path', 'logs/newmusic.log')
+ setup_logging(level=log_level, log_file=log_file)
- app = Flask(__name__, template_folder='webui', static_folder='webui/static')
+ logger.info("Starting Soulsync application")
- # Initialize service clients (same as GUI mode)
- spotify_client = SpotifyClient()
- plex_client = PlexClient()
- jellyfin_client = JellyfinClient()
- soulseek_client = SoulseekClient()
+ if not config_manager.config_path.exists():
+ logger.error("Configuration file not found. Please check config/config.json")
+ sys.exit(1)
- @app.route('/')
- def home():
- return render_template('index.html')
-
- @app.route('/status')
- def status():
- """Return service connection status as JSON"""
- try:
- # Check Spotify
- spotify_status = spotify_client.sp is not None
-
- # Check active media server
- active_server = config_manager.get_active_media_server()
- if active_server == "plex":
- media_status = plex_client.is_connected()
- elif active_server == "jellyfin":
- media_status = jellyfin_client.is_connected()
- else:
- media_status = False
-
- # Check Soulseek
- soulseek_status = soulseek_client.is_configured()
-
- return jsonify({
- 'spotify': spotify_status,
- 'media_server': media_status,
- 'soulseek': soulseek_status,
- 'active_media_server': active_server
- })
- except Exception as e:
- logger.error(f"Error getting service status: {e}")
- return jsonify({'error': str(e)}), 500
-
- @app.route('/config')
- def config_info():
- """Return basic configuration info"""
- try:
- validation = config_manager.validate_config()
- return jsonify({
- 'configured': config_manager.is_configured(),
- 'active_media_server': config_manager.get_active_media_server(),
- 'validation': validation
- })
- except Exception as e:
- logger.error(f"Error getting config info: {e}")
- return jsonify({'error': str(e)}), 500
-
- logger.info("Starting SoulSync in web mode...")
- logger.info("Web interface available at http://localhost:5000")
-
- # Run Flask app in debug mode for auto-reload
- app.run(host='0.0.0.0', port=5000, debug=True)
-
-def run_gui_app():
- """Run the application in GUI mode using PyQt6"""
app = QApplication(sys.argv)
app.setApplicationName("SoulSync")
app.setApplicationVersion("0.6")
@@ -488,31 +433,5 @@ def run_gui_app():
logger.error(f"Unexpected error: {e}")
sys.exit(1)
-def main():
- # Parse command line arguments
- parser = argparse.ArgumentParser(description='SoulSync - Music Sync & Manager')
- parser.add_argument('--headless', action='store_true',
- help='Run in headless mode with web interface instead of GUI')
- args = parser.parse_args()
-
- # Setup logging
- logging_config = config_manager.get_logging_config()
- log_level = logging_config.get('level', 'INFO')
- log_file = logging_config.get('path', 'logs/newmusic.log')
- setup_logging(level=log_level, log_file=log_file)
-
- # Check configuration
- if not config_manager.config_path.exists():
- logger.error("Configuration file not found. Please check config/config.json")
- sys.exit(1)
-
- # Choose mode based on arguments
- if args.headless:
- logger.info("Starting SoulSync in headless web mode")
- run_web_app()
- else:
- logger.info("Starting SoulSync in GUI mode")
- run_gui_app()
-
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()
diff --git a/web_server.py b/web_server.py
new file mode 100644
index 00000000..8f5e6a08
--- /dev/null
+++ b/web_server.py
@@ -0,0 +1,404 @@
+import os
+import json
+import asyncio
+import requests
+import socket
+import ipaddress
+import subprocess
+import platform
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from flask import Flask, render_template, request, jsonify, redirect
+
+# --- Core Application Imports ---
+# Import the same core clients and config manager used by the GUI app
+from config.settings import config_manager
+from core.spotify_client import SpotifyClient
+from core.plex_client import PlexClient
+from core.jellyfin_client import JellyfinClient
+from core.soulseek_client import SoulseekClient
+from core.tidal_client import TidalClient # Added import for Tidal
+
+# --- Flask App Setup ---
+base_dir = os.path.abspath(os.path.dirname(__file__))
+
+# Correctly point to the 'webui' directory for templates and static files
+app = Flask(
+ __name__,
+ template_folder=os.path.join(base_dir, 'webui'),
+ static_folder=os.path.join(base_dir, 'webui', 'static')
+)
+
+# --- Initialize Core Application Components ---
+print("π Initializing SoulSync services for Web UI...")
+try:
+ spotify_client = SpotifyClient()
+ plex_client = PlexClient()
+ jellyfin_client = JellyfinClient()
+ soulseek_client = SoulseekClient()
+ tidal_client = TidalClient()
+ print("β Core service clients initialized.")
+except Exception as e:
+ print(f"π΄ FATAL: Error initializing service clients: {e}")
+ spotify_client = plex_client = jellyfin_client = soulseek_client = tidal_client = None
+
+# --- Refactored Logic from GUI Threads ---
+# This logic is extracted from your QThread classes to be used directly by Flask.
+
+def run_service_test(service, test_config):
+ """
+ Performs the actual connection test for a given service.
+ This logic is adapted from your ServiceTestThread.
+ It temporarily modifies the config, runs the test, then restores the config.
+ """
+ original_config = {}
+ try:
+ # 1. Save original config for the specific service
+ original_config = config_manager.get(service, {})
+
+ # 2. Temporarily set the new config for the test
+ for key, value in test_config.items():
+ config_manager.set(f"{service}.{key}", value)
+
+ # 3. Run the test with the temporary config
+ if service == "spotify":
+ temp_client = SpotifyClient()
+ if temp_client.is_authenticated():
+ return True, "Spotify connection successful!"
+ else:
+ return False, "Spotify authentication failed. Check credentials and complete OAuth flow in browser if prompted."
+ elif service == "tidal":
+ temp_client = TidalClient()
+ if temp_client.is_authenticated():
+ user_info = temp_client.get_user_info()
+ username = user_info.get('display_name', 'Tidal User') if user_info else 'Tidal User'
+ return True, f"Tidal connection successful! Connected as: {username}"
+ else:
+ return False, "Tidal authentication failed. Please use the 'Authenticate' button and complete the flow in your browser."
+ elif service == "plex":
+ temp_client = PlexClient()
+ if temp_client.is_connected():
+ return True, f"Successfully connected to Plex server: {temp_client.server.friendlyName}"
+ else:
+ return False, "Could not connect to Plex. Check URL and Token."
+ elif service == "jellyfin":
+ temp_client = JellyfinClient()
+ if temp_client.is_connected():
+ # FIX: Check if server_info exists before accessing it.
+ server_name = "Unknown Server"
+ if hasattr(temp_client, 'server_info') and temp_client.server_info:
+ server_name = temp_client.server_info.get('ServerName', 'Unknown Server')
+ return True, f"Successfully connected to Jellyfin server: {server_name}"
+ else:
+ return False, "Could not connect to Jellyfin. Check URL and API Key."
+ elif service == "soulseek":
+ temp_client = SoulseekClient()
+ async def check():
+ return await temp_client.check_connection()
+ if asyncio.run(check()):
+ return True, "Successfully connected to slskd."
+ else:
+ return False, "Could not connect to slskd. Check URL and API Key."
+ return False, "Unknown service."
+ except AttributeError as e:
+ # This specifically catches the error you reported for Jellyfin
+ if "'JellyfinClient' object has no attribute 'server_info'" in str(e):
+ return False, "Connection failed. Please check your Jellyfin URL and API Key."
+ else:
+ return False, f"An unexpected error occurred: {e}"
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ return False, str(e)
+ finally:
+ # 4. CRITICAL: Restore the original config
+ if original_config:
+ for key, value in original_config.items():
+ config_manager.set(f"{service}.{key}", value)
+ print(f"β Restored original config for '{service}' after test.")
+
+
+def run_detection(server_type):
+ """
+ Performs network detection for a given server type (plex, jellyfin, slskd).
+ This is a blocking function that scans the network.
+ """
+ # This is a simplified version of the logic in your QThreads.
+ # A full implementation would be more extensive.
+ # For demonstration, we'll check common local addresses.
+ print(f"Running detection for {server_type}...")
+ common_ips = ["localhost", "127.0.0.1"]
+ ports = {
+ 'plex': 32400,
+ 'jellyfin': 8096,
+ 'slskd': 5030
+ }
+ port = ports.get(server_type)
+ if not port:
+ return None
+
+ for ip in common_ips:
+ url = f"http://{ip}:{port}"
+ try:
+ if server_type == 'slskd':
+ # slskd check is different
+ response = requests.get(f"{url}/api/v0/session", timeout=1)
+ if response.status_code in [200, 401]:
+ print(f"Found {server_type} at {url}")
+ return url
+ else:
+ response = requests.get(url, timeout=1)
+ # A simple 200 OK is a good sign
+ if response.status_code == 200:
+ print(f"Found {server_type} at {url}")
+ return url
+ except requests.RequestException:
+ continue
+ return None
+
+# --- Web UI Routes ---
+
+@app.route('/')
+def index():
+ return render_template('index.html')
+
+# --- API Endpoints ---
+
+@app.route('/status')
+def get_status():
+ if not all([spotify_client, plex_client, jellyfin_client, soulseek_client, config_manager]):
+ return jsonify({"error": "Core services not initialized."}), 500
+ try:
+ active_server = config_manager.get_active_media_server()
+ media_server_status = False
+ if active_server == "plex":
+ media_server_status = plex_client.is_connected()
+ elif active_server == "jellyfin":
+ media_server_status = jellyfin_client.is_connected()
+
+ status_data = {
+ 'spotify': spotify_client.is_authenticated(),
+ 'media_server': media_server_status,
+ 'soulseek': soulseek_client.is_configured(),
+ 'active_media_server': active_server
+ }
+ return jsonify(status_data)
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+@app.route('/api/settings', methods=['GET', 'POST'])
+def handle_settings():
+ global tidal_client # Declare that we might modify the global instance
+ if not config_manager:
+ return jsonify({"error": "Server configuration manager is not initialized."}), 500
+ if request.method == 'POST':
+ try:
+ new_settings = request.get_json()
+ if not new_settings:
+ return jsonify({"success": False, "error": "No data received."}), 400
+
+ if 'active_media_server' in new_settings:
+ config_manager.set_active_media_server(new_settings['active_media_server'])
+
+ for service in ['spotify', 'plex', 'jellyfin', 'soulseek', 'settings', 'database', 'metadata_enhancement', 'playlist_sync', 'tidal']:
+ if service in new_settings:
+ for key, value in new_settings[service].items():
+ config_manager.set(f'{service}.{key}', value)
+
+ print("β Settings saved successfully via Web UI.")
+ spotify_client._setup_client()
+ plex_client.server = None
+ jellyfin_client.server = None
+ soulseek_client._setup_client()
+ # FIX: Re-instantiate the global tidal_client to pick up new settings
+ tidal_client = TidalClient()
+ print("β Service clients re-initialized with new settings.")
+ return jsonify({"success": True, "message": "Settings saved successfully."})
+ except Exception as e:
+ return jsonify({"success": False, "error": str(e)}), 500
+ else: # GET request
+ try:
+ return jsonify(config_manager.config_data)
+ except Exception as e:
+ return jsonify({"error": str(e)}), 500
+
+@app.route('/api/test-connection', methods=['POST'])
+def test_connection_endpoint():
+ data = request.get_json()
+ service = data.get('service')
+ if not service:
+ return jsonify({"success": False, "error": "No service specified."}), 400
+
+ print(f"Received test connection request for: {service}")
+
+ # Get the current settings from the main config manager to test with
+ test_config = config_manager.get(service, {})
+
+ # For media servers, the service name might be 'server'
+ if service == 'server':
+ active_server = config_manager.get_active_media_server()
+ test_config = config_manager.get(active_server, {})
+ service = active_server # use the actual server name for the test
+
+ success, message = run_service_test(service, test_config)
+ return jsonify({"success": success, "error": "" if success else message, "message": message if success else ""})
+
+@app.route('/api/detect-media-server', methods=['POST'])
+def detect_media_server_endpoint():
+ data = request.get_json()
+ server_type = data.get('server_type')
+ print(f"Received auto-detect request for: {server_type}")
+ found_url = run_detection(server_type)
+ if found_url:
+ return jsonify({"success": True, "found_url": found_url})
+ else:
+ return jsonify({"success": False, "error": f"No {server_type} server found on common local addresses."})
+
+@app.route('/api/detect-soulseek', methods=['POST'])
+def detect_soulseek_endpoint():
+ print("Received auto-detect request for slskd")
+ found_url = run_detection('slskd')
+ if found_url:
+ return jsonify({"success": True, "found_url": found_url})
+ else:
+ return jsonify({"success": False, "error": "No slskd server found on common local addresses."})
+
+# --- Full Tidal Authentication Flow ---
+
+@app.route('/auth/tidal')
+def auth_tidal():
+ """
+ Initiates the Tidal OAuth authentication flow by calling the client's
+ authenticate method, which should handle opening the browser.
+ This now mirrors the GUI's approach.
+ """
+ # FIX: Create a fresh client instance to ensure it uses the latest settings from config.json
+ temp_tidal_client = TidalClient()
+ if not temp_tidal_client:
+ return "Tidal client could not be initialized on the server.", 500
+
+ # The authenticate() method in your GUI likely opens a browser and blocks.
+ # The web server will also block here until authentication is complete.
+ # The user will see the URL to visit in the console where the server is running.
+ print(" tidal_client.authenticate() to start the flow.")
+ print("Please follow the instructions in the console to log in to Tidal.")
+
+ if temp_tidal_client.authenticate():
+ # Re-initialize the main client instance after successful auth
+ global tidal_client
+ tidal_client = TidalClient()
+ return "
β Tidal Authentication Successful!
You can now close this window and return to the SoulSync application.
"
+ else:
+ return "
β Tidal Authentication Failed
Please check the console output of the server for a login URL and follow the instructions.
", 400
+
+
+@app.route('/tidal/callback')
+def tidal_callback():
+ """
+ Handles the callback from Tidal after the user authorizes the application.
+ It receives an authorization code, exchanges it for an access token,
+ and saves the token.
+ """
+ global tidal_client # We will re-initialize the global client
+ auth_code = request.args.get('code')
+
+ if not auth_code:
+ error = request.args.get('error', 'Unknown error')
+ error_description = request.args.get('error_description', 'No description provided.')
+ return f"
Tidal Authentication Failed
Error: {error}
{error_description}
Please close this window and try again.
", 400
+
+ try:
+ # Create a temporary client for the token exchange
+ temp_tidal_client = TidalClient()
+ success = temp_tidal_client.fetch_token_from_code(auth_code)
+
+ if success:
+ # Re-initialize the main global tidal_client instance with the new token
+ tidal_client = TidalClient()
+ return "
β Tidal Authentication Successful!
You can now close this window and return to the SoulSync application.
"
+ else:
+ return "
β Tidal Authentication Failed
Could not exchange authorization code for a token. Please try again.
", 400
+ except Exception as e:
+ print(f"π΄ Error during Tidal token exchange: {e}")
+ return f"
β An Error Occurred
An unexpected error occurred during the authentication process: {e}
", 500
+
+
+# --- Placeholder API Endpoints for Other Pages ---
+
+@app.route('/api/activity')
+def get_activity():
+ # Placeholder: returns mock activity data
+ mock_activity = [
+ {"time": "1 min ago", "text": "Service status checked."},
+ {"time": "5 min ago", "text": "Application server started."}
+ ]
+ return jsonify({"activities": mock_activity})
+
+@app.route('/api/playlists')
+def get_playlists():
+ # Placeholder: returns mock playlist data
+ if spotify_client and spotify_client.is_authenticated():
+ # In a real implementation, you would call spotify_client.get_user_playlists()
+ mock_playlists = [
+ {"id": "1", "name": "Chill Vibes"},
+ {"id": "2", "name": "Workout Mix"},
+ {"id": "3", "name": "Liked Songs"}
+ ]
+ return jsonify({"playlists": mock_playlists})
+ return jsonify({"playlists": [], "error": "Spotify not authenticated."})
+
+@app.route('/api/sync', methods=['POST'])
+def start_sync():
+ # Placeholder: simulates starting a sync
+ return jsonify({"success": True, "message": "Sync process started."})
+
+@app.route('/api/search', methods=['POST'])
+def search_music():
+ # Placeholder: simulates a music search
+ data = request.get_json()
+ query = data.get('query', '')
+ print(f"Simulating search for: {query}")
+ # In a real implementation, you would call soulseek_client.search()
+ mock_results = [
+ {"title": "Bohemian Rhapsody", "artist": "Queen", "album": "A Night at the Opera", "type": "track", "quality": "FLAC", "username": "user1", "filename": "Queen - Bohemian Rhapsody.flac", "file_size": 35000000},
+ {"title": "A Night at the Opera", "artist": "Queen", "type": "album", "track_count": 12, "size_mb": 350, "username": "user2"}
+ ]
+ return jsonify({"results": mock_results})
+
+@app.route('/api/artists')
+def get_artists():
+ # Placeholder: returns mock artist data
+ mock_artists = [
+ {"name": "Queen", "album_count": 15, "image": None},
+ {"name": "Led Zeppelin", "album_count": 9, "image": None}
+ ]
+ return jsonify({"artists": mock_artists})
+
+@app.route('/api/stream/start', methods=['POST'])
+def stream_start():
+ # Placeholder: simulates starting a media stream
+ data = request.get_json()
+ print(f"Simulating stream start for: {data.get('filename')}")
+ return jsonify({"success": True, "track": data})
+
+@app.route('/api/stream/status')
+def stream_status():
+ # Placeholder: simulates stream status
+ return jsonify({"status": "playing", "progress": 50, "track": {"title": "Bohemian Rhapsody"}})
+
+@app.route('/api/stream/toggle', methods=['POST'])
+def stream_toggle():
+ # Placeholder: simulates toggling play/pause
+ return jsonify({"playing": False})
+
+@app.route('/api/stream/stop', methods=['POST'])
+def stream_stop():
+ # Placeholder: simulates stopping a stream
+ return jsonify({"success": True})
+
+
+# --- Main Execution ---
+
+if __name__ == '__main__':
+ print("π Starting SoulSync Web UI Server...")
+ print("Open your browser and navigate to http://127.0.0.1:5001")
+ app.run(host='0.0.0.0', port=5001, debug=True)
diff --git a/webui/index.html b/webui/index.html
index ca644b57..07238a85 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -1,41 +1,468 @@
-
+
- SoulSync - Web Mode
+
+ SoulSync - Music Sync & Manager
-
-
π΅ SoulSync - Web Mode
-
Your music sync application is running in headless mode.