Files
openclaw-config/skill-workshop/proposals/calibre-cleaner-20260624-cdbeb219a3/rollback.json
T
2026-07-02 11:00:05 +08:00

2 lines
6.7 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"schema":"openclaw.skill-workshop.rollback.v1","proposalId":"calibre-cleaner-20260624-cdbeb219a3","writtenAt":"2026-06-24T03:03:30.908Z","targetSkillFile":"/home/yangxuan/.openclaw/workspace/storage/skills/calibre-cleaner/SKILL.md","action":"update","previousContent":"---\nname: \"calibre-cleaner\"\ndescription: \"清洗 Calibre 书库:修复损坏书名/提取作者/检测编码损坏、小文件的批量删除\"\n---\n\n# Calibre 书库清洗 Skill\n\n## 概述\n\nCalibre 书库整理三件套:\n\n1. **书名清洗** —— 修复从 txt 导入产生的 HTML 标签/长文本/乱码书名\n2. **作者提取** —— 从书名中自动提取作者名(匹配 `作者:xxx`、`-xxx`、`BY xxx` 等格式)\n3. **脏数据清理** —— 检测并删除编码损坏的 txt、小于 10KB 的垃圾文件\n\n## 前置条件\n\n- 书库路径:`/home/yangxuan/calibre/books`\n- 元数据文件:`/home/yangxuan/calibre/books/metadata.db`\n- 运行前须停止其他 Calibre 进程(桌面版、Docker Calibre-Web\n- 运行前 **必须备份** `metadata.db`\n\n---\n\n## 一、书名清洗 & 作者提取\n\n(详见 skill 原内容,此处保留完整清洗逻辑)\n\n---\n\n## 二、脏数据清理\n\n### 2.1 检测编码损坏的 TXT\n\n检测条件:中文比例 < 10% 且含有乱码字符(`Ŀ¼ĴƿȿɿʼÈÌÒÙ(` 等 UTF-8 编码错误字)\n\n```python\nimport sqlite3, os\n\nbad_char_set = set('Ŀ¼ĴƿȿɿʼÈÌÒÙ(')\nbad_ids = []\n\nfor bid, title, bpath, fname in rows:\n full_path = os.path.join('/home/yangxuan/calibre/books', bpath, fname + '.txt')\n if not os.path.exists(full_path):\n continue\n with open(full_path, 'rb') as f:\n raw = f.read(20480)\n text = raw.decode('utf-8', errors='replace')\n chinese = sum(1 for c in text if '\\u4e00' <= c <= '\\u9fff')\n total = len(text)\n ratio = chinese / total if total > 0 else 0\n has_bad = any(c in bad_char_set for c in text)\n if ratio < 0.10 and has_bad:\n bad_ids.append(bid)\n```\n\n### 2.2 检测小文件\n\n```python\nc.execute('''\n SELECT b.id, b.path FROM books b \n JOIN data d ON d.book = b.id \n WHERE d.format = 'TXT' AND d.uncompressed_size < 10240\n''')\n```\n\n### 2.3 批量删除(通过 SQLite 直写)\n\n停掉 calibre 进程后,通过 SQLite 删除记录并清理文件系统:\n\n```sql\nDELETE FROM data WHERE book = ?\nDELETE FROM books_authors_link WHERE book = ?\nDELETE FROM books_languages_link WHERE book = ?\nDELETE FROM books_publishers_link WHERE book = ?\nDELETE FROM books_series_link WHERE book = ?\nDELETE FROM books_tags_link WHERE book = ?\nDELETE FROM comments WHERE book = ?\nDELETE FROM identifiers WHERE book = ?\nDELETE FROM books WHERE id = ?\n```\n\n同时删除对应目录:`shutil.rmtree(os.path.join('/home/yangxuan/calibre/books', bpath))`\n\n### 2.4 完整删除脚本\n\n```python\nimport sqlite3, os, shutil\n\ndb_path = '/home/yangxuan/calibre/books/metadata.db'\nconn = sqlite3.connect(db_path)\nc = conn.cursor()\n\nc.execute('''\n SELECT b.id, b.title, b.path, d.name, d.uncompressed_size\n FROM books b \n JOIN data d ON d.book = b.id \n WHERE d.format = 'TXT'\n''')\nrows = c.fetchall()\n\nbad_char_set = set('Ŀ¼ĴƿȿɿʼÈÌÒÙ(')\nto_delete = []\n\nfor bid, title, bpath, fname, size in rows:\n full_path = os.path.join('/home/yangxuan/calibre/books', bpath, fname + '.txt')\n if not os.path.exists(full_path):\n continue\n with open(full_path, 'rb') as f:\n raw = f.read(20480)\n text = raw.decode('utf-8', errors='replace')\n chinese = sum(1 for c in text if '\\u4e00' <= c <= '\\u9fff')\n ratio = chinese / len(text) if len(text) > 0 else 0\n has_bad = any(c in bad_char_set for c in text)\n if (ratio < 0.10 and has_bad) or size < 10240:\n to_delete.append((bid, bpath))\n\nprint(f\"待删除: {len(to_delete)}\")\nfor bid, bpath in to_delete:\n c.execute('DELETE FROM data WHERE book=?', (bid,))\n c.execute('DELETE FROM books_authors_link WHERE book=?', (bid,))\n c.execute('DELETE FROM books_languages_link WHERE book=?', (bid,))\n c.execute('DELETE FROM books_publishers_link WHERE book=?', (bid,))\n c.execute('DELETE FROM books_series_link WHERE book=?', (bid,))\n c.execute('DELETE FROM books_tags_link WHERE book=?', (bid,))\n c.execute('DELETE FROM comments WHERE book=?', (bid,))\n c.execute('DELETE FROM identifiers WHERE book=?', (bid,))\n c.execute('DELETE FROM books WHERE id=?', (bid,))\n full_dir = os.path.join('/home/yangxuan/calibre/books', bpath)\n if os.path.exists(full_dir):\n shutil.rmtree(full_dir)\n\nconn.commit()\nconn.close()\nprint(f\"完成,删除: {len(to_delete)}\")\n```\n\n---\n\n## 三、常用操作\n\n### 3.1 查看 XX 作者的书\n\n```bash\nsqlite3 /home/yangxuan/calibre/books/metadata.db \"\nSELECT b.id, b.title\nFROM books b JOIN books_authors_link l ON b.id = l.book\nJOIN authors a ON l.author = a.id\nWHERE a.name = 'Unknown'\nLIMIT 20;\n\"\n```\n\n### 3.2 统计作者分布\n\n```bash\nsqlite3 /home/yangxuan/calibre/books/metadata.db \"\nSELECT a.name, COUNT(*) as cnt\nFROM books b JOIN books_authors_link l ON b.id = l.book\nJOIN authors a ON l.author = a.id\nGROUP BY a.name ORDER BY cnt DESC LIMIT 20;\n\"\n```\n\n### 3.3 书库统计\n\n```bash\npython3 -c \"\nimport sqlite3\nconn = sqlite3.connect('/home/yangxuan/calibre/books/metadata.db')\nc = conn.cursor()\nc.execute('SELECT COUNT(*) FROM books')\nprint(f'总书籍: {c.fetchone()[0]}')\nc.execute('SELECT COUNT(*) FROM data WHERE format=\\\"TXT\\\"')\nprint(f'TXT 文件: {c.fetchone()[0]}')\nconn.close()\n\"\n```\n\n---\n\n## 四、操作流程(推荐顺序)\n\n```bash\ncd /home/yangxuan/calibre\n\n# 1. 停服务 + 备份\ndocker compose down\ncp books/metadata.db books/metadata.db.backup.$(date +%Y%m%d)\nkillall calibre 2>/dev/null\n\n# 2. 脏数据清理(编码损坏 + 小文件)\npython3 /path/to/clean_dirty_data.py\n\n# 3. 书名清洗 & 作者提取\ncalibre-debug /path/to/fix_titles.py\n\n# 4. 重启\ndocker compose up -d\n```\n\n---\n\n## 五、已知问题\n\n### `set_metadata` 报 `Errno 36 文件名过长`\n\n当 `author_sort` 字段超 80 字节时,Calibre 路径会触及 ext4 255 字节限制。先清理极长作者名:\n\n```bash\nsqlite3 /home/yangxuan/calibre/books/metadata.db \"\nUPDATE authors SET name='Unknown' WHERE LENGTH(name) > 30;\n\"\n```\n\n### WSL `/mnt/f` 删除 NTFS 文件权限不够\n\nWSL 的 drvfs 默认不带 metadata 选项,`sudo rm`、`os.remove`、`cmd.exe /c del` 均无法删除 NTFS 文件。不要在 WSL 下尝试跨分区删除操作,直接让 Windows 侧(文件资源管理器/PowerShell)操作。\n","previousContentHash":"dd59723daea61a10edc128f446e41d71c07b03cbe6064ea5140e260a21fce51e"}