mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
fix: support multiple include folders in LoRA pool widget
- Add folder_include parameter support in backend API handlers - Add folder_include to FilterCriteria and implement multi-folder filtering logic - Update frontend to send all include folders instead of only the first - Add tests for single/multiple include folders, include with exclude, and non-recursive filtering
This commit is contained in:
@@ -211,6 +211,7 @@ class ModelListingHandler:
|
|||||||
page_size = min(int(request.query.get("page_size", "20")), 100)
|
page_size = min(int(request.query.get("page_size", "20")), 100)
|
||||||
sort_by = request.query.get("sort_by", "name")
|
sort_by = request.query.get("sort_by", "name")
|
||||||
folder = request.query.get("folder")
|
folder = request.query.get("folder")
|
||||||
|
folder_include = list(request.query.getall("folder_include", []))
|
||||||
search = request.query.get("search")
|
search = request.query.get("search")
|
||||||
fuzzy_search = request.query.get("fuzzy_search", "false").lower() == "true"
|
fuzzy_search = request.query.get("fuzzy_search", "false").lower() == "true"
|
||||||
|
|
||||||
@@ -290,6 +291,7 @@ class ModelListingHandler:
|
|||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
"sort_by": sort_by,
|
"sort_by": sort_by,
|
||||||
"folder": folder,
|
"folder": folder,
|
||||||
|
"folder_include": folder_include,
|
||||||
"folder_exclude": folder_exclude,
|
"folder_exclude": folder_exclude,
|
||||||
"search": search,
|
"search": search,
|
||||||
"fuzzy_search": fuzzy_search,
|
"fuzzy_search": fuzzy_search,
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class BaseModelService(ABC):
|
|||||||
page_size: int,
|
page_size: int,
|
||||||
sort_by: str = "name",
|
sort_by: str = "name",
|
||||||
folder: str = None,
|
folder: str = None,
|
||||||
|
folder_include: list = None,
|
||||||
folder_exclude: list = None,
|
folder_exclude: list = None,
|
||||||
search: str = None,
|
search: str = None,
|
||||||
fuzzy_search: bool = False,
|
fuzzy_search: bool = False,
|
||||||
@@ -101,6 +102,7 @@ class BaseModelService(ABC):
|
|||||||
filtered_data = await self._apply_common_filters(
|
filtered_data = await self._apply_common_filters(
|
||||||
sorted_data,
|
sorted_data,
|
||||||
folder=folder,
|
folder=folder,
|
||||||
|
folder_include=folder_include,
|
||||||
folder_exclude=folder_exclude,
|
folder_exclude=folder_exclude,
|
||||||
base_models=base_models,
|
base_models=base_models,
|
||||||
model_types=model_types,
|
model_types=model_types,
|
||||||
@@ -232,6 +234,7 @@ class BaseModelService(ABC):
|
|||||||
self,
|
self,
|
||||||
data: List[Dict],
|
data: List[Dict],
|
||||||
folder: str = None,
|
folder: str = None,
|
||||||
|
folder_include: list = None,
|
||||||
folder_exclude: list = None,
|
folder_exclude: list = None,
|
||||||
base_models: list = None,
|
base_models: list = None,
|
||||||
model_types: list = None,
|
model_types: list = None,
|
||||||
@@ -243,6 +246,7 @@ class BaseModelService(ABC):
|
|||||||
normalized_options = self.search_strategy.normalize_options(search_options)
|
normalized_options = self.search_strategy.normalize_options(search_options)
|
||||||
criteria = FilterCriteria(
|
criteria = FilterCriteria(
|
||||||
folder=folder,
|
folder=folder,
|
||||||
|
folder_include=folder_include,
|
||||||
folder_exclude=folder_exclude,
|
folder_exclude=folder_exclude,
|
||||||
base_models=base_models,
|
base_models=base_models,
|
||||||
model_types=model_types,
|
model_types=model_types,
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ class FilterCriteria:
|
|||||||
"""Container for model list filtering options."""
|
"""Container for model list filtering options."""
|
||||||
|
|
||||||
folder: Optional[str] = None
|
folder: Optional[str] = None
|
||||||
|
folder_include: Optional[Sequence[str]] = None
|
||||||
folder_exclude: Optional[Sequence[str]] = None
|
folder_exclude: Optional[Sequence[str]] = None
|
||||||
base_models: Optional[Sequence[str]] = None
|
base_models: Optional[Sequence[str]] = None
|
||||||
tags: Optional[Dict[str, str]] = None
|
tags: Optional[Dict[str, str]] = None
|
||||||
@@ -159,6 +160,7 @@ class ModelFilterSet:
|
|||||||
|
|
||||||
folder_duration = 0
|
folder_duration = 0
|
||||||
folder = criteria.folder
|
folder = criteria.folder
|
||||||
|
folder_include = criteria.folder_include or []
|
||||||
folder_exclude = criteria.folder_exclude or []
|
folder_exclude = criteria.folder_exclude or []
|
||||||
options = criteria.search_options or {}
|
options = criteria.search_options or {}
|
||||||
recursive = bool(options.get("recursive", True))
|
recursive = bool(options.get("recursive", True))
|
||||||
@@ -198,6 +200,66 @@ class ModelFilterSet:
|
|||||||
items = [item for item in items if item.get("folder") == folder]
|
items = [item for item in items if item.get("folder") == folder]
|
||||||
folder_duration = time.perf_counter() - t0 + folder_duration
|
folder_duration = time.perf_counter() - t0 + folder_duration
|
||||||
|
|
||||||
|
# Apply folder include filters
|
||||||
|
if folder_include:
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
matched_items = []
|
||||||
|
for include_folder in folder_include:
|
||||||
|
if include_folder:
|
||||||
|
if recursive:
|
||||||
|
# Normalize folder for prefix matching (similar to exclude logic)
|
||||||
|
if not include_folder.endswith("/"):
|
||||||
|
folder_prefix = f"{include_folder}/"
|
||||||
|
else:
|
||||||
|
folder_prefix = include_folder
|
||||||
|
folder_items = [
|
||||||
|
item
|
||||||
|
for item in items
|
||||||
|
if item.get("folder") == include_folder
|
||||||
|
or item.get("folder", "").startswith(folder_prefix)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
folder_items = [
|
||||||
|
item
|
||||||
|
for item in items
|
||||||
|
if item.get("folder") == include_folder
|
||||||
|
]
|
||||||
|
matched_items.extend(folder_items)
|
||||||
|
# Remove duplicates while preserving order
|
||||||
|
seen = set()
|
||||||
|
items = []
|
||||||
|
for item in matched_items:
|
||||||
|
# Use sha256 or id as unique identifier if available, otherwise use tuple representation
|
||||||
|
item_id = item.get("sha256") or item.get("id")
|
||||||
|
if item_id is not None:
|
||||||
|
identifier = item_id
|
||||||
|
else:
|
||||||
|
# For items without explicit id, use a tuple of key values
|
||||||
|
identifier = tuple(sorted((k, str(v)) for k, v in item.items()))
|
||||||
|
if identifier not in seen:
|
||||||
|
seen.add(identifier)
|
||||||
|
items.append(item)
|
||||||
|
folder_duration = time.perf_counter() - t0 + folder_duration
|
||||||
|
# Apply folder include filters (legacy single folder)
|
||||||
|
elif folder is not None:
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
if recursive:
|
||||||
|
if folder:
|
||||||
|
# Normalize folder for prefix matching
|
||||||
|
if not folder.endswith("/"):
|
||||||
|
folder_prefix = f"{folder}/"
|
||||||
|
else:
|
||||||
|
folder_prefix = folder
|
||||||
|
items = [
|
||||||
|
item
|
||||||
|
for item in items
|
||||||
|
if item.get("folder") == folder
|
||||||
|
or item.get("folder", "").startswith(folder_prefix)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
items = [item for item in items if item.get("folder") == folder]
|
||||||
|
folder_duration = time.perf_counter() - t0 + folder_duration
|
||||||
|
|
||||||
base_models_duration = 0
|
base_models_duration = 0
|
||||||
base_models = criteria.base_models or []
|
base_models = criteria.base_models or []
|
||||||
if base_models:
|
if base_models:
|
||||||
|
|||||||
@@ -102,6 +102,87 @@ def test_model_filter_set_folder_exclude_with_include():
|
|||||||
assert result[0]["model_name"] == "item1"
|
assert result[0]["model_name"] == "item1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_filter_set_folder_include_single():
|
||||||
|
filter_set = ModelFilterSet(MockSettings())
|
||||||
|
items = [
|
||||||
|
{"model_name": "item1", "folder": "characters/"},
|
||||||
|
{"model_name": "item2", "folder": "styles/"},
|
||||||
|
{"model_name": "item3", "folder": "characters/anime/"},
|
||||||
|
{"model_name": "item4", "folder": "concepts/"},
|
||||||
|
]
|
||||||
|
criteria = FilterCriteria(
|
||||||
|
folder_include=["characters/"], search_options={"recursive": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = filter_set.apply(items, criteria)
|
||||||
|
|
||||||
|
assert len(result) == 2
|
||||||
|
model_names = {i["model_name"] for i in result}
|
||||||
|
assert model_names == {"item1", "item3"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_filter_set_folder_include_multiple():
|
||||||
|
filter_set = ModelFilterSet(MockSettings())
|
||||||
|
items = [
|
||||||
|
{"model_name": "item1", "folder": "characters/"},
|
||||||
|
{"model_name": "item2", "folder": "styles/"},
|
||||||
|
{"model_name": "item3", "folder": "characters/anime/"},
|
||||||
|
{"model_name": "item4", "folder": "styles/painting/"},
|
||||||
|
{"model_name": "item5", "folder": "concepts/"},
|
||||||
|
]
|
||||||
|
criteria = FilterCriteria(
|
||||||
|
folder_include=["characters/", "styles/"], search_options={"recursive": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = filter_set.apply(items, criteria)
|
||||||
|
|
||||||
|
assert len(result) == 4
|
||||||
|
model_names = {i["model_name"] for i in result}
|
||||||
|
assert model_names == {"item1", "item2", "item3", "item4"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_filter_set_folder_include_with_exclude():
|
||||||
|
filter_set = ModelFilterSet(MockSettings())
|
||||||
|
items = [
|
||||||
|
{"model_name": "item1", "folder": "characters/"},
|
||||||
|
{"model_name": "item2", "folder": "styles/"},
|
||||||
|
{"model_name": "item3", "folder": "characters/anime/"},
|
||||||
|
{"model_name": "item4", "folder": "styles/painting/"},
|
||||||
|
{"model_name": "item5", "folder": "concepts/"},
|
||||||
|
]
|
||||||
|
criteria = FilterCriteria(
|
||||||
|
folder_include=["characters/", "styles/"],
|
||||||
|
folder_exclude=["characters/anime/"],
|
||||||
|
search_options={"recursive": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
result = filter_set.apply(items, criteria)
|
||||||
|
|
||||||
|
assert len(result) == 3
|
||||||
|
model_names = {i["model_name"] for i in result}
|
||||||
|
assert model_names == {"item1", "item2", "item4"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_filter_set_folder_include_non_recursive():
|
||||||
|
filter_set = ModelFilterSet(MockSettings())
|
||||||
|
items = [
|
||||||
|
{"model_name": "item1", "folder": "characters/"},
|
||||||
|
{"model_name": "item2", "folder": "styles/"},
|
||||||
|
{"model_name": "item3", "folder": "characters/anime/"},
|
||||||
|
{"model_name": "item4", "folder": "styles/painting/"},
|
||||||
|
{"model_name": "item5", "folder": "concepts/"},
|
||||||
|
]
|
||||||
|
criteria = FilterCriteria(
|
||||||
|
folder_include=["characters/", "styles/"], search_options={"recursive": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = filter_set.apply(items, criteria)
|
||||||
|
|
||||||
|
assert len(result) == 2
|
||||||
|
model_names = {i["model_name"] for i in result}
|
||||||
|
assert model_names == {"item1", "item2"}
|
||||||
|
|
||||||
|
|
||||||
# --- Recipe Filtering Tests ---
|
# --- Recipe Filtering Tests ---
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export function useLoraPoolApi() {
|
|||||||
|
|
||||||
// Folder filters
|
// Folder filters
|
||||||
if (params.foldersInclude && params.foldersInclude.length > 0) {
|
if (params.foldersInclude && params.foldersInclude.length > 0) {
|
||||||
urlParams.set('folder', params.foldersInclude[0])
|
params.foldersInclude.forEach(folder => urlParams.append('folder_include', folder))
|
||||||
urlParams.set('recursive', 'true')
|
urlParams.set('recursive', 'true')
|
||||||
}
|
}
|
||||||
params.foldersExclude?.forEach(folder => urlParams.append('folder_exclude', folder))
|
params.foldersExclude?.forEach(folder => urlParams.append('folder_exclude', folder))
|
||||||
|
|||||||
@@ -11000,7 +11000,7 @@ function useLoraPoolApi() {
|
|||||||
(_b = params.tagsInclude) == null ? void 0 : _b.forEach((tag) => urlParams.append("tag_include", tag));
|
(_b = params.tagsInclude) == null ? void 0 : _b.forEach((tag) => urlParams.append("tag_include", tag));
|
||||||
(_c = params.tagsExclude) == null ? void 0 : _c.forEach((tag) => urlParams.append("tag_exclude", tag));
|
(_c = params.tagsExclude) == null ? void 0 : _c.forEach((tag) => urlParams.append("tag_exclude", tag));
|
||||||
if (params.foldersInclude && params.foldersInclude.length > 0) {
|
if (params.foldersInclude && params.foldersInclude.length > 0) {
|
||||||
urlParams.set("folder", params.foldersInclude[0]);
|
params.foldersInclude.forEach((folder) => urlParams.append("folder_include", folder));
|
||||||
urlParams.set("recursive", "true");
|
urlParams.set("recursive", "true");
|
||||||
}
|
}
|
||||||
(_d = params.foldersExclude) == null ? void 0 : _d.forEach((folder) => urlParams.append("folder_exclude", folder));
|
(_d = params.foldersExclude) == null ? void 0 : _d.forEach((folder) => urlParams.append("folder_exclude", folder));
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user