's'
This commit is contained in:
@@ -44,7 +44,6 @@
|
|||||||
```bash
|
```bash
|
||||||
cd electron
|
cd electron
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm run prepare-easytier # 下载 Windows easytier 二进制(可选,见 package.json)
|
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
https://live.douyin.com/116645105013,主播: 张火火摆摊日记
|
https://live.douyin.com/11664523242
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"networkName": "lr_自动生成",
|
|
||||||
"networkSecret": "自动生成请勿手填",
|
|
||||||
"peerUrls": []
|
|
||||||
}
|
|
||||||
@@ -1,3 +1 @@
|
|||||||
# 每行一个抖音直播间地址,例如:
|
https://live.douyin.com/11664523242
|
||||||
# https://live.douyin.com/123456789
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
https://live.douyin.com/28358181657,主播: 罗瑞和夜市_印度飞饼_(阿泽)
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# 每行一个抖音直播间地址
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[youtube]
|
[youtube]
|
||||||
key = ue78-1c3e-mr9g-14mz-9r4z
|
key = 112313123124124124124124124df
|
||||||
fast_audio = 1
|
fast_audio = 1
|
||||||
output_resolution = 720p
|
output_resolution = 720p
|
||||||
use_gpu = 无
|
use_gpu = 无
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
[youtube]
|
|
||||||
key = h018-hqur-uhtv-5bt2-1x3q
|
|
||||||
fast_audio = 1
|
|
||||||
output_resolution = 720p
|
|
||||||
use_gpu = 无
|
|
||||||
audio_denoise = 否
|
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[youtube]
|
[youtube]
|
||||||
key = ue78-1c3e-mr9g-14mz-9r4z
|
key = 112313123124124124124124124df
|
||||||
fast_audio = 1
|
fast_audio = 1
|
||||||
output_resolution = 720p
|
output_resolution = 720p
|
||||||
use_gpu = 无
|
use_gpu = 无
|
||||||
|
|||||||
@@ -2,17 +2,11 @@
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"autoManaged": true,
|
"autoManaged": true,
|
||||||
"channels": [
|
"channels": [
|
||||||
{
|
|
||||||
"id": "ch_default",
|
|
||||||
"name": "默认线路",
|
|
||||||
"douyinUrl": "",
|
|
||||||
"youtubeKey": "ue78-1c3e-mr9g-14mz-9r4z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "ch_1774436412",
|
"id": "ch_1774436412",
|
||||||
"name": "频道 2",
|
"name": "频道 2",
|
||||||
"douyinUrl": "",
|
"douyinUrl": "",
|
||||||
"youtubeKey": ""
|
"youtubeKey": "112313123124124124124124124df"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ch_1774436414",
|
"id": "ch_1774436414",
|
||||||
@@ -98,12 +92,6 @@
|
|||||||
"douyinUrl": "",
|
"douyinUrl": "",
|
||||||
"youtubeKey": ""
|
"youtubeKey": ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "ch_1774438395",
|
|
||||||
"name": "频道 17",
|
|
||||||
"douyinUrl": "",
|
|
||||||
"youtubeKey": "h018-hqur-uhtv-5bt2-1x3q"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "ch_1774439571",
|
"id": "ch_1774439571",
|
||||||
"name": "频道 18",
|
"name": "频道 18",
|
||||||
|
|||||||
25
web2.py
25
web2.py
@@ -46,8 +46,9 @@ from web2_relay_pro import (
|
|||||||
write_url_config_for_channel,
|
write_url_config_for_channel,
|
||||||
)
|
)
|
||||||
from web2_youtube_ini import parse_youtube_ini, serialize_youtube_ini
|
from web2_youtube_ini import parse_youtube_ini, serialize_youtube_ini
|
||||||
from web2_host_caps import gpu_status_payload
|
from web2_host_caps import gpu_status_payload, machine_summary_payload
|
||||||
from web2_douyin_preview import fetch_douyin_anchor_name
|
from web2_douyin_preview import fetch_douyin_anchor_name
|
||||||
|
from web2_pocketbase import REQUIRE_PB, pb_auth_refresh_and_validate
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -243,7 +244,7 @@ register_wechat_chatbot_routes(app, BASE_DIR)
|
|||||||
|
|
||||||
@app.get("/client/overview")
|
@app.get("/client/overview")
|
||||||
async def client_overview():
|
async def client_overview():
|
||||||
"""Electron 工作台:频道列表、微信摘要(EasyTier 节点由主进程 easytier-cli 查询)。"""
|
"""Electron 工作台:频道列表、微信摘要。"""
|
||||||
return build_client_overview(CONFIG_DIR, BASE_DIR)
|
return build_client_overview(CONFIG_DIR, BASE_DIR)
|
||||||
|
|
||||||
|
|
||||||
@@ -385,6 +386,12 @@ async def host_gpu_status():
|
|||||||
return gpu_status_payload()
|
return gpu_status_payload()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/host/machine_summary")
|
||||||
|
async def host_machine_summary():
|
||||||
|
"""本机 hostname / OS / 架构等,供会员遥测写入 PocketBase。"""
|
||||||
|
return machine_summary_payload()
|
||||||
|
|
||||||
|
|
||||||
@app.post("/douyin/room_previews")
|
@app.post("/douyin/room_previews")
|
||||||
async def douyin_room_previews(request: Request):
|
async def douyin_room_previews(request: Request):
|
||||||
try:
|
try:
|
||||||
@@ -580,7 +587,12 @@ async def _stop_legacy_youtube_if_online() -> None:
|
|||||||
|
|
||||||
# ---------------- PM2 控制路由 ----------------
|
# ---------------- PM2 控制路由 ----------------
|
||||||
@app.get("/start")
|
@app.get("/start")
|
||||||
async def start(process: str = Query(..., description="进程名")):
|
async def start(request: Request, process: str = Query(..., description="进程名")):
|
||||||
|
if REQUIRE_PB:
|
||||||
|
tok = (request.headers.get("x-pb-token") or "").strip()
|
||||||
|
ok, msg, _ = await pb_auth_refresh_and_validate(tok)
|
||||||
|
if not ok:
|
||||||
|
return JSONResponse({"output": msg}, status_code=403)
|
||||||
if not process_valid(process):
|
if not process_valid(process):
|
||||||
return JSONResponse({"output": f"无效进程名: {process}"}, status_code=400)
|
return JSONResponse({"output": f"无效进程名: {process}"}, status_code=400)
|
||||||
cid = relay_channel_id_from_pm2_name(process)
|
cid = relay_channel_id_from_pm2_name(process)
|
||||||
@@ -633,7 +645,12 @@ async def stop(process: str = Query(..., description="进程名")):
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/restart")
|
@app.get("/restart")
|
||||||
async def restart(process: str = Query(..., description="进程名")):
|
async def restart(request: Request, process: str = Query(..., description="进程名")):
|
||||||
|
if REQUIRE_PB:
|
||||||
|
tok = (request.headers.get("x-pb-token") or "").strip()
|
||||||
|
ok, msg, _ = await pb_auth_refresh_and_validate(tok)
|
||||||
|
if not ok:
|
||||||
|
return JSONResponse({"output": msg}, status_code=403)
|
||||||
if not process_valid(process):
|
if not process_valid(process):
|
||||||
return JSONResponse({"output": f"无效进程名: {process}"}, status_code=400)
|
return JSONResponse({"output": f"无效进程名: {process}"}, status_code=400)
|
||||||
cid = relay_channel_id_from_pm2_name(process)
|
cid = relay_channel_id_from_pm2_name(process)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
工作台 /client/overview:YouTube 频道摘要、微信绑定摘要(EasyTier 节点数由 Electron 主进程补充)。
|
工作台 /client/overview:YouTube 频道摘要、微信绑定摘要。
|
||||||
|
|
||||||
多路转播语义:每条线路为「一个抖音直播间 ↔ 一个 YouTube 串流」一对一;
|
多路转播语义:每条线路为「一个抖音直播间 ↔ 一个 YouTube 串流」一对一;
|
||||||
禁止同一抖音直播间同时向多个 YouTube 频道推流;禁止多条线路共用同一串流密钥(避免串台)。
|
禁止同一抖音直播间同时向多个 YouTube 频道推流;禁止多条线路共用同一串流密钥(避免串台)。
|
||||||
@@ -251,5 +251,4 @@ def build_client_overview(config_dir: Path, base_dir: Path) -> dict[str, Any]:
|
|||||||
"relayValidationErrors": relay_errors,
|
"relayValidationErrors": relay_errors,
|
||||||
"relayNotes": relay_notes,
|
"relayNotes": relay_notes,
|
||||||
"wechat": wechat,
|
"wechat": wechat,
|
||||||
"easyTierPeersNote": "peer 数量由 Electron 调用 easytier-cli peer list 获取",
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import platform
|
import platform
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -39,3 +40,14 @@ def gpu_status_payload() -> dict[str, Any]:
|
|||||||
"nvidia_detected": nv,
|
"nvidia_detected": nv,
|
||||||
"nvenc_available": enc,
|
"nvenc_available": enc,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def machine_summary_payload() -> dict[str, Any]:
|
||||||
|
"""供远程 App 遥测:本机标识与系统信息。"""
|
||||||
|
return {
|
||||||
|
"hostname": socket.gethostname(),
|
||||||
|
"os": platform.system(),
|
||||||
|
"os_release": platform.release(),
|
||||||
|
"machine": platform.machine(),
|
||||||
|
"python": platform.python_version(),
|
||||||
|
}
|
||||||
|
|||||||
39
web2_pocketbase.py
Normal file
39
web2_pocketbase.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"""PocketBase:校验用户令牌与 live_allowed;供 /start /restart 可选强制。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Any, Tuple
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
PB_URL = os.environ.get("WEB2_PB_URL", "https://pocketbase.hackrobot.cn").rstrip("/")
|
||||||
|
REQUIRE_PB = os.environ.get("WEB2_REQUIRE_PB_AUTH", "").strip().lower() in ("1", "true", "yes")
|
||||||
|
|
||||||
|
|
||||||
|
async def pb_auth_refresh_and_validate(token: str) -> Tuple[bool, str, dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
使用 auth-refresh 校验令牌并拉取最新用户记录。
|
||||||
|
返回 (ok, error_message, record)
|
||||||
|
"""
|
||||||
|
t = (token or "").strip()
|
||||||
|
if not t:
|
||||||
|
return False, "缺少 PocketBase 授权(请在手机端登录会员账号)", {}
|
||||||
|
url = f"{PB_URL}/api/collections/users/auth-refresh"
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=20.0) as client:
|
||||||
|
r = await client.post(url, headers={"Authorization": f"Bearer {t}"})
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"PocketBase 不可达: {e}", {}
|
||||||
|
try:
|
||||||
|
data = r.json()
|
||||||
|
except Exception:
|
||||||
|
return False, "PocketBase 响应无效", {}
|
||||||
|
if r.status_code != 200:
|
||||||
|
msg = str(data.get("message") or "授权无效或已过期")
|
||||||
|
return False, msg, {}
|
||||||
|
rec = data.get("record") or {}
|
||||||
|
if not isinstance(rec, dict):
|
||||||
|
return False, "用户记录异常", {}
|
||||||
|
if rec.get("live_allowed") is False:
|
||||||
|
return False, "账号已被管理员停用远程直播权限", {}
|
||||||
|
return True, "", rec
|
||||||
Reference in New Issue
Block a user