Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import logging
import asyncio
import aiohttp
import folder_paths
from aiohttp import web
from server import PromptServer
Expand Down Expand Up @@ -559,6 +560,53 @@ async def cancel_download(request):
return web.json_response({"error": str(e)}, status=500)


@PromptServer.instance.routes.post(f"/{API_PREFIX}/server_download/filesize")
async def get_filesize(request):
"""Get file size from URL via HEAD request before downloading"""
try:
data = await request.json()
url = data.get("url", "").strip()

if not url:
return web.json_response({"error": "No URL provided"}, status=400)

total_size = 0
timeout = aiohttp.ClientTimeout(total=15)

async with aiohttp.ClientSession(timeout=timeout) as session:
# Try HEAD request first
try:
async with session.head(url, allow_redirects=True) as response:
if response.status == 200:
total_size = int(response.headers.get('content-length', 0))
except Exception as e:
logging.warning(f"[ComfyUI-Downloader] HEAD request failed for filesize: {e}")

# Fallback: GET with Range header
if total_size == 0:
try:
headers = {'Range': 'bytes=0-0'}
async with session.get(url, headers=headers, allow_redirects=True) as response:
if response.status in [200, 206]:
content_range = response.headers.get('content-range', '')
if content_range:
parts = content_range.split('/')
if len(parts) == 2 and parts[1] != '*':
total_size = int(parts[1])
if total_size == 0:
total_size = int(response.headers.get('content-length', 0))
except Exception as e:
logging.warning(f"[ComfyUI-Downloader] GET Range request failed for filesize: {e}")

return web.json_response({
"success": True,
"size": total_size
})
except Exception as e:
logging.error(f"[ComfyUI-Downloader] Error getting filesize: {e}")
return web.json_response({"error": str(e)}, status=500)


@PromptServer.instance.routes.get(f"/{API_PREFIX}/supported_extensions")
async def get_supported_extensions(request):
"""Get supported model file extensions from folder_paths"""
Expand Down
101 changes: 96 additions & 5 deletions web/js/UI.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,44 @@ export class DownloaderUI {
}
}

/**
* Format bytes to human-readable size
*/
formatFileSize(bytes) {
if (!bytes || bytes === 0) return null;
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0;
let size = bytes;
while (size >= 1024 && i < units.length - 1) {
size /= 1024;
i++;
}
return `${size.toFixed(i > 0 ? 2 : 0)} ${units[i]}`;
}

/**
* Fetch file size from URL via HEAD request
*/
async fetchFileSize(url) {
if (!url) return null;
try {
const response = await api.fetchApi(`/${API_PREFIX}/server_download/filesize`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url })
});
if (!response.ok) return null;
const data = await response.json();
if (data.success && data.size > 0) {
return data.size;
}
return null;
} catch (error) {
console.warn("[DownloaderUI] Error fetching file size:", error);
return null;
}
}

/**
* Start a server download
*/
Expand Down Expand Up @@ -435,7 +473,8 @@ export class DownloaderUI {
placeholder="Filename (auto-detected)"
style="flex: 1; min-width: 200px; padding: 5px;"
/>
<button
<span id="downloader-free-filesize" style="font-size: 0.85em; color: #aaa; min-width: 80px; text-align: right;"></span>
<button
id="downloader-free-download-btn"
style="padding: 6px 20px; cursor: pointer; background-color: #4CAF50; color: white; border: none; border-radius: 3px; font-weight: bold;"
>
Expand Down Expand Up @@ -466,10 +505,12 @@ export class DownloaderUI {
this.scanWorkflowForModels();
});

// Auto-extract filename from URL
// Auto-extract filename from URL and fetch file size
const urlInput = modal.querySelector("#downloader-free-url");
const filenameInput = modal.querySelector("#downloader-free-filename");

const fileSizeSpan = modal.querySelector("#downloader-free-filesize");
let fileSizeTimer = null;

urlInput.addEventListener("input", () => {
const url = urlInput.value.trim();
if (url) {
Expand All @@ -488,6 +529,18 @@ export class DownloaderUI {
filenameInput.value = decodeURIComponent(lastPart.split('?')[0]);
}
}

// Debounce file size fetch (500ms after user stops typing)
if (fileSizeTimer) clearTimeout(fileSizeTimer);
fileSizeSpan.textContent = '...';
fileSizeTimer = setTimeout(async () => {
const size = await this.fetchFileSize(url);
const formatted = this.formatFileSize(size);
fileSizeSpan.textContent = formatted || '';
}, 500);
} else {
if (fileSizeTimer) clearTimeout(fileSizeTimer);
fileSizeSpan.textContent = '';
}
});

Expand Down Expand Up @@ -812,8 +865,13 @@ export class DownloaderUI {
data-model-index="${index}"
style="flex: 2; min-width: 300px; padding: 5px;"
/>
<button
class="downloader-download-btn"
<span
class="downloader-filesize"
data-model-index="${index}"
style="font-size: 0.85em; color: #aaa; min-width: 80px; text-align: right;"
></span>
<button
class="downloader-download-btn"
data-model-index="${index}"
data-download-id=""
style="padding: 6px 20px; cursor: pointer; background-color: #4CAF50; color: white; border: none; border-radius: 3px;"
Expand Down Expand Up @@ -885,6 +943,39 @@ export class DownloaderUI {
}
});
});

// Fetch file sizes for models that already have URLs, and listen for URL changes
const urlInputs = listContainer.querySelectorAll('.downloader-url-input');
urlInputs.forEach((input) => {
const modelIndex = input.dataset.modelIndex;
const sizeSpan = listContainer.querySelector(`.downloader-filesize[data-model-index="${modelIndex}"]`);
let sizeTimer = null;

// Fetch size if URL is already set
const existingUrl = input.value.trim();
if (existingUrl && sizeSpan) {
sizeSpan.textContent = '...';
this.fetchFileSize(existingUrl).then(size => {
sizeSpan.textContent = this.formatFileSize(size) || '';
});
}

// Fetch size when URL changes
input.addEventListener("input", () => {
if (!sizeSpan) return;
const url = input.value.trim();
if (sizeTimer) clearTimeout(sizeTimer);
if (url) {
sizeSpan.textContent = '...';
sizeTimer = setTimeout(async () => {
const size = await this.fetchFileSize(url);
sizeSpan.textContent = this.formatFileSize(size) || '';
}, 500);
} else {
sizeSpan.textContent = '';
}
});
});
}

/**
Expand Down