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;