diff --git a/javascript/index.js b/javascript/index.js index c497e08..a98e45f 100644 --- a/javascript/index.js +++ b/javascript/index.js @@ -13,8 +13,8 @@ Promise.resolve().then(async () => { Infinite Image Browsing - - + + diff --git a/scripts/iib/api.py b/scripts/iib/api.py index c6bdc53..fd6bc7b 100644 --- a/scripts/iib/api.py +++ b/scripts/iib/api.py @@ -710,13 +710,21 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs): @app.get(api_base + "/image_geninfo", dependencies=[Depends(verify_secret)]) async def image_geninfo(path: str): - return parse_image_info(path).raw_info + # 使用 get_exif_data 函数,它已经支持视频文件 + from scripts.iib.db.update_image_data import get_exif_data + try: + result = get_exif_data(path) + return result.raw_info or "" + except Exception as e: + logger.error(f"Failed to get geninfo for {path}: {e}") + return "" class GeninfoBatchReq(BaseModel): paths: List[str] @app.post(api_base + "/image_geninfo_batch", dependencies=[Depends(verify_secret)]) async def image_geninfo_batch(req: GeninfoBatchReq): + from scripts.iib.db.update_image_data import get_exif_data res = {} conn = DataBase.get_conn() for path in req.paths: @@ -725,9 +733,11 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs): if img: res[path] = img.exif else: - res[path] = parse_image_info(path).raw_info + result = get_exif_data(path) + res[path] = result.raw_info or "" except Exception as e: - logger.error(e, stack_info=True) + logger.error(f"Failed to get geninfo for {path}: {e}", stack_info=True) + res[path] = "" return res @@ -919,6 +929,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs): cursor: str folder_paths: List[str] = None size: Optional[int] = 200 + random_sort: Optional[bool] = False @app.post(db_api_base + "/match_images_by_tags", dependencies=[Depends(verify_secret)]) async def match_image_by_tags(req: MatchImagesByTagsReq): @@ -933,7 +944,8 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs): tag_dict={"and": req.and_tags, "or": req.or_tags, "not": req.not_tags}, cursor=req.cursor, folder_paths=folder_paths, - limit=req.size + limit=req.size, + random_sort=req.random_sort ) return { "files": filter_allowed_files([x.to_file_info() for x in imgs]), diff --git a/scripts/iib/db/datamodel.py b/scripts/iib/db/datamodel.py index 3f0b5a5..e7f82da 100644 --- a/scripts/iib/db/datamodel.py +++ b/scripts/iib/db/datamodel.py @@ -533,6 +533,7 @@ class ImageTag: limit: int = 500, cursor="", folder_paths: List[str] = None, + random_sort: bool = False, ) -> tuple[List[Image], Cursor]: query = """ SELECT image.id, image.path, image.size,image.date @@ -583,7 +584,7 @@ class ImageTag: print(folder_path) where_clauses.append("(" + " OR ".join(folder_clauses) + ")") - if cursor: + if cursor and not random_sort: where_clauses.append("(image.date < ?)") params.append(cursor) if where_clauses: @@ -593,7 +594,17 @@ class ImageTag: query += " HAVING COUNT(DISTINCT tag_id) = ?" params.append(len(tag_dict["and"])) - query += " ORDER BY date DESC LIMIT ?" + if random_sort: + query += " ORDER BY RANDOM() LIMIT ?" + # For random sort, use offset-based pagination + if cursor: + try: + offset = int(cursor) + query = query.replace("LIMIT ?", f"LIMIT ? OFFSET {offset}") + except (ValueError, TypeError): + pass # Invalid cursor, start from beginning + else: + query += " ORDER BY date DESC LIMIT ?" params.append(limit) api_cur = Cursor() with closing(conn.cursor()) as cur: @@ -610,7 +621,13 @@ class ImageTag: Image.safe_batch_remove(conn, deleted_ids) api_cur.has_next = len(rows) >= limit if images: - api_cur.next = str(images[-1].date) + if random_sort: + # For random sort, use offset-based cursor + current_offset = int(cursor) if cursor else 0 + api_cur.next = str(current_offset + len(images)) + else: + # For date sort, use date-based cursor + api_cur.next = str(images[-1].date) return images, api_cur @classmethod diff --git a/scripts/iib/db/update_image_data.py b/scripts/iib/db/update_image_data.py index 720a47f..6d9e18f 100644 --- a/scripts/iib/db/update_image_data.py +++ b/scripts/iib/db/update_image_data.py @@ -9,7 +9,9 @@ from scripts.iib.tool import ( is_dev, get_modified_date, is_image_file, - case_insensitive_get + case_insensitive_get, + get_img_geninfo_txt_path, + parse_generation_parameters ) from scripts.iib.parsers.model import ImageGenerationInfo, ImageGenerationParams from scripts.iib.logger import logger @@ -19,6 +21,26 @@ from scripts.iib.plugin import plugin_inst_map # 定义一个函数来获取图片文件的EXIF数据 def get_exif_data(file_path): if get_video_type(file_path): + # 对于视频文件,尝试读取对应的txt标签文件 + txt_path = get_img_geninfo_txt_path(file_path) + if txt_path: + try: + with open(txt_path, 'r', encoding='utf-8') as f: + content = f.read().strip() + if content: + # 复用现有解析逻辑,添加视频标识 + params = parse_generation_parameters(content + "\nSource Identifier: Video Tags") + return ImageGenerationInfo( + content, + ImageGenerationParams( + meta=params["meta"], + pos_prompt=params["pos_prompt"], + extra=params, + ), + ) + except Exception as e: + if is_dev: + logger.error("Failed to read video txt file %s: %s", txt_path, e) return ImageGenerationInfo() try: return parse_image_info(file_path) diff --git a/vue/src/api/db.ts b/vue/src/api/db.ts index 08313a8..8691ccd 100644 --- a/vue/src/api/db.ts +++ b/vue/src/api/db.ts @@ -43,6 +43,7 @@ export interface MatchImageByTagsReq { and_tags: TagId[] or_tags: TagId[] not_tags: TagId[] + random_sort?: boolean } export const getImagesByTags = async (req: MatchImageByTagsReq, cursor: string) => { diff --git a/vue/src/i18n/de.ts b/vue/src/i18n/de.ts index bae8aa5..45a0d56 100644 --- a/vue/src/i18n/de.ts +++ b/vue/src/i18n/de.ts @@ -141,5 +141,7 @@ export const de: Partial = { randomImageSettingNotification: 'Tipp: Sie können in den globalen Einstellungen steuern, ob die Zufallsbild-Option auf der Startseite angezeigt wird', mediaType: 'Medientyp', all: 'Alle', - video: 'Video' + video: 'Video', + randomSort: 'Zufällig sortieren', + sortByDate: 'Nach Datum sortieren' } diff --git a/vue/src/i18n/en.ts b/vue/src/i18n/en.ts index 577fa00..c578d3f 100644 --- a/vue/src/i18n/en.ts +++ b/vue/src/i18n/en.ts @@ -351,5 +351,7 @@ You can specify which snapshot to restore to when starting IIB in the global set tagOperationFailed: 'Tag operation failed', mediaType: 'Media Type', all: 'All', - video: 'Video' + video: 'Video', + randomSort: 'Random Sort', + sortByDate: 'Sort by Date' } diff --git a/vue/src/i18n/zh-hans.ts b/vue/src/i18n/zh-hans.ts index d8cf36f..1d61ef4 100644 --- a/vue/src/i18n/zh-hans.ts +++ b/vue/src/i18n/zh-hans.ts @@ -330,5 +330,7 @@ export const zhHans = { tagOperationFailed: '标签操作失败', mediaType: '媒体类型', all: '全部', - video: '视频' + video: '视频', + randomSort: '随机排序', + sortByDate: '按日期排序' } diff --git a/vue/src/i18n/zh-hant.ts b/vue/src/i18n/zh-hant.ts index f826e4f..ac1d395 100644 --- a/vue/src/i18n/zh-hant.ts +++ b/vue/src/i18n/zh-hant.ts @@ -335,5 +335,7 @@ export const zhHant: Partial = { tagOperationFailed: '標籤操作失敗', mediaType: '媒體類型', all: '全部', - video: '視頻' + video: '視頻', + randomSort: '隨機排序', + sortByDate: '按日期排序' } diff --git a/vue/src/page/TagSearch/MatchedImageGrid.vue b/vue/src/page/TagSearch/MatchedImageGrid.vue index f964011..ea539ed 100644 --- a/vue/src/page/TagSearch/MatchedImageGrid.vue +++ b/vue/src/page/TagSearch/MatchedImageGrid.vue @@ -5,7 +5,7 @@ import '@zanllp/vue-virtual-scroller/dist/vue-virtual-scroller.css' import { RecycleScroller } from '@zanllp/vue-virtual-scroller' import { toRawFileUrl } from '@/util/file' import { getImagesByTags, type MatchImageByTagsReq } from '@/api/db' -import { nextTick, watch } from 'vue' +import { nextTick, watch, ref } from 'vue' import { copy2clipboardI18n } from '@/util' import fullScreenContextMenu from '@/page/fileTransfer/fullScreenContextMenu.vue' import { LeftCircleOutlined, RightCircleOutlined } from '@/icon' @@ -23,7 +23,13 @@ const props = defineProps<{ }>() -const iter = createImageSearchIter(cursor => getImagesByTags(props.selectedTagIds, cursor)) +// 添加随机排序状态 +const randomSort = ref(true) + +// 创建搜索迭代器,根据随机排序状态决定参数 +const iter = createImageSearchIter(cursor => { + return getImagesByTags({...props.selectedTagIds, random_sort: randomSort.value}, cursor) +}) const { queue, images, @@ -67,6 +73,17 @@ watch( { immediate: true } ) +// 监听随机排序状态变化 +watch( + randomSort, + async () => { + await iter.reset() + await nextTick() + scroller.value?.scrollToItem(0) + onScroll() // 重新获取 + } +) + watch( () => props, @@ -114,6 +131,9 @@ const onTiktokViewClick = () => {
+ + {{ randomSort ? '🎲 ' + $t('randomSort') : '📅 ' + $t('sortByDate') }} + {{ $t('tiktokView') }} {{ $t('saveLoadedImageAsJson') }} {{ $t('saveAllAsJson') }}