diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py index 1d255141..ab4feace 100644 --- a/py/routes/api_routes.py +++ b/py/routes/api_routes.py @@ -579,16 +579,36 @@ class ApiRoutes: download_url=data.get('download_url'), save_dir=data.get('lora_root'), relative_path=data.get('relative_path'), - progress_callback=progress_callback # Add progress callback + progress_callback=progress_callback ) if not result.get('success', False): - return web.Response(status=500, text=result.get('error', 'Unknown error')) + error_message = result.get('error', 'Unknown error') + + # Return 401 for early access errors + if 'early access' in error_message.lower(): + logger.warning(f"Early access download failed: {error_message}") + return web.Response( + status=401, # Use 401 status code to match Civitai's response + text=f"Early Access Restriction: {error_message}" + ) + + return web.Response(status=500, text=error_message) return web.json_response(result) except Exception as e: - logger.error(f"Error downloading LoRA: {e}") - return web.Response(status=500, text=str(e)) + error_message = str(e) + + # Check if this might be an early access error + if '401' in error_message: + logger.warning(f"Early access error (401): {error_message}") + return web.Response( + status=401, + text="Early Access Restriction: This LoRA requires purchase. Please buy early access on Civitai.com." + ) + + logger.error(f"Error downloading LoRA: {error_message}") + return web.Response(status=500, text=error_message) async def update_settings(self, request: web.Request) -> web.Response: """Update application settings""" diff --git a/py/routes/recipe_routes.py b/py/routes/recipe_routes.py index dc56c7b1..e447ac06 100644 --- a/py/routes/recipe_routes.py +++ b/py/routes/recipe_routes.py @@ -1,4 +1,5 @@ import os +import time import logging from aiohttp import web from typing import Dict @@ -13,7 +14,7 @@ from ..services.recipe_scanner import RecipeScanner from ..services.lora_scanner import LoraScanner from ..config import config from ..workflow.parser import WorkflowParser -import time # Add this import at the top +from ..utils.utils import download_twitter_image logger = logging.getLogger(__name__) @@ -234,7 +235,6 @@ class RecipeRoutes: }, status=400) # Download image from URL - from ..utils.utils import download_twitter_image temp_path = download_twitter_image(url) if not temp_path: diff --git a/py/services/civitai_client.py b/py/services/civitai_client.py index 91387ebe..13fc55c6 100644 --- a/py/services/civitai_client.py +++ b/py/services/civitai_client.py @@ -76,6 +76,17 @@ class CivitaiClient: headers = self._get_request_headers() async with session.get(url, headers=headers, allow_redirects=True) as response: if response.status != 200: + # Handle early access 401 unauthorized responses + if response.status == 401: + logger.warning(f"Unauthorized access to resource: {url} (Status 401)") + return False, "Early access restriction: You must purchase early access to download this LoRA." + + # Handle other client errors that might be permission-related + if response.status == 403: + logger.warning(f"Forbidden access to resource: {url} (Status 403)") + return False, "Access forbidden: You don't have permission to download this file." + + # Generic error response for other status codes return False, f"Download failed with status {response.status}" # Get filename from content-disposition header diff --git a/py/services/download_manager.py b/py/services/download_manager.py index 66330a68..5e6746f7 100644 --- a/py/services/download_manager.py +++ b/py/services/download_manager.py @@ -28,6 +28,25 @@ class DownloadManager: if not version_info: return {'success': False, 'error': 'Failed to fetch model metadata'} + # Check if this is an early access LoRA + if 'earlyAccessEndsAt' in version_info: + early_access_date = version_info.get('earlyAccessEndsAt', '') + # Convert to a readable date if possible + try: + from datetime import datetime + date_obj = datetime.fromisoformat(early_access_date.replace('Z', '+00:00')) + formatted_date = date_obj.strftime('%Y-%m-%d') + early_access_msg = f"This LoRA requires early access payment (until {formatted_date}). " + except: + early_access_msg = "This LoRA requires early access payment. " + + early_access_msg += "Please ensure you have purchased early access and are logged in to Civitai." + logger.warning(f"Early access LoRA detected: {version_info.get('name', 'Unknown')}") + + # We'll still try to download, but log a warning and prepare for potential failure + if progress_callback: + await progress_callback(1) # Show minimal progress to indicate we're trying + # Report initial progress if progress_callback: await progress_callback(0) @@ -82,6 +101,10 @@ class DownloadManager: except Exception as e: logger.error(f"Error in download_from_civitai: {e}", exc_info=True) + # Check if this might be an early access error + error_str = str(e).lower() + if "403" in error_str or "401" in error_str or "unauthorized" in error_str or "early access" in error_str: + return {'success': False, 'error': f"Early access restriction: {str(e)}. Please ensure you have purchased early access and are logged in to Civitai."} return {'success': False, 'error': str(e)} async def _execute_download(self, download_url: str, save_dir: str, diff --git a/py/utils/recipe_parsers.py b/py/utils/recipe_parsers.py index 581eda74..c83ed587 100644 --- a/py/utils/recipe_parsers.py +++ b/py/utils/recipe_parsers.py @@ -111,6 +111,13 @@ class RecipeFormatParser(RecipeMetadataParser): try: civitai_info = await civitai_client.get_model_version_info(lora['modelVersionId']) if civitai_info and civitai_info.get("error") != "Model not found": + # Check if this is an early access lora + if 'earlyAccessEndsAt' in civitai_info: + # Convert earlyAccessEndsAt to a human-readable date + early_access_date = civitai_info.get('earlyAccessEndsAt', '') + lora_entry['isEarlyAccess'] = True + lora_entry['earlyAccessEndsAt'] = early_access_date + # Get thumbnail URL from first image if 'images' in civitai_info and civitai_info['images']: lora_entry['thumbnailUrl'] = civitai_info['images'][0].get('url', '') @@ -219,6 +226,13 @@ class StandardMetadataParser(RecipeMetadataParser): # Check if this LoRA exists locally by SHA256 hash if civitai_info and civitai_info.get("error") != "Model not found": + # Check if this is an early access lora + if 'earlyAccessEndsAt' in civitai_info: + # Convert earlyAccessEndsAt to a human-readable date + early_access_date = civitai_info.get('earlyAccessEndsAt', '') + lora_entry['isEarlyAccess'] = True + lora_entry['earlyAccessEndsAt'] = early_access_date + # LoRA exists on Civitai, process its information if 'files' in civitai_info: # Find the model file (type="Model") in the files list @@ -427,6 +441,13 @@ class A1111MetadataParser(RecipeMetadataParser): try: civitai_info = await civitai_client.get_model_by_hash(hash_value) if civitai_info and civitai_info.get("error") != "Model not found": + # Check if this is an early access lora + if 'earlyAccessEndsAt' in civitai_info: + # Convert earlyAccessEndsAt to a human-readable date + early_access_date = civitai_info.get('earlyAccessEndsAt', '') + lora_entry['isEarlyAccess'] = True + lora_entry['earlyAccessEndsAt'] = early_access_date + # Get model version ID lora_entry['id'] = civitai_info.get('id', '') diff --git a/static/css/components/download-modal.css b/static/css/components/download-modal.css index 6ba34053..e220a104 100644 --- a/static/css/components/download-modal.css +++ b/static/css/components/download-modal.css @@ -98,6 +98,7 @@ .version-info { display: flex; flex-wrap: wrap; + flex-direction: row !important; gap: 8px; align-items: center; font-size: 0.9em; diff --git a/static/css/components/import-modal.css b/static/css/components/import-modal.css index 76711dc4..4349c749 100644 --- a/static/css/components/import-modal.css +++ b/static/css/components/import-modal.css @@ -207,6 +207,15 @@ border-left: 4px solid var(--lora-warning); } +.lora-item.is-early-access { + background: rgba(0, 184, 122, 0.05); + border-left: 4px solid #00B87A; +} + +.lora-item.missing-locally { + border-left: 4px solid var(--lora-error); +} + .lora-thumbnail { width: 80px; height: 80px; @@ -297,6 +306,12 @@ border-bottom: none; } +.missing-lora-item.is-early-access { + background: rgba(0, 184, 122, 0.05); + border-left: 3px solid #00B87A; + padding-left: 10px; +} + .missing-badge { display: inline-flex; align-items: center; @@ -682,3 +697,39 @@ min-height: 20px; /* Ensure there's always space for the error message */ font-weight: 500; } + +.early-access-warning { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 16px; + background: rgba(0, 184, 122, 0.1); + border: 1px solid #00B87A; + border-radius: var(--border-radius-sm); + color: var(--text-color); + margin-bottom: var(--space-2); +} + +/* Add special styling for early access badge in the missing loras list */ +.missing-lora-item .early-access-badge { + padding: 2px 6px; + font-size: 0.75em; + margin-top: 4px; + display: inline-flex; +} + +/* Specific styling for the early access warning container in import modal */ +.early-access-warning .warning-icon { + color: #00B87A; + font-size: 1.2em; +} + +.early-access-warning .warning-title { + font-weight: 600; + margin-bottom: 4px; +} + +.early-access-warning .warning-text { + font-size: 0.9em; + line-height: 1.4; +} diff --git a/static/css/components/shared.css b/static/css/components/shared.css index d1ba7c3f..8d436ec8 100644 --- a/static/css/components/shared.css +++ b/static/css/components/shared.css @@ -21,6 +21,59 @@ font-size: 0.9em; } +/* Early Access Badge */ +.early-access-badge { + display: inline-flex; + align-items: center; + background: #00B87A; /* Green for early access */ + color: white; + padding: 4px 8px; + border-radius: var(--border-radius-xs); + font-size: 0.8em; + font-weight: 500; + white-space: nowrap; + flex-shrink: 0; + position: relative; + /* Force hardware acceleration to prevent Chrome scroll issues */ + transform: translateZ(0); + will-change: transform; +} + +.early-access-badge i { + margin-right: 4px; + font-size: 0.9em; +} + +.early-access-info { + display: none; + position: absolute; + top: 100%; + right: 0; + background: var(--card-bg); + border: 1px solid #00B87A; + border-radius: var(--border-radius-xs); + padding: var(--space-1); + margin-top: 4px; + font-size: 0.9em; + color: var(--text-color); + white-space: normal; + word-break: break-all; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + z-index: 100; /* Higher z-index to ensure it's above other elements */ + min-width: 300px; + max-width: 300px; + /* Create a separate layer with hardware acceleration */ + transform: translateZ(0); + /* Use a fixed position to ensure it's in a separate layer from scrollable content */ + position: fixed; + pointer-events: none; /* Don't block mouse events */ +} + +.early-access-badge:hover .early-access-info { + display: block; + pointer-events: auto; /* Allow interaction with the tooltip when visible */ +} + .local-path { display: none; position: absolute; diff --git a/static/css/components/toast.css b/static/css/components/toast.css index 955c13fc..f8b692f8 100644 --- a/static/css/components/toast.css +++ b/static/css/components/toast.css @@ -120,4 +120,63 @@ .tooltip:hover::after { opacity: 1; +} + +/* Toast Container for stacked notifications */ +.toast-container { + position: fixed; + top: 0; + right: 0; + z-index: calc(var(--z-overlay) + 10); + display: flex; + flex-direction: column; + gap: 10px; + padding: 20px; + pointer-events: none; /* Allow clicking through the container */ + width: 400px; + max-width: 100%; +} + +/* Ensure each toast has pointer events */ +.toast-container .toast { + pointer-events: auto; + position: relative; /* Override fixed positioning */ + top: 0 !important; /* Let the container handle positioning */ + right: 0 !important; + margin-bottom: 10px; +} + +/* Add missing warning toast style */ +.toast-warning { + border-left: 4px solid var(--lora-warning); +} + +.toast-warning::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ff9800'%3E%3Cpath d='M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z'/%3E%3C/svg%3E"); +} + +/* Improve toast animation */ +.toast { + transform: translateX(120%); + opacity: 0; + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.toast.show { + transform: translateX(0); + opacity: 1; +} + +/* Responsive adjustments */ +@media (max-width: 480px) { + .toast-container { + width: 100%; + padding: 10px; + } + + .toast { + width: 100%; + max-width: none; + } } \ No newline at end of file diff --git a/static/js/managers/DownloadManager.js b/static/js/managers/DownloadManager.js index 2e3f5fc8..b6ccf89b 100644 --- a/static/js/managers/DownloadManager.js +++ b/static/js/managers/DownloadManager.js @@ -130,7 +130,22 @@ export class DownloadManager { const existsLocally = version.existsLocally; const localPath = version.localPath; - // 更新本地状态指示器为badge样式 + // Check if this is an early access version + const isEarlyAccess = version.availability === 'EarlyAccess'; + + // Create early access badge if needed + let earlyAccessBadge = ''; + if (isEarlyAccess) { + earlyAccessBadge = ` +
+ Early Access +
+ `; + } + + console.log(earlyAccessBadge); + + // Status badge for local models const localStatus = existsLocally ? `
In Library @@ -138,7 +153,9 @@ export class DownloadManager {
` : ''; return ` -
Version preview @@ -150,6 +167,7 @@ export class DownloadManager {
${version.baseModel ? `
${version.baseModel}
` : ''} + ${earlyAccessBadge}
${new Date(version.createdAt).toLocaleDateString()} diff --git a/static/js/managers/ImportManager.js b/static/js/managers/ImportManager.js index 152c1e3a..7e2154dc 100644 --- a/static/js/managers/ImportManager.js +++ b/static/js/managers/ImportManager.js @@ -265,11 +265,6 @@ export class ImportManager { throw new Error('No LoRA information found in this image'); } - // Store generation parameters if available - if (this.recipeData.gen_params) { - console.log('Generation parameters found:', this.recipeData.gen_params); - } - // Find missing LoRAs this.missingLoras = this.recipeData.loras.filter(lora => !lora.existsLocally); @@ -418,6 +413,7 @@ export class ImportManager { lorasList.innerHTML = this.recipeData.loras.map(lora => { const existsLocally = lora.existsLocally; const isDeleted = lora.isDeleted; + const isEarlyAccess = lora.isEarlyAccess; const localPath = lora.localPath || ''; // Create status badge based on LoRA status @@ -437,19 +433,43 @@ export class ImportManager {
`; } + // Early access badge (shown additionally with other badges) + let earlyAccessBadge = ''; + if (isEarlyAccess) { + // Format the early access end date if available + let earlyAccessInfo = 'This LoRA requires early access payment to download.'; + if (lora.earlyAccessEndsAt) { + try { + const endDate = new Date(lora.earlyAccessEndsAt); + const formattedDate = endDate.toLocaleDateString(); + earlyAccessInfo += ` Early access ends on ${formattedDate}.`; + } catch (e) { + console.warn('Failed to format early access date', e); + } + } + + earlyAccessBadge = `
+ Early Access +
${earlyAccessInfo} Verify that you have purchased early access before downloading.
+
`; + } + // Format size if available const sizeDisplay = lora.size ? `
${this.formatFileSize(lora.size)}
` : ''; return ` -
+
LoRA preview

${lora.name}

-
${statusBadge}
+
+ ${statusBadge} + ${earlyAccessBadge} +
${lora.version ? `
${lora.version}
` : ''}
@@ -463,6 +483,41 @@ export class ImportManager { }).join(''); } + // Check for early access loras and show warning if any exist + const earlyAccessLoras = this.recipeData.loras.filter(lora => + lora.isEarlyAccess && !lora.existsLocally && !lora.isDeleted); + if (earlyAccessLoras.length > 0) { + // Show a warning about early access loras + const warningMessage = ` +
+
+
+
${earlyAccessLoras.length} LoRA(s) require Early Access
+
+ These LoRAs require a payment to access. Download will fail if you haven't purchased access. + You may need to log in to your Civitai account in browser settings. +
+
+
+ `; + + // Show the warning message + const buttonsContainer = document.querySelector('#detailsStep .modal-actions'); + if (buttonsContainer) { + // Remove existing warning if any + const existingWarning = document.getElementById('earlyAccessWarning'); + if (existingWarning) { + existingWarning.remove(); + } + + // Add new warning + const warningContainer = document.createElement('div'); + warningContainer.id = 'earlyAccessWarning'; + warningContainer.innerHTML = warningMessage; + buttonsContainer.parentNode.insertBefore(warningContainer, buttonsContainer); + } + } + // Update Next button state based on missing LoRAs this.updateNextButtonState(); } @@ -581,6 +636,40 @@ export class ImportManager { // Update missing LoRAs list to exclude deleted LoRAs this.missingLoras = this.recipeData.loras.filter(lora => !lora.existsLocally && !lora.isDeleted); + + // Check for early access loras and show warning if any exist + const earlyAccessLoras = this.missingLoras.filter(lora => lora.isEarlyAccess); + if (earlyAccessLoras.length > 0) { + // Show a warning about early access loras + const warningMessage = ` +
+
+
+
${earlyAccessLoras.length} LoRA(s) require Early Access
+
+ These LoRAs require a payment to access. Download will fail if you haven't purchased access. + You may need to log in to your Civitai account in browser settings. +
+
+
+ `; + + // Show the warning message + const buttonsContainer = document.querySelector('#detailsStep .modal-actions'); + if (buttonsContainer) { + // Remove existing warning if any + const existingWarning = document.getElementById('earlyAccessWarning'); + if (existingWarning) { + existingWarning.remove(); + } + + // Add new warning + const warningContainer = document.createElement('div'); + warningContainer.id = 'earlyAccessWarning'; + warningContainer.innerHTML = warningMessage; + buttonsContainer.parentNode.insertBefore(warningContainer, buttonsContainer); + } + } // If we have downloadable missing LoRAs, go to location step if (this.missingLoras.length > 0) { @@ -646,12 +735,22 @@ export class ImportManager { missingLorasList.innerHTML = this.downloadableLoRAs.map(lora => { const sizeDisplay = lora.size ? this.formatFileSize(lora.size) : 'Unknown size'; const baseModel = lora.baseModel ? `${lora.baseModel}` : ''; + const isEarlyAccess = lora.isEarlyAccess; + + // Early access badge + let earlyAccessBadge = ''; + if (isEarlyAccess) { + earlyAccessBadge = ` + Early Access + `; + } return ` -
+
${lora.name}
${baseModel} + ${earlyAccessBadge}
${sizeDisplay}
@@ -822,6 +921,8 @@ export class ImportManager { const updateProgress = this.loadingManager.showDownloadProgress(this.downloadableLoRAs.length); let completedDownloads = 0; + let failedDownloads = 0; + let earlyAccessFailures = 0; let currentLoraProgress = 0; // Set up progress tracking for current download @@ -832,7 +933,7 @@ export class ImportManager { currentLoraProgress = data.progress; // Get current LoRA name - const currentLora = this.downloadableLoRAs[completedDownloads]; + const currentLora = this.downloadableLoRAs[completedDownloads + failedDownloads]; const loraName = currentLora ? currentLora.name : ''; // Update progress display @@ -841,19 +942,19 @@ export class ImportManager { // Add more detailed status messages based on progress if (currentLoraProgress < 3) { this.loadingManager.setStatus( - `Preparing download for LoRA ${completedDownloads+1}/${this.downloadableLoRAs.length}` + `Preparing download for LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}` ); } else if (currentLoraProgress === 3) { this.loadingManager.setStatus( - `Downloaded preview for LoRA ${completedDownloads+1}/${this.downloadableLoRAs.length}` + `Downloaded preview for LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}` ); } else if (currentLoraProgress > 3 && currentLoraProgress < 100) { this.loadingManager.setStatus( - `Downloading LoRA ${completedDownloads+1}/${this.downloadableLoRAs.length}` + `Downloading LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}` ); } else { this.loadingManager.setStatus( - `Finalizing LoRA ${completedDownloads+1}/${this.downloadableLoRAs.length}` + `Finalizing LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}` ); } } @@ -884,6 +985,18 @@ export class ImportManager { if (!response.ok) { const errorText = await response.text(); console.error(`Failed to download LoRA ${lora.name}: ${errorText}`); + + // Check if this is an early access error (status 401 is the key indicator) + if (response.status === 401 || + (errorText.toLowerCase().includes('early access') || + errorText.toLowerCase().includes('purchase'))) { + earlyAccessFailures++; + this.loadingManager.setStatus( + `Failed to download ${lora.name}: Early Access required` + ); + } + + failedDownloads++; // Continue with next download } else { completedDownloads++; @@ -891,7 +1004,7 @@ export class ImportManager { // Update progress to show completion of current LoRA updateProgress(100, completedDownloads, ''); - if (completedDownloads < this.downloadableLoRAs.length) { + if (completedDownloads + failedDownloads < this.downloadableLoRAs.length) { this.loadingManager.setStatus( `Completed ${completedDownloads}/${this.downloadableLoRAs.length} LoRAs. Starting next download...` ); @@ -899,6 +1012,7 @@ export class ImportManager { } } catch (downloadError) { console.error(`Error downloading LoRA ${lora.name}:`, downloadError); + failedDownloads++; // Continue with next download } } @@ -906,11 +1020,18 @@ export class ImportManager { // Close WebSocket ws.close(); - // Show final completion message - if (completedDownloads === this.downloadableLoRAs.length) { + // Show appropriate completion message based on results + if (failedDownloads === 0) { showToast(`All ${completedDownloads} LoRAs downloaded successfully`, 'success'); } else { - showToast(`Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs`, 'warning'); + if (earlyAccessFailures > 0) { + showToast( + `Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs. ${earlyAccessFailures} failed due to Early Access restrictions.`, + 'error' + ); + } else { + showToast(`Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs`, 'error'); + } } } diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index 4d2a55aa..9c2a6dd1 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -6,11 +6,54 @@ export function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; - document.body.append(toast); + + // Get or create toast container + let toastContainer = document.querySelector('.toast-container'); + if (!toastContainer) { + toastContainer = document.createElement('div'); + toastContainer.className = 'toast-container'; + document.body.append(toastContainer); + } + + toastContainer.append(toast); + + // Calculate vertical position for stacked toasts + const existingToasts = Array.from(toastContainer.querySelectorAll('.toast')); + const toastIndex = existingToasts.indexOf(toast); + const topOffset = 20; // Base offset from top + const spacing = 10; // Space between toasts + + // Set position based on existing toasts + toast.style.top = `${topOffset + (toastIndex * (toast.offsetHeight || 60 + spacing))}px`; requestAnimationFrame(() => { toast.classList.add('show'); - setTimeout(() => toast.remove(), 2300); + + // Set timeout based on type + let timeout = 2000; // Default (info) + if (type === 'warning' || type === 'error') { + timeout = 5000; + } + + setTimeout(() => { + toast.classList.remove('show'); + toast.addEventListener('transitionend', () => { + toast.remove(); + + // Reposition remaining toasts + if (toastContainer) { + const remainingToasts = Array.from(toastContainer.querySelectorAll('.toast')); + remainingToasts.forEach((t, index) => { + t.style.top = `${topOffset + (index * (t.offsetHeight || 60 + spacing))}px`; + }); + + // Remove container if empty + if (remainingToasts.length === 0) { + toastContainer.remove(); + } + } + }); + }, timeout); }); }