settings page ready
This commit is contained in:
parent
ec79162618
commit
d631ad61f8
6 changed files with 3791 additions and 160 deletions
323
headless.md
Normal file
323
headless.md
Normal file
|
|
@ -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
|
||||
<div id="app">
|
||||
<aside id="sidebar">
|
||||
<div id="sidebar-header">
|
||||
<h1>SoulSync</h1>
|
||||
<p>Music Sync & Manager</p>
|
||||
</div>
|
||||
<nav id="sidebar-nav">
|
||||
<!-- Navigation buttons -->
|
||||
</nav>
|
||||
<div id="media-player">
|
||||
<!-- Media player component -->
|
||||
</div>
|
||||
<div id="crypto-donation">
|
||||
<!-- Donation component -->
|
||||
</div>
|
||||
<div id="version-info">
|
||||
<!-- Version button -->
|
||||
</div>
|
||||
<div id="service-status">
|
||||
<!-- Status indicators -->
|
||||
</div>
|
||||
</aside>
|
||||
<main id="content">
|
||||
<div id="dashboard-page" class="page active">...</div>
|
||||
<div id="sync-page" class="page">...</div>
|
||||
<div id="downloads-page" class="page">...</div>
|
||||
<div id="artists-page" class="page">...</div>
|
||||
<div id="settings-page" class="page">...</div>
|
||||
</main>
|
||||
</div>
|
||||
```
|
||||
|
||||
**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
|
||||
<!-- Service status cards -->
|
||||
<!-- Activity feed with real-time updates -->
|
||||
<!-- Quick action buttons -->
|
||||
<!-- Statistics displays (tracks, artists, downloads) -->
|
||||
<!-- Progress indicators for ongoing operations -->
|
||||
<!-- Database status and update controls -->
|
||||
```
|
||||
|
||||
**4.2 Sync Page**
|
||||
```html
|
||||
<!-- Playlist selection dropdown -->
|
||||
<!-- Sync progress tracking with animated bars -->
|
||||
<!-- Track listing tables with status columns -->
|
||||
<!-- Action buttons (sync, analyze, cancel) -->
|
||||
<!-- Status indicators for sync operations -->
|
||||
<!-- Missing/found track summaries -->
|
||||
```
|
||||
|
||||
**4.3 Downloads/Search Page**
|
||||
```html
|
||||
<!-- Search interface with filters -->
|
||||
<!-- Results grid/list with album art -->
|
||||
<!-- Download queue with progress bars -->
|
||||
<!-- Progress tracking for active downloads -->
|
||||
<!-- Media player integration -->
|
||||
<!-- Search history and suggestions -->
|
||||
```
|
||||
|
||||
**4.4 Artists Page**
|
||||
```html
|
||||
<!-- Artist grid layout with images -->
|
||||
<!-- Album cards with completion indicators -->
|
||||
<!-- Search/filter interface -->
|
||||
<!-- Collection management tools -->
|
||||
<!-- Image lazy loading system -->
|
||||
<!-- Artist details modal/expansion -->
|
||||
```
|
||||
|
||||
**4.5 Settings Page**
|
||||
```html
|
||||
<!-- Service configuration forms (Spotify, Plex, Jellyfin, Soulseek) -->
|
||||
<!-- Input validation and feedback -->
|
||||
<!-- Save/test functionality -->
|
||||
<!-- Configuration display and status -->
|
||||
<!-- Auto-detection tools (Plex discovery) -->
|
||||
<!-- Path settings and validation -->
|
||||
```
|
||||
|
||||
### 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.
|
||||
103
main.py
103
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()
|
||||
main()
|
||||
|
|
|
|||
404
web_server.py
Normal file
404
web_server.py
Normal file
|
|
@ -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 "<h1>✅ Tidal Authentication Successful!</h1><p>You can now close this window and return to the SoulSync application.</p>"
|
||||
else:
|
||||
return "<h1>❌ Tidal Authentication Failed</h1><p>Please check the console output of the server for a login URL and follow the instructions.</p>", 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"<h1>Tidal Authentication Failed</h1><p>Error: {error}</p><p>{error_description}</p><p>Please close this window and try again.</p>", 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 "<h1>✅ Tidal Authentication Successful!</h1><p>You can now close this window and return to the SoulSync application.</p>"
|
||||
else:
|
||||
return "<h1>❌ Tidal Authentication Failed</h1><p>Could not exchange authorization code for a token. Please try again.</p>", 400
|
||||
except Exception as e:
|
||||
print(f"🔴 Error during Tidal token exchange: {e}")
|
||||
return f"<h1>❌ An Error Occurred</h1><p>An unexpected error occurred during the authentication process: {e}</p>", 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)
|
||||
475
webui/index.html
475
webui/index.html
|
|
@ -1,41 +1,468 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>SoulSync - Web Mode</title>
|
||||
<meta charset="UTF-8">
|
||||
<title>SoulSync - Music Sync & Manager</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎵 SoulSync - Web Mode</h1>
|
||||
<p>Your music sync application is running in headless mode.</p>
|
||||
|
||||
<div class="status">
|
||||
<h3>Service Status</h3>
|
||||
<div id="status-container">
|
||||
<div class="service">
|
||||
<span>Spotify:</span>
|
||||
<span id="spotify-status" class="disconnected">Checking...</span>
|
||||
<div class="main-container">
|
||||
<!-- Sidebar - Always Visible -->
|
||||
<div class="sidebar">
|
||||
<!-- Header Section -->
|
||||
<div class="sidebar-header">
|
||||
<h1 class="app-name">SoulSync</h1>
|
||||
<p class="app-subtitle">Music Sync & Manager</p>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Section -->
|
||||
<nav class="sidebar-nav">
|
||||
<button class="nav-button active" data-page="dashboard">
|
||||
<span class="nav-icon">📊</span>
|
||||
<span class="nav-text">Dashboard</span>
|
||||
</button>
|
||||
<button class="nav-button" data-page="sync">
|
||||
<span class="nav-icon">🔄</span>
|
||||
<span class="nav-text">Sync</span>
|
||||
</button>
|
||||
<button class="nav-button" data-page="downloads">
|
||||
<span class="nav-icon">📥</span>
|
||||
<span class="nav-text">Search</span>
|
||||
</button>
|
||||
<button class="nav-button" data-page="artists">
|
||||
<span class="nav-icon">🎵</span>
|
||||
<span class="nav-text">Artists</span>
|
||||
</button>
|
||||
<button class="nav-button" data-page="settings">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
<span class="nav-text">Settings</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Spacer -->
|
||||
<div class="sidebar-spacer"></div>
|
||||
|
||||
<!-- Media Player Section -->
|
||||
<div class="media-player" id="media-player">
|
||||
<!-- Loading Animation -->
|
||||
<div class="loading-animation hidden" id="loading-animation">
|
||||
<div class="loading-bar">
|
||||
<div class="loading-progress"></div>
|
||||
</div>
|
||||
<div class="loading-text">0%</div>
|
||||
</div>
|
||||
<div class="service">
|
||||
<span>Media Server:</span>
|
||||
<span id="media-status" class="disconnected">Checking...</span>
|
||||
|
||||
<!-- Header (always visible) -->
|
||||
<div class="media-header">
|
||||
<div class="media-info">
|
||||
<div class="track-title" id="track-title">No track</div>
|
||||
<div class="artist-name" id="artist-name">Unknown Artist</div>
|
||||
</div>
|
||||
<button class="play-button" id="play-button" disabled>▷</button>
|
||||
</div>
|
||||
<div class="service">
|
||||
<span>Soulseek:</span>
|
||||
<span id="soulseek-status" class="disconnected">Checking...</span>
|
||||
|
||||
<!-- Expanded Content (hidden initially) -->
|
||||
<div class="media-expanded hidden" id="media-expanded">
|
||||
<div class="album-name" id="album-name">Unknown Album</div>
|
||||
<div class="media-controls">
|
||||
<div class="volume-control">
|
||||
<span class="volume-icon">🔊</span>
|
||||
<input type="range" class="volume-slider" id="volume-slider" min="0" max="100" value="70">
|
||||
</div>
|
||||
<button class="stop-button" id="stop-button" disabled>⏹</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Track Message -->
|
||||
<div class="no-track-message" id="no-track-message">
|
||||
Start playing music to see controls
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Crypto Donation Section -->
|
||||
<div class="crypto-donation">
|
||||
<div class="donation-header">
|
||||
<span class="donation-title">Support Development</span>
|
||||
<button class="toggle-button" id="donation-toggle">Show</button>
|
||||
</div>
|
||||
<div class="donation-addresses hidden" id="donation-addresses">
|
||||
<div class="donation-item" onclick="openKofi()">
|
||||
<span class="donation-name">Ko-fi</span>
|
||||
<span class="donation-link">Click to open</span>
|
||||
</div>
|
||||
<div class="donation-item" onclick="copyAddress('3JVWrRSkozAQSmw5DXYVxYKsM9bndPTqdS', 'Bitcoin')">
|
||||
<span class="donation-name">Bitcoin</span>
|
||||
<span class="donation-address">3JVWrR...dPTqdS</span>
|
||||
</div>
|
||||
<div class="donation-item" onclick="copyAddress('0x343fC48c2cd1C6332b0df9a58F86e6520a026AC5', 'Ethereum')">
|
||||
<span class="donation-name">Ethereum</span>
|
||||
<span class="donation-address">0x343f...026AC5</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Version Section -->
|
||||
<div class="version-section">
|
||||
<button class="version-button" onclick="showVersionInfo()">v.0.65</button>
|
||||
</div>
|
||||
|
||||
<!-- Status Section -->
|
||||
<div class="status-section">
|
||||
<h4 class="status-title">Service Status</h4>
|
||||
<div class="status-indicator" id="spotify-indicator">
|
||||
<span class="status-dot disconnected"></span>
|
||||
<span class="status-name">Spotify</span>
|
||||
</div>
|
||||
<div class="status-indicator" id="media-server-indicator">
|
||||
<span class="status-dot disconnected"></span>
|
||||
<span class="status-name" id="media-server-name">Plex</span>
|
||||
</div>
|
||||
<div class="status-indicator" id="soulseek-indicator">
|
||||
<span class="status-dot disconnected"></span>
|
||||
<span class="status-name">Soulseek</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<h3>Available Endpoints</h3>
|
||||
<ul>
|
||||
<li><a href="/status" style="color: #4caf50;">/status</a> - Service status JSON</li>
|
||||
<li><a href="/config" style="color: #4caf50;">/config</a> - Configuration info</li>
|
||||
</ul>
|
||||
<!-- Main Content Area -->
|
||||
<div class="main-content">
|
||||
<!-- Dashboard Page -->
|
||||
<div class="page active" id="dashboard-page">
|
||||
<div class="page-header">
|
||||
<h2>Dashboard</h2>
|
||||
</div>
|
||||
<div class="dashboard-content">
|
||||
<!-- Activity Feed -->
|
||||
<div class="activity-section">
|
||||
<h3>Recent Activity</h3>
|
||||
<div class="activity-feed" id="activity-feed">
|
||||
<div class="activity-item">
|
||||
<span class="activity-time">Just now</span>
|
||||
<span class="activity-text">Web UI initialized</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">0</div>
|
||||
<div class="stat-label">Tracks Synced</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">0</div>
|
||||
<div class="stat-label">Downloads</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">3</div>
|
||||
<div class="stat-label">Services</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync Page -->
|
||||
<div class="page" id="sync-page">
|
||||
<div class="page-header">
|
||||
<h2>Sync</h2>
|
||||
</div>
|
||||
<div class="sync-content">
|
||||
<div class="playlist-section">
|
||||
<h3>Select Playlist</h3>
|
||||
<select class="playlist-selector" id="playlist-selector">
|
||||
<option value="">Loading playlists...</option>
|
||||
</select>
|
||||
<button class="sync-button" id="sync-button">Start Sync</button>
|
||||
</div>
|
||||
<div class="sync-progress hidden" id="sync-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill"></div>
|
||||
</div>
|
||||
<div class="progress-text">0% Complete</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Downloads/Search Page -->
|
||||
<div class="page" id="downloads-page">
|
||||
<div class="page-header">
|
||||
<h2>Search & Download</h2>
|
||||
</div>
|
||||
<div class="downloads-content">
|
||||
<div class="search-section">
|
||||
<input type="text" class="search-input" id="search-input" placeholder="Search for music...">
|
||||
<button class="search-button" id="search-button">Search</button>
|
||||
</div>
|
||||
<div class="search-results" id="search-results">
|
||||
<!-- Results will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Artists Page -->
|
||||
<div class="page" id="artists-page">
|
||||
<div class="page-header">
|
||||
<h2>Artists</h2>
|
||||
</div>
|
||||
<div class="artists-content">
|
||||
<div class="artists-grid" id="artists-grid">
|
||||
<!-- Artists will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Page -->
|
||||
<div class="page" id="settings-page">
|
||||
<div class="page-header">
|
||||
<h2>Settings</h2>
|
||||
</div>
|
||||
<div class="settings-content">
|
||||
<!-- Two Column Layout -->
|
||||
<div class="settings-columns">
|
||||
<!-- Left Column - API Configuration -->
|
||||
<div class="settings-left-column">
|
||||
<!-- API Configuration -->
|
||||
<div class="settings-group">
|
||||
<h3>API Configuration</h3>
|
||||
|
||||
<!-- Spotify Settings -->
|
||||
<div class="api-service-frame">
|
||||
<h4 class="service-title spotify-title">Spotify</h4>
|
||||
<div class="form-group">
|
||||
<label>Client ID:</label>
|
||||
<input type="text" id="spotify-client-id" placeholder="Spotify Client ID">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Client Secret:</label>
|
||||
<input type="password" id="spotify-client-secret" placeholder="Spotify Client Secret">
|
||||
</div>
|
||||
<div class="callback-info">
|
||||
<div class="callback-label">Required Redirect URI:</div>
|
||||
<div class="callback-url">http://127.0.0.1:8888/callback</div>
|
||||
<div class="callback-help">Add this URL to your Spotify app's 'Redirect URIs' in the Spotify Developer Dashboard</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tidal Settings -->
|
||||
<div class="api-service-frame">
|
||||
<h4 class="service-title tidal-title">Tidal</h4>
|
||||
<div class="form-group">
|
||||
<label>Client ID:</label>
|
||||
<input type="text" id="tidal-client-id" placeholder="Tidal Client ID">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Client Secret:</label>
|
||||
<input type="password" id="tidal-client-secret" placeholder="Tidal Client Secret">
|
||||
</div>
|
||||
<div class="callback-info">
|
||||
<div class="callback-label">Required Redirect URI:</div>
|
||||
<div class="callback-url">http://127.0.0.1:8889/tidal/callback</div>
|
||||
<div class="callback-help">Add this URL to your Tidal app configuration</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="auth-button" onclick="authenticateTidal()">🔐 Authenticate</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Soulseek Settings -->
|
||||
<div class="api-service-frame">
|
||||
<h4 class="service-title soulseek-title">Soulseek</h4>
|
||||
<div class="form-group">
|
||||
<label>slskd URL:</label>
|
||||
<div class="path-input-group">
|
||||
<input type="url" id="soulseek-url" placeholder="http://localhost:5030">
|
||||
<button class="detect-button" onclick="autoDetectSlskd()">Auto-detect</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>API Key:</label>
|
||||
<input type="password" id="soulseek-api-key" placeholder="Slskd API Key">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Connection Buttons -->
|
||||
<div class="api-test-buttons">
|
||||
<button class="test-button" onclick="testConnection('spotify')">Test Spotify</button>
|
||||
<button class="test-button" onclick="testConnection('tidal')">Test Tidal</button>
|
||||
<button class="test-button" onclick="testConnection('soulseek')">Test Soulseek</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Connections -->
|
||||
<div class="settings-group">
|
||||
<h3>Server Connections</h3>
|
||||
|
||||
<!-- Server Toggle Buttons -->
|
||||
<div class="server-toggle-container">
|
||||
<button class="server-toggle-btn active" id="plex-toggle" onclick="toggleServer('plex')">Plex</button>
|
||||
<button class="server-toggle-btn" id="jellyfin-toggle" onclick="toggleServer('jellyfin')">Jellyfin</button>
|
||||
</div>
|
||||
|
||||
<!-- Plex Settings -->
|
||||
<div class="server-config-container" id="plex-container">
|
||||
<div class="form-group">
|
||||
<label>Plex Server URL:</label>
|
||||
<input type="url" id="plex-url" placeholder="http://localhost:32400">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Plex Token:</label>
|
||||
<input type="password" id="plex-token" placeholder="X-Plex-Token">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="detect-button" onclick="autoDetectPlex()">Auto-detect</button>
|
||||
<button class="test-button" onclick="testConnection('plex')">Test</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jellyfin Settings -->
|
||||
<div class="server-config-container hidden" id="jellyfin-container">
|
||||
<div class="form-group">
|
||||
<label>Jellyfin Server URL:</label>
|
||||
<input type="url" id="jellyfin-url" placeholder="http://localhost:8096">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>API Key:</label>
|
||||
<input type="password" id="jellyfin-api-key" placeholder="Jellyfin API Key">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="detect-button" onclick="autoDetectJellyfin()">Auto-detect</button>
|
||||
<button class="test-button" onclick="testConnection('jellyfin')">Test</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Test Button -->
|
||||
<div class="server-test-section">
|
||||
<button class="test-button server-test-btn" onclick="testConnection('server')">Test Server</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column - Download Settings, Database, Metadata, Logging -->
|
||||
<div class="settings-right-column">
|
||||
<!-- Download Settings -->
|
||||
<div class="settings-group">
|
||||
<h3>Download Settings</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Preferred Quality:</label>
|
||||
<select id="preferred-quality">
|
||||
<option value="flac">FLAC</option>
|
||||
<option value="mp3_320">320 kbps MP3</option>
|
||||
<option value="mp3_256">256 kbps MP3</option>
|
||||
<option value="mp3_192">192 kbps MP3</option>
|
||||
<option value="any">Any</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Slskd Download Dir:</label>
|
||||
<div class="path-input-group">
|
||||
<input type="text" id="download-path" placeholder="./downloads">
|
||||
<button class="browse-button" onclick="browsePath('download')">Browse</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Matched Transfer Dir (Plex Music Dir?):</label>
|
||||
<div class="path-input-group">
|
||||
<input type="text" id="transfer-path" placeholder="./Transfer">
|
||||
<button class="browse-button" onclick="browsePath('transfer')">Browse</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Database Settings -->
|
||||
<div class="settings-group">
|
||||
<h3>Database Settings</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Concurrent Workers:</label>
|
||||
<select id="max-workers">
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5" selected>5</option>
|
||||
<option value="6">6</option>
|
||||
<option value="7">7</option>
|
||||
<option value="8">8</option>
|
||||
<option value="9">9</option>
|
||||
<option value="10">10</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="help-text">Number of parallel threads for database updates. Higher values = faster updates but more server load.</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata Enhancement Settings -->
|
||||
<div class="settings-group">
|
||||
<h3>🎵 Metadata Enhancement</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="metadata-enabled" checked>
|
||||
Enable metadata enhancement with Spotify data
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="embed-album-art" checked>
|
||||
Embed high-quality album art from Spotify
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Supported Formats:</label>
|
||||
<div class="supported-formats">MP3, FLAC, MP4/M4A, OGG</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Playlist Sync Settings -->
|
||||
<div class="settings-group">
|
||||
<h3>Playlist Sync Settings</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="create-backup" checked>
|
||||
Create playlist backups before sync
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logging Information (Read-only) -->
|
||||
<div class="settings-group">
|
||||
<h3>Logging Information</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Log Level:</label>
|
||||
<div class="readonly-field" id="log-level-display">DEBUG</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Log Path:</label>
|
||||
<div class="readonly-field" id="log-path-display">logs/app.log</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="settings-actions">
|
||||
<button class="save-button" id="save-settings">💾 Save Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div class="loading-overlay hidden" id="loading-overlay">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-message">Processing...</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notifications -->
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
|
||||
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue