Improve Docker path handling and filename extraction

Added helper functions to resolve Windows paths for Docker containers and to extract filenames in a cross-platform manner. Updated usage throughout web_server.py and soulseek_client.py to ensure correct path mapping and filename handling. Modified docker-compose.yml to use named volumes and mount the E: drive for better Docker compatibility.
This commit is contained in:
Broque Thomas 2025-09-11 17:46:49 -07:00
parent 008653a69c
commit 24b789660a
3 changed files with 77 additions and 32 deletions

View file

@ -1,6 +1,7 @@
import requests
import asyncio
import aiohttp
import os
from typing import List, Optional, Dict, Any
from dataclasses import dataclass
import time
@ -228,7 +229,17 @@ class SoulseekClient:
self.base_url = slskd_url.rstrip('/')
self.api_key = config.get('api_key', '')
self.download_path = Path(config.get('download_path', './downloads'))
# Handle download path with Docker translation
download_path_str = config.get('download_path', './downloads')
if os.path.exists('/.dockerenv') and len(download_path_str) >= 3 and download_path_str[1] == ':' and download_path_str[0].isalpha():
# Convert Windows path (E:/path) to WSL mount path (/mnt/e/path)
drive_letter = download_path_str[0].lower()
rest_of_path = download_path_str[2:].replace('\\', '/') # Remove E: and convert backslashes
download_path_str = f"/host/mnt/{drive_letter}{rest_of_path}"
logger.info(f"Docker detected, using {download_path_str} for downloads")
self.download_path = Path(download_path_str)
self.download_path.mkdir(parents=True, exist_ok=True)
logger.info(f"Soulseek client configured with slskd at {self.base_url}")

View file

@ -11,13 +11,13 @@ services:
volumes:
# Persistent data volumes
- ./config:/app/config
- ./database:/app/database
- ./logs:/app/logs
# Soulseek download and transfer directories (mapped to host E: drive paths)
- /mnt/e/Broque Projects/newMusic/downloads:/app/downloads
- /mnt/e/Broque Projects/newMusic/transfer:/app/transfer
# Stream folder for temporary playback files
- /mnt/e/Broque Projects/newMusic/stream:/app/stream
- ./downloads:/app/downloads
- ./Transfer:/app/Transfer
# Use named volume for database persistence (separate from host database)
- soulsync_database:/app/database
# Mount only E: drive through WSL for better performance
- /mnt/e:/host/mnt/e:rw
# Optional: Mount your music library for Plex/Jellyfin access
- /path/to/your/music:/music:ro
environment:
@ -48,6 +48,11 @@ services:
cpus: '0.5'
memory: 512M
# Named volumes for persistent data
volumes:
soulsync_database:
driver: local
# Optional: Add external network for communication with other containers
networks:
default:

View file

@ -62,6 +62,33 @@ app = Flask(
static_folder=os.path.join(base_dir, 'webui', 'static')
)
# --- Docker Helper Functions ---
def docker_resolve_path(path_str):
"""
Resolve absolute paths for Docker container access
In Docker, Windows drive paths (E:/) need to be mapped to WSL mount points (/mnt/e/)
"""
if os.path.exists('/.dockerenv') and len(path_str) >= 3 and path_str[1] == ':' and path_str[0].isalpha():
# Convert Windows path (E:/path) to WSL mount path (/mnt/e/path)
drive_letter = path_str[0].lower()
rest_of_path = path_str[2:].replace('\\', '/') # Remove E: and convert backslashes
return f"/host/mnt/{drive_letter}{rest_of_path}"
return path_str
def extract_filename(full_path):
"""
Extract filename by working backwards from the end until we hit a separator.
This is cross-platform compatible and handles both Windows and Unix path separators.
"""
if not full_path:
return ""
last_slash = max(full_path.rfind('/'), full_path.rfind('\\'))
if last_slash != -1:
return full_path[last_slash + 1:]
else:
return full_path
# --- Initialize Core Application Components ---
print("🚀 Initializing SoulSync services for Web UI...")
try:
@ -174,7 +201,7 @@ def get_cached_transfer_data():
file_info['username'] = username
all_transfers.append(file_info)
for transfer in all_transfers:
key = f"{transfer.get('username')}::{os.path.basename(transfer.get('filename', ''))}"
key = f"{transfer.get('username')}::{extract_filename(transfer.get('filename', ''))}"
live_transfers_lookup[key] = transfer
# Update cache
@ -275,7 +302,7 @@ class WebUIDownloadMonitor:
for directory in user_data['directories']:
if 'files' in directory:
for file_info in directory['files']:
key = f"{username}::{os.path.basename(file_info.get('filename', ''))}"
key = f"{username}::{extract_filename(file_info.get('filename', ''))}"
live_transfers[key] = file_info
return live_transfers
except Exception as e:
@ -298,7 +325,7 @@ class WebUIDownloadMonitor:
if not task_filename or not task_username:
return False
lookup_key = f"{task_username}::{os.path.basename(task_filename)}"
lookup_key = f"{task_username}::{extract_filename(task_filename)}"
live_info = live_transfers_lookup.get(lookup_key)
if not live_info:
@ -776,7 +803,7 @@ def _prepare_stream_task(track_data):
})
# Get paths
download_path = config_manager.get('soulseek.download_path', './downloads')
download_path = docker_resolve_path(config_manager.get('soulseek.download_path', './downloads'))
project_root = os.path.dirname(os.path.abspath(__file__))
stream_folder = os.path.join(project_root, 'Stream')
@ -898,7 +925,7 @@ def _prepare_stream_task(track_data):
print(f"✓ Found downloaded file: {found_file}")
# Move file to Stream folder
original_filename = os.path.basename(found_file)
original_filename = extract_filename(found_file)
stream_path = os.path.join(stream_folder, original_filename)
try:
@ -1008,11 +1035,11 @@ def _find_streaming_download_in_transfers(transfers_data, track_data):
all_transfers.extend(directory['files'])
# Look for our specific file by filename and username
target_filename = os.path.basename(track_data.get('filename', ''))
target_filename = extract_filename(track_data.get('filename', ''))
target_username = track_data.get('username', '')
for transfer in all_transfers:
transfer_filename = os.path.basename(transfer.get('filename', ''))
transfer_filename = extract_filename(transfer.get('filename', ''))
transfer_username = transfer.get('username', '')
if (transfer_filename == target_filename and
@ -1027,7 +1054,7 @@ def _find_streaming_download_in_transfers(transfers_data, track_data):
def _find_downloaded_file(download_path, track_data):
"""Find the downloaded audio file in the downloads directory tree"""
audio_extensions = {'.mp3', '.flac', '.ogg', '.aac', '.wma', '.wav', '.m4a'}
target_filename = os.path.basename(track_data.get('filename', ''))
target_filename = extract_filename(track_data.get('filename', ''))
try:
# Walk through the downloads directory to find the file
@ -1956,14 +1983,15 @@ def _find_completed_file_robust(download_dir, api_filename, transfer_dir=None):
return best_match_path, highest_similarity
target_basename = os.path.basename(api_filename)
# Extract filename using the helper function
target_basename = extract_filename(api_filename)
normalized_target = normalize_for_finding(target_basename)
# First search in downloads directory
best_downloads_path, downloads_similarity = search_in_directory(download_dir, 'downloads')
# Use a high confidence threshold for fuzzy matches to avoid incorrect files
if downloads_similarity > 0.85:
# Use a reasonable confidence threshold for fuzzy matches
if downloads_similarity > 0.60:
location = 'downloads'
if downloads_similarity == 1.0:
print(f"✅ Found exact match in downloads: {best_downloads_path}")
@ -1977,7 +2005,7 @@ def _find_completed_file_robust(download_dir, api_filename, transfer_dir=None):
print(f"🔍 File not found in downloads, checking transfer folder...")
best_transfer_path, transfer_similarity = search_in_directory(transfer_dir, 'transfer')
if transfer_similarity > 0.85:
if transfer_similarity > 0.60:
location = 'transfer'
if transfer_similarity == 1.0:
print(f"✅ Found exact match in transfer: {best_transfer_path}")
@ -2032,7 +2060,7 @@ def get_download_status():
context = matched_downloads_context.get(context_key)
if context and context_key not in _processed_download_ids:
download_dir = config_manager.get('soulseek.download_path', './downloads')
download_dir = docker_resolve_path(config_manager.get('soulseek.download_path', './downloads'))
# Use the new robust file finder (only search downloads for post-processing candidates)
found_result = _find_completed_file_robust(download_dir, filename_from_api)
found_path = found_result[0] if found_result and found_result[0] else None
@ -4553,7 +4581,7 @@ def _post_process_matched_download_with_verification(context_key, context, file_
album_name_sanitized = clean_album_name.replace('/', '_').replace('\\', '_').replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_').replace('<', '_').replace('>', '_').replace('|', '_')
track_name_sanitized = clean_track_name.replace('/', '_').replace('\\', '_').replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_').replace('<', '_').replace('>', '_').replace('|', '_')
transfer_dir = config_manager.get('soulseek.transfer_path', './transfers')
transfer_dir = docker_resolve_path(config_manager.get('soulseek.transfer_path', './transfers'))
artist_dir = os.path.join(transfer_dir, artist_name_sanitized)
album_folder_name = f"{clean_album_name} ({spotify_album.get('release_date', '').split('-')[0] if spotify_album.get('release_date') else 'Unknown'})"
album_folder_name = album_folder_name.replace('/', '_').replace('\\', '_').replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_').replace('<', '_').replace('>', '_').replace('|', '_')
@ -4569,7 +4597,7 @@ def _post_process_matched_download_with_verification(context_key, context, file_
track_name = original_search.get('spotify_clean_title') or original_search.get('title', 'Unknown Track')
track_name_sanitized = track_name.replace('/', '_').replace('\\', '_').replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_').replace('<', '_').replace('>', '_').replace('|', '_')
transfer_dir = config_manager.get('soulseek.transfer_path', './transfers')
transfer_dir = docker_resolve_path(config_manager.get('soulseek.transfer_path', './transfers'))
artist_name_sanitized = spotify_artist.name.replace('/', '_').replace('\\', '_').replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_').replace('<', '_').replace('>', '_').replace('|', '_')
artist_dir = os.path.join(transfer_dir, artist_name_sanitized)
single_dir = os.path.join(artist_dir, "Singles")
@ -4739,7 +4767,7 @@ def _post_process_matched_download(context_key, context, file_path):
print(f"🔗 ✅ Album grouping complete!\n")
# 1. Get transfer path and create artist directory
transfer_dir = config_manager.get('soulseek.transfer_path', './Transfer')
transfer_dir = docker_resolve_path(config_manager.get('soulseek.transfer_path', './Transfer'))
artist_name_sanitized = _sanitize_filename(spotify_artist["name"])
artist_dir = os.path.join(transfer_dir, artist_name_sanitized)
os.makedirs(artist_dir, exist_ok=True)
@ -4861,7 +4889,7 @@ def _post_process_matched_download(context_key, context, file_path):
_download_cover_art(album_info, os.path.dirname(final_path))
downloads_path = config_manager.get('soulseek.download_path', './downloads')
downloads_path = docker_resolve_path(config_manager.get('soulseek.download_path', './downloads'))
_cleanup_empty_directories(downloads_path, file_path)
print(f"✅ Post-processing complete for: {final_path}")
@ -6529,11 +6557,12 @@ def _run_post_processing_worker(task_id, batch_id):
_on_download_completed(batch_id, task_id, success=False)
return
download_dir = config_manager.get('soulseek.download_path', './downloads')
transfer_dir = config_manager.get('soulseek.transfer_path', './transfer')
download_dir = docker_resolve_path(config_manager.get('soulseek.download_path', './downloads'))
transfer_dir = docker_resolve_path(config_manager.get('soulseek.transfer_path', './transfer'))
# Try to get context for generating the correct final filename
context_key = f"{task_username}::{os.path.basename(task_filename)}"
task_basename = extract_filename(task_filename)
context_key = f"{task_username}::{task_basename}"
expected_final_filename = None
print(f"🔍 [Post-Processing] Looking up context with key: {context_key}")
@ -6570,7 +6599,7 @@ def _run_post_processing_worker(task_id, batch_id):
else:
print(f"❌ [Post-Processing] No context found for key: {context_key}")
# Try fuzzy matching with similar keys containing the filename
similar_keys = [k for k in matched_downloads_context.keys() if os.path.basename(task_filename) in k]
similar_keys = [k for k in matched_downloads_context.keys() if task_basename in k]
if similar_keys:
# Use the first similar key found
fuzzy_key = similar_keys[0]
@ -6600,7 +6629,7 @@ def _run_post_processing_worker(task_id, batch_id):
import traceback
traceback.print_exc()
else:
print(f"🔍 [Post-Processing] No similar keys found containing '{os.path.basename(task_filename)}'")
print(f"🔍 [Post-Processing] No similar keys found containing '{task_basename}'")
# Show a sample of what keys actually exist for debugging
sample_keys = list(matched_downloads_context.keys())[:5]
print(f"🔍 [Post-Processing] Sample of existing keys: {sample_keys}")
@ -6610,7 +6639,7 @@ def _run_post_processing_worker(task_id, batch_id):
file_location = None
for retry_count in range(3):
print(f"🔍 [Post-Processing] Attempt {retry_count + 1}/3 to find file")
print(f"🔍 [Post-Processing] Original filename: {os.path.basename(task_filename)}")
print(f"🔍 [Post-Processing] Original filename: {task_basename}")
if expected_final_filename:
print(f"🔍 [Post-Processing] Expected final filename: {expected_final_filename}")
else:
@ -6745,7 +6774,7 @@ def _run_post_processing_worker(task_id, batch_id):
# File found in downloads folder - attempt post-processing
try:
# Create context for post-processing (similar to existing matched download logic)
context_key = f"{task_username}::{os.path.basename(task_filename)}"
context_key = f"{task_username}::{task_basename}"
# Check if this download has matched context for post-processing
with matched_context_lock:
@ -7371,7 +7400,7 @@ def _build_batch_status_data(batch_id, batch, live_transfers_lookup):
task_filename = task.get('filename') or task['track_info'].get('filename')
task_username = task.get('username') or task['track_info'].get('username')
if task_filename and task_username:
lookup_key = f"{task_username}::{os.path.basename(task_filename)}"
lookup_key = f"{task_username}::{extract_filename(task_filename)}"
if lookup_key in live_transfers_lookup:
live_info = live_transfers_lookup[lookup_key]