feat(大屏): 顶部加「📤 发布」按钮 —— 选设备直接注入视频/图片、注入剪贴板
用户要求:"大屏按钮右边加一个按钮叫发布,就是选择设备号注入视频或者图片,位置就和发布页面的一样, 然后还有在选择设备号注入剪贴板"。 大屏(`/wall`)顶栏「一键亮屏」右边加 **「📤 发布」**,点开是一块面板: · **设备**下拉(设备名 + 地址,离线会标注;每次打开现取最新列表) · **视频 / 图片**:选文件 → 「注入素材」→ 推到该机相册 `/sdcard/DCIM/Camera` · **剪贴板文字**:填一段 → 「注入剪贴板」→ 写进该机剪贴板(发文案用) · 面板下方是操作日志(✅/❌ + 原因),失败会写清为什么 后端(`web/tools_api.py`,都按 **设备位** 放行 —— 大屏上随手一推是设备操作,不该要管理员位): · `POST /api/tools/push_material`(form-data:file + serial): 保存上传 → `publish_flow.push_video` 推到相册(文件名带 `wall` 前缀、mtime 改成现在, 保证它在相册里排最前)→ `verify_pushed` 校验进没进相册索引 → 返回 `{ok,msg,remote,verify}` · **不占位、不写库**:与「发布计划 → 推送」是两回事(那条按计划走占位/去重/回写状态); 这条是"手上有一段素材,想马上在这台机器上发" · 扩展名按 `video_plan.MEDIA_EXTS` 校验(视频+图片),不在其中 → 400;设备不在线 → 400 · 临时目录用完即删(`finally` 里 rmtree) · `POST /api/tools/push_clipboard`:与 `/api/tools/clipboard/set` **同一个实现** (`_set_device_clipboard`),只是权限位不同 真机验过(A01):剪贴板注入 ✅;1×1 测试图推到相册 ✅(`verify=no_index` 的 ⚠ 是这张图太小、 媒体库没索引它,真视频/图片是 ✅);`.txt` 被 400 拦住 ✅;测试文件已从手机删掉、不留痕。 文档:doc/API.md 加两条接口(含请求/返回/错误口径)、README 实时看屏那行补上发布面板。
This commit is contained in:
+101
-2
@@ -1,7 +1,11 @@
|
||||
"""工具域 API:adb 终端/剪贴板注入/应用版本。"""
|
||||
"""工具域 API:adb 终端/剪贴板注入/应用版本/大屏「发布」注入素材。"""
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import shlex
|
||||
import tempfile
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
@@ -11,7 +15,7 @@ from core.adb_helper import adb_connect, adb_connect_light, _ADB_LOCK, _adb
|
||||
from config import ADB_PATH
|
||||
from core.logger import get_logger
|
||||
from web import context
|
||||
from web.auth import admin_required, perm_required
|
||||
from web.auth import admin_required, perm_required, PERM_DEVICES
|
||||
from web.common import _merged_device_list
|
||||
|
||||
_log = get_logger("web")
|
||||
@@ -99,6 +103,101 @@ def _set_device_clipboard(serial, text):
|
||||
return inject_clipboard(serial, text)
|
||||
|
||||
|
||||
@bp.route("/api/tools/push_clipboard", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_tools_push_clipboard():
|
||||
"""大屏「发布」面板:往指定设备注入剪贴板文字。
|
||||
|
||||
与 `/api/tools/clipboard/set` 是**同一个实现**(`_set_device_clipboard`),
|
||||
只是权限按「设备」位放行 —— 大屏上随手把一段文案抄到手机上,是设备操作,
|
||||
不该要求管理员位。请求:`{"serial": "..."}` 或 `{"serials": ["..."]}`,外加 `text`。
|
||||
"""
|
||||
data = request.json or {}
|
||||
text = (data.get("text") or "").strip()
|
||||
serials = data.get("serials")
|
||||
if not isinstance(serials, list) or not serials:
|
||||
one = (data.get("serial") or "").strip()
|
||||
serials = [one] if one else []
|
||||
if not serials:
|
||||
return jsonify({"ok": False, "error": "未选择设备"}), 400
|
||||
if not text:
|
||||
return jsonify({"ok": False, "error": "注入内容不能为空"}), 400
|
||||
results = {}
|
||||
for serial in serials:
|
||||
ok, msg = _set_device_clipboard(serial, text)
|
||||
results[serial] = {"ok": ok, "msg": msg}
|
||||
ok_count = sum(1 for v in results.values() if v["ok"])
|
||||
return jsonify({"ok": ok_count == len(results), "results": results,
|
||||
"ok_count": ok_count, "fail_count": len(results) - ok_count,
|
||||
"msg": "已注入 %d 台" % ok_count if ok_count else "注入失败",
|
||||
"error": None if ok_count == len(results)
|
||||
else "%d 台注入失败" % (len(results) - ok_count)})
|
||||
|
||||
|
||||
@bp.route("/api/tools/push_material", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_tools_push_material():
|
||||
"""大屏「发布」面板:把上传的**视频/图片直接推到指定那台设备**的相册。
|
||||
|
||||
form-data:`file`(视频/图片)、`serial`、`album_dir`(可省,默认 /sdcard/DCIM/Camera)
|
||||
返回 `{ok, msg, remote, verify}`
|
||||
|
||||
与「发布计划 → 推送」的区别:那条是**按计划**推(原子占位、去重、回写状态、
|
||||
按账号名找设备);这条是**临时手动推一个文件** —— 给"手上有一段素材,想马上
|
||||
在这台机器上发"用,所以**不占位、不写库**,手机相册里就多一个文件
|
||||
(名字带 `wall` 前缀,和计划推送的素材分得开)。
|
||||
|
||||
为什么还要 u2 连接:推完要 `touch` 改时间(相册/抖音按时间倒序,"第一个素材"
|
||||
才是刚推的这个)+ 校验进没进相册索引 —— 这些都走设备 shell,见 publish_flow。
|
||||
"""
|
||||
from core import video_plan as vp
|
||||
from tasks.generic.publish_flow import ALBUM_DIR, push_video, verify_pushed
|
||||
from core.account_probe import _connect
|
||||
|
||||
f = request.files.get("file")
|
||||
serial = (request.form.get("serial") or "").strip()
|
||||
album_dir = (request.form.get("album_dir") or "").strip() or ALBUM_DIR
|
||||
if not serial:
|
||||
return jsonify({"ok": False, "error": "未选择设备"}), 400
|
||||
if not f or not f.filename:
|
||||
return jsonify({"ok": False, "error": "未选择文件"}), 400
|
||||
ext = os.path.splitext(f.filename)[1].lower()
|
||||
if ext not in vp.MEDIA_EXTS:
|
||||
return jsonify({"ok": False, "error": "只支持视频/图片(%s),当前 %s"
|
||||
% ("、".join(vp.MEDIA_EXTS), ext or "无扩展名")}), 400
|
||||
try:
|
||||
online = set(device_pool.list_online())
|
||||
except Exception:
|
||||
online = set()
|
||||
if online and serial not in online:
|
||||
return jsonify({"ok": False, "error": "这台设备当前不在线"}), 400
|
||||
|
||||
tmpdir = tempfile.mkdtemp(prefix="wall_push_")
|
||||
local = os.path.join(tmpdir, vp.safe_name(f.filename))
|
||||
try:
|
||||
f.save(local)
|
||||
size = os.path.getsize(local)
|
||||
d = _connect(serial, timeout=25)
|
||||
remote, err = push_video(d, serial, local, "wall" + uuid.uuid4().hex[:8],
|
||||
album_dir=album_dir)
|
||||
if err:
|
||||
return jsonify({"ok": False, "error": err})
|
||||
verify, vmsg = verify_pushed(d, remote)
|
||||
if verify == "nofile":
|
||||
return jsonify({"ok": False, "error": "推送校验失败:%s" % vmsg})
|
||||
ok = verify == "ok"
|
||||
_log.info("大屏发布:%s ← %s(%d 字节)→ %s [%s]"
|
||||
% (serial, f.filename, size, remote, verify))
|
||||
return jsonify({"ok": True, "remote": remote, "verify": verify, "size": size,
|
||||
"msg": "已推到 %s" % remote if ok else "已推到 %s(⚠ %s)" % (remote, vmsg)})
|
||||
except Exception as e:
|
||||
_log.warning("大屏发布失败: %s: %s" % (type(e).__name__, e))
|
||||
return jsonify({"ok": False, "error": "推送异常: %s: %s"
|
||||
% (type(e).__name__, str(e)[:120])})
|
||||
finally:
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
|
||||
|
||||
def _app_ver_on_device(serial, pkg):
|
||||
"""单设备包版本查询:pm path 检查安装 → dumpsys 取 versionName/versionCode。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user