feat: Add video tag search and random sort with i18n support

- Added video tag support for enhanced media file handling:
  * Video files can now use tag-based search functionality
  * Added support for reading video generation info from txt files
  * Enhanced EXIF data handling for video media types

- Implemented random sort feature for image grid:
  * Added random sort toggle button with visual indicators (🎲/📅)
  * Supports both random and date-based sorting modes
  * Optimized pagination for random sorting with offset-based cursors

- Complete internationalization support:
  * Added translations for random sort and date sort in all languages
  * Supported languages: zh-hans, en, de, zh-hant
  * Improved UI consistency across language variants

- Backend improvements:
  * Enhanced database queries to support random sorting
  * Added random_sort parameter to API endpoints
  * Improved error handling for video file processing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
pull/838/head
bupro 2025-08-23 00:41:10 +08:00
parent fc9acbc181
commit 198de49e58
10 changed files with 96 additions and 16 deletions

View File

@ -13,8 +13,8 @@ Promise.resolve().then(async () => {
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Infinite Image Browsing</title>
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-56203bfb.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-d06b4bd6.css">
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-e00fca09.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-727d406a.css">
</head>
<body>

View File

@ -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]),

View File

@ -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

View File

@ -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)

View File

@ -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) => {

View File

@ -141,5 +141,7 @@ export const de: Partial<IIBI18nMap> = {
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'
}

View File

@ -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'
}

View File

@ -330,5 +330,7 @@ export const zhHans = {
tagOperationFailed: '标签操作失败',
mediaType: '媒体类型',
all: '全部',
video: '视频'
video: '视频',
randomSort: '随机排序',
sortByDate: '按日期排序'
}

View File

@ -335,5 +335,7 @@ export const zhHant: Partial<IIBI18nMap> = {
tagOperationFailed: '標籤操作失敗',
mediaType: '媒體類型',
all: '全部',
video: '視頻'
video: '視頻',
randomSort: '隨機排序',
sortByDate: '按日期排序'
}

View File

@ -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 = () => {
</ASkeleton>
</AModal>
<div class="action-bar">
<a-button @click="randomSort = !randomSort" :type="randomSort ? 'primary' : 'default'">
{{ randomSort ? '🎲 ' + $t('randomSort') : '📅 ' + $t('sortByDate') }}
</a-button>
<a-button @click="onTiktokViewClick" type="primary" :disabled="!images?.length">{{ $t('tiktokView') }}</a-button>
<a-button @click="saveLoadedFileAsJson">{{ $t('saveLoadedImageAsJson') }}</a-button>
<a-button @click="saveAllFileAsJson">{{ $t('saveAllAsJson') }}</a-button>