Rebuild the import feature and move it to its own page.
This commit is contained in:
parent
a91b4fae2d
commit
1aa6cdc9b6
5 changed files with 1744 additions and 1031 deletions
267
web_server.py
267
web_server.py
|
|
@ -28411,13 +28411,13 @@ def import_staging_files():
|
|||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/import/staging/suggestions', methods=['GET'])
|
||||
def import_staging_suggestions():
|
||||
"""Suggest albums based on staging folder contents (tags + folder names)."""
|
||||
@app.route('/api/import/staging/hints', methods=['GET'])
|
||||
def import_staging_hints():
|
||||
"""Extract album search hints from staging folder (tags + folder names). Fast — no Spotify calls."""
|
||||
try:
|
||||
staging_path = _get_staging_path()
|
||||
if not os.path.isdir(staging_path):
|
||||
return jsonify({'success': True, 'suggestions': []})
|
||||
return jsonify({'success': True, 'hints': []})
|
||||
|
||||
# Collect hints from tags and folder structure
|
||||
tag_albums = {} # (album, artist) -> file count
|
||||
|
|
@ -28463,7 +28463,6 @@ def import_staging_suggestions():
|
|||
|
||||
# Folder-based: parse "Artist - Album" pattern or use as-is
|
||||
for folder, count in sorted(folder_hints.items(), key=lambda x: -x[1]):
|
||||
# Try to parse "Artist - Album" folder name
|
||||
q = folder.replace('_', ' ')
|
||||
if q.lower() not in seen_queries_lower:
|
||||
seen_queries_lower.add(q.lower())
|
||||
|
|
@ -28472,34 +28471,9 @@ def import_staging_suggestions():
|
|||
# Cap at 5 queries to keep it fast
|
||||
queries = queries[:5]
|
||||
|
||||
if not queries:
|
||||
return jsonify({'success': True, 'suggestions': []})
|
||||
|
||||
# Search Spotify for each hint, take top 1-2 results per query
|
||||
suggestions = []
|
||||
seen_ids = set()
|
||||
for q in queries:
|
||||
try:
|
||||
albums = spotify_client.search_albums(q, limit=2)
|
||||
for a in albums:
|
||||
if a.id not in seen_ids:
|
||||
seen_ids.add(a.id)
|
||||
suggestions.append({
|
||||
'id': a.id,
|
||||
'name': a.name,
|
||||
'artist': ', '.join(a.artists) if a.artists else 'Unknown Artist',
|
||||
'release_date': a.release_date or '',
|
||||
'total_tracks': a.total_tracks,
|
||||
'image_url': a.image_url,
|
||||
'album_type': a.album_type or 'album',
|
||||
'hint_query': q
|
||||
})
|
||||
except Exception as search_err:
|
||||
logger.warning(f"Suggestion search failed for '{q}': {search_err}")
|
||||
|
||||
return jsonify({'success': True, 'suggestions': suggestions[:8]})
|
||||
return jsonify({'success': True, 'hints': queries})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting staging suggestions: {e}")
|
||||
logger.error(f"Error getting staging hints: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
|
|
@ -28837,6 +28811,10 @@ def import_album_process():
|
|||
|
||||
add_activity_item("📥", "Album Imported", f"{album_name} by {artist_name} ({processed}/{len(matches)} tracks)", "Now")
|
||||
|
||||
# Rebuild suggestions cache since staging contents changed
|
||||
if processed > 0:
|
||||
refresh_import_suggestions_cache()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'processed': processed,
|
||||
|
|
@ -28848,6 +28826,42 @@ def import_album_process():
|
|||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/import/search/tracks', methods=['GET'])
|
||||
def import_search_tracks():
|
||||
"""Search Spotify for individual tracks (used for manual singles identification)."""
|
||||
try:
|
||||
query = request.args.get('q', '').strip()
|
||||
if not query:
|
||||
return jsonify({'success': False, 'error': 'Missing query parameter'}), 400
|
||||
|
||||
limit = min(int(request.args.get('limit', 10)), 30)
|
||||
|
||||
if _is_hydrabase_active():
|
||||
tracks = hydrabase_client.search_tracks(query, limit=limit)
|
||||
else:
|
||||
if hydrabase_worker and dev_mode_enabled:
|
||||
hydrabase_worker.enqueue(query, 'track')
|
||||
tracks = spotify_client.search_tracks(query, limit=limit)
|
||||
|
||||
results = []
|
||||
for t in tracks:
|
||||
results.append({
|
||||
'id': t.id,
|
||||
'name': t.name,
|
||||
'artist': ', '.join(t.artists) if hasattr(t, 'artists') and t.artists else 'Unknown Artist',
|
||||
'album': t.album if hasattr(t, 'album') else '',
|
||||
'album_id': t.album_id if hasattr(t, 'album_id') else '',
|
||||
'duration_ms': t.duration_ms if hasattr(t, 'duration_ms') else 0,
|
||||
'image_url': t.image_url if hasattr(t, 'image_url') else '',
|
||||
'track_number': t.track_number if hasattr(t, 'track_number') else 1,
|
||||
})
|
||||
|
||||
return jsonify({'success': True, 'tracks': results})
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching tracks for import: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/import/singles/process', methods=['POST'])
|
||||
def import_singles_process():
|
||||
"""Process individual staging files as singles through the post-processing pipeline."""
|
||||
|
|
@ -28869,9 +28883,10 @@ def import_singles_process():
|
|||
|
||||
title = file_info.get('title', '')
|
||||
artist = file_info.get('artist', '')
|
||||
spotify_override = file_info.get('spotify_override', None)
|
||||
|
||||
# Fallback to filename parsing if no metadata
|
||||
if not title:
|
||||
if not title and not spotify_override:
|
||||
parsed = _parse_filename_metadata(file_info.get('filename', ''))
|
||||
title = parsed.get('title', os.path.splitext(file_info.get('filename', 'Unknown'))[0])
|
||||
if not artist:
|
||||
|
|
@ -28882,7 +28897,51 @@ def import_singles_process():
|
|||
spotify_artist_data = None
|
||||
spotify_album_data = None
|
||||
|
||||
if title:
|
||||
# If a manual spotify_override is provided, look up that specific track
|
||||
if spotify_override and spotify_override.get('id'):
|
||||
try:
|
||||
override_id = spotify_override['id']
|
||||
sp_track = spotify_client.sp.track(override_id) if hasattr(spotify_client, 'sp') and spotify_client.sp else None
|
||||
if sp_track:
|
||||
sp_track_artists = sp_track.get('artists', [])
|
||||
spotify_track_data = {
|
||||
'name': sp_track.get('name', ''),
|
||||
'id': override_id,
|
||||
'track_number': sp_track.get('track_number', 1),
|
||||
'disc_number': sp_track.get('disc_number', 1),
|
||||
'duration_ms': sp_track.get('duration_ms', 0),
|
||||
'artists': [{'name': a.get('name', '')} for a in sp_track_artists],
|
||||
'uri': sp_track.get('uri', f"spotify:track:{override_id}")
|
||||
}
|
||||
title = sp_track.get('name', title)
|
||||
artist = sp_track_artists[0].get('name', artist) if sp_track_artists else artist
|
||||
# Get album info
|
||||
sp_album_info = sp_track.get('album', {})
|
||||
if sp_album_info:
|
||||
spotify_album_data = {
|
||||
'id': sp_album_info.get('id', ''),
|
||||
'name': sp_album_info.get('name', ''),
|
||||
'release_date': sp_album_info.get('release_date', ''),
|
||||
'total_tracks': sp_album_info.get('total_tracks', 1),
|
||||
'image_url': (sp_album_info.get('images', [{}])[0].get('url') if sp_album_info.get('images') else '')
|
||||
}
|
||||
album_artists = sp_album_info.get('artists', [])
|
||||
if album_artists:
|
||||
spotify_artist_data = {
|
||||
'name': album_artists[0].get('name', artist),
|
||||
'id': album_artists[0].get('id', ''),
|
||||
'genres': []
|
||||
}
|
||||
try:
|
||||
sp_a = spotify_client.sp.artist(album_artists[0]['id']) if hasattr(spotify_client, 'sp') and spotify_client.sp else None
|
||||
if sp_a:
|
||||
spotify_artist_data['genres'] = sp_a.get('genres', [])
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as override_err:
|
||||
logger.warning(f"Spotify override lookup failed for track {spotify_override.get('id')}: {override_err}")
|
||||
|
||||
if not spotify_track_data and title:
|
||||
try:
|
||||
search_q = f"{title} {artist}" if artist else title
|
||||
tracks = spotify_client.search_tracks(search_q, limit=1)
|
||||
|
|
@ -28997,6 +29056,10 @@ def import_singles_process():
|
|||
|
||||
add_activity_item("📥", "Singles Imported", f"{processed}/{len(files)} tracks processed", "Now")
|
||||
|
||||
# Rebuild suggestions cache since staging contents changed
|
||||
if processed > 0:
|
||||
refresh_import_suggestions_cache()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'processed': processed,
|
||||
|
|
@ -29007,6 +29070,138 @@ def import_singles_process():
|
|||
logger.error(f"Error processing singles import: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
# --- Import Suggestion Cache (server-side background builder) ---
|
||||
|
||||
_import_suggestions_cache = {
|
||||
'suggestions': [],
|
||||
'building': False,
|
||||
'built': False,
|
||||
}
|
||||
|
||||
def _build_import_suggestions_background():
|
||||
"""Background thread: extract hints from staging folder, search Spotify, cache results."""
|
||||
cache = _import_suggestions_cache
|
||||
if cache['building']:
|
||||
return
|
||||
cache['building'] = True
|
||||
|
||||
try:
|
||||
staging_path = _get_staging_path()
|
||||
if not os.path.isdir(staging_path):
|
||||
cache['suggestions'] = []
|
||||
cache['built'] = True
|
||||
cache['building'] = False
|
||||
return
|
||||
|
||||
# Reuse the hint extraction logic
|
||||
tag_albums = {}
|
||||
folder_hints = {}
|
||||
|
||||
for root, _dirs, filenames in os.walk(staging_path):
|
||||
audio_files = [f for f in filenames if os.path.splitext(f)[1].lower() in AUDIO_EXTENSIONS]
|
||||
if not audio_files:
|
||||
continue
|
||||
rel_dir = os.path.relpath(root, staging_path)
|
||||
if rel_dir != '.':
|
||||
top_folder = rel_dir.split(os.sep)[0]
|
||||
folder_hints[top_folder] = folder_hints.get(top_folder, 0) + len(audio_files)
|
||||
for fname in audio_files:
|
||||
full_path = os.path.join(root, fname)
|
||||
try:
|
||||
from mutagen import File as MutagenFile
|
||||
tags = MutagenFile(full_path, easy=True)
|
||||
if tags:
|
||||
album = (tags.get('album') or [None])[0]
|
||||
artist = (tags.get('artist') or (tags.get('albumartist') or [None]))[0]
|
||||
if album:
|
||||
key = (album.strip(), (artist or '').strip())
|
||||
tag_albums[key] = tag_albums.get(key, 0) + 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
queries = []
|
||||
seen_lower = set()
|
||||
for (album, artist), _ in sorted(tag_albums.items(), key=lambda x: -x[1]):
|
||||
q = f"{album} {artist}".strip() if artist else album
|
||||
if q.lower() not in seen_lower:
|
||||
seen_lower.add(q.lower())
|
||||
queries.append(q)
|
||||
for folder, _ in sorted(folder_hints.items(), key=lambda x: -x[1]):
|
||||
q = folder.replace('_', ' ')
|
||||
if q.lower() not in seen_lower:
|
||||
seen_lower.add(q.lower())
|
||||
queries.append(q)
|
||||
queries = queries[:5]
|
||||
|
||||
if not queries:
|
||||
cache['suggestions'] = []
|
||||
cache['built'] = True
|
||||
cache['building'] = False
|
||||
return
|
||||
|
||||
suggestions = []
|
||||
seen_ids = set()
|
||||
for q in queries:
|
||||
try:
|
||||
if not spotify_client:
|
||||
break
|
||||
albums = spotify_client.search_albums(q, limit=2)
|
||||
for a in albums:
|
||||
if a.id not in seen_ids:
|
||||
seen_ids.add(a.id)
|
||||
suggestions.append({
|
||||
'id': a.id,
|
||||
'name': a.name,
|
||||
'artist': ', '.join(a.artists) if a.artists else 'Unknown Artist',
|
||||
'release_date': a.release_date or '',
|
||||
'total_tracks': a.total_tracks,
|
||||
'image_url': a.image_url,
|
||||
'album_type': a.album_type or 'album',
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"Import suggestion search failed for '{q}': {e}")
|
||||
|
||||
cache['suggestions'] = suggestions[:8]
|
||||
cache['built'] = True
|
||||
logger.info(f"Import suggestions cache built: {len(cache['suggestions'])} suggestions from {len(queries)} hints")
|
||||
except Exception as e:
|
||||
logger.error(f"Error building import suggestions cache: {e}")
|
||||
cache['suggestions'] = []
|
||||
cache['built'] = True
|
||||
finally:
|
||||
cache['building'] = False
|
||||
|
||||
|
||||
def start_import_suggestions_cache():
|
||||
"""Start building the import suggestions cache in a background thread (called on server startup)."""
|
||||
threading.Thread(
|
||||
target=_build_import_suggestions_background,
|
||||
daemon=True,
|
||||
name='import-suggestions-cache'
|
||||
).start()
|
||||
|
||||
|
||||
def refresh_import_suggestions_cache():
|
||||
"""Invalidate and rebuild the suggestions cache (called after imports change staging contents)."""
|
||||
_import_suggestions_cache['built'] = False
|
||||
threading.Thread(
|
||||
target=_build_import_suggestions_background,
|
||||
daemon=True,
|
||||
name='import-suggestions-cache'
|
||||
).start()
|
||||
|
||||
|
||||
@app.route('/api/import/staging/suggestions', methods=['GET'])
|
||||
def import_staging_suggestions():
|
||||
"""Return cached import suggestions. If cache isn't built yet, returns partial/empty with a flag."""
|
||||
cache = _import_suggestions_cache
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'suggestions': cache['suggestions'],
|
||||
'ready': cache['built'],
|
||||
})
|
||||
|
||||
|
||||
# ================================================================================================
|
||||
# END IMPORT / STAGING SYSTEM
|
||||
# ================================================================================================
|
||||
|
|
@ -29049,6 +29244,10 @@ if __name__ == '__main__':
|
|||
start_watchlist_auto_scanning()
|
||||
print("✅ Automatic watchlist scanning started (5 minute initial delay, 24 hour cycles)")
|
||||
|
||||
# Pre-build import suggestions cache in background
|
||||
print("🔧 Pre-building import suggestions cache...")
|
||||
start_import_suggestions_cache()
|
||||
|
||||
# Initialize app start time for uptime tracking
|
||||
import time
|
||||
app.start_time = time.time()
|
||||
|
|
|
|||
201
webui/index.html
201
webui/index.html
|
|
@ -54,6 +54,10 @@
|
|||
<span class="nav-icon">📚</span>
|
||||
<span class="nav-text">Library</span>
|
||||
</button>
|
||||
<button class="nav-button" data-page="import">
|
||||
<span class="nav-icon">📂</span>
|
||||
<span class="nav-text">Import</span>
|
||||
</button>
|
||||
<button class="nav-button" data-page="settings">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
<span class="nav-text">Settings</span>
|
||||
|
|
@ -330,7 +334,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="import-button" id="import-button" onclick="openImportModal()"
|
||||
<button class="import-button" id="import-button" onclick="navigateToPage('import')"
|
||||
title="Import Music from Staging">
|
||||
<img src="https://cdn-icons-png.flaticon.com/512/8765/8765164.png" alt="Import"
|
||||
class="import-logo">
|
||||
|
|
@ -3650,9 +3654,106 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import Page -->
|
||||
<div class="page" id="import-page">
|
||||
<div class="import-page-container">
|
||||
<!-- Header with staging info -->
|
||||
<div class="import-page-header">
|
||||
<div class="import-page-title-row">
|
||||
<h1 class="import-page-title">Import Music</h1>
|
||||
<button class="import-page-refresh-btn" onclick="importPageRefreshStaging()" title="Re-scan staging folder">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M13.65 2.35A8 8 0 1 0 16 8h-2a6 6 0 1 1-1.76-4.24L10 6h6V0l-2.35 2.35z"/></svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="import-page-staging-bar" id="import-staging-bar">
|
||||
<span class="import-staging-path" id="import-page-staging-path">Staging folder: loading...</span>
|
||||
<span class="import-staging-stats" id="import-page-staging-stats"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Processing Queue -->
|
||||
<div class="import-page-queue hidden" id="import-page-queue">
|
||||
<div class="import-page-queue-header">
|
||||
<span class="import-page-queue-title">Processing</span>
|
||||
<button class="import-page-queue-clear" id="import-page-queue-clear" onclick="importPageClearFinishedJobs()">Clear finished</button>
|
||||
</div>
|
||||
<div class="import-page-queue-list" id="import-page-queue-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Bar -->
|
||||
<div class="import-page-tab-bar">
|
||||
<button class="import-page-tab active" id="import-page-tab-album" onclick="importPageSwitchTab('album')">Albums</button>
|
||||
<button class="import-page-tab" id="import-page-tab-singles" onclick="importPageSwitchTab('singles')">Singles</button>
|
||||
</div>
|
||||
|
||||
<!-- Album Tab -->
|
||||
<div class="import-page-tab-content active" id="import-page-album-content">
|
||||
<!-- Search state -->
|
||||
<div id="import-page-album-search-section">
|
||||
<div class="import-page-suggestions" id="import-page-suggestions">
|
||||
<div class="import-page-section-label">Suggested from your staging folder</div>
|
||||
<div class="import-page-album-grid" id="import-page-suggestions-grid"></div>
|
||||
</div>
|
||||
<div class="import-page-search-bar">
|
||||
<input type="text" id="import-page-album-search-input" class="import-page-search-input"
|
||||
placeholder="Search for an album..." onkeydown="if(event.key==='Enter')importPageSearchAlbum()">
|
||||
<button class="import-page-search-btn" onclick="importPageSearchAlbum()">Search</button>
|
||||
<button class="import-page-clear-btn hidden" id="import-page-album-clear-btn"
|
||||
onclick="importPageResetAlbumSearch()" title="Clear search">✕</button>
|
||||
</div>
|
||||
<div class="import-page-album-grid" id="import-page-album-results"></div>
|
||||
</div>
|
||||
|
||||
<!-- Match state (hidden initially) -->
|
||||
<div id="import-page-album-match-section" class="hidden">
|
||||
<div class="import-page-album-hero" id="import-page-album-hero"></div>
|
||||
<div class="import-page-match-header">
|
||||
<h3>Track Matching</h3>
|
||||
<div class="import-page-match-actions">
|
||||
<button class="import-page-secondary-btn" onclick="importPageAutoRematch()">Re-match Automatically</button>
|
||||
<button class="import-page-back-btn" onclick="importPageResetAlbumSearch()">Back to Search</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="import-page-match-list" id="import-page-match-list"></div>
|
||||
|
||||
<!-- Unmatched file pool for drag-drop -->
|
||||
<div class="import-page-unmatched-pool" id="import-page-unmatched-pool">
|
||||
<div class="import-page-pool-label">Unmatched Files (<span id="import-page-unmatched-count">0</span>)</div>
|
||||
<div class="import-page-pool-chips" id="import-page-pool-chips"></div>
|
||||
</div>
|
||||
|
||||
<div class="import-page-match-footer">
|
||||
<div class="import-page-match-stats" id="import-page-match-stats"></div>
|
||||
<button class="import-page-process-btn" id="import-page-album-process-btn"
|
||||
onclick="importPageProcessAlbum()">Process Album</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Singles Tab -->
|
||||
<div class="import-page-tab-content" id="import-page-singles-content">
|
||||
<div class="import-page-singles-header">
|
||||
<div class="import-page-singles-actions">
|
||||
<button class="import-page-secondary-btn" onclick="importPageSelectAllSingles()">
|
||||
<span id="import-page-select-all-text">Select All</span>
|
||||
</button>
|
||||
<button class="import-page-process-btn" id="import-page-singles-process-btn"
|
||||
onclick="importPageProcessSingles()" disabled>Process Selected (0)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="import-page-singles-list" id="import-page-singles-list">
|
||||
<div class="import-page-empty-state">Navigate to this page to scan your staging folder for audio files.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- /main-content -->
|
||||
</div> <!-- /main-container -->
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div class="loading-overlay hidden" id="loading-overlay">
|
||||
<div class="loading-spinner"></div>
|
||||
|
|
@ -3757,104 +3858,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import Modal -->
|
||||
<div class="modal-overlay hidden" id="import-modal-overlay" onclick="if(event.target===this)closeImportModal()">
|
||||
<div class="import-modal" id="import-modal">
|
||||
<div class="import-modal-header">
|
||||
<h2 class="import-modal-title">Import Music</h2>
|
||||
<div class="import-tab-bar">
|
||||
<button class="import-tab-btn active" id="import-tab-album-btn"
|
||||
onclick="switchImportTab('album')">Album</button>
|
||||
<button class="import-tab-btn" id="import-tab-singles-btn"
|
||||
onclick="switchImportTab('singles')">Singles</button>
|
||||
</div>
|
||||
<span class="import-modal-close" onclick="closeImportModal()">×</span>
|
||||
</div>
|
||||
<div class="import-modal-body">
|
||||
<!-- Album Tab -->
|
||||
<div class="import-tab-content active" id="import-tab-album">
|
||||
<!-- Search state -->
|
||||
<div id="import-album-search-section">
|
||||
<div class="import-suggestions-section" id="import-suggestions-section">
|
||||
<div class="import-suggestions-label">Suggested from your staging folder</div>
|
||||
<div class="import-album-grid" id="import-suggestions-grid"></div>
|
||||
</div>
|
||||
<div class="import-search-bar">
|
||||
<input type="text" id="import-album-search-input" placeholder="Search for an album..."
|
||||
onkeydown="if(event.key==='Enter')searchImportAlbum()">
|
||||
<button class="import-search-btn" onclick="searchImportAlbum()">Search</button>
|
||||
<button class="import-clear-btn hidden" id="import-album-clear-btn"
|
||||
onclick="clearImportAlbumSearch()" title="Clear search">✕</button>
|
||||
</div>
|
||||
<div class="import-album-grid" id="import-album-results"></div>
|
||||
</div>
|
||||
<!-- Match state (hidden initially) -->
|
||||
<div id="import-album-match-section" class="hidden">
|
||||
<div class="import-album-hero" id="import-album-hero"></div>
|
||||
<div class="import-match-header">
|
||||
<h3>Track Matching</h3>
|
||||
<button class="import-back-btn" onclick="resetImportAlbumSearch()">← Back to Search</button>
|
||||
</div>
|
||||
<div class="import-match-list" id="import-match-list"></div>
|
||||
<div class="import-match-footer">
|
||||
<div class="import-match-stats" id="import-match-stats"></div>
|
||||
<button class="import-process-btn" id="import-album-process-btn"
|
||||
onclick="processImportAlbum()">Process Album</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Progress state -->
|
||||
<div id="import-album-progress-section" class="hidden">
|
||||
<div class="import-progress-container">
|
||||
<div class="import-progress-text" id="import-album-progress-text">Processing...</div>
|
||||
<div class="import-progress-bar" id="import-album-progress-bar">
|
||||
<div class="import-progress-fill" id="import-album-progress-fill"></div>
|
||||
</div>
|
||||
<div class="import-progress-result" id="import-album-progress-result"></div>
|
||||
<div class="import-progress-actions hidden" id="import-album-progress-actions">
|
||||
<button class="import-secondary-btn" onclick="resetImportAlbumSearch()">Import
|
||||
Another</button>
|
||||
<button class="import-process-btn" onclick="closeImportModal()">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Singles Tab -->
|
||||
<div class="import-tab-content" id="import-tab-singles">
|
||||
<div id="import-singles-controls">
|
||||
<div class="import-singles-header">
|
||||
<div class="import-singles-info">
|
||||
<span id="import-staging-path-display"></span>
|
||||
</div>
|
||||
<div class="import-singles-actions">
|
||||
<button class="import-action-btn" onclick="loadStagingFiles()">Refresh</button>
|
||||
<button class="import-action-btn" onclick="selectAllStagingFiles()">Select All</button>
|
||||
<button class="import-process-btn" id="import-singles-process-btn"
|
||||
onclick="processImportSingles()" disabled>Process Selected (0)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="import-singles-list" id="import-singles-list">
|
||||
<div class="import-singles-empty">Click "Refresh" to scan staging folder for audio files</div>
|
||||
</div>
|
||||
<!-- Singles Progress -->
|
||||
<div id="import-singles-progress-section" class="hidden">
|
||||
<div class="import-progress-container">
|
||||
<div class="import-progress-text" id="import-singles-progress-text">Processing...</div>
|
||||
<div class="import-progress-bar" id="import-singles-progress-bar">
|
||||
<div class="import-progress-fill" id="import-singles-progress-fill"></div>
|
||||
</div>
|
||||
<div class="import-progress-result" id="import-singles-progress-result"></div>
|
||||
<div class="import-progress-actions hidden" id="import-singles-progress-actions">
|
||||
<button class="import-secondary-btn" onclick="resetImportSingles()">Import More</button>
|
||||
<button class="import-process-btn" onclick="closeImportModal()">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add to Wishlist Modal -->
|
||||
<div class="modal-overlay hidden" id="add-to-wishlist-modal-overlay">
|
||||
<div class="add-to-wishlist-modal" id="add-to-wishlist-modal">
|
||||
|
|
|
|||
|
|
@ -1925,41 +1925,82 @@
|
|||
transition: opacity 0.1s ease;
|
||||
}
|
||||
|
||||
/* Import Modal - Mobile */
|
||||
.import-modal {
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
border-radius: 0;
|
||||
height: 100vh;
|
||||
/* Import Page - Touch fallback */
|
||||
.import-page-file-chip {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.import-album-grid {
|
||||
.import-page-match-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
/* Import Page — small screen */
|
||||
@media (max-width: 768px) {
|
||||
.import-page-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.import-page-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.import-page-album-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.import-singles-header {
|
||||
.import-page-album-hero {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.import-page-album-hero img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.import-page-match-row {
|
||||
grid-template-columns: 28px 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.import-page-match-file {
|
||||
grid-column: 1 / -1;
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
.import-page-match-unmatch {
|
||||
grid-column: 1 / -1;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.import-page-singles-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.import-singles-actions {
|
||||
.import-page-singles-actions {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.import-match-item {
|
||||
flex-wrap: wrap;
|
||||
.import-page-single-item {
|
||||
grid-template-columns: 28px 1fr;
|
||||
}
|
||||
|
||||
.import-match-file {
|
||||
text-align: left;
|
||||
flex-basis: 100%;
|
||||
.import-page-single-actions {
|
||||
grid-column: 1 / -1;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.import-album-hero {
|
||||
.import-page-single-search-panel {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.import-page-staging-bar {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
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