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
parent
fc9acbc181
commit
198de49e58
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -330,5 +330,7 @@ export const zhHans = {
|
|||
tagOperationFailed: '标签操作失败',
|
||||
mediaType: '媒体类型',
|
||||
all: '全部',
|
||||
video: '视频'
|
||||
video: '视频',
|
||||
randomSort: '随机排序',
|
||||
sortByDate: '按日期排序'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -335,5 +335,7 @@ export const zhHant: Partial<IIBI18nMap> = {
|
|||
tagOperationFailed: '標籤操作失敗',
|
||||
mediaType: '媒體類型',
|
||||
all: '全部',
|
||||
video: '視頻'
|
||||
video: '視頻',
|
||||
randomSort: '隨機排序',
|
||||
sortByDate: '按日期排序'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue