Files
wechatauto-replica/wechatauto/demo_media.py
T

226 lines
8.9 KiB
Python
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.
# -*- coding: utf-8 -*-
"""wechatauto 演示:读取并下载会话中的媒体文件(图片/语音/视频/文件)。
从微信本地数据库直接取媒体消息并解密落地,无需 GUI 操作。
用法:
python demo_media.py [会话] [--out 目录] [--photos N]
[--limit N] [--ids 3,5,7] [--filter 图片,文件]
[--list] [--open]
默认行为(不带任何选项):
下载文件传输助手里最近 N 张真实照片(jpg/png,自动跳过表情)和所有视频。
参数:
会话 会话名(昵称/备注/username),默认「文件传输助手」
--photos 默认模式下下载照片张数(默认 3)
--limit 列出最近 N 条消息(默认 200)
--ids 仅下载指定 local_id(逗号分隔,此时忽略 --limit)
--filter 只处理指定类型,逗号分隔,可选 图片/语音/视频/文件
--out 下载保存目录(默认 ~/Documents/wechatauto_media
--list 仅列出媒体消息,不下载
--open 下载完成后用系统默认程序打开(仅单文件时有效)
例:
python demo_media.py # 最近 3 张照片+视频
python demo_media.py 兔仔仔 --photos 5 # 指定会话和数量
python demo_media.py 文件传输助手 --list --limit 30
python demo_media.py 我的群 --ids 105,107,109 --out D:\\media
python demo_media.py 文件传输助手 --filter 文件 --open
原理:
WeChatDB.get_messages 列出消息(local_type 识别媒体类型),
MediaDownloader.download_media 按类型自动分发:图片解密 .dat、
语音 SILK 提取、视频/文件从缓存目录复制。
"""
from __future__ import annotations
import argparse
import os
import sys
import time
try:
os.system("chcp 65001 >nul 2>&1")
except Exception:
pass
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except AttributeError:
pass
from wechatauto import WeChatDB, MediaDownloader
MEDIA_TYPES = {"图片": 3, "语音": 34, "视频": 43, "文件": 49}
def resolve_target(db: WeChatDB, raw: str) -> str:
hits = db.search_contact(raw)
return hits[0]["username"] if hits else raw
def fmt_size(n: int) -> str:
if n >= 1024 * 1024:
return f"{n / 1024 / 1024:.1f}MB"
if n >= 1024:
return f"{n / 1024:.1f}KB"
return f"{n}B"
def media_failure_reason(md, who: str, local_id: int) -> str:
"""下载失败时给出细分原因(表情容器 / 未落地 / 不支持)。
图片消息本地缓存的 .dat 解密后若为 wxgf(微信动画表情容器),
库会按设计不落盘;本函数复现该判定以区分「表情」与「真失败」。
"""
row = md.db.get_message_row(who, local_id)
if not row:
return "消息不存在"
t = row["local_type"]
if t == 3: # 图片
md5 = md._img_md5(row)
dat = md._find_dat(who, md5, row["create_time"]) if md5 else None
if not dat:
return "本地无 .dat 缓存(未在微信查看过/已清理)"
try:
data = md.decrypt_image(dat)
except Exception as e:
return f"解密失败:{str(e)[:40]}"
if data[:4] == b"wxgf":
return "微信动画表情容器(wxgf"
return "未知图片格式"
if t in (34, 43, 49):
return "本地缓存未落地或文件已清理"
return "类型不支持"
def main():
parser = argparse.ArgumentParser(
description="wechatauto 演示:读取/下载微信会话中的媒体文件",
add_help=False)
parser.add_argument("target", nargs="?", default="26级金高新生群1群",
help="会话名(昵称/备注/username),默认文件传输助手")
parser.add_argument("--limit", type=int, default=1000, help="列出最近 N 条(默认 200")
parser.add_argument("--photos", type=int, default=10, help="默认模式下载照片张数(默认 3)")
parser.add_argument("--ids", default=None,
help="仅下载指定 local_id,逗号分隔,如 105,107")
parser.add_argument("--filter", default=None,
help="仅处理指定类型,逗号分隔:图片/语音/视频/文件")
parser.add_argument("--out", default=None, help="下载保存目录")
parser.add_argument("--image-key", default=None,
help="图片 AES 密钥(16 位,若内存扫描不可用)")
parser.add_argument("--list", action="store_true", help="仅列出媒体消息,不下载")
parser.add_argument("--open", action="store_true",
help="下载后用系统默认程序打开(仅单文件)")
parser.add_argument("-h", "--help", action="help")
args = parser.parse_args()
filt = None
if args.filter:
filt = set(t.strip() for t in args.filter.split(",") if t.strip())
unknown = [t for t in filt if t not in MEDIA_TYPES]
if unknown:
print(f"未知类型:{', '.join(unknown)}(可选:图片/语音/视频/文件)")
sys.exit(1)
print("=" * 60)
print("wechatauto 媒体读取演示")
print("=" * 60)
db = WeChatDB()
info = db.get_self_info()
print(f"账号:{info.get('nick_name') or info.get('username')}")
who = resolve_target(db, args.target)
print(f"会话:{args.target} -> {who}")
# ---- 指定 local_id:直接下载 ----
if args.ids:
ids = [int(x.strip()) for x in args.ids.split(",") if x.strip().isdigit()]
if not ids:
print("--ids 无效")
sys.exit(1)
md = MediaDownloader(db, save_dir=args.out, image_key=args.image_key)
for lid in ids:
row = db.get_message_row(who, lid)
if not row:
print(f" local_id={lid} 未找到,跳过")
continue
name = next((k for k, v in MEDIA_TYPES.items() if v == row["local_type"]), "")
t = time.strftime("%m-%d %H:%M", time.localtime(row["create_time"]))
label = name or ("类型%d" % row["local_type"])
print(f"\n[local_id={lid}] {t} {label}")
out = md.download_media(who, lid, args.out)
if out:
print(f" ✅ 已保存:{out} ({fmt_size(os.path.getsize(out))})")
if args.open:
os.startfile(out)
else:
print(f" ⚠️ 下载失败:{media_failure_reason(md, who, lid)}")
print("\n完成。")
sys.exit(0)
# ---- 列出最近消息并筛选媒体 ----
mode = "默认照片+视频" if filt is None else "+".join(sorted(filt))
msgs = db.get_messages(who, limit=args.limit)
media_msgs = []
print(f"\n最近 {len(msgs)} 条消息中的媒体项(模式:{mode}):")
for m in reversed(msgs):
name = MEDIA_TYPES.get(m.get("type"), "")
if not name:
continue
if filt and m.get("type") not in filt:
continue
if filt is None and m.get("type") not in ("图片", "视频"):
continue
t = time.strftime("%m-%d %H:%M", time.localtime(m["create_time"]))
sender = "我" if m["sender_id"] == 2 else "对方"
media_msgs.append(m)
print(f" {m['local_id']:<6} {t} {sender} {m.get('type')} {m.get('content', '')[:40]}")
if args.list:
print(f"\n{len(media_msgs)} 条媒体消息(用 --ids 或 --filter 配合下载)。")
sys.exit(0)
if not media_msgs:
print("\n没有符合条件的媒体消息。")
sys.exit(0)
# ---- 下载筛选出的媒体 ----
md = MediaDownloader(db, save_dir=args.out, image_key=args.image_key)
ok = skip = fail = 0
photo_done = 0
for m in media_msgs:
t = time.strftime("%H:%M:%S", time.localtime(m["create_time"]))
print(f"\n[local_id={m['local_id']}] {t} {m.get('type')}")
is_photo_target = filt is None and m.get("type") == "图片"
if is_photo_target and photo_done >= args.photos:
print(f" 已达照片上限 {args.photos} 张,停止")
break
out = md.download_media(who, m["local_id"], args.out)
if out:
print(f" ✅ {fmt_size(os.path.getsize(out))} {out}")
ok += 1
if is_photo_target:
photo_done += 1
else:
reason = media_failure_reason(md, who, m["local_id"])
if "表情容器" in reason:
print(f" {reason}")
skip += 1
else:
print(f" ⚠️ 下载失败:{reason}")
fail += 1
print(f"\n完成:成功 {ok} 项,跳过 {skip} 项(表情),失败 {fail} 项。"
+ (f" 保存目录:{os.path.abspath(args.out or md.save_dir)}" if ok else ""))
if args.open and ok == 1:
out = md.download_media(who, media_msgs[0]["local_id"], args.out)
if out:
os.startfile(out)
sys.exit(0 if fail == 0 else 1)
if __name__ == "__main__":
main()