diff --git a/README.md b/README.md
index de0d045..603538e 100644
--- a/README.md
+++ b/README.md
@@ -100,6 +100,18 @@ https://github.com/BlafKing/sd-civitai-browser-plus/assets/9644716/44c5c7a0-4854
# Changelog 📋
+
v3.6.0
+
+* Feature: Ability to set custom default sub folders.
+* Feature: Automatically fetches latest available Basemodels.
+* Bug fix: Lag fixed on SD-WebUI Forge/Gradio 4+, thanks to @BenjaminSymons and @channelcat!
+* Bug fix: Version ID has been suffixed to filename to avoid detecting different models as installed.
+* Bug fix: Filename comparing to detect installed models is no longer case sensitive.
+* Bug fix: CivitAI button on model cards correctly works again.
+* Bug fix: Correct image path now gets used when local images in HTML are used.
+* Bug fix: Any trailing or leading spaces get removed from model/version names now.
+
+---
v3.5.4
* Feature: Added support for DoRA (Requires SD-WebUI v1.9)
diff --git a/aria2/lin/aria2 b/aria2/lin/aria2
old mode 100644
new mode 100755
diff --git a/javascript/civitai-html.js b/javascript/civitai-html.js
index fe5111d..49b515d 100644
--- a/javascript/civitai-html.js
+++ b/javascript/civitai-html.js
@@ -367,7 +367,7 @@ function updateBackToTopVisibility(entries) {
// Create the accordion dropdown inside the settings tab
function createAccordion(containerDiv, subfolders, name, id_name) {
- if (containerDiv == null || subfolders.length == 0) {
+ if (containerDiv == null) {
return;
}
var accordionContainer = document.createElement('div');
@@ -380,12 +380,15 @@ function createAccordion(containerDiv, subfolders, name, id_name) {
accordionDiv.style.display = (accordionDiv.style.display === 'none') ? 'block' : 'none';
toggleButton.lastChild.style.transform = accordionDiv.style.display === 'none' ? 'rotate(90deg)' : 'rotate(0)';
};
-
+
accordionContainer.appendChild(toggleButton);
var accordionDiv = document.createElement('div');
accordionDiv.classList.add('accordion');
- accordionDiv.append(...subfolders);
- accordionDiv.style.display = 'none';
+ if (subfolders && subfolders.length > 0) {
+ accordionDiv.append(...subfolders);
+ }
+
+ accordionDiv.style.display = 'none'; // Initially hidden
accordionContainer.appendChild(accordionDiv);
containerDiv.appendChild(accordionContainer);
}
@@ -486,14 +489,15 @@ function addOnClickToButtons() {
});
}
-function modelInfoPopUp(modelName, content_type) {
+function modelInfoPopUp(modelName=null, content_type=null, no_message=false) {
const sendToBrowserElement = gradioApp().querySelector('#setting_civitai_send_to_browser input');
let sendToBrowser = false;
if (sendToBrowserElement) {
sendToBrowser = sendToBrowserElement.checked;
}
-
- select_model(modelName, null, true, content_type, sendToBrowser);
+ if (modelName) {
+ select_model(modelName, null, true, content_type, sendToBrowser);
+ }
if (sendToBrowser) {
const tabNav = document.querySelector('.tab-nav');
const buttons = tabNav.querySelectorAll('button');
@@ -558,19 +562,24 @@ function modelInfoPopUp(modelName, content_type) {
zIndex: '1001'
});
inner.classList.add('civitai-overlay-inner');
-
- const modelInfo = createElementWithStyle('div', {
- fontSize: '24px',
- color: 'white',
- fontFamily: 'var(--font)'
- });
- modelInfo.classList.add('civitai-overlay-text');
- modelInfo.textContent = 'Loading model info, please wait!';
+
+ var modelInfo;
+ if (!no_message) {
+ modelInfo = createElementWithStyle('div', {
+ fontSize: '24px',
+ color: 'white',
+ fontFamily: 'var(--font)'
+ });
+ modelInfo.classList.add('civitai-overlay-text');
+ modelInfo.textContent = 'Loading model info, please wait!';
+ }
document.body.style.overflow = 'hidden';
document.body.appendChild(overlay);
overlay.append(closeButton, inner);
- inner.appendChild(modelInfo);
+ if (!no_message) {
+ inner.appendChild(modelInfo);
+ }
setDynamicWidth(inner);
window.addEventListener('resize', () => setDynamicWidth(inner));
@@ -601,11 +610,15 @@ function handleKeyPress(event) {
}
function inputHTMLPreviewContent(html_input) {
+ //console.log("Last 500 characters of HTML input:", html_input.slice(-500));
var inner = document.querySelector('.civitai-overlay-inner')
let startIndex = html_input.indexOf("'value': '");
if (startIndex !== -1) {
startIndex += "'value': '".length;
- const endIndex = html_input.indexOf("', 'type': None,", startIndex);
+ let endIndex = html_input.indexOf(", 'placeholder'", startIndex);
+ if (endIndex === -1) {
+ endIndex = html_input.indexOf("', 'type': None,", startIndex);
+ }
if (endIndex !== -1) {
let extractedText = html_input.substring(startIndex, endIndex);
var modelIdNotFound = extractedText.includes(">Model ID not found.
The");
@@ -626,6 +639,9 @@ function inputHTMLPreviewContent(html_input) {
modelInfo.innerHTML = extractedText;
inner.appendChild(modelInfo);
+ inner.style.top = 'unset';
+ inner.style.transform = 'translate(-50%, 0%)'
+
setDescriptionToggle();
}
}
@@ -963,6 +979,188 @@ function setDescriptionToggle() {
}
}
+function submitNewSubfolder(subfolderId, subfolderValue) {
+ const output = gradioApp().querySelector('#create_subfolder textarea');
+ output.value = subfolderId + ".add." + subfolderValue;
+ updateInput(output)
+}
+
+function deleteSubfolder(subfolderId) {
+ const output = gradioApp().querySelector('#create_subfolder textarea');
+ output.value = subfolderId + ".delete.";
+ updateInput(output)
+}
+
+function createCustomSubfolder(subfolderDiv, subfolderId, subfolderValue) {
+ if (typeof subfolderId === 'undefined') {
+ console.error('subfolderId is required.');
+ return;
+ }
+
+ const newContainerDiv = document.createElement("div");
+ newContainerDiv.classList.add("svelte-1f354aw", "container", "CivitDefaultSubfolder");
+ newContainerDiv.style.display = "flex";
+ newContainerDiv.style.alignItems = "center";
+
+ newContainerDiv.setAttribute("subfolder_id", subfolderId);
+
+ const newTextArea = document.createElement("textarea");
+ newTextArea.setAttribute("data-testid", "textbox");
+ newTextArea.classList.add("scroll-hide", "svelte-1f354aw");
+ newTextArea.setAttribute("dir", "ltr");
+ newTextArea.setAttribute("placeholder", "{BASEMODEL}/{NSFW}/{AUTHOR}/{MODELNAME}/{MODELID}/{VERSIONNAME}/{VERSIONID}");
+ newTextArea.setAttribute("rows", "1");
+ newTextArea.style.overflowY = "scroll";
+ newTextArea.style.height = "42px";
+ newTextArea.style.flex = "1";
+
+ if (typeof subfolderValue !== 'undefined') {
+ newTextArea.value = subfolderValue;
+ }
+
+ newTextArea.addEventListener("keydown", function(event) {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ submitNewSubfolder(subfolderId, newTextArea.value);
+ }
+ });
+
+ const saveButton = document.createElement("button");
+ saveButton.textContent = "Save";
+ saveButton.classList.add("save-button", "lg", "primary", "gradio-button", "svelte-cmf5ev");
+ saveButton.setAttribute("title", "")
+ saveButton.style.marginRight = "10px";
+ saveButton.addEventListener("click", function() {
+ submitNewSubfolder(subfolderId, newTextArea.value);
+ });
+
+ const deleteButton = document.createElement("button");
+ deleteButton.textContent = "Delete";
+ deleteButton.classList.add("delete-button", "lg", "primary", "gradio-button", "svelte-cmf5ev");
+ deleteButton.style.marginRight = "10px";
+ deleteButton.addEventListener("click", function() {
+ deleteSubfolder(subfolderId);
+ newContainerDiv.remove();
+ });
+
+ newContainerDiv.appendChild(deleteButton);
+ newContainerDiv.appendChild(saveButton);
+ newContainerDiv.appendChild(newTextArea);
+
+ subfolderDiv.appendChild(newContainerDiv);
+}
+
+function insertExistingSubfolders(input) {
+ const subfolder = document.querySelectorAll("civitai-custom-subfolder-div");
+ createCustomSubfolder(subfolder, Id, Value);
+}
+
+function createSubfolderButton() {
+ const subfolderParent = document.getElementById("create-sub-accordion");
+ const subfolderDiv = subfolderParent.querySelector(".accordion");
+
+ const subfolder = document.createElement("div");
+ subfolder.classList.add("flex-column-layout", "civitai-custom-subfolder-div");
+
+ const customSubfoldersList = document.querySelector('#custom_subfolders_list');
+ const textarea = customSubfoldersList.querySelector('textarea');
+ const subfoldersString = textarea ? textarea.value : '';
+
+ const subfoldersArray = subfoldersString.split('␞␞');
+
+ for (let i = 0; i < subfoldersArray.length; i += 2) {
+ const subfolderId = subfoldersArray[i];
+ const subfolderValue = subfoldersArray[i + 1];
+
+ createCustomSubfolder(subfolder, subfolderId, subfolderValue);
+ }
+
+ const buttonContainer = document.createElement("div");
+ buttonContainer.classList.add("sub-folder-button-container");
+ buttonContainer.style.display = "flex";
+ buttonContainer.style.gap = "10px";
+
+ const optionsDiv = document.createElement("div");
+ optionsDiv.classList.add("placeholder-options-container");
+ optionsDiv.style.display = "flex";
+ optionsDiv.style.justifyContent = "center";
+
+ const plusButton = document.createElement("button");
+ plusButton.textContent = "Create new default sub folder entry";
+ plusButton.classList.add("plus-button", "lg", "primary", "gradio-button", "svelte-cmf5ev");
+ plusButton.style.marginTop = "10px";
+ plusButton.addEventListener("click", function() {
+ const existingSubfolderDivs = document.querySelectorAll("div.CivitDefaultSubfolder");
+ let highestSubfolderId = 0;
+
+ existingSubfolderDivs.forEach((div) => {
+ const subfolderId = parseInt(div.getAttribute('subfolder_id'), 10);
+ if (subfolderId > highestSubfolderId) {
+ highestSubfolderId = subfolderId;
+ }
+ });
+
+ const newSubfolderId = highestSubfolderId + 1;
+ createCustomSubfolder(subfolder, newSubfolderId);
+ });
+
+ // Create the guide button
+ const guide_html = `
+
+
These options can be used to add sub-folder options.
+
There are a few placeholders you can use which will be automatically replaced with the selected model's information:
+
+
{BASEMODEL}: Replaced with the base model name.
+
{NSFW}: Creates a folder named "nsfw", folder will not be created if model is sfw.
+
{AUTHOR}: Replaced with the author of the model.
+
{MODELNAME}: Replaced with the name of the model.
+
{MODELID}: Replaced with the unique ID of the model.
+
{VERSIONNAME}: Replaced with the version name of the model.
+
{VERSIONID}: Replaced with the unique ID of the model version.
+
+
For example, if I select a model called 'ReV Animated'
+
and it's version is called 'V2 Rebirth' then the following:
+
{MODELNAME}/{VERSIONNAME}
+
Will be replaced with:
+
ReV Animated/V2 Rebirth
+
+
Always use '/' as a seperator, regardless of your OS
+
+ `;
+ const guideButton = document.createElement("button");
+ guideButton.textContent = "Guide";
+ guideButton.classList.add("guide-button", "lg", "primary", "gradio-button", "svelte-cmf5ev");
+ guideButton.style.marginTop = "10px";
+ guideButton.addEventListener("click", function() {
+ modelInfoPopUp(null, null, true);
+ insertGuideMessage(guide_html);
+ });
+
+ const optionsText = document.createElement("span");
+ optionsText.textContent = "Available options: {BASEMODEL} {NSFW} {AUTHOR} {MODELNAME} {MODELID} {VERSIONNAME} {VERSIONID}";
+
+ // Append buttons to the container
+ buttonContainer.appendChild(guideButton);
+ buttonContainer.appendChild(plusButton);
+
+ optionsDiv.appendChild(optionsText);
+
+ subfolder.insertBefore(optionsDiv, subfolder.firstChild);
+ subfolder.insertBefore(buttonContainer, subfolder.firstChild);
+ subfolderDiv.appendChild(subfolder);
+}
+
+function insertGuideMessage(html_input) {
+ const overlayContainer = document.querySelector(".civitai-overlay-inner");
+ if (overlayContainer) {
+ const guideHtml = document.createElement('div');
+ guideHtml.innerHTML = html_input;
+ while (guideHtml.firstChild) {
+ overlayContainer.appendChild(guideHtml.firstChild);
+ }
+ }
+}
+
// Runs all functions when the page is fully loaded
function onPageLoad() {
updateSVGIcons();
@@ -975,9 +1173,8 @@ function onPageLoad() {
let div = subfolderDiv || downloadDiv;
let subfolders = div.querySelectorAll("[id$='subfolder']");
createAccordion(div, subfolders, "Default sub folders", 'default-sub-accordion');
-
- subfolders = div.querySelectorAll("[id^='setting_insert_sub']");
- createAccordion(div, subfolders, "Insert sub folder options", 'insert-sub-accordion');
+ createAccordion(div, null, "Create sub folder entries", 'create-sub-accordion');
+ createSubfolderButton();
}
if (subfolderDiv || settingsDiv) {
diff --git a/scripts/civitai_api.py b/scripts/civitai_api.py
index 7143584..7ab2fcb 100644
--- a/scripts/civitai_api.py
+++ b/scripts/civitai_api.py
@@ -19,6 +19,7 @@ from html import escape
from scripts.civitai_global import print, debug_print
import scripts.civitai_global as gl
import scripts.civitai_download as _download
+import scripts.civitai_file_manage as _file
gl.init()
@@ -188,7 +189,7 @@ def model_list_html(json_data):
for folder in model_folders:
for root, dirs, files in os.walk(folder, followlinks=True):
for file in files:
- existing_files.add(file)
+ existing_files.add(file.lower())
if file.endswith('.json'):
json_path = os.path.join(root, file)
with open(json_path, 'r', encoding="utf-8") as f:
@@ -244,10 +245,13 @@ def model_list_html(json_data):
for version in reversed(item['modelVersions']):
for file in version.get('files', []):
- file_name = file['name']
+ file_name = os.path.splitext(file['name'])[0]
+ file_extension = os.path.splitext(file['name'])[1]
+ file_name = f"{file_name}_{file['id']}{file_extension}"
file_sha256 = file.get('hashes', {}).get('SHA256', "").upper()
- name_match = file_name in existing_files
+ #filename_check
+ name_match = file_name.lower() in existing_files
sha256_match = file_sha256 in existing_files_sha256
if name_match or sha256_match:
if version == item['modelVersions'][0]:
@@ -500,7 +504,9 @@ def update_model_versions(model_id, json_input=None):
versions_dict[version['name']].append(item["name"])
for version_file in version['files']:
file_sha256 = version_file.get('hashes', {}).get('SHA256', "").upper()
- version_filename = version_file['name']
+ version_filename = os.path.splitext(version_file['name'])[0]
+ version_extension = os.path.splitext(version_file['name'])[1]
+ version_filename = f"{version_filename}_{version_file['id']}{version_extension}"
version_files.add((version['name'], version_filename, file_sha256))
for root, _, files in os.walk(model_folder, followlinks=True):
@@ -520,8 +526,9 @@ def update_model_versions(model_id, json_input=None):
except Exception as e:
print(f"failed to read: \"{file}\": {e}")
+ #filename_check
for version_name, version_filename, _ in version_files:
- if file == version_filename:
+ if file.lower() == version_filename.lower():
installed_versions.add(version_name)
break
@@ -542,6 +549,7 @@ def cleaned_name(file_name):
name, extension = os.path.splitext(file_name)
clean_name = re.sub(illegal_chars_pattern, '', name)
+ clean_name = re.sub(r'\s+', ' ', clean_name.strip())
return f"{clean_name}{extension}"
@@ -616,6 +624,7 @@ def update_model_info(model_string=None, model_version=None, only_html=False, in
model_folder = os.path.join(contenttype_folder(content_type, desc))
model_uploader = None
uploader_avatar = None
+ nsfw = item['nsfw']
creator = item.get('creator', None)
if creator:
model_uploader = creator.get('username', None)
@@ -639,6 +648,8 @@ def update_model_info(model_string=None, model_version=None, only_html=False, in
model_availability = selected_version.get('availability', 'Unknown')
model_date_published = selected_version.get('publishedAt', '').split('T')[0]
+ version_name = selected_version['name']
+ version_id = selected_version['id']
if selected_version['trainedWords']:
output_training = ",".join(selected_version['trainedWords'])
@@ -651,7 +662,9 @@ def update_model_info(model_string=None, model_version=None, only_html=False, in
dl_dict[file['name']] = file['downloadUrl']
if not model_filename:
- model_filename = file['name']
+ model_filename = os.path.splitext(file['name'])[0]
+ model_extension = os.path.splitext(file['name'])[1]
+ model_filename = f"{model_filename}_{file['id']}{model_extension}"
dl_url = file['downloadUrl']
gl.json_info = item
sha256_value = file['hashes'].get('SHA256', 'Unknown')
@@ -671,7 +684,9 @@ def update_model_info(model_string=None, model_version=None, only_html=False, in
})
if is_primary:
default_file = unique_file_name
- model_filename = file['name']
+ model_filename = os.path.splitext(file['name'])[0]
+ model_extension = os.path.splitext(file['name'])[1]
+ model_filename = f"{model_filename}_{file['id']}{model_extension}"
dl_url = file['downloadUrl']
gl.json_info = item
sha256_value = file['hashes'].get('SHA256', 'Unknown')
@@ -855,7 +870,7 @@ def update_model_info(model_string=None, model_version=None, only_html=False, in
folder_location = "None"
default_subfolder = "None"
- sub_folders = ["None"]
+ sub_folders = _file.getSubfolders(model_folder, output_basemodel, nsfw, model_uploader, model_name, model_id, version_name, version_id)
for root, dirs, files in os.walk(model_folder, followlinks=True):
for filename in files:
@@ -876,8 +891,9 @@ def update_model_info(model_string=None, model_version=None, only_html=False, in
except Exception as e:
print(f"Error decoding JSON: {str(e)}")
else:
+ #filename_check
for filename in files:
- if filename == model_filename or filename == cleaned_name(model_filename):
+ if filename.lower() == model_filename.lower() or filename.lower() == cleaned_name(model_filename).lower():
folder_location = root
BtnDownInt = False
BtnDel = True
@@ -886,99 +902,20 @@ def update_model_info(model_string=None, model_version=None, only_html=False, in
if folder_location != "None":
break
- insert_sub_1 = getattr(opts, "insert_sub_1", False)
- insert_sub_2 = getattr(opts, "insert_sub_2", False)
- insert_sub_3 = getattr(opts, "insert_sub_3", False)
- insert_sub_4 = getattr(opts, "insert_sub_4", False)
- insert_sub_5 = getattr(opts, "insert_sub_5", False)
- insert_sub_6 = getattr(opts, "insert_sub_6", False)
- insert_sub_7 = getattr(opts, "insert_sub_7", False)
- insert_sub_8 = getattr(opts, "insert_sub_8", False)
- insert_sub_9 = getattr(opts, "insert_sub_9", False)
- insert_sub_10 = getattr(opts, "insert_sub_10", False)
- insert_sub_11 = getattr(opts, "insert_sub_11", False)
- insert_sub_12 = getattr(opts, "insert_sub_12", False)
- insert_sub_13 = getattr(opts, "insert_sub_13", False)
- insert_sub_14 = getattr(opts, "insert_sub_14", False)
- dot_subfolders = getattr(opts, "dot_subfolders", True)
-
- try:
- sub_folders = ["None"]
- for root, dirs, _ in os.walk(model_folder, followlinks=True):
- if dot_subfolders:
- dirs = [d for d in dirs if not d.startswith('.')]
- dirs = [d for d in dirs if not any(part.startswith('.') for part in os.path.join(root, d).split(os.sep))]
- for d in dirs:
- sub_folder = os.path.relpath(os.path.join(root, d), model_folder)
- if sub_folder:
- sub_folders.append(f'{os.sep}{sub_folder}')
-
- sub_folders.remove("None")
- sub_folders = sorted(sub_folders, key=lambda x: (x.lower(), x))
- sub_folders.insert(0, "None")
- base = cleaned_name(output_basemodel)
- author = cleaned_name(model_uploader)
- name = cleaned_name(model_name)
- ver = cleaned_name(model_version)
-
- if insert_sub_1:
- sub_folders.insert(1, os.path.join(os.sep, base))
- if insert_sub_2:
- sub_folders.insert(2, os.path.join(os.sep, base, author))
- if insert_sub_3:
- sub_folders.insert(3, os.path.join(os.sep, base, author, name))
- if insert_sub_4:
- sub_folders.insert(4, os.path.join(os.sep, base, author, name, ver))
- if insert_sub_5:
- sub_folders.insert(5, os.path.join(os.sep, base, name))
- if insert_sub_6:
- sub_folders.insert(6, os.path.join(os.sep, base, name, ver))
- if insert_sub_7:
- sub_folders.insert(7, os.path.join(os.sep, author))
- if insert_sub_8:
- sub_folders.insert(8, os.path.join(os.sep, author, base))
- if insert_sub_9:
- sub_folders.insert(9, os.path.join(os.sep, author, base, name))
- if insert_sub_10:
- sub_folders.insert(10, os.path.join(os.sep, author, base, name, ver))
- if insert_sub_11:
- sub_folders.insert(11, os.path.join(os.sep, author, name))
- if insert_sub_12:
- sub_folders.insert(12, os.path.join(os.sep, author, name, ver))
- if insert_sub_13:
- sub_folders.insert(13, os.path.join(os.sep, name))
- if insert_sub_14:
- sub_folders.insert(14, os.path.join(os.sep, name, ver))
-
- list = set()
- sub_folders = [x for x in sub_folders if not (x in list or list.add(x))]
- except Exception as e:
- print(e)
- sub_folders = ["None"]
-
- default_sub = sub_folder_value(content_type, desc)
-
- variable_mapping = {
- "Base model": base,
- "Author name": author,
- "Model name": name,
- "Model version": ver
- }
-
- if any(key in default_sub for key in variable_mapping.keys()):
- path_components = [variable_mapping.get(component.strip(os.sep), component.strip(os.sep)) for component in default_sub.split(os.sep)]
- default_sub = os.path.join(os.sep, *path_components)
-
+ default_subfolder = sub_folder_value(content_type, desc)
+ if default_subfolder != "None":
+ default_subfolder = _file.convertCustomFolder(default_subfolder, output_basemodel, nsfw, model_uploader, model_name, model_id, version_name, version_id)
if folder_location == "None":
folder_location = model_folder
- if default_sub != "None":
- folder_path = folder_location + default_sub
+ if default_subfolder != "None":
+ folder_path = folder_location + default_subfolder
else:
folder_path = folder_location
else:
folder_path = folder_location
+
relative_path = os.path.relpath(folder_location, model_folder)
- default_subfolder = f'{os.sep}{relative_path}' if relative_path != "." else default_sub if BtnDel == False else "None"
+ default_subfolder = f'{os.sep}{relative_path}' if relative_path != "." else default_subfolder if BtnDel == False else "None"
if gl.isDownloading:
item = gl.download_queue[0]
if int(model_id) == int(item['model_id']):
@@ -1026,14 +963,14 @@ def update_model_info(model_string=None, model_version=None, only_html=False, in
def sub_folder_value(content_type, desc=None):
use_LORA = getattr(opts, "use_LORA", False)
if content_type in ["LORA", "LoCon"] and use_LORA:
- folder = getattr(opts, "LORA_LoCon_subfolder", "None")
+ folder = getattr(opts, "LORA_LoCon_default_subfolder", "None")
elif content_type == "Upscaler":
for upscale_type in ["SWINIR", "REALESRGAN", "GFPGAN", "BSRGAN"]:
if upscale_type in desc:
- folder = getattr(opts, f"{upscale_type}_subfolder", "None")
- folder = getattr(opts, "ESRGAN_subfolder", "None")
+ folder = getattr(opts, f"{upscale_type}_default_subfolder", "None")
+ folder = getattr(opts, "ESRGAN_default_subfolder", "None")
else:
- folder = getattr(opts, f"{content_type}_subfolder", "None")
+ folder = getattr(opts, f"{content_type}_default_subfolder", "None")
if folder == None:
return "None"
return folder
@@ -1186,11 +1123,15 @@ def get_headers(referer=None, no_api=None):
return headers
-def request_civit_api(api_url=None):
+def request_civit_api(api_url=None, skip_error_check=False):
headers = get_headers()
proxies, ssl = get_proxies()
try:
response = requests.get(api_url, headers=headers, timeout=(60,30), proxies=proxies, verify=ssl)
+ if skip_error_check:
+ response.encoding = "utf-8"
+ data = json.loads(response.text)
+ return data
response.raise_for_status()
except requests.exceptions.Timeout as e:
print("The request timed out. Please try again later.")
diff --git a/scripts/civitai_download.py b/scripts/civitai_download.py
index 869ebeb..8a98b8e 100644
--- a/scripts/civitai_download.py
+++ b/scripts/civitai_download.py
@@ -180,16 +180,11 @@ def selected_to_queue(model_list, subfolder, download_start, create_json, curren
break
model_folder = _api.contenttype_folder(content_type, desc)
-
- sub_opt1 = os.path.join(os.sep, _api.cleaned_name(model_name))
- sub_opt2 = os.path.join(os.sep, _api.cleaned_name(model_name), _api.cleaned_name(version_name))
- default_sub = _api.sub_folder_value(content_type, desc)
- if default_sub == f"{os.sep}Model Name":
- default_sub = sub_opt1
- elif default_sub == f"{os.sep}Model Name{os.sep}Version Name":
- default_sub = sub_opt2
-
+ default_subfolder = _api.sub_folder_value(content_type, desc)
+ if default_subfolder != "None":
+ default_subfolder = _file.convertCustomFolder(default_subfolder, output_basemodel, nsfw, model_uploader, model_name, model_id, version_name, version_id)
+
if subfolder and subfolder != "None" and subfolder != "Only available if the selected files are of the same model type":
from_batch = False
if platform.system() == "Windows":
@@ -200,8 +195,8 @@ def selected_to_queue(model_list, subfolder, download_start, create_json, curren
install_path = model_folder + subfolder
else:
from_batch = True
- if default_sub != "None":
- install_path = model_folder + default_sub
+ if default_subfolder != "None":
+ install_path = model_folder + default_subfolder
else:
install_path = model_folder
diff --git a/scripts/civitai_file_manage.py b/scripts/civitai_file_manage.py
index 105b38d..709dd88 100644
--- a/scripts/civitai_file_manage.py
+++ b/scripts/civitai_file_manage.py
@@ -363,7 +363,6 @@ def convert_local_images(html):
return str(soup)
def model_from_sent(model_name, content_type):
-
modelID_failed = False
output_html = None
model_file = None
@@ -459,6 +458,8 @@ def model_from_sent(model_name, content_type):
output_html = output_html.replace('zoom-overlay', 'zoom-preview-overlay')
output_html = output_html.replace('resetZoom', 'resetPreviewZoom')
+ debug_print(output_html)
+
number = _download.random_number()
return (
@@ -511,13 +512,99 @@ def send_to_browser(model_name, content_type, click_first_item):
number = _download.random_number(click_first_item)
return (
- gr.Textbox.update(output_html), # Card HTML
+ gr.Textbox.update(value=output_html), # Card HTML
gr.Button.update(interactive=False), # Prev Button
gr.Button.update(interactive=False), # Next Button
gr.Slider.update(value=1, maximum=1), # Page Slider
- gr.Textbox.update(number) # Click first card trigger
+ gr.Textbox.update(value=number) # Click first card trigger
)
+def convertCustomFolder(folderValue, basemodel, nsfw, author, modelName, modelId, versionName, versionId):
+ replacements = {
+ "BASEMODEL": _api.cleaned_name(str(basemodel)),
+ "AUTHOR": _api.cleaned_name(str(author)),
+ "MODELNAME": _api.cleaned_name(str(modelName)),
+ "MODELID": _api.cleaned_name(str(modelId)),
+ "VERSIONNAME": _api.cleaned_name(str(versionName)),
+ "VERSIONID": _api.cleaned_name(str(versionId))
+ }
+
+ if not nsfw:
+ segments = folderValue.split(os.sep)
+ segments = [seg for seg in segments if "{NSFW}" not in seg]
+ folderValue = os.sep.join(segments)
+ else:
+ replacements["NSFW"] = "nsfw"
+
+ formatted_value = folderValue.format(**replacements)
+
+ converted_folder = formatted_value.replace('/', os.sep).replace('\\', os.sep)
+ converted_folder = os.sep.join(part for part in converted_folder.split(os.sep) if part)
+
+ if not converted_folder.startswith(os.sep):
+ converted_folder = os.sep + converted_folder
+
+ return converted_folder
+
+def getSubfolders(model_folder, basemodel=None, nsfw=None, author=None, modelName=None, modelId=None, versionName=None, versionId=None):
+ try:
+ dot_subfolders = getattr(opts, "dot_subfolders", True)
+ sub_folders = ["None"]
+ for root, dirs, _ in os.walk(model_folder, followlinks=True):
+ if dot_subfolders:
+ dirs = [d for d in dirs if not d.startswith('.')]
+ dirs = [d for d in dirs if not any(part.startswith('.') for part in os.path.join(root, d).split(os.sep))]
+ for d in dirs:
+ sub_folder = os.path.relpath(os.path.join(root, d), model_folder)
+ if sub_folder:
+ if not sub_folder.startswith(os.sep):
+ sub_folder = os.sep + sub_folder
+ sub_folders.append(sub_folder)
+
+ with open(gl.subfolder_json, 'r') as json_file:
+ config_data = json.load(json_file)
+
+ for key, value in config_data.items():
+ if basemodel:
+ try:
+ converted_value = convertCustomFolder(value, basemodel, nsfw, author, modelName, modelId, versionName, versionId)
+ sub_folders.append(converted_value)
+ except Exception as e:
+ print(f"Error: Failed to process custom subfolder: {e}")
+ else:
+ upper_value = value.upper()
+ if not upper_value.startswith(os.sep):
+ upper_value = os.sep + upper_value
+ sub_folders.append(upper_value)
+
+ sub_folders.remove("None")
+ sub_folders = sorted(sub_folders, key=lambda x: (x.lower(), x))
+ sub_folders.insert(0, "None")
+
+ except Exception as e:
+ print(e)
+ sub_folders = ["None"]
+
+ list = set()
+ sub_folders = [x for x in sub_folders if not (x in list or list.add(x))]
+
+ return sub_folders
+
+def updateSubfolder(subfolderInput):
+ with open(gl.subfolder_json, 'r') as f:
+ data = json.load(f)
+
+ index, action, value = subfolderInput.split('.', 2)
+ index = str(index)
+
+ if action == "delete":
+ data.pop(index, None)
+ elif action == "add":
+ data[index] = value
+
+ with open(gl.subfolder_json, 'w') as f:
+ json.dump(data, f, indent=4)
+
def is_image_url(url):
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp']
parsed = urlparse(url)
@@ -558,6 +645,7 @@ def make_dir(path):
def save_model_info(install_path, file_name, sub_folder, sha256=None, preview_html=None, overwrite_toggle=False, api_response=None):
save_path, filename = get_save_path_and_name(install_path, file_name, api_response, sub_folder)
+ image_path = get_image_path(install_path, api_response, sub_folder)
json_file = os.path.join(install_path, f'{filename}.json')
make_dir(install_path)
@@ -576,7 +664,7 @@ def save_model_info(install_path, file_name, sub_folder, sha256=None, preview_ht
img_urls = re.findall(r'data-sampleimg="true" src=[\'"]?([^\'" >]+)', preview_html)
for i, img_url in enumerate(img_urls):
img_name = f'{filename}_{i}.jpg'
- preview_html = preview_html.replace(img_url,f'{os.path.join(save_path, img_name)}')
+ preview_html = preview_html.replace(img_url,f'{os.path.join(image_path, img_name)}')
match = re.search(r'(\s*)', preview_html)
if match:
diff --git a/scripts/civitai_global.py b/scripts/civitai_global.py
index 2a6614b..ab8ef36 100644
--- a/scripts/civitai_global.py
+++ b/scripts/civitai_global.py
@@ -1,11 +1,15 @@
from modules.shared import opts
do_debug_print = getattr(opts, "civitai_debug_prints", False)
def init():
- import warnings
+ import warnings, os, json
from urllib3.exceptions import InsecureRequestWarning
warnings.simplefilter('ignore', InsecureRequestWarning)
+
+ config_folder = os.path.join(os.getcwd(), "config_states")
+ if not os.path.exists(config_folder):
+ os.mkdir(config_folder)
- global download_queue, last_version, cancel_status, recent_model, last_url, json_data, json_info, main_folder, previous_inputs, download_fail, sortNewest, isDownloading, old_download, scan_files, from_update_tab, url_list, print
+ global download_queue, last_version, cancel_status, recent_model, last_url, json_data, json_info, main_folder, previous_inputs, download_fail, sortNewest, isDownloading, old_download, scan_files, from_update_tab, url_list, print, subfolder_json
cancel_status = None
recent_model = None
@@ -16,6 +20,11 @@ def init():
last_version = None
url_list = {}
download_queue = []
+
+ subfolder_json = os.path.join(config_folder, "civitai_subfolders.json")
+ if not os.path.exists(subfolder_json):
+ with open(subfolder_json, 'w') as json_file:
+ json.dump({}, json_file)
from_update_tab = False
scan_files = False
diff --git a/scripts/civitai_gui.py b/scripts/civitai_gui.py
index b3e5fef..c292583 100644
--- a/scripts/civitai_gui.py
+++ b/scripts/civitai_gui.py
@@ -146,6 +146,24 @@ def txt2img_output(image_url):
geninfo = nr + geninfo
return gr.Textbox.update(value=geninfo)
+def get_base_models():
+ api_url = 'https://civitai.com/api/v1/models?baseModels=GetModels'
+ json_return = _api.request_civit_api(api_url, True)
+ default_options = ["SD 1.4","SD 1.5","SD 1.5 LCM","SD 2.0","SD 2.0 768","SD 2.1","SD 2.1 768",
+ "SD 2.1 Unclip","SDXL 0.9","SDXL 1.0","SDXL 1.0 LCM","SDXL Distilled","SDXL Turbo","SDXL Lightning",
+ "Stable Cascade","Pony","SVD","SVD XT","Playground v2","PixArt a", "Flux.1 S", "Flux.1 D","Other"]
+
+ if not isinstance(json_return, dict):
+ print("Couldn't fetch latest baseModel options, using default.")
+ return default_options
+
+ try:
+ options = json_return['error']['issues'][0]['unionErrors'][0]['issues'][0]['options']
+ return options
+ except (KeyError, IndexError) as e:
+ print(f"Basemodel fetch error extracting options: {e}")
+ return default_options
+
def on_ui_tabs():
page_header = getattr(opts, "page_header", False)
lobe_directory = None
@@ -176,7 +194,7 @@ def on_ui_tabs():
else:
toggle4 = "toggle4L" if lobe_directory else "toggle4"
show_only_liked = False
-
+
content_choices = _file.get_content_choices()
scan_choices = _file.get_content_choices(scan_choices=True)
with gr.Blocks() as civitai_interface:
@@ -188,7 +206,7 @@ def on_ui_tabs():
with gr.Row():
content_type = gr.Dropdown(label='Content type:', choices=content_choices, value=None, type="value", multiselect=True, elem_id="centerText")
with gr.Row():
- base_filter = gr.Dropdown(label='Base model:', multiselect=True, choices=["SD 1.4","SD 1.5","SD 1.5 LCM","SD 2.0","SD 2.0 768","SD 2.1","SD 2.1 768","SD 2.1 Unclip","SDXL 0.9","SDXL 1.0","SDXL 1.0 LCM","SDXL Distilled","SDXL Turbo","SDXL Lightning","Stable Cascade","Pony","SVD","SVD XT","Playground v2","PixArt a", "Flux.1 S", "Flux.1 D","Other"], value=None, type="value", elem_id="centerText")
+ base_filter = gr.Dropdown(label='Base model:', multiselect=True, choices=get_base_models(), value=None, type="value", elem_id="centerText")
with gr.Row():
period_type = gr.Dropdown(label='Time period:', choices=["All Time", "Year", "Month", "Week", "Day"], value="All Time", type="value", elem_id="centerText")
sort_type = gr.Dropdown(label='Sort by:', choices=["Newest","Oldest","Most Downloaded","Highest Rated","Most Liked","Most Buzz","Most Discussed","Most Collected","Most Images"], value="Most Downloaded", type="value", elem_id="centerText")
@@ -301,9 +319,17 @@ def on_ui_tabs():
''')
- #Invisible triggers/variables
- #Yes, there is probably a much better way of passing variables/triggering functions
+ def format_custom_subfolders():
+ separator = '␞␞'
+ with open(gl.subfolder_json, 'r') as f:
+ data = json.load(f)
+ result = separator.join([f"{key}{separator}{value}" for key, value in data.items()])
+ return result
+ #Invisible triggers/variables
+ #Yes, there is probably a much better way of passing variables/triggering functions between javascript and python
+
+ gr.Textbox(elem_id="custom_subfolders_list", visible=False, value=format_custom_subfolders())
model_id = gr.Textbox(visible=False)
queue_trigger = gr.Textbox(visible=False)
dl_url = gr.Textbox(visible=False)
@@ -316,6 +342,7 @@ def on_ui_tabs():
queue_html_input = gr.Textbox(elem_id="queue_html_input", visible=False)
list_html_input = gr.Textbox(elem_id="list_html_input", visible=False)
preview_html_input = gr.Textbox(elem_id="preview_html_input", visible=False)
+ create_subfolder = gr.Textbox(elem_id="create_subfolder", visible=False)
send_to_browser = gr.Textbox(elem_id="send_to_browser", visible=False)
arrange_dl_id = gr.Textbox(elem_id="arrange_dl_id", visible=False)
remove_dl_id = gr.Textbox(elem_id="remove_dl_id", visible=False)
@@ -977,6 +1004,13 @@ def on_ui_tabs():
outputs=browser_list
)
+ # Settings function
+ create_subfolder.change(
+ fn=_file.updateSubfolder,
+ inputs=create_subfolder,
+ outputs=[]
+ )
+
if ver_bool:
tab_name = "CivitAI Browser+"
else:
@@ -985,72 +1019,10 @@ def on_ui_tabs():
return (civitai_interface, tab_name, "civitai_interface"),
def subfolder_list(folder, desc=None):
- insert_sub_1 = getattr(opts, "insert_sub_1", False)
- insert_sub_2 = getattr(opts, "insert_sub_2", False)
- insert_sub_3 = getattr(opts, "insert_sub_3", False)
- insert_sub_4 = getattr(opts, "insert_sub_4", False)
- insert_sub_5 = getattr(opts, "insert_sub_5", False)
- insert_sub_6 = getattr(opts, "insert_sub_6", False)
- insert_sub_7 = getattr(opts, "insert_sub_7", False)
- insert_sub_8 = getattr(opts, "insert_sub_8", False)
- insert_sub_9 = getattr(opts, "insert_sub_9", False)
- insert_sub_10 = getattr(opts, "insert_sub_10", False)
- insert_sub_11 = getattr(opts, "insert_sub_11", False)
- insert_sub_12 = getattr(opts, "insert_sub_12", False)
- insert_sub_13 = getattr(opts, "insert_sub_13", False)
- insert_sub_14 = getattr(opts, "insert_sub_14", False)
- dot_subfolders = getattr(opts, "dot_subfolders", True)
-
if folder == None:
return
- try:
- model_folder = _api.contenttype_folder(folder, desc)
- sub_folders = ["None"]
- for root, dirs, _ in os.walk(model_folder, followlinks=True):
- if dot_subfolders:
- dirs = [d for d in dirs if not d.startswith('.')]
- dirs = [d for d in dirs if not any(part.startswith('.') for part in os.path.join(root, d).split(os.sep))]
- for d in dirs:
- sub_folder = os.path.relpath(os.path.join(root, d), model_folder)
- if sub_folder:
- sub_folders.append(f'{os.sep}{sub_folder}')
-
- sub_folders.remove("None")
- sub_folders = sorted(sub_folders, key=lambda x: (x.lower(), x))
- sub_folders.insert(0, "None")
- if insert_sub_1:
- sub_folders.insert(1, f"{os.sep}Base model")
- if insert_sub_2:
- sub_folders.insert(2, f"{os.sep}Base model{os.sep}Author name")
- if insert_sub_3:
- sub_folders.insert(3, f"{os.sep}Base model{os.sep}Author name{os.sep}Model name")
- if insert_sub_4:
- sub_folders.insert(4, f"{os.sep}Base model{os.sep}Author name{os.sep}Model name{os.sep}Model version")
- if insert_sub_5:
- sub_folders.insert(5, f"{os.sep}Base model{os.sep}Model name")
- if insert_sub_6:
- sub_folders.insert(6, f"{os.sep}Base model{os.sep}Model name{os.sep}Model version")
- if insert_sub_7:
- sub_folders.insert(7, f"{os.sep}Author name")
- if insert_sub_8:
- sub_folders.insert(8, f"{os.sep}Author name{os.sep}Base model")
- if insert_sub_9:
- sub_folders.insert(9, f"{os.sep}Author name{os.sep}Base model{os.sep}Model name")
- if insert_sub_10:
- sub_folders.insert(10, f"{os.sep}Author name{os.sep}Base model{os.sep}Model name{os.sep}Model version")
- if insert_sub_11:
- sub_folders.insert(11, f"{os.sep}Author name{os.sep}Model name")
- if insert_sub_12:
- sub_folders.insert(12, f"{os.sep}Author name{os.sep}Model name{os.sep}Model version")
- if insert_sub_13:
- sub_folders.insert(13, f"{os.sep}Model name")
- if insert_sub_14:
- sub_folders.insert(14, f"{os.sep}Model name{os.sep}Model version")
-
- list = set()
- sub_folders = [x for x in sub_folders if not (x in list or list.add(x))]
- except:
- return None
+ model_folder = _api.contenttype_folder(folder, desc)
+ sub_folders = _file.getSubfolders(model_folder)
return sub_folders
def make_lambda(folder, desc):
@@ -1340,37 +1312,8 @@ def on_ui_settings():
).info("Not recommended for security, may be required if you do not have the correct CA Bundle available")
)
- id_and_sub_options = {
- "1" : f"{os.sep}Base model",
- "2" : f"{os.sep}Base model{os.sep}Author name",
- "3" : f"{os.sep}Base model{os.sep}Author name{os.sep}Model name",
- "4" : f"{os.sep}Base model{os.sep}Author name{os.sep}Model name{os.sep}Model version",
- "5" : f"{os.sep}Base model{os.sep}Model name",
- "6" : f"{os.sep}Base model{os.sep}Model name{os.sep}Model version",
- "7" : f"{os.sep}Author name",
- "8" : f"{os.sep}Author name{os.sep}Base model",
- "9" : f"{os.sep}Author name{os.sep}Base model{os.sep}Model name",
- "10" : f"{os.sep}Author name{os.sep}Base model{os.sep}Model name{os.sep}Model version",
- "11" : f"{os.sep}Author name{os.sep}Model name",
- "12" : f"{os.sep}Author name{os.sep}Model name{os.sep}Model version",
- "13" : f"{os.sep}Model name",
- "14" : f"{os.sep}Model name{os.sep}Model version",
- }
-
- for number, string in id_and_sub_options.items():
- shared.opts.add_option(
- f"insert_sub_{number}",
- shared.OptionInfo(
- False,
- f"Insert: [{string}]",
- section=download,
- **({'category_id': cat_id} if ver_bool else {})
- )
- )
-
- use_LORA = getattr(opts, "use_LORA", False)
-
# Default sub folders
+ use_LORA = getattr(opts, "use_LORA", False)
folders = [
"Checkpoint",
"LORA, LoCon, DoRA" if use_LORA else "LORA",
@@ -1409,7 +1352,7 @@ def on_ui_settings():
folder = "LORA"
setting_name = "LORA_LoCon"
- shared.opts.add_option(f"{setting_name}_subfolder", shared.OptionInfo("None", folder_name, gr.Dropdown, make_lambda(folder, desc), section=download, **({'category_id': cat_id} if ver_bool else {})))
+ shared.opts.add_option(f"{setting_name}_default_subfolder", shared.OptionInfo("None", folder_name, gr.Dropdown, make_lambda(folder, desc), section=download, **({'category_id': cat_id} if ver_bool else {})))
script_callbacks.on_ui_tabs(on_ui_tabs)
script_callbacks.on_ui_settings(on_ui_settings)
diff --git a/style.css b/style.css
index d81943c..41a2ed4 100644
--- a/style.css
+++ b/style.css
@@ -537,6 +537,29 @@
padding: 8px 8px;
}
+.flex-column-layout {
+ display: flex;
+ gap: 10px;
+ flex-direction: column;
+}
+
+.sub-folder-button-container {
+ width: 100%;
+}
+
+.guide-button {
+ position: relative;
+ z-index: 1;
+}
+
+.plus-button {
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 0;
+}
+
+
#accordionToggle {
width: 100%;
display: flex;