sync from WSL host: latest openclaw config update
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: "f-download-sorter"
|
||||
description: "整理 /mnt/f 下下载的视频文件:扫描→清垃圾→按规则归档到7个大类目录"
|
||||
---
|
||||
|
||||
# /mnt/f 下载文件批量归档整理
|
||||
|
||||
## 用途
|
||||
把 source_dirs 里堆叠的下载视频,按规则自动清理、归类到大类目录。
|
||||
|
||||
## Touch point
|
||||
用户说"整理 /mnt/f 视频""归档 collections""下载文件太多理一下"等,调用此 skill。
|
||||
|
||||
## 流程
|
||||
1. 运行 `scripts/scan_and_clean.py` → 扫描 + 清垃圾
|
||||
2. 运行 `scripts/classify_and_move.py` → 按规则归档
|
||||
3. 报告结果
|
||||
|
||||
## 配置
|
||||
核心配置在 `config.json`,可自定义:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `source_dirs` | 源目录列表,默认 `["/mnt/f/collections", "/mnt/f/video"]` |
|
||||
| `keep_as_is` | 已有元数据不动的内容 |
|
||||
| `min_size_mb` | 小于此值且无实质视频的目录自动删除 |
|
||||
| `categories` | 分类规则数组,每个有 `dir`(目标路径)和 `keywords`(关键词列表) |
|
||||
| `fallback_dir` | 无关键词匹配时的兜底目录 |
|
||||
|
||||
### 添加新分类
|
||||
在 `categories` 数组中新增一项:
|
||||
```json
|
||||
{
|
||||
"dir": "/mnt/f/新分类",
|
||||
"keywords": ["关键词A", "关键词B"]
|
||||
}
|
||||
```
|
||||
|
||||
### 添加新源目录
|
||||
在 `source_dirs` 中追加路径。
|
||||
|
||||
### 保护不移动的内容
|
||||
在 `keep_as_is` 列表追加名称。
|
||||
|
||||
## 注意事项
|
||||
- 同一分区 mv,不复制数据
|
||||
- 不预览文件内容,只基于文件名关键词
|
||||
- WSL `/mnt/` 跨文件系统扫描慢,大数量时建议用 Windows 原生 Python 执行
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"source_dirs": [
|
||||
"/mnt/f/collections",
|
||||
"/mnt/f/video"
|
||||
],
|
||||
"keep_as_is": ["重生之征服郭伯母"],
|
||||
"min_size_mb": 50,
|
||||
"categories": [
|
||||
{
|
||||
"dir": "/mnt/f/过那条巷子",
|
||||
"keywords": ["过那条巷子"]
|
||||
},
|
||||
{
|
||||
"dir": "/mnt/f/2048合集",
|
||||
"keywords": ["2048"]
|
||||
},
|
||||
{
|
||||
"dir": "/mnt/f/AI短剧",
|
||||
"keywords": ["AI短剧", "AI自制", "AI视频", "美丽新世界", "少年阿宾", "NTR寝取", "嫂子", "魔女小姨子", "桃运圣医", "我和老师的约定"]
|
||||
},
|
||||
{
|
||||
"dir": "/mnt/f/FC2福利姬",
|
||||
"keywords": ["FC2", "Comatozze", "Dainty", "KAKAE", "lananlanan", "P站"]
|
||||
},
|
||||
{
|
||||
"dir": "/mnt/f/合集种子",
|
||||
"keywords": ["BT-btt"]
|
||||
},
|
||||
{
|
||||
"dir": "/mnt/f/国产露脸",
|
||||
"keywords": ["19岁", "冯帆", "momocats", "露脸才是王道", "香蕉"]
|
||||
},
|
||||
{
|
||||
"dir": "/mnt/f/自拍泄漏",
|
||||
"keywords": ["泄密", "自拍", "自慰", "原相机", "情侣", "校花", "嫩妹", "福利姬", "裸聊", "绿帽", "良家", "约炮", "狗哥", "土豪", "原档", "维度", "桃子", "娇娇", "学妹", "反差"]
|
||||
}
|
||||
],
|
||||
"fallback_dir": "/mnt/f/AI短剧"
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
按规则分类归档 source_dirs 中的目录到 categories 定义的大类目录
|
||||
读配置文件:与脚本同目录下的 config.json
|
||||
"""
|
||||
import os, json, shutil
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def load_config():
|
||||
cfg_path = os.path.join(SCRIPT_DIR, "..", "config.json")
|
||||
with open(cfg_path) as f:
|
||||
return json.load(f)
|
||||
|
||||
def classify(name, cfg):
|
||||
"""返回目标目录路径,或 None(不移动)"""
|
||||
if name in cfg.get("keep_as_is", []):
|
||||
return None
|
||||
for cat in cfg["categories"]:
|
||||
keywords = cat["keywords"]
|
||||
if any(k in name for k in keywords):
|
||||
return cat["dir"]
|
||||
fallback = cfg.get("fallback_dir")
|
||||
if fallback:
|
||||
return fallback
|
||||
return None
|
||||
|
||||
def main():
|
||||
cfg = load_config()
|
||||
src_dirs = cfg["source_dirs"]
|
||||
|
||||
moves = []
|
||||
for base in src_dirs:
|
||||
if not os.path.isdir(base):
|
||||
print(f" [WARN] 目录不存在: {base}")
|
||||
continue
|
||||
for entry in sorted(os.listdir(base)):
|
||||
full = os.path.join(base, entry)
|
||||
if not os.path.isdir(full):
|
||||
continue
|
||||
dst_base = classify(entry, cfg)
|
||||
if dst_base is None:
|
||||
print(f" ○ 不动: {entry}")
|
||||
continue
|
||||
dst = os.path.join(dst_base, entry)
|
||||
if os.path.exists(dst):
|
||||
i = 1
|
||||
while os.path.exists(f"{dst}_{i}"): i += 1
|
||||
dst = f"{dst}_{i}"
|
||||
moves.append((full, dst))
|
||||
|
||||
# 创建目录
|
||||
for _, d in moves:
|
||||
os.makedirs(os.path.dirname(d), exist_ok=True)
|
||||
|
||||
# 执行移动
|
||||
print(f"开始移动 {len(moves)} 个条目...")
|
||||
for src, dst in moves:
|
||||
try:
|
||||
shutil.move(src, dst)
|
||||
print(f" ✓ {os.path.basename(src)}")
|
||||
except Exception as e:
|
||||
print(f" ✗ {os.path.basename(src)} -> {e}")
|
||||
|
||||
# 清理空源目录
|
||||
for d in src_dirs:
|
||||
if os.path.isdir(d) and not os.listdir(d):
|
||||
os.rmdir(d)
|
||||
print(f" ✓ 已删空目录: {d}")
|
||||
|
||||
# 报告
|
||||
base = os.path.commonpath([os.path.dirname(d) for _, d in moves])
|
||||
for _, d in moves:
|
||||
base = os.path.commonpath([base, os.path.dirname(d)])
|
||||
base = base or "/"
|
||||
|
||||
print(f"\n=== 归档完成 ===")
|
||||
seen = set()
|
||||
for _, d in moves:
|
||||
parent = os.path.dirname(d)
|
||||
if parent not in seen:
|
||||
sz = 0
|
||||
cnt = 0
|
||||
for dp, dn, fn in os.walk(parent):
|
||||
for f in fn:
|
||||
cnt += 1
|
||||
try: sz += os.path.getsize(os.path.join(dp, f))
|
||||
except: pass
|
||||
print(f" {parent}/ ({cnt} 文件, {sz/1024**3:.1f}GB)")
|
||||
seen.add(parent)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
扫描 + 清理垃圾
|
||||
遍历 source_dirs,清理空壳目录和小视频残片
|
||||
读配置文件:与脚本同目录下的 config.json
|
||||
"""
|
||||
import os, json, sys
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def load_config():
|
||||
cfg_path = os.path.join(SCRIPT_DIR, "..", "config.json")
|
||||
with open(cfg_path) as f:
|
||||
return json.load(f)
|
||||
|
||||
def scan_and_clean(cfg):
|
||||
src_dirs = cfg["source_dirs"]
|
||||
min_size = cfg["min_size_mb"] * 1024 * 1024
|
||||
video_exts = {'.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.ts','.m4v','.rmvb'}
|
||||
|
||||
targets = []
|
||||
print("=== 扫描 ===")
|
||||
for base in src_dirs:
|
||||
if not os.path.isdir(base):
|
||||
print(f" [WARN] 目录不存在: {base}")
|
||||
continue
|
||||
for entry in sorted(os.listdir(base)):
|
||||
full = os.path.join(base, entry)
|
||||
if not os.path.isdir(full):
|
||||
continue
|
||||
total_sz = 0
|
||||
real_video = 0
|
||||
try:
|
||||
for dp, dn, fn in os.walk(full):
|
||||
for f in fn:
|
||||
fp = os.path.join(dp, f)
|
||||
try:
|
||||
total_sz += os.path.getsize(fp)
|
||||
except: pass
|
||||
if os.path.splitext(f)[1].lower() in video_exts:
|
||||
real_video += 1
|
||||
except: pass
|
||||
|
||||
action = "KEEP"
|
||||
if total_sz < min_size and real_video == 0:
|
||||
action = "DELETE"
|
||||
elif total_sz < min_size and real_video > 0:
|
||||
action = "DELETE"
|
||||
|
||||
print(f" [{action:>6}] {entry} ({total_sz/1024**2:.0f}MB, {real_video}视频)")
|
||||
|
||||
if action == "DELETE":
|
||||
targets.append(full)
|
||||
|
||||
if targets:
|
||||
print(f"\n待删除: {len(targets)} 个")
|
||||
print("开始删除...")
|
||||
import shutil
|
||||
for d in targets:
|
||||
shutil.rmtree(d)
|
||||
print(f" ✓ 已删: {os.path.basename(d)}")
|
||||
else:
|
||||
print("\n无需清理。")
|
||||
|
||||
if __name__ == "__main__":
|
||||
cfg = load_config()
|
||||
scan_and_clean(cfg)
|
||||
Reference in New Issue
Block a user