Added new 'Save as playlist' functionality for sync page.
This commit is contained in:
parent
bbc7fdbfd4
commit
06a3328b65
2 changed files with 164 additions and 44 deletions
181
web_server.py
181
web_server.py
|
|
@ -6524,47 +6524,71 @@ def _post_process_matched_download_with_verification(context_key, context, file_
|
|||
spotify_artist = context.get("spotify_artist")
|
||||
if not spotify_artist:
|
||||
raise Exception("Missing spotify_artist context for verification")
|
||||
|
||||
is_album_download = context.get("is_album_download", False)
|
||||
has_clean_spotify_data = context.get("has_clean_spotify_data", False)
|
||||
|
||||
# Reconstruct the final path logic (mirrors the logic in _post_process_matched_download)
|
||||
if is_album_download and has_clean_spotify_data:
|
||||
original_search = context.get("original_search_result", {})
|
||||
spotify_album = context.get("spotify_album", {})
|
||||
clean_track_name = original_search.get('spotify_clean_title', 'Unknown Track')
|
||||
clean_album_name = original_search.get('spotify_clean_album', 'Unknown Album')
|
||||
track_number = original_search.get('track_number', 1)
|
||||
|
||||
# Construct the expected final path
|
||||
artist_name_sanitized = spotify_artist.name.replace('/', '_').replace('\\', '_').replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_').replace('<', '_').replace('>', '_').replace('|', '_')
|
||||
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 = 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('|', '_')
|
||||
album_dir = os.path.join(artist_dir, album_folder_name)
|
||||
|
||||
file_ext = os.path.splitext(file_path)[1]
|
||||
new_filename = f"{track_number:02d} - {track_name_sanitized}{file_ext}"
|
||||
expected_final_path = os.path.join(album_dir, new_filename)
|
||||
|
||||
else:
|
||||
# For singles or fallback logic
|
||||
|
||||
# Check if playlist folder mode is enabled
|
||||
track_info = context.get("track_info", {})
|
||||
playlist_folder_mode = track_info.get("_playlist_folder_mode", False)
|
||||
|
||||
# Handle playlist folder mode verification
|
||||
if playlist_folder_mode:
|
||||
playlist_name = track_info.get("_playlist_name", "Unknown Playlist")
|
||||
playlist_name_sanitized = _sanitize_filename(playlist_name)
|
||||
|
||||
transfer_dir = docker_resolve_path(config_manager.get('soulseek.transfer_path', './Transfer'))
|
||||
playlist_dir = os.path.join(transfer_dir, playlist_name_sanitized)
|
||||
|
||||
original_search = context.get("original_search_result", {})
|
||||
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 = 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")
|
||||
|
||||
track_name_sanitized = _sanitize_filename(track_name)
|
||||
artist_name_sanitized = _sanitize_filename(spotify_artist.get("name", "Unknown Artist") if isinstance(spotify_artist, dict) else spotify_artist.name)
|
||||
|
||||
file_ext = os.path.splitext(file_path)[1]
|
||||
new_filename = f"{track_name_sanitized}{file_ext}"
|
||||
expected_final_path = os.path.join(single_dir, new_filename)
|
||||
new_filename = f"{artist_name_sanitized} - {track_name_sanitized}{file_ext}"
|
||||
expected_final_path = os.path.join(playlist_dir, new_filename)
|
||||
|
||||
print(f"📁 [Verification - Playlist Mode] Expected path: {expected_final_path}")
|
||||
|
||||
else:
|
||||
# Original album/artist folder verification logic
|
||||
is_album_download = context.get("is_album_download", False)
|
||||
has_clean_spotify_data = context.get("has_clean_spotify_data", False)
|
||||
|
||||
if is_album_download and has_clean_spotify_data:
|
||||
original_search = context.get("original_search_result", {})
|
||||
spotify_album = context.get("spotify_album", {})
|
||||
clean_track_name = original_search.get('spotify_clean_title', 'Unknown Track')
|
||||
clean_album_name = original_search.get('spotify_clean_album', 'Unknown Album')
|
||||
track_number = original_search.get('track_number', 1)
|
||||
|
||||
# Construct the expected final path
|
||||
artist_name_sanitized = spotify_artist.name.replace('/', '_').replace('\\', '_').replace(':', '_').replace('*', '_').replace('?', '_').replace('"', '_').replace('<', '_').replace('>', '_').replace('|', '_')
|
||||
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 = 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('|', '_')
|
||||
album_dir = os.path.join(artist_dir, album_folder_name)
|
||||
|
||||
file_ext = os.path.splitext(file_path)[1]
|
||||
new_filename = f"{track_number:02d} - {track_name_sanitized}{file_ext}"
|
||||
expected_final_path = os.path.join(album_dir, new_filename)
|
||||
|
||||
else:
|
||||
# For singles or fallback logic
|
||||
original_search = context.get("original_search_result", {})
|
||||
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 = 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")
|
||||
|
||||
file_ext = os.path.splitext(file_path)[1]
|
||||
new_filename = f"{track_name_sanitized}{file_ext}"
|
||||
expected_final_path = os.path.join(single_dir, new_filename)
|
||||
|
||||
# VERIFICATION: Check if file exists at expected final path
|
||||
if os.path.exists(expected_final_path):
|
||||
|
|
@ -6638,12 +6662,69 @@ def _post_process_matched_download(context_key, context, file_path):
|
|||
# --- END OF FIX ---
|
||||
|
||||
print(f"🎯 Starting robust post-processing for: {context_key}")
|
||||
|
||||
|
||||
spotify_artist = context.get("spotify_artist")
|
||||
if not spotify_artist:
|
||||
print(f"❌ Post-processing failed: Missing spotify_artist context.")
|
||||
return
|
||||
|
||||
# Check if playlist folder mode is enabled (sync page playlists only)
|
||||
track_info = context.get("track_info", {})
|
||||
playlist_folder_mode = track_info.get("_playlist_folder_mode", False)
|
||||
|
||||
print(f"🔍 [Debug] Post-processing - track_info type: {type(track_info)}, is None: {track_info is None}, is empty: {not track_info}")
|
||||
print(f"🔍 [Debug] Post-processing - playlist_folder_mode: {playlist_folder_mode}")
|
||||
if track_info:
|
||||
print(f"🔍 [Debug] Post-processing - track_info keys: {list(track_info.keys())}")
|
||||
|
||||
if playlist_folder_mode:
|
||||
# Use playlist folder structure: Transfer/Playlist Name/Artist - Track.ext
|
||||
playlist_name = track_info.get("_playlist_name", "Unknown Playlist")
|
||||
print(f"📁 [Playlist Folder Mode] Organizing in playlist folder: {playlist_name}")
|
||||
|
||||
transfer_dir = docker_resolve_path(config_manager.get('soulseek.transfer_path', './Transfer'))
|
||||
playlist_name_sanitized = _sanitize_filename(playlist_name)
|
||||
playlist_dir = os.path.join(transfer_dir, playlist_name_sanitized)
|
||||
os.makedirs(playlist_dir, exist_ok=True)
|
||||
|
||||
# Get track name from context
|
||||
original_search = context.get("original_search_result", {})
|
||||
track_name = original_search.get('spotify_clean_title') or original_search.get('title', 'Unknown Track')
|
||||
track_name_sanitized = _sanitize_filename(track_name)
|
||||
artist_name_sanitized = _sanitize_filename(spotify_artist["name"])
|
||||
|
||||
# Create filename: Artist - Track.ext
|
||||
file_ext = os.path.splitext(file_path)[1]
|
||||
new_filename = f"{artist_name_sanitized} - {track_name_sanitized}{file_ext}"
|
||||
final_path = os.path.join(playlist_dir, new_filename)
|
||||
|
||||
print(f"📁 Playlist folder: '{playlist_name_sanitized}'")
|
||||
print(f"🎵 Track filename: '{new_filename}'")
|
||||
|
||||
# Move file to playlist folder
|
||||
print(f"🚚 Moving '{os.path.basename(file_path)}' to '{final_path}'")
|
||||
if os.path.exists(final_path):
|
||||
print(f"⚠️ File already exists, overwriting: {os.path.basename(final_path)}")
|
||||
os.remove(final_path)
|
||||
|
||||
shutil.move(file_path, final_path)
|
||||
|
||||
# Clean up empty directories in downloads folder
|
||||
downloads_path = docker_resolve_path(config_manager.get('soulseek.download_path', './downloads'))
|
||||
_cleanup_empty_directories(downloads_path, file_path)
|
||||
|
||||
print(f"✅ [Playlist Folder Mode] Post-processing complete: {final_path}")
|
||||
|
||||
# WISHLIST REMOVAL: Check if this track should be removed from wishlist
|
||||
try:
|
||||
_check_and_remove_from_wishlist(context)
|
||||
except Exception as wishlist_error:
|
||||
print(f"⚠️ [Playlist Folder] Error checking wishlist removal: {wishlist_error}")
|
||||
|
||||
# NOTE: Don't call callbacks here - let verification function handle completion
|
||||
# The verification function will check file exists and then call callbacks
|
||||
return # Skip normal album/artist folder structure processing
|
||||
|
||||
is_album_download = context.get("is_album_download", False)
|
||||
has_clean_spotify_data = context.get("has_clean_spotify_data", False)
|
||||
|
||||
|
|
@ -8792,6 +8873,8 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
|
|||
batch_album_context = batch.get('album_context')
|
||||
batch_artist_context = batch.get('artist_context')
|
||||
batch_is_album = batch.get('is_album_download', False)
|
||||
batch_playlist_folder_mode = batch.get('playlist_folder_mode', False)
|
||||
batch_playlist_name = batch.get('playlist_name', 'Unknown Playlist')
|
||||
|
||||
for res in missing_tracks:
|
||||
task_id = str(uuid.uuid4())
|
||||
|
|
@ -8804,6 +8887,14 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
|
|||
track_info['_is_explicit_album_download'] = True
|
||||
print(f"🎵 [Task Creation] Added explicit album context for: {track_info.get('name')}")
|
||||
|
||||
# Add playlist folder mode flag for sync page playlists
|
||||
if batch_playlist_folder_mode:
|
||||
track_info['_playlist_folder_mode'] = True
|
||||
track_info['_playlist_name'] = batch_playlist_name
|
||||
print(f"📁 [Task Creation] Added playlist folder mode for: {track_info.get('name')} → {batch_playlist_name}")
|
||||
else:
|
||||
print(f"🔍 [Debug] Task Creation - playlist folder mode NOT enabled for: {track_info.get('name')}")
|
||||
|
||||
download_tasks[task_id] = {
|
||||
'status': 'pending', 'track_info': track_info,
|
||||
'playlist_id': playlist_id, 'batch_id': batch_id,
|
||||
|
|
@ -9543,10 +9634,12 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
|
|||
"is_album_download": is_album_context, # Critical fix: Use actual album context
|
||||
"has_clean_spotify_data": has_clean_spotify_data, # Flag for post-processing
|
||||
"task_id": task_id, # Add task_id for completion callbacks
|
||||
"batch_id": batch_id # Add batch_id for completion callbacks
|
||||
"batch_id": batch_id, # Add batch_id for completion callbacks
|
||||
"track_info": track_info # Add track_info for playlist folder mode
|
||||
}
|
||||
|
||||
|
||||
print(f"🎯 [Context] Set is_album_download: {is_album_context} (has clean data: {has_clean_spotify_data})")
|
||||
print(f"🔍 [Debug] Context creation - track_info: {track_info is not None}, playlist_folder_mode: {track_info.get('_playlist_folder_mode', False) if track_info else False}")
|
||||
|
||||
# Update task with successful download info
|
||||
with tasks_lock:
|
||||
|
|
@ -10619,6 +10712,7 @@ def start_missing_tracks_process(playlist_id):
|
|||
tracks = data.get('tracks', [])
|
||||
playlist_name = data.get('playlist_name', 'Unknown Playlist')
|
||||
force_download_all = data.get('force_download_all', False)
|
||||
playlist_folder_mode = data.get('playlist_folder_mode', False)
|
||||
|
||||
# Get album/artist context for artist album downloads
|
||||
is_album_download = data.get('is_album_download', False)
|
||||
|
|
@ -10633,6 +10727,10 @@ def start_missing_tracks_process(playlist_id):
|
|||
print(f"🎵 [Artist Album] Received album context: '{album_context.get('name')}' by '{artist_context.get('name')}' ({album_context.get('album_type', 'album')})")
|
||||
print(f" Release: {album_context.get('release_date', 'Unknown')}, Tracks: {album_context.get('total_tracks', len(tracks))}")
|
||||
|
||||
# Log playlist folder mode if enabled
|
||||
if playlist_folder_mode:
|
||||
print(f"📁 [Playlist Folder] Enabled for playlist: '{playlist_name}'")
|
||||
|
||||
# Limit concurrent analysis processes to prevent resource exhaustion
|
||||
with tasks_lock:
|
||||
active_analysis_count = sum(1 for batch in download_batches.values()
|
||||
|
|
@ -10661,6 +10759,7 @@ def start_missing_tracks_process(playlist_id):
|
|||
'analysis_processed': 0,
|
||||
'analysis_results': [],
|
||||
'force_download_all': force_download_all, # Pass the force flag to the batch
|
||||
'playlist_folder_mode': playlist_folder_mode, # Organize downloads by playlist folder
|
||||
# Album context for artist album downloads (explicit folder structure)
|
||||
'is_album_download': is_album_download,
|
||||
'album_context': album_context,
|
||||
|
|
|
|||
|
|
@ -3850,11 +3850,15 @@ async function openDownloadMissingModal(playlistId) {
|
|||
|
||||
<div class="download-missing-modal-footer">
|
||||
<div class="download-phase-controls">
|
||||
<div class="force-download-toggle-container" style="margin-bottom: 0px;">
|
||||
<div class="force-download-toggle-container" style="margin-bottom: 0px; display: flex; flex-direction: column; gap: 8px; align-items: flex-start;">
|
||||
<label class="force-download-toggle">
|
||||
<input type="checkbox" id="force-download-all-${playlistId}">
|
||||
<span>Force Download All</span>
|
||||
</label>
|
||||
<label class="force-download-toggle">
|
||||
<input type="checkbox" id="playlist-folder-mode-${playlistId}">
|
||||
<span>Organize by Playlist (Downloads/Playlist/Artist - Track.ext)</span>
|
||||
</label>
|
||||
</div>
|
||||
<button class="download-control-btn primary" id="begin-analysis-btn-${playlistId}" onclick="startMissingTracksProcess('${playlistId}')">
|
||||
Begin Analysis
|
||||
|
|
@ -4016,11 +4020,15 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam
|
|||
|
||||
<div class="download-missing-modal-footer">
|
||||
<div class="download-phase-controls">
|
||||
<div class="force-download-toggle-container" style="margin-bottom: 0px;">
|
||||
<div class="force-download-toggle-container" style="margin-bottom: 0px; display: flex; flex-direction: column; gap: 8px; align-items: flex-start;">
|
||||
<label class="force-download-toggle">
|
||||
<input type="checkbox" id="force-download-all-${virtualPlaylistId}">
|
||||
<span>Force Download All</span>
|
||||
</label>
|
||||
<label class="force-download-toggle">
|
||||
<input type="checkbox" id="playlist-folder-mode-${virtualPlaylistId}">
|
||||
<span>Organize by Playlist (Downloads/Playlist/Artist - Track.ext)</span>
|
||||
</label>
|
||||
</div>
|
||||
<button class="download-control-btn primary" id="begin-analysis-btn-${virtualPlaylistId}" onclick="startMissingTracksProcess('${virtualPlaylistId}')">
|
||||
Begin Analysis
|
||||
|
|
@ -4530,6 +4538,10 @@ async function startMissingTracksProcess(playlistId) {
|
|||
const forceDownloadCheckbox = document.getElementById(`force-download-all-${playlistId}`);
|
||||
const forceDownloadAll = forceDownloadCheckbox ? forceDownloadCheckbox.checked : false;
|
||||
|
||||
// Check if playlist folder mode toggle is enabled (only for sync page playlists)
|
||||
const playlistFolderModeCheckbox = document.getElementById(`playlist-folder-mode-${playlistId}`);
|
||||
const playlistFolderMode = playlistFolderModeCheckbox ? playlistFolderModeCheckbox.checked : false;
|
||||
|
||||
// Hide the force download toggle during processing
|
||||
const forceToggleContainer = forceDownloadCheckbox ? forceDownloadCheckbox.closest('.force-download-toggle-container') : null;
|
||||
if (forceToggleContainer) {
|
||||
|
|
@ -4552,6 +4564,11 @@ async function startMissingTracksProcess(playlistId) {
|
|||
} else {
|
||||
// For playlists/wishlists, use the virtual playlist name
|
||||
requestBody.playlist_name = process.playlist.name;
|
||||
// Add playlist folder mode flag for sync page playlists
|
||||
requestBody.playlist_folder_mode = playlistFolderMode;
|
||||
if (playlistFolderMode) {
|
||||
console.log(`📁 [Playlist Folder] Enabled for playlist: ${process.playlist.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/playlists/${playlistId}/start-missing-process`, {
|
||||
|
|
@ -10376,11 +10393,15 @@ async function openDownloadMissingModalForTidal(virtualPlaylistId, playlistName,
|
|||
|
||||
<div class="download-missing-modal-footer">
|
||||
<div class="download-phase-controls">
|
||||
<div class="force-download-toggle-container" style="margin-bottom: 0px;">
|
||||
<div class="force-download-toggle-container" style="margin-bottom: 0px; display: flex; flex-direction: column; gap: 8px; align-items: flex-start;">
|
||||
<label class="force-download-toggle">
|
||||
<input type="checkbox" id="force-download-all-${virtualPlaylistId}">
|
||||
<span>Force Download All</span>
|
||||
</label>
|
||||
<label class="force-download-toggle">
|
||||
<input type="checkbox" id="playlist-folder-mode-${virtualPlaylistId}">
|
||||
<span>Organize by Playlist (Downloads/Playlist/Artist - Track.ext)</span>
|
||||
</label>
|
||||
</div>
|
||||
<button class="download-control-btn primary" id="begin-analysis-btn-${virtualPlaylistId}" onclick="startMissingTracksProcess('${virtualPlaylistId}')">
|
||||
Begin Analysis
|
||||
|
|
|
|||
Loading…
Reference in a new issue