结构调整

This commit is contained in:
2026-07-14 16:36:24 +08:00
parent 585e9d8e15
commit 57f354cf9f
254 changed files with 84114 additions and 44 deletions
@@ -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)