diff --git a/app/api/endpoints/history.py b/app/api/endpoints/history.py index 00bbbd07..27a14138 100644 --- a/app/api/endpoints/history.py +++ b/app/api/endpoints/history.py @@ -235,6 +235,14 @@ async def delete_download_history( return schemas.Response(success=True) +def _glob_to_like(pattern: str) -> str: + """ + 将 glob 通配符模式转换为 SQL LIKE 模式(使用 \\ 作为转义字符) + """ + result = pattern.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + return result.replace("*", "%").replace("?", "_") + + @router.get("/transfer", summary="查询整理记录", response_model=schemas.Response) async def transfer_history( title: Optional[str] = None, @@ -245,7 +253,7 @@ async def transfer_history( _: schemas.TokenPayload = Depends(verify_token), ) -> Any: """ - 查询整理记录 + 查询整理记录,title 支持通配符 * 和 ?(如 *.mkv、*2024*) """ if title == "失败": title = None @@ -255,14 +263,23 @@ async def transfer_history( status = True if title: - words = jieba.cut(title, HMM=False) - title = "%".join(words) - total = await TransferHistory.async_count_by_title( - db, title=title, status=status - ) - result = await TransferHistory.async_list_by_title( - db, title=title, page=page, count=count, status=status - ) + if "*" in title or "?" in title: + like_pattern = _glob_to_like(title) + total = await TransferHistory.async_count_by_title( + db, title=like_pattern, status=status, wildcard=True + ) + result = await TransferHistory.async_list_by_title( + db, title=like_pattern, page=page, count=count, status=status, wildcard=True + ) + else: + words = jieba.cut(title, HMM=False) + like_pattern = "%".join(words) + total = await TransferHistory.async_count_by_title( + db, title=like_pattern, status=status + ) + result = await TransferHistory.async_list_by_title( + db, title=like_pattern, page=page, count=count, status=status + ) else: result = await TransferHistory.async_list_by_page( db, page=page, count=count, status=status diff --git a/app/api/endpoints/storage.py b/app/api/endpoints/storage.py index 04b0598a..571e0bdf 100644 --- a/app/api/endpoints/storage.py +++ b/app/api/endpoints/storage.py @@ -1,4 +1,6 @@ +import fnmatch import math +import re from pathlib import Path from typing import Any, List, Optional @@ -88,17 +90,22 @@ def reset(name: str, _: User = Depends(get_current_active_superuser)) -> Any: def list_files( fileitem: schemas.FileItem, sort: Optional[str] = "updated_at", + keyword: Optional[str] = None, _: User = Depends(get_current_active_superuser), ) -> Any: """ 查询当前目录下所有目录和文件 :param fileitem: 文件项 :param sort: 排序方式,name:按名称排序,time:按修改时间排序 + :param keyword: 通配符过滤,支持 * 和 ?,如 *.mkv、movie?.* :param _: token :return: 所有目录和文件 """ file_list = StorageChain().list_files(fileitem) if file_list: + if keyword: + _pat = re.compile(fnmatch.translate(keyword), re.IGNORECASE) + file_list = [f for f in file_list if _pat.match(f.name or "")] if sort == "name": file_list.sort(key=lambda x: StringUtils.natural_sort_key(x.name or "")) else: diff --git a/app/db/models/transferhistory.py b/app/db/models/transferhistory.py index a02c80a5..7fa3a352 100644 --- a/app/db/models/transferhistory.py +++ b/app/db/models/transferhistory.py @@ -68,51 +68,55 @@ class TransferHistory(Base): @classmethod @db_query def list_by_title(cls, db: Session, title: str, page: Optional[int] = 1, count: Optional[int] = 30, - status: bool = None): - if status is not None: - query = db.query(cls).filter( - cls.status == status - ).order_by( - cls.date.desc() + status: bool = None, wildcard: bool = False): + if wildcard: + text_filter = or_( + cls.title.like(title, escape='\\'), + cls.src.like(title, escape='\\'), + cls.dest.like(title, escape='\\'), ) else: - query = db.query(cls).filter(or_( + text_filter = or_( cls.title.like(f'%{title}%'), cls.src.like(f'%{title}%'), cls.dest.like(f'%{title}%'), - )).order_by( - cls.date.desc() ) - + query = db.query(cls).filter(text_filter) + if status is not None: + query = query.filter(cls.status == status) + query = query.order_by(cls.date.desc()) + # 当count为负数时,不限制页数查询所有 if count >= 0: query = query.offset((page - 1) * count).limit(count) - + return query.all() @classmethod @async_db_query async def async_list_by_title(cls, db: AsyncSession, title: str, page: Optional[int] = 1, count: Optional[int] = 30, - status: bool = None): - if status is not None: - query = select(cls).filter( - cls.status == status - ).order_by( - cls.date.desc() + status: bool = None, wildcard: bool = False): + if wildcard: + text_filter = or_( + cls.title.like(title, escape='\\'), + cls.src.like(title, escape='\\'), + cls.dest.like(title, escape='\\'), ) else: - query = select(cls).filter(or_( + text_filter = or_( cls.title.like(f'%{title}%'), cls.src.like(f'%{title}%'), cls.dest.like(f'%{title}%'), - )).order_by( - cls.date.desc() ) - + query = select(cls).filter(text_filter) + if status is not None: + query = query.filter(cls.status == status) + query = query.order_by(cls.date.desc()) + # 当count为负数时,不限制页数查询所有 if count >= 0: query = query.offset((page - 1) * count).limit(count) - + result = await db.execute(query) return result.scalars().all() @@ -232,31 +236,43 @@ class TransferHistory(Base): @classmethod @db_query - def count_by_title(cls, db: Session, title: str, status: bool = None): - if status is not None: - return db.query(func.count(cls.id)).filter(cls.status == status).first()[0] + def count_by_title(cls, db: Session, title: str, status: bool = None, wildcard: bool = False): + if wildcard: + text_filter = or_( + cls.title.like(title, escape='\\'), + cls.src.like(title, escape='\\'), + cls.dest.like(title, escape='\\'), + ) else: - return db.query(func.count(cls.id)).filter(or_( + text_filter = or_( cls.title.like(f'%{title}%'), cls.src.like(f'%{title}%'), - cls.dest.like(f'%{title}%') - )).first()[0] + cls.dest.like(f'%{title}%'), + ) + query = db.query(func.count(cls.id)).filter(text_filter) + if status is not None: + query = query.filter(cls.status == status) + return query.first()[0] @classmethod @async_db_query - async def async_count_by_title(cls, db: AsyncSession, title: str, status: bool = None): - if status is not None: - result = await db.execute( - select(func.count(cls.id)).filter(cls.status == status) + async def async_count_by_title(cls, db: AsyncSession, title: str, status: bool = None, wildcard: bool = False): + if wildcard: + text_filter = or_( + cls.title.like(title, escape='\\'), + cls.src.like(title, escape='\\'), + cls.dest.like(title, escape='\\'), ) else: - result = await db.execute( - select(func.count(cls.id)).filter(or_( - cls.title.like(f'%{title}%'), - cls.src.like(f'%{title}%'), - cls.dest.like(f'%{title}%') - )) + text_filter = or_( + cls.title.like(f'%{title}%'), + cls.src.like(f'%{title}%'), + cls.dest.like(f'%{title}%'), ) + stmt = select(func.count(cls.id)).filter(text_filter) + if status is not None: + stmt = stmt.filter(cls.status == status) + result = await db.execute(stmt) return result.scalar() @classmethod