diff --git a/py/routes/base_model_routes.py b/py/routes/base_model_routes.py index e304bbab..d4522871 100644 --- a/py/routes/base_model_routes.py +++ b/py/routes/base_model_routes.py @@ -852,7 +852,7 @@ class BaseModelRoutes(ABC): current_dir = os.path.dirname(file_path) # Skip if already in correct location - if os.path.normpath(current_dir) == os.path.normpath(target_dir): + if current_dir.replace(os.sep, '/') == target_dir.replace(os.sep, '/'): skipped_count += 1 processed += 1 continue @@ -914,6 +914,25 @@ class BaseModelRoutes(ABC): await asyncio.sleep(0.1) # Send completion message + await ws_manager.broadcast({ + 'type': 'auto_organize_progress', + 'status': 'cleaning', + 'total': total_models, + 'processed': processed, + 'success': success_count, + 'failures': failure_count, + 'skipped': skipped_count, + 'message': 'Cleaning up empty directories...' + }) + + # Clean up empty directories after organizing + from ..utils.utils import remove_empty_dirs + cleanup_counts = {} + for root in model_roots: + removed = remove_empty_dirs(root) + cleanup_counts[root] = removed + + # Send cleanup completed message await ws_manager.broadcast({ 'type': 'auto_organize_progress', 'status': 'completed', @@ -921,7 +940,8 @@ class BaseModelRoutes(ABC): 'processed': processed, 'success': success_count, 'failures': failure_count, - 'skipped': skipped_count + 'skipped': skipped_count, + 'cleanup': cleanup_counts }) # Prepare response with limited details @@ -933,7 +953,8 @@ class BaseModelRoutes(ABC): 'success': success_count, 'skipped': skipped_count, 'failures': failure_count, - 'organization_type': 'flat' if is_flat_structure else 'structured' + 'organization_type': 'flat' if is_flat_structure else 'structured', + 'cleaned_dirs': cleanup_counts } } diff --git a/py/utils/utils.py b/py/utils/utils.py index 7067d262..fe97c5da 100644 --- a/py/utils/utils.py +++ b/py/utils/utils.py @@ -133,54 +133,87 @@ def calculate_recipe_fingerprint(loras): return fingerprint def calculate_relative_path_for_model(model_data: Dict) -> str: - """Calculate relative path for existing model using template from settings - - Args: - model_data: Model data from scanner cache - - Returns: - Relative path string (empty string for flat structure) - """ - # Get path template from settings, default to '{base_model}/{first_tag}' - path_template = settings.get('download_path_template', '{base_model}/{first_tag}') - - # If template is empty, return empty path (flat structure) - if not path_template: - return '' - - # Get base model name from model metadata - civitai_data = model_data.get('civitai', {}) - - # For CivitAI models, prefer civitai data; for non-CivitAI models, use model_data directly - if civitai_data: - base_model = civitai_data.get('baseModel', '') - else: - # Fallback to model_data fields for non-CivitAI models - base_model = model_data.get('base_model', '') + """Calculate relative path for existing model using template from settings - model_tags = model_data.get('tags', []) - - # Apply mapping if available - base_model_mappings = settings.get('base_model_path_mappings', {}) - mapped_base_model = base_model_mappings.get(base_model, base_model) - - # Find the first Civitai model tag that exists in model_tags - first_tag = '' - for civitai_tag in CIVITAI_MODEL_TAGS: - if civitai_tag in model_tags: - first_tag = civitai_tag - break - - # If no Civitai model tag found, fallback to first tag - if not first_tag and model_tags: - first_tag = model_tags[0] + Args: + model_data: Model data from scanner cache - if not first_tag: - first_tag = 'no tags' # Default if no tags available + Returns: + Relative path string (empty string for flat structure) + """ + # Get path template from settings, default to '{base_model}/{first_tag}' + path_template = settings.get('download_path_template', '{base_model}/{first_tag}') + + # If template is empty, return empty path (flat structure) + if not path_template: + return '' + + # Get base model name from model metadata + civitai_data = model_data.get('civitai', {}) + + # For CivitAI models, prefer civitai data only if 'id' exists; for non-CivitAI models, use model_data directly + if civitai_data and civitai_data.get('id') is not None: + base_model = civitai_data.get('baseModel', '') + else: + # Fallback to model_data fields for non-CivitAI models + base_model = model_data.get('base_model', '') + + model_tags = model_data.get('tags', []) + + # Apply mapping if available + base_model_mappings = settings.get('base_model_path_mappings', {}) + mapped_base_model = base_model_mappings.get(base_model, base_model) + + # Find the first Civitai model tag that exists in model_tags + first_tag = '' + for civitai_tag in CIVITAI_MODEL_TAGS: + if civitai_tag in model_tags: + first_tag = civitai_tag + break + + # If no Civitai model tag found, fallback to first tag + if not first_tag and model_tags: + first_tag = model_tags[0] + + if not first_tag: + first_tag = 'no tags' # Default if no tags available + + # Format the template with available data + formatted_path = path_template + formatted_path = formatted_path.replace('{base_model}', mapped_base_model) + formatted_path = formatted_path.replace('{first_tag}', first_tag) + + return formatted_path + +def remove_empty_dirs(path): + """Recursively remove empty directories starting from the given path. + + Args: + path (str): Root directory to start cleaning from - # Format the template with available data - formatted_path = path_template - formatted_path = formatted_path.replace('{base_model}', mapped_base_model) - formatted_path = formatted_path.replace('{first_tag}', first_tag) + Returns: + int: Number of empty directories removed + """ + removed_count = 0 + + if not os.path.isdir(path): + return removed_count - return formatted_path + # List all files in directory + files = os.listdir(path) + + # Process all subdirectories first + for file in files: + full_path = os.path.join(path, file) + if os.path.isdir(full_path): + removed_count += remove_empty_dirs(full_path) + + # Check if directory is now empty (after processing subdirectories) + if not os.listdir(path): + try: + os.rmdir(path) + removed_count += 1 + except OSError: + pass + + return removed_count