用户实测反馈:"图片流程一样,现在也是点第一个" → 图片不做单独一套流程,只放开上传侧; 扩展名一路原样带到手机上(抖音按扩展名/MIME 判断视频还是图文,图片推成 .mp4 会变坏视频)。 · core/video_plan.py:VIDEO_EXTS + IMAGE_EXTS(jpg/jpeg/png/webp/heic) → MEDIA_EXTS; 报错文案改"不是视频/图片文件" · tasks/generic/publish_flow.push_video:ext 默认**从本地文件名取**(不再硬编码 .mp4); drop_old_copies 收到真实扩展名 · tasks/generic/task.py:删手机素材优先用推送时记下的 push_remote,其次按素材扩展名现算; 切号:等面板开稳再点(刚出现就点会被入场动画吞掉)、点完面板还开着(点的就是当前号) 就先关面板、一切以**回读**为准、回读窗口 12s→30s(换号会触发抖音整体重启,实测 77s 才回来); 失败时日志附带"当时屏幕上是什么"(亮屏/前台包/屏上文字)—— "点了没反应"一眼可判 · 骨架:先 stop_app 冷启动(抖音可能停在发布页/别的视频页,那种页面上没有「我」页签, 切号第一步就废);「校验账号」的选择器与「读名字」统一从 .env 取(漏了这一步会静默跳过 整个发布流程 —— 用户实际遇到的"直接完成、没发布"就是这个) · 界面:上传视频 → 上传视频/图片,accept 加 image/*,明细表"视频文件"→"素材文件",help 文案 · 文档:TASK_DEV §4.7 / API / DATA_MODEL / ARCHITECTURE / README 同步 · 测试:解析用例补图片(jpg/png/webp/大写 JPG 认,gif 不收)+ 改报错文案期望
130 lines
7.9 KiB
Python
130 lines
7.9 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""P1 纯函数测试:`名字_日期_编号` 解析(不碰 DB、不碰用户数据)。"""
|
||
import io
|
||
import os
|
||
import sys
|
||
|
||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||
# 仓库根 = scripts/tests/ 往上三级(不写死绝对路径,换机器也能跑)
|
||
_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
sys.path.insert(0, _ROOT)
|
||
os.chdir(_ROOT)
|
||
from core import video_plan as vp # noqa: E402
|
||
|
||
fails = []
|
||
|
||
|
||
def check(label, got, want):
|
||
ok = got == want
|
||
if not ok:
|
||
fails.append(label)
|
||
print((' ✅ ' if ok else ' ❌ ') + label + ('' if ok else '\n 得到 %r\n 期望 %r' % (got, want)))
|
||
|
||
|
||
print('=== parse_video_name:合法 ===')
|
||
check('名字_日期_编号', vp.parse_video_name('小王_20260912_1.mp4')[0],
|
||
{"name": "小王", "date": "2026-09-12", "seq": 1, "seq_given": True})
|
||
check('名字_日期(无编号)', vp.parse_video_name('建材王总_20260913.mp4')[0],
|
||
{"name": "建材王总", "date": "2026-09-13", "seq": None, "seq_given": False})
|
||
check('名字里带数字', vp.parse_video_name('翻斗花园我最强2号_20260913.mp4')[0]['name'], '翻斗花园我最强2号')
|
||
check('名字里带空格/表情', vp.parse_video_name('有牛奶面包 你吃吗?_20260912_2.mp4')[0]['name'],
|
||
'有牛奶面包 你吃吗?')
|
||
check('名字里带表情', vp.parse_video_name('🐟小鱼_20260912.mp4')[0]['name'], '🐟小鱼')
|
||
check('大写扩展名', vp.parse_video_name('小王_20260912.MP4')[0]['date'], '2026-09-12')
|
||
check('编号 3 位', vp.parse_video_name('小王_20260912_999.mp4')[0]['seq'], 999)
|
||
|
||
print('=== parse_video_name:图片(图文)也认 ===')
|
||
check('jpg', vp.parse_video_name('小王_20260912_1.jpg')[0],
|
||
{"name": "小王", "date": "2026-09-12", "seq": 1, "seq_given": True})
|
||
check('png(无编号)', vp.parse_video_name('建材王总_20260913.png')[0],
|
||
{"name": "建材王总", "date": "2026-09-13", "seq": None, "seq_given": False})
|
||
check('大写 JPG', vp.parse_video_name('小王_20260912.JPG')[0]['name'], '小王')
|
||
check('webp', vp.parse_video_name('小王_20260912.webp')[0]['date'], '2026-09-12')
|
||
check('gif 不收(没进白名单)',
|
||
'不是视频/图片' in (vp.parse_video_name('小王_20260912.gif')[1] or ''), True)
|
||
|
||
print('=== parse_video_name:拒收 ===')
|
||
|
||
|
||
def err_of(fn):
|
||
return fn[1]
|
||
|
||
|
||
check('名字带下划线', err_of(vp.parse_video_name('小_王_20260912.mp4')),
|
||
'名字里不能有下划线:`小_王_20260912` 里日期前面有 2 段 ——应为 名字_日期_编号')
|
||
check('名字是纯数字', err_of(vp.parse_video_name('12345678_20260912.mp4')),
|
||
'名字不能是纯数字(12345678 会被当成日期/编号)—— 请填抖音上的昵称')
|
||
check('结尾不是日期', err_of(vp.parse_video_name('小王_20260912_1234.mp4')),
|
||
'文件名格式不对:`小王_20260912_1234` 的结尾既不是日期(8 位数字,如 20260912),'
|
||
'也不是「日期_编号」—— 应为 名字_日期_编号')
|
||
check('老格式(手机号在前)', err_of(vp.parse_video_name('13382508423_20261011_01.mp4')),
|
||
'这一段(13382508423)是手机号 —— 文件名第一段要填**抖音上的名字**,'
|
||
'格式是 名字_日期_编号(现在不认手机号了)')
|
||
check('非法日期', err_of(vp.parse_video_name('小王_20260230.mp4')),
|
||
'日期 20260230 不是合法日期(应为 20260912 这种 8 位)')
|
||
check('只有一段', err_of(vp.parse_video_name('小王.mp4')),
|
||
'文件名格式不对,应为 名字_日期_编号(编号可省略,如 小王_20260912.mp4)')
|
||
check('不是视频/图片', err_of(vp.parse_video_name('小王_20260912.txt')),
|
||
'不是视频/图片文件(支持 mp4/mov/mkv/avi/3gp/flv/wmv/m4v/ts/webm/jpg/jpeg/png/webp/heic)')
|
||
check('空文件名', err_of(vp.parse_video_name('')), '文件名为空')
|
||
check('多段(名字含下划线还带日期)', err_of(vp.parse_video_name('小_王_总_20260912.mp4')),
|
||
'名字里不能有下划线:`小_王_总_20260912` 里日期前面有 3 段 ——应为 名字_日期_编号')
|
||
|
||
print('=== parse_title_line:合法 ===')
|
||
check('标题 || 正文_名字_日期_编号', vp.parse_title_line('中秋祝福 || 祝大家中秋快乐_小王_20260912_1')[0],
|
||
{"title": "中秋祝福", "body": "祝大家中秋快乐", "name": "小王",
|
||
"date": "2026-09-12", "seq": 1, "seq_given": True})
|
||
check('只有标题', vp.parse_title_line('中秋祝福_小王_20260912')[0],
|
||
{"title": "中秋祝福", "body": "", "name": "小王",
|
||
"date": "2026-09-12", "seq": None, "seq_given": False})
|
||
check('只有正文(|| 在最前)', vp.parse_title_line(' || 只有正文_小王_20260912')[0]['body'], '只有正文')
|
||
check('标题里带下划线', vp.parse_title_line('名字带_下划线也行_小王_20260912')[0]['title'], '名字带_下划线也行')
|
||
check('#话题开头的标题', vp.parse_title_line('#中秋快乐_小王_20260929')[0]['title'], '#中秋快乐')
|
||
check('正文里再有 || 原样保留', vp.parse_title_line('标题 || 正文 || 又一处_小王_20260912')[0]['body'],
|
||
'正文 || 又一处')
|
||
check('标题是纯数字', vp.parse_title_line('2026_小王_20260912_1')[0]['title'], '2026')
|
||
|
||
print('=== parse_title_line:拒收 ===')
|
||
check('只有名字/日期(没文字)', err_of(vp.parse_title_line('小王_20260912')),
|
||
'缺标题/正文内容(只有名字/日期/编号,没有文字)')
|
||
check('名字/日期/编号(没文字)', err_of(vp.parse_title_line('小王_20260912_1')),
|
||
'缺标题/正文内容(只有名字/日期/编号,没有文字)')
|
||
check('老写法(名字在前)', err_of(vp.parse_title_line('19286456013_20260912_1_标题')),
|
||
"格式不对,应为「标题 || 正文_名字_日期_编号」(编号可省略;日期是 8 位数字,如 20260912)")
|
||
check('名字是手机号', err_of(vp.parse_title_line('标题_13382508423_20260912')),
|
||
'这一段(13382508423)是手机号 —— 文件名第一段要填**抖音上的名字**,'
|
||
'格式是 名字_日期_编号(现在不认手机号了)')
|
||
check('标题里带下划线(名字取紧挨日期那段)',
|
||
vp.parse_title_line('标题_小_王_20260912')[0],
|
||
{"title": "标题_小", "body": "", "name": "王", "date": "2026-09-12",
|
||
"seq": None, "seq_given": False})
|
||
check('空行', err_of(vp.parse_title_line(' ')), '空行')
|
||
|
||
print('=== parse_titles_text:注释与多行 ===')
|
||
rows, errors = vp.parse_titles_text(
|
||
'标题一_小王_20260912\n# 这是注释\n\n#中秋快乐_小王_20260913\n'
|
||
'#真注释_没有日期\n标题没有日期')
|
||
check('收下 2 条标题', [r['title'] for r in rows], ['标题一', '#中秋快乐'])
|
||
check('# 认不出名字/日期 → 当注释忽略', [(e['line'], e['raw']) for e in errors], [(6, '标题没有日期')])
|
||
|
||
print('=== pair_slots:按名字配对 ===')
|
||
videos = [dict(vp.parse_video_name('小王_20260912_1.mp4')[0], ref='v1'),
|
||
dict(vp.parse_video_name('小王_20260912_2.mp4')[0], ref='v2'),
|
||
dict(vp.parse_video_name('小李_20260912.mp4')[0], ref='v3')]
|
||
titles = [dict(vp.parse_title_line('标题A_小王_20260912')[0], ref='t1'),
|
||
dict(vp.parse_title_line('标题B_小王_20260912')[0], ref='t2'),
|
||
dict(vp.parse_title_line('标题C_小李_20260912')[0], ref='t3')]
|
||
pairs, rejected = vp.pair_slots(videos, titles)
|
||
check('配成 3 对', len(pairs), 3)
|
||
check('没有拒收', rejected, [])
|
||
check('按 (日期,名字,编号) 排序', [(p['date'], p['name'], p['seq']) for p in pairs],
|
||
[('2026-09-12', '小李', 1), ('2026-09-12', '小王', 1), ('2026-09-12', '小王', 2)])
|
||
check('每对都配上标题', sorted(p['title']['title'] for p in pairs), ['标题A', '标题B', '标题C'])
|
||
check('配对结果里的键是 name', all('name' in p and 'phone' not in p for p in pairs), True)
|
||
|
||
print()
|
||
if fails:
|
||
print('❌ 失败 %d 项:%s' % (len(fails), ' / '.join(fails)))
|
||
sys.exit(1)
|
||
print('✅ 全部通过')
|