Compare commits
120 Commits
Author | SHA1 | Date | |
---|---|---|---|
3d796ce447 | |||
a0fa46355b | |||
41afd11036 | |||
58ef92792f | |||
db6e0be776 | |||
95c984573f | |||
5840b75b24 | |||
ce701816b9 | |||
d83761afc0 | |||
798af607bb | |||
3096df86b2 | |||
0505aeaace | |||
551a8b1f18 | |||
e646085f0b | |||
b8d76f6273 | |||
29d23a38d2 | |||
bf2b8956ef | |||
f14860703f | |||
8d98918e39 | |||
9198413cd9 | |||
04fb6e956f | |||
|
8ed3e71539 | ||
1feed57b4e | |||
b1f45ee90d | |||
2facc798cd | |||
1e24129ee7 | |||
0723817bc8 | |||
d882b03e12 | |||
8a810d1869 | |||
3e49fa59bb | |||
f4d65e9595 | |||
d36e7e2c5c | |||
c83c984509 | |||
821244a0c5 | |||
11a73f1aca | |||
734a7204f8 | |||
27f4d25591 | |||
8bbb74eae9 | |||
8fbe139ba0 | |||
|
81dad07fd5 | ||
|
30a91a38f9 | ||
0b4068b9e9 | |||
302b4d4596 | |||
d64639af20 | |||
9e1b725c0c | |||
ecdcde9230 | |||
977cb9877f | |||
6b1a4a7b0a | |||
2281f872bf | |||
bf5c4f760a | |||
5ee9a63a4e | |||
be194f4b64 | |||
b72437758f | |||
0e00579e13 | |||
5cf4ab2fc1 | |||
6f9616ccf3 | |||
7beddaf91e | |||
e0736f1658 | |||
9c5d14213e | |||
88012002a5 | |||
290767dc29 | |||
c8d8ba8435 | |||
e10c1ff928 | |||
28a62ae6c3 | |||
09564f1748 | |||
47baa8b2e4 | |||
df4c3a34b1 | |||
b38431bf4c | |||
3b7943ca23 | |||
632c0ff3ca | |||
c31e3a8106 | |||
1cc824d624 | |||
113c8f2a53 | |||
9d7939061c | |||
44b026f5ae | |||
d0a6aadb56 | |||
47cccb4aa8 | |||
9e9cb58838 | |||
866b789a3d | |||
0260fce845 | |||
3ae8176abb | |||
3225952c69 | |||
44211bcbc9 | |||
bd4082f690 | |||
9b69e90021 | |||
5a72550598 | |||
db961deace | |||
9757051c89 | |||
18c02b5156 | |||
cadfe922c0 | |||
53fd5c3f65 | |||
179dc18ec8 | |||
0f81dfd030 | |||
11843661c0 | |||
39318a4cc9 | |||
68fecca012 | |||
74120850a4 | |||
0f71209fe8 | |||
fdd809fdc2 | |||
02562b63dc | |||
6b0433138a | |||
fbeff099d0 | |||
17f0f4aa4e | |||
0643fe0076 | |||
ff90542cc0 | |||
ab545da6cd | |||
b3fb7dca43 | |||
e29ef65d71 | |||
6a482af97c | |||
ce152f617e | |||
6d4127c90b | |||
c335e45852 | |||
81fe49daec | |||
c4f2776239 | |||
7200f28cca | |||
ea7620bced | |||
1590729b51 | |||
9d246dfcba | |||
d051250959 | |||
3cdd12644e |
5
.env
Normal file
5
.env
Normal file
@ -0,0 +1,5 @@
|
||||
FLASK_ENV=development
|
||||
FLASK_DEBUG=False
|
||||
FLASK_RUN_PORT=5000
|
||||
FLASK_RUN_HOST=0.0.0.0
|
||||
FLASK_APP=WebMain.py
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -84,7 +84,6 @@ celerybeat-schedule
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
@ -178,4 +177,7 @@ fabric.properties
|
||||
pyvenv.cfg
|
||||
.venv
|
||||
pip-selfcheck.json
|
||||
|
||||
*.mp4
|
||||
*.flv
|
||||
config*
|
||||
.*
|
||||
|
2
CHANGELOG
Normal file
2
CHANGELOG
Normal file
@ -0,0 +1,2 @@
|
||||
# 接口版本9.4.2(94214)
|
||||
弹幕接口改为`/webcast/im/fetch/`,发现会先连接websocket,然而websocket又是魔改protobuf,实在是弄不懂
|
478
Common.py
Normal file
478
Common.py
Normal file
@ -0,0 +1,478 @@
|
||||
import os
|
||||
import queue
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import psutil
|
||||
from api import XiGuaLiveApi
|
||||
import json
|
||||
import threading
|
||||
from bilibili import Bilibili, VideoPart
|
||||
|
||||
# 默认设置
|
||||
config = {
|
||||
# 录像的主播ID
|
||||
"l_u": "97621754276",
|
||||
# 视频位置
|
||||
"path": ".",
|
||||
# 标题及预留时间位置
|
||||
"t_t": "【永恒de草薙直播录播】直播于 {}",
|
||||
# 标签
|
||||
"tag": ["永恒de草薙", "三国", "三国战记", "直播录像", "录播", "怀旧", "街机"],
|
||||
# 描述
|
||||
"des": "西瓜直播 https://live.ixigua.com/userlive/97621754276 \n自动投递\n原主播:永恒de草薙\n直播时间:晚上6点多到凌晨4点左右",
|
||||
# 来源, 空则为自制
|
||||
"src": "",
|
||||
# Log条数
|
||||
"l_c": 5,
|
||||
# 错误Log条数
|
||||
"elc": 10,
|
||||
# 每一chunk大小
|
||||
"c_s": 16 * 1024,
|
||||
# 每一块视频大小
|
||||
"p_s": 2141000000,
|
||||
# 忽略的大小
|
||||
"i_s": 2048000,
|
||||
"max": 75,
|
||||
"dow": "echo 'clean'",
|
||||
# 仅下载
|
||||
"dlO": True,
|
||||
# 下播延迟投稿
|
||||
"dly": 30,
|
||||
# 短的时间的格式
|
||||
"sdf": "%Y%m%d",
|
||||
"enc": "ffmpeg -i {f} -c:v copy -c:a copy -f mp4 {t} -y"
|
||||
}
|
||||
doCleanTime = datetime.fromtimestamp(0)
|
||||
loginTime = datetime.fromtimestamp(0)
|
||||
_clean_flag = None
|
||||
delay = datetime.fromtimestamp(0)
|
||||
b = Bilibili()
|
||||
|
||||
network = [{
|
||||
"currentTime": datetime.now(),
|
||||
"out": {
|
||||
"currentByte": psutil.net_io_counters().bytes_sent,
|
||||
},
|
||||
"in": {
|
||||
"currentByte": psutil.net_io_counters().bytes_recv,
|
||||
}
|
||||
}, {
|
||||
"currentTime": datetime.now(),
|
||||
"out": {
|
||||
"currentByte": psutil.net_io_counters().bytes_sent,
|
||||
},
|
||||
"in": {
|
||||
"currentByte": psutil.net_io_counters().bytes_recv,
|
||||
}
|
||||
}]
|
||||
|
||||
|
||||
def reloadConfig():
|
||||
global config
|
||||
if os.path.exists('config.json'):
|
||||
_config_fp = open("config.json", "r", encoding="utf8")
|
||||
_config = json.load(_config_fp)
|
||||
config.update(_config)
|
||||
_config_fp.close()
|
||||
|
||||
|
||||
def resetDelay():
|
||||
global delay
|
||||
delay = datetime.now() + timedelta(minutes=int(config['dly']))
|
||||
|
||||
|
||||
def doDelay():
|
||||
global delay
|
||||
if -60 < getTimeDelta(datetime.now(), delay) < 60:
|
||||
delay = datetime.fromtimestamp(0)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def updateNetwork():
|
||||
global network
|
||||
network.append({
|
||||
"currentTime": datetime.now(),
|
||||
"out": {
|
||||
"currentByte": psutil.net_io_counters().bytes_sent,
|
||||
},
|
||||
"in": {
|
||||
"currentByte": psutil.net_io_counters().bytes_recv,
|
||||
}
|
||||
})
|
||||
network = network[-3:]
|
||||
|
||||
|
||||
def getTimeDelta(a, b):
|
||||
return (a - b).total_seconds()
|
||||
|
||||
|
||||
def _doClean(_force=False):
|
||||
global doCleanTime, _clean_flag
|
||||
_disk = psutil.disk_usage(".")
|
||||
if _disk.percent > config["max"] or _force:
|
||||
_clean_flag = True
|
||||
doCleanTime = datetime.now()
|
||||
appendOperation("执行配置的清理命令")
|
||||
os.system(config["dow"])
|
||||
appendOperation("执行配置的清理命令完毕")
|
||||
doCleanTime = datetime.now()
|
||||
_clean_flag = False
|
||||
|
||||
|
||||
def doClean(_force=False):
|
||||
if _clean_flag:
|
||||
return
|
||||
p = threading.Thread(target=_doClean, args=(_force,))
|
||||
p.setDaemon(True)
|
||||
p.start()
|
||||
|
||||
|
||||
def getCurrentStatus():
|
||||
_disk = psutil.disk_usage(".")
|
||||
_mem = psutil.virtual_memory()
|
||||
_net = psutil.net_io_counters()
|
||||
_delta = getTimeDelta(network[-1]["currentTime"], network[-2]["currentTime"])
|
||||
if 60 > _delta > 1:
|
||||
_inSpeed = (network[-1]["in"]["currentByte"] - network[-2]["in"]["currentByte"]) / _delta
|
||||
_outSpeed = (network[-1]["out"]["currentByte"] - network[-2]["out"]["currentByte"]) / _delta
|
||||
else:
|
||||
_outSpeed = (network[-1]["in"]["currentByte"] - network[-2]["in"]["currentByte"])
|
||||
_inSpeed = (network[-1]["out"]["currentByte"] - network[-2]["out"]["currentByte"])
|
||||
updateNetwork()
|
||||
return {
|
||||
"memTotal": parseSize(_mem.total),
|
||||
"memUsed": parseSize(_mem.used),
|
||||
"memUsage": _mem.percent,
|
||||
"diskTotal": parseSize(_disk.total),
|
||||
"diskUsed": parseSize(_disk.used),
|
||||
"diskUsage": _disk.percent,
|
||||
"cpu": psutil.cpu_percent(),
|
||||
"outSpeed": parseSize(_outSpeed),
|
||||
"inSpeed": parseSize(_inSpeed),
|
||||
"doCleanTime": datetime.strftime(doCleanTime, dt_format),
|
||||
}
|
||||
|
||||
|
||||
dt_format = "%Y/%m/%d %H:%M:%S"
|
||||
reloadConfig()
|
||||
broadcaster = ""
|
||||
streamUrl = ""
|
||||
|
||||
forceNotDownload = False
|
||||
forceNotBroadcasting = False
|
||||
forceNotUpload = False
|
||||
forceNotEncode = False
|
||||
if config["dlO"] is True:
|
||||
forceNotUpload = True
|
||||
forceNotEncode = True
|
||||
forceStartEncodeThread = False
|
||||
forceStartUploadThread = False
|
||||
|
||||
uploadQueue = queue.Queue()
|
||||
encodeQueue = queue.Queue()
|
||||
|
||||
uploadStatus = []
|
||||
downloadStatus = []
|
||||
encodeStatus = []
|
||||
errors = []
|
||||
operations = []
|
||||
|
||||
|
||||
def appendOperation(obj):
|
||||
global operations
|
||||
if isinstance(obj, dict):
|
||||
if "datetime" not in obj:
|
||||
obj["datetime"] = datetime.strftime(datetime.now(), dt_format)
|
||||
operations.append(obj)
|
||||
else:
|
||||
operations.append({
|
||||
"datetime": datetime.strftime(datetime.now(), dt_format),
|
||||
"message": str(obj)
|
||||
})
|
||||
operations = operations[-config["elc"]:]
|
||||
|
||||
|
||||
def parseSize(size):
|
||||
K = size / 1024.0
|
||||
if K > 1000:
|
||||
M = K / 1024.0
|
||||
if M > 1000:
|
||||
return "{:.2f}GB".format(M / 1024.0)
|
||||
else:
|
||||
return "{:.2f}MB".format(M)
|
||||
else:
|
||||
return "{:.2f}KB".format(K)
|
||||
|
||||
|
||||
def appendUploadStatus(obj):
|
||||
global uploadStatus
|
||||
if isinstance(obj, dict):
|
||||
if "datetime" not in obj:
|
||||
obj["datetime"] = datetime.strftime(datetime.now(), dt_format)
|
||||
uploadStatus.append(obj)
|
||||
else:
|
||||
uploadStatus.append({
|
||||
"datetime": datetime.strftime(datetime.now(), dt_format),
|
||||
"message": str(obj)
|
||||
})
|
||||
uploadStatus = uploadStatus[-config["l_c"]:]
|
||||
|
||||
|
||||
def modifyLastUploadStatus(obj):
|
||||
global uploadStatus
|
||||
if isinstance(obj, dict):
|
||||
if "datetime" not in obj:
|
||||
obj["datetime"] = datetime.strftime(datetime.now(), dt_format)
|
||||
uploadStatus[-1] = obj
|
||||
else:
|
||||
uploadStatus[-1]["message"] = str(obj)
|
||||
uploadStatus[-1]["datetime"] = datetime.strftime(datetime.now(), dt_format)
|
||||
|
||||
|
||||
def appendEncodeStatus(obj):
|
||||
global encodeStatus
|
||||
if isinstance(obj, dict):
|
||||
if "datetime" not in obj:
|
||||
obj["datetime"] = datetime.strftime(datetime.now(), dt_format)
|
||||
encodeStatus.append(obj)
|
||||
else:
|
||||
encodeStatus.append({
|
||||
"datetime": datetime.strftime(datetime.now(), dt_format),
|
||||
"message": str(obj)
|
||||
})
|
||||
encodeStatus = encodeStatus[-config["l_c"]:]
|
||||
|
||||
|
||||
def modifyLastEncodeStatus(obj):
|
||||
global encodeStatus
|
||||
if isinstance(obj, dict):
|
||||
if "datetime" not in obj:
|
||||
obj["datetime"] = datetime.strftime(datetime.now(), dt_format)
|
||||
encodeStatus[-1] = obj
|
||||
else:
|
||||
encodeStatus[-1]["message"] = str(obj)
|
||||
encodeStatus[-1]["datetime"] = datetime.strftime(datetime.now(), dt_format)
|
||||
|
||||
|
||||
def appendDownloadStatus(obj):
|
||||
global downloadStatus
|
||||
if isinstance(obj, dict):
|
||||
if "datetime" not in obj:
|
||||
obj["datetime"] = datetime.strftime(datetime.now(), dt_format)
|
||||
downloadStatus.append(obj)
|
||||
else:
|
||||
downloadStatus.append({
|
||||
"datetime": datetime.strftime(datetime.now(), dt_format),
|
||||
"message": str(obj)
|
||||
})
|
||||
downloadStatus = downloadStatus[-config["l_c"]:]
|
||||
|
||||
|
||||
def modifyLastDownloadStatus(obj):
|
||||
global downloadStatus
|
||||
if isinstance(obj, dict):
|
||||
if "datetime" not in obj:
|
||||
obj["datetime"] = datetime.strftime(datetime.now(), dt_format)
|
||||
downloadStatus[-1] = obj
|
||||
else:
|
||||
downloadStatus[-1]["message"] = str(obj)
|
||||
downloadStatus[-1]["datetime"] = datetime.strftime(datetime.now(), dt_format)
|
||||
|
||||
|
||||
def appendError(obj):
|
||||
global errors
|
||||
if isinstance(obj, dict):
|
||||
if "datetime" not in obj:
|
||||
obj["datetime"] = datetime.strftime(datetime.now(), dt_format)
|
||||
errors.append(obj)
|
||||
else:
|
||||
errors.append({
|
||||
"datetime": datetime.strftime(datetime.now(), dt_format),
|
||||
"message": str(obj)
|
||||
})
|
||||
errors = errors[-config["elc"]:]
|
||||
|
||||
|
||||
def loginBilibili(force=False):
|
||||
if config["dlO"] is False or forceNotUpload is False:
|
||||
global loginTime
|
||||
global b
|
||||
if getTimeDelta(datetime.now(), loginTime) < 86400 * 10 and not force:
|
||||
return False
|
||||
try:
|
||||
b.login()
|
||||
loginTime = datetime.now()
|
||||
return True
|
||||
except Exception as e:
|
||||
appendError(e)
|
||||
appendOperation("登录失败")
|
||||
return False
|
||||
else:
|
||||
appendOperation("设置了不上传,所以不会登陆")
|
||||
|
||||
|
||||
class downloader(XiGuaLiveApi):
|
||||
__playlist = None
|
||||
__danmakuFile = None
|
||||
__danmakuBiasTime = None
|
||||
|
||||
def getDanmaku(self):
|
||||
super(downloader, self).getDanmaku()
|
||||
if self.__danmakuFile is not None and self.__danmakuFile.writable():
|
||||
self.__danmakuFile.flush()
|
||||
|
||||
def onPresentEnd(self, gift):
|
||||
if self.__danmakuFile is not None and self.__danmakuFile.writable():
|
||||
now = datetime.now()
|
||||
if self.__danmakuBiasTime is None:
|
||||
return
|
||||
ts = (now - self.__danmakuBiasTime).total_seconds()
|
||||
_c = """<gift ts="{:.2f}" user="{}" giftname="{}" giftcount="{}"></gift>\r\n""".format(ts, gift.user.name, gift.name, gift.count)
|
||||
self.__danmakuFile.write(_c.encode("UTF-8"))
|
||||
|
||||
def onChat(self, chat):
|
||||
if self.__danmakuFile is not None and self.__danmakuFile.writable():
|
||||
now = datetime.now()
|
||||
if self.__danmakuBiasTime is None:
|
||||
return
|
||||
ts = (now - self.__danmakuBiasTime).total_seconds()
|
||||
_c = """<d p="{:.2f},1,24,16777215,{:.0f},0,{},0" user="{}">{}</d>\r\n""".format(ts, now.timestamp()*1000, chat.user.ID, chat.user.name, chat.content)
|
||||
self.__danmakuFile.write(_c.encode("UTF-8"))
|
||||
|
||||
@property
|
||||
def playlist(self):
|
||||
return self.__playlist
|
||||
|
||||
@playlist.setter
|
||||
def playlist(self, value):
|
||||
global streamUrl
|
||||
self.__playlist = value
|
||||
streamUrl = value
|
||||
|
||||
def _checkUsernameIsMatched(self, compare=None):
|
||||
return True
|
||||
|
||||
def updRoomInfo(self, force=False):
|
||||
global broadcaster
|
||||
_prev_status = self.isLive
|
||||
doClean()
|
||||
if not force and self.isLive:
|
||||
return _prev_status
|
||||
_result = super(downloader, self).updRoomInfo(force)
|
||||
if _prev_status != self.isLive and not self.isLive:
|
||||
# 及时保存
|
||||
self.__danmakuFile.close()
|
||||
self.__danmakuFile = None
|
||||
self.__danmakuBiasTime = None
|
||||
resetDelay()
|
||||
broadcaster = self.broadcaster
|
||||
if _result:
|
||||
if self.isLive:
|
||||
self.updPlayList()
|
||||
else:
|
||||
self.playlist = False
|
||||
return _result
|
||||
|
||||
def updPlayList(self):
|
||||
if self.isLive and "stream_url" in self._rawRoomInfo:
|
||||
if 'rtmp_pull_url' in self._rawRoomInfo["stream_url"]:
|
||||
self.playlist = self._rawRoomInfo["stream_url"]['rtmp_pull_url']
|
||||
elif 'flv_pull_url' in self._rawRoomInfo["stream_url"]:
|
||||
_playlist = self._rawRoomInfo["stream_url"]["flv_pull_url"]
|
||||
if type(_playlist) is dict:
|
||||
for _ in _playlist.values():
|
||||
self.playlist = _
|
||||
break
|
||||
self.playlist = self.playlist.replace("_hd5", "").replace("_sd5", "").replace("_ld5", "").replace("_md", "")
|
||||
else:
|
||||
self.playlist = None
|
||||
|
||||
def initSave(self, f):
|
||||
if self.__danmakuFile is not None and not self.__danmakuFile.closed:
|
||||
self.__danmakuFile.close()
|
||||
self.__danmakuBiasTime = datetime.now()
|
||||
self.__danmakuFile = open(f, "wb")
|
||||
|
||||
|
||||
api = downloader(config["l_u"])
|
||||
|
||||
|
||||
def doUpdatePlaylist(_force=False):
|
||||
p = threading.Thread(target=api.updRoomInfo, args=(_force,))
|
||||
p.setDaemon(True)
|
||||
p.start()
|
||||
|
||||
|
||||
def refreshDownloader():
|
||||
global api
|
||||
api = downloader(config["l_u"])
|
||||
|
||||
|
||||
def uploadVideo(name):
|
||||
if not os.path.exists(name):
|
||||
appendError("Upload File Not Exist {}".format(name))
|
||||
return
|
||||
loginBilibili()
|
||||
doClean()
|
||||
if forceNotUpload is False:
|
||||
b.preUpload(VideoPart(path=name, title=os.path.basename(name)))
|
||||
else:
|
||||
appendUploadStatus("设置了不上传,所以[{}]不会上传了".format(name))
|
||||
if not forceNotEncode:
|
||||
os.remove(name)
|
||||
|
||||
|
||||
def publishVideo(date):
|
||||
if forceNotUpload is False:
|
||||
b.finishUpload(config["t_t"].format(date), 17, config["tag"], config["des"],
|
||||
source=config["src"], no_reprint=0)
|
||||
b.clear()
|
||||
else:
|
||||
appendUploadStatus("设置了不上传,所以[{}]的录播不会投了".format(date))
|
||||
|
||||
|
||||
def encodeVideo(name):
|
||||
if forceNotEncode:
|
||||
appendEncodeStatus("设置了不编码,所以[{}]不会编码".format(name))
|
||||
return False
|
||||
if not os.path.exists(name):
|
||||
appendEncodeStatus("文件[{}]不存在".format(name))
|
||||
return False
|
||||
if os.path.getsize(name) < 8 * 1024 * 1024:
|
||||
appendEncodeStatus("Encoded File >{}< is too small, will ignore it".format(name))
|
||||
return False
|
||||
appendEncodeStatus("Encoding >{}< Start".format(name))
|
||||
_new_name = os.path.splitext(name)[0] + ".mp4"
|
||||
_code = os.system(config["enc"].format(f=name, t=_new_name))
|
||||
if _code != 0:
|
||||
appendError("Encode {} with Non-Zero Return.".format(name))
|
||||
return False
|
||||
modifyLastEncodeStatus("Encode >{}< Finished".format(name))
|
||||
uploadQueue.put(_new_name)
|
||||
|
||||
|
||||
def collectInfomation():
|
||||
return {
|
||||
"download": downloadStatus,
|
||||
"encode": encodeStatus,
|
||||
"encodeQueueSize": encodeQueue.qsize(),
|
||||
"upload": uploadStatus,
|
||||
"uploadQueueSize": uploadQueue.qsize(),
|
||||
"error": errors,
|
||||
"operation": operations,
|
||||
"broadcast": {
|
||||
"broadcaster": broadcaster.__str__(),
|
||||
"isBroadcasting": api.isLive,
|
||||
"streamUrl": streamUrl,
|
||||
"updateTime": api.updateAt.strftime(dt_format),
|
||||
"delayTime": delay.strftime(dt_format)
|
||||
},
|
||||
"config": {
|
||||
"forceNotBroadcasting": forceNotBroadcasting,
|
||||
"forceNotDownload": forceNotDownload,
|
||||
"forceNotUpload": forceNotUpload,
|
||||
"forceNotEncode": forceNotEncode,
|
||||
"downloadOnly": config['dlO'],
|
||||
},
|
||||
}
|
BIN
Demo/242_.txt
Normal file
BIN
Demo/242_.txt
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
Demo/324_.txt
BIN
Demo/324_.txt
Binary file not shown.
1104
Demo/result.txt
1104
Demo/result.txt
File diff suppressed because it is too large
Load Diff
4406
Demo/result2.txt
4406
Demo/result2.txt
File diff suppressed because it is too large
Load Diff
1434
Demo/result3.txt
1434
Demo/result3.txt
File diff suppressed because it is too large
Load Diff
2576
Demo/result4.json
2576
Demo/result4.json
File diff suppressed because it is too large
Load Diff
6007
Demo/result4.txt
6007
Demo/result4.txt
File diff suppressed because it is too large
Load Diff
1036
Demo/v926.txt
Normal file
1036
Demo/v926.txt
Normal file
File diff suppressed because it is too large
Load Diff
256
Demo/v926_fg.txt
Normal file
256
Demo/v926_fg.txt
Normal file
@ -0,0 +1,256 @@
|
||||
1 {
|
||||
1: "WebcastChatMessage"
|
||||
2 {
|
||||
1 {
|
||||
1: "WebcastChatMessage"
|
||||
2: 6902669212519992068
|
||||
3: 6902629276546566925
|
||||
6: 1
|
||||
8 {
|
||||
1: "webcast_chat_display_text"
|
||||
2: "{0:user}{1:string}"
|
||||
3 {
|
||||
1: "#ff36c0cf"
|
||||
4: 400
|
||||
}
|
||||
4 {
|
||||
1: 11
|
||||
2 {
|
||||
1: "#60000000"
|
||||
4: 400
|
||||
}
|
||||
21 {
|
||||
1 {
|
||||
1: 5518138898
|
||||
3: "\345\247\232\345\247\232\347\220\263\347\232\2042\345\247\220\345\244\253"
|
||||
4: 1
|
||||
9 {
|
||||
1: "https://p1-dy.bytexservice.com/img/user-avatar/deb96a5e7e07f60a613531670a570736~300x300.image"
|
||||
}
|
||||
21 {
|
||||
1: "http://p9-webcast-ttcdn.byteimg.com/img/webcast/xigua_admin_badge_v2.png~tplv-obj.image"
|
||||
1: "http://p1-webcast-ttcdn.byteimg.com/img/webcast/xigua_admin_badge_v2.png~tplv-obj.image"
|
||||
2: "webcast/xigua_admin_badge_v2.png"
|
||||
3: 16
|
||||
4: 28
|
||||
6: 3
|
||||
}
|
||||
21 {
|
||||
1: "http://p1-webcast-ttcdn.byteimg.com/img/webcast/25_xigua_honor_level.png~tplv-obj.png"
|
||||
1: "http://p9-webcast-ttcdn.byteimg.com/img/webcast/25_xigua_honor_level.png~tplv-obj.png"
|
||||
2: "webcast/25_xigua_honor_level.png"
|
||||
3: 16
|
||||
4: 30
|
||||
6: 1
|
||||
7: "sslocal://webcast_webview?url=https%3A%2F%2Fwebcast.ixigua.com%2Ffalcon%2Fwebcast_xigua%2Fpage%2Fhonor_level%2Fuser%2Findex.html&hide_nav_bar=1&hide_status_bar=0&__live_platform__=webcast&type=fullscreen"
|
||||
}
|
||||
21 {
|
||||
1: "http://p1-webcast-ttcdn.byteimg.com/img/webcast/xigua_fansclub_medal_15.png~tplv-obj.image"
|
||||
1: "http://p6-webcast-ttcdn.byteimg.com/img/webcast/xigua_fansclub_medal_15.png~tplv-obj.image"
|
||||
2: "webcast/xigua_fansclub_medal_15.png"
|
||||
6: 7
|
||||
8 {
|
||||
1: "\345\247\232\345\247\232\347\220\263"
|
||||
2: "#FFFFFF"
|
||||
3: 15
|
||||
}
|
||||
}
|
||||
22 {
|
||||
1: 13
|
||||
2: 86
|
||||
3: 2
|
||||
}
|
||||
23 {
|
||||
6: 25
|
||||
19 {
|
||||
1: "http://p1-webcast-ttcdn.byteimg.com/img/webcast/25_xigua_honor_level.png~tplv-obj.png"
|
||||
1: "http://p9-webcast-ttcdn.byteimg.com/img/webcast/25_xigua_honor_level.png~tplv-obj.png"
|
||||
2: "webcast/25_xigua_honor_level.png"
|
||||
3: 16
|
||||
4: 30
|
||||
6: 1
|
||||
7: "sslocal://webcast_webview?url=https%3A%2F%2Fwebcast.ixigua.com%2Ffalcon%2Fwebcast_xigua%2Fpage%2Fhonor_level%2Fuser%2Findex.html&hide_nav_bar=1&hide_status_bar=0&__live_platform__=webcast&type=fullscreen"
|
||||
}
|
||||
}
|
||||
24 {
|
||||
1 {
|
||||
1: "\345\247\232\345\247\232\347\220\263"
|
||||
2: 15
|
||||
3: 1
|
||||
4 {
|
||||
1: "\010\002\022\342\001\nZhttp://p1-webcast-ttcdn.byteimg.com/img/webcast/xigua_fansclub_medal_15.png~tplv-obj.image\nZhttp://p6-webcast-ttcdn.byteimg.com/img/webcast/xigua_fansclub_medal_15.png~tplv-obj.image\022#webcast/xigua_fansclub_medal_15.png\0300 \226\001"
|
||||
2: "\345\247\232\345\247\232\347\220\263"
|
||||
}
|
||||
6: 61788610240
|
||||
}
|
||||
}
|
||||
32 {
|
||||
2: 1
|
||||
}
|
||||
38: "0"
|
||||
46: "MS4wLjABAAAAKkWCgUKAN3GtNdQ0jqr8zAt3KtIc9kAc1GaJ32VcH3E"
|
||||
54: 3
|
||||
61 {
|
||||
1: "http://p9-webcast-ttcdn.byteimg.com/img/webcast/xigua_admin_badge_v2.png~tplv-obj.image"
|
||||
1: "http://p1-webcast-ttcdn.byteimg.com/img/webcast/xigua_admin_badge_v2.png~tplv-obj.image"
|
||||
2: "webcast/xigua_admin_badge_v2.png"
|
||||
3: 16
|
||||
4: 28
|
||||
6: 3
|
||||
}
|
||||
61 {
|
||||
1: "http://p1-webcast-ttcdn.byteimg.com/img/webcast/25_xigua_honor_level.png~tplv-obj.png"
|
||||
1: "http://p9-webcast-ttcdn.byteimg.com/img/webcast/25_xigua_honor_level.png~tplv-obj.png"
|
||||
2: "webcast/25_xigua_honor_level.png"
|
||||
3: 16
|
||||
4: 30
|
||||
6: 1
|
||||
7: "sslocal://webcast_webview?url=https%3A%2F%2Fwebcast.ixigua.com%2Ffalcon%2Fwebcast_xigua%2Fpage%2Fhonor_level%2Fuser%2Findex.html&hide_nav_bar=1&hide_status_bar=0&__live_platform__=webcast&type=fullscreen"
|
||||
}
|
||||
61 {
|
||||
1: "http://p1-webcast-ttcdn.byteimg.com/img/webcast/xigua_fansclub_medal_15.png~tplv-obj.image"
|
||||
1: "http://p6-webcast-ttcdn.byteimg.com/img/webcast/xigua_fansclub_medal_15.png~tplv-obj.image"
|
||||
2: "webcast/xigua_fansclub_medal_15.png"
|
||||
6: 7
|
||||
8 {
|
||||
1: "\345\247\232\345\247\232\347\220\263"
|
||||
2: "#FFFFFF"
|
||||
3: 15
|
||||
}
|
||||
}
|
||||
}
|
||||
2: 1
|
||||
}
|
||||
}
|
||||
4 {
|
||||
1: 1
|
||||
11: "@\345\247\232\345\256\266\344\272\214\345\247\221\345\207\211 \346\210\221\345\211\215\345\244\251\346\235\245\346\267\261\345\234\263\344\272\206"
|
||||
}
|
||||
}
|
||||
11: 31003
|
||||
}
|
||||
2 {
|
||||
1: 5518138898
|
||||
3: "\345\247\232\345\247\232\347\220\263\347\232\2042\345\247\220\345\244\253"
|
||||
4: 1
|
||||
9 {
|
||||
1: "https://p1-dy.bytexservice.com/img/user-avatar/deb96a5e7e07f60a613531670a570736~300x300.image"
|
||||
}
|
||||
21 {
|
||||
1: "http://p9-webcast-ttcdn.byteimg.com/img/webcast/xigua_admin_badge_v2.png~tplv-obj.image"
|
||||
1: "http://p1-webcast-ttcdn.byteimg.com/img/webcast/xigua_admin_badge_v2.png~tplv-obj.image"
|
||||
2: "webcast/xigua_admin_badge_v2.png"
|
||||
3: 16
|
||||
4: 28
|
||||
6: 3
|
||||
}
|
||||
21 {
|
||||
1: "http://p1-webcast-ttcdn.byteimg.com/img/webcast/25_xigua_honor_level.png~tplv-obj.png"
|
||||
1: "http://p9-webcast-ttcdn.byteimg.com/img/webcast/25_xigua_honor_level.png~tplv-obj.png"
|
||||
2: "webcast/25_xigua_honor_level.png"
|
||||
3: 16
|
||||
4: 30
|
||||
6: 1
|
||||
7: "sslocal://webcast_webview?url=https%3A%2F%2Fwebcast.ixigua.com%2Ffalcon%2Fwebcast_xigua%2Fpage%2Fhonor_level%2Fuser%2Findex.html&hide_nav_bar=1&hide_status_bar=0&__live_platform__=webcast&type=fullscreen"
|
||||
}
|
||||
21 {
|
||||
1: "http://p1-webcast-ttcdn.byteimg.com/img/webcast/xigua_fansclub_medal_15.png~tplv-obj.image"
|
||||
1: "http://p6-webcast-ttcdn.byteimg.com/img/webcast/xigua_fansclub_medal_15.png~tplv-obj.image"
|
||||
2: "webcast/xigua_fansclub_medal_15.png"
|
||||
6: 7
|
||||
8 {
|
||||
1: "\345\247\232\345\247\232\347\220\263"
|
||||
2: "#FFFFFF"
|
||||
3: 15
|
||||
}
|
||||
}
|
||||
22 {
|
||||
1: 13
|
||||
2: 86
|
||||
3: 2
|
||||
}
|
||||
23 {
|
||||
6: 25
|
||||
19 {
|
||||
1: "http://p1-webcast-ttcdn.byteimg.com/img/webcast/25_xigua_honor_level.png~tplv-obj.png"
|
||||
1: "http://p9-webcast-ttcdn.byteimg.com/img/webcast/25_xigua_honor_level.png~tplv-obj.png"
|
||||
2: "webcast/25_xigua_honor_level.png"
|
||||
3: 16
|
||||
4: 30
|
||||
6: 1
|
||||
7: "sslocal://webcast_webview?url=https%3A%2F%2Fwebcast.ixigua.com%2Ffalcon%2Fwebcast_xigua%2Fpage%2Fhonor_level%2Fuser%2Findex.html&hide_nav_bar=1&hide_status_bar=0&__live_platform__=webcast&type=fullscreen"
|
||||
}
|
||||
}
|
||||
24 {
|
||||
1 {
|
||||
1: "\345\247\232\345\247\232\347\220\263"
|
||||
2: 15
|
||||
3: 1
|
||||
4 {
|
||||
1 {
|
||||
1: 2
|
||||
2 {
|
||||
1: "http://p1-webcast-ttcdn.byteimg.com/img/webcast/xigua_fansclub_medal_15.png~tplv-obj.image"
|
||||
1: "http://p6-webcast-ttcdn.byteimg.com/img/webcast/xigua_fansclub_medal_15.png~tplv-obj.image"
|
||||
2: "webcast/xigua_fansclub_medal_15.png"
|
||||
3: 48
|
||||
4: 150
|
||||
}
|
||||
}
|
||||
2: "\345\247\232\345\247\232\347\220\263"
|
||||
}
|
||||
6: 61788610240
|
||||
}
|
||||
}
|
||||
32 {
|
||||
2: 1
|
||||
}
|
||||
38: "0"
|
||||
46: "MS4wLjABAAAAKkWCgUKAN3GtNdQ0jqr8zAt3KtIc9kAc1GaJ32VcH3E"
|
||||
54: 3
|
||||
61 {
|
||||
1: "http://p9-webcast-ttcdn.byteimg.com/img/webcast/xigua_admin_badge_v2.png~tplv-obj.image"
|
||||
1: "http://p1-webcast-ttcdn.byteimg.com/img/webcast/xigua_admin_badge_v2.png~tplv-obj.image"
|
||||
2: "webcast/xigua_admin_badge_v2.png"
|
||||
3: 16
|
||||
4: 28
|
||||
6: 3
|
||||
}
|
||||
61 {
|
||||
1: "http://p1-webcast-ttcdn.byteimg.com/img/webcast/25_xigua_honor_level.png~tplv-obj.png"
|
||||
1: "http://p9-webcast-ttcdn.byteimg.com/img/webcast/25_xigua_honor_level.png~tplv-obj.png"
|
||||
2: "webcast/25_xigua_honor_level.png"
|
||||
3: 16
|
||||
4: 30
|
||||
6: 1
|
||||
7: "sslocal://webcast_webview?url=https%3A%2F%2Fwebcast.ixigua.com%2Ffalcon%2Fwebcast_xigua%2Fpage%2Fhonor_level%2Fuser%2Findex.html&hide_nav_bar=1&hide_status_bar=0&__live_platform__=webcast&type=fullscreen"
|
||||
}
|
||||
61 {
|
||||
1: "http://p1-webcast-ttcdn.byteimg.com/img/webcast/xigua_fansclub_medal_15.png~tplv-obj.image"
|
||||
1: "http://p6-webcast-ttcdn.byteimg.com/img/webcast/xigua_fansclub_medal_15.png~tplv-obj.image"
|
||||
2: "webcast/xigua_fansclub_medal_15.png"
|
||||
6: 7
|
||||
8 {
|
||||
1: "\345\247\232\345\247\232\347\220\263"
|
||||
2: "#FFFFFF"
|
||||
3: 15
|
||||
}
|
||||
}
|
||||
}
|
||||
3: "@\345\247\232\345\256\266\344\272\214\345\247\221\345\207\211 \346\210\221\345\211\215\345\244\251\346\235\245\346\267\261\345\234\263\344\272\206"
|
||||
9 {
|
||||
1 {
|
||||
1: "http://p3-webcast-ttcdn.byteimg.com/img/webcast/userlabel_regular_chat.png~tplv-obj.image"
|
||||
1: "http://p6-webcast-ttcdn.byteimg.com/img/webcast/userlabel_regular_chat.png~tplv-obj.image"
|
||||
2: "webcast/userlabel_regular_chat.png"
|
||||
5: "#E0BCD4"
|
||||
}
|
||||
2: 11
|
||||
}
|
||||
}
|
||||
3: 6902669212519992068
|
||||
}
|
||||
2: "1607153101490_6902670004165030044_6902669995575083008_1"
|
||||
3: 1000
|
||||
4: 1607153101490
|
||||
5: "fetch_time:1607153101490|start_time:0|fetch_id:6902670004165030042|flag:0|seq:2080|next_cursor:1607153101490_6902670004165030044_6902669995575083008_1"
|
364
Demo/v926_leave.txt
Normal file
364
Demo/v926_leave.txt
Normal file
@ -0,0 +1,364 @@
|
||||
1 {
|
||||
1: "WebcastControlMessage"
|
||||
2 {
|
||||
1 {
|
||||
1: "WebcastControlMessage"
|
||||
2: 6902672961476774663
|
||||
3: 6902629276546566925
|
||||
4: 1607153789195
|
||||
6: 1
|
||||
}
|
||||
2: 3
|
||||
}
|
||||
3: 6902672961476774663
|
||||
}
|
||||
1 {
|
||||
1: "WebcastMemberMessage"
|
||||
2 {
|
||||
1 {
|
||||
1: "WebcastMemberMessage"
|
||||
2: 6902672961728662285
|
||||
3: 6902629276546566925
|
||||
6: 1
|
||||
8 {
|
||||
1: "live_room_enter_toast"
|
||||
2: "{0:user} \346\235\245\344\272\206{1:string}"
|
||||
3 {
|
||||
1: "#de000000"
|
||||
4: 400
|
||||
}
|
||||
4 {
|
||||
1: 11
|
||||
2 {
|
||||
1: "#61000000"
|
||||
4: 400
|
||||
}
|
||||
21 {
|
||||
1 {
|
||||
1: 2638466963214383
|
||||
2: 1520817
|
||||
3: "\350\213\261\345\256\207girl"
|
||||
9 {
|
||||
1: "https://sf3-ttcdn-tos.pstatp.com/img/user-avatar/a15266741ef770c810e18e5e6d073da9~300x300.image"
|
||||
}
|
||||
21 {
|
||||
1: "http://p1-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
1: "http://p3-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
2: "webcast/3_xigua_honor_level.png"
|
||||
3: 16
|
||||
4: 30
|
||||
6: 1
|
||||
7: "sslocal://webcast_webview?url=https%3A%2F%2Fwebcast.ixigua.com%2Ffalcon%2Fwebcast_xigua%2Fpage%2Fhonor_level%2Fuser%2Findex.html&type=fullscreen&hide_nav_bar=1&hide_status_bar=0&__live_platform__=webcast"
|
||||
}
|
||||
22 {
|
||||
1: 60
|
||||
2: 120
|
||||
}
|
||||
23 {
|
||||
6: 3
|
||||
19 {
|
||||
1: "http://p1-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
1: "http://p3-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
2: "webcast/3_xigua_honor_level.png"
|
||||
3: 16
|
||||
4: 30
|
||||
6: 1
|
||||
7: "sslocal://webcast_webview?url=https%3A%2F%2Fwebcast.ixigua.com%2Ffalcon%2Fwebcast_xigua%2Fpage%2Fhonor_level%2Fuser%2Findex.html&type=fullscreen&hide_nav_bar=1&hide_status_bar=0&__live_platform__=webcast"
|
||||
}
|
||||
}
|
||||
24 {
|
||||
1 {
|
||||
4 {
|
||||
1: "\010\000\022\000"
|
||||
}
|
||||
}
|
||||
}
|
||||
32: ""
|
||||
38: "1520817"
|
||||
46: "MS4wLjABAAAATNQnQSVI6ePiovK48LHJHvefLO_4RAmla1Hkn0GYTWc8oE5u0a0VZnmPM3VKnaSh"
|
||||
54: 3
|
||||
61 {
|
||||
1: "http://p1-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
1: "http://p3-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
2: "webcast/3_xigua_honor_level.png"
|
||||
3: 16
|
||||
4: 30
|
||||
6: 1
|
||||
7: "sslocal://webcast_webview?url=https%3A%2F%2Fwebcast.ixigua.com%2Ffalcon%2Fwebcast_xigua%2Fpage%2Fhonor_level%2Fuser%2Findex.html&type=fullscreen&hide_nav_bar=1&hide_status_bar=0&__live_platform__=webcast"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9: 1
|
||||
10: 1
|
||||
11: 42000
|
||||
}
|
||||
2 {
|
||||
1: 2638466963214383
|
||||
2: 1520817
|
||||
3: "\350\213\261\345\256\207girl"
|
||||
9 {
|
||||
1: "https://sf3-ttcdn-tos.pstatp.com/img/user-avatar/a15266741ef770c810e18e5e6d073da9~300x300.image"
|
||||
}
|
||||
21 {
|
||||
1: "http://p1-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
1: "http://p3-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
2: "webcast/3_xigua_honor_level.png"
|
||||
3: 16
|
||||
4: 30
|
||||
6: 1
|
||||
7: "sslocal://webcast_webview?url=https%3A%2F%2Fwebcast.ixigua.com%2Ffalcon%2Fwebcast_xigua%2Fpage%2Fhonor_level%2Fuser%2Findex.html&type=fullscreen&hide_nav_bar=1&hide_status_bar=0&__live_platform__=webcast"
|
||||
}
|
||||
22 {
|
||||
1: 60
|
||||
2: 120
|
||||
}
|
||||
23 {
|
||||
6: 3
|
||||
19 {
|
||||
1: "http://p1-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
1: "http://p3-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
2: "webcast/3_xigua_honor_level.png"
|
||||
3: 16
|
||||
4: 30
|
||||
6: 1
|
||||
7: "sslocal://webcast_webview?url=https%3A%2F%2Fwebcast.ixigua.com%2Ffalcon%2Fwebcast_xigua%2Fpage%2Fhonor_level%2Fuser%2Findex.html&type=fullscreen&hide_nav_bar=1&hide_status_bar=0&__live_platform__=webcast"
|
||||
}
|
||||
}
|
||||
24 {
|
||||
1 {
|
||||
4 {
|
||||
1 {
|
||||
1: 0
|
||||
2: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32: ""
|
||||
38: "1520817"
|
||||
46: "MS4wLjABAAAATNQnQSVI6ePiovK48LHJHvefLO_4RAmla1Hkn0GYTWc8oE5u0a0VZnmPM3VKnaSh"
|
||||
54: 3
|
||||
61 {
|
||||
1: "http://p1-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
1: "http://p3-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
2: "webcast/3_xigua_honor_level.png"
|
||||
3: 16
|
||||
4: 30
|
||||
6: 1
|
||||
7: "sslocal://webcast_webview?url=https%3A%2F%2Fwebcast.ixigua.com%2Ffalcon%2Fwebcast_xigua%2Fpage%2Fhonor_level%2Fuser%2Findex.html&type=fullscreen&hide_nav_bar=1&hide_status_bar=0&__live_platform__=webcast"
|
||||
}
|
||||
}
|
||||
3: 31
|
||||
10: 1
|
||||
14: "0\344\272\272"
|
||||
18 {
|
||||
1: "live_room_enter_toast"
|
||||
2: "{0:user} \346\235\245\344\272\206{1:string}"
|
||||
3 {
|
||||
1: "#de000000"
|
||||
4: 400
|
||||
}
|
||||
4 {
|
||||
1: 11
|
||||
2 {
|
||||
1: "#61000000"
|
||||
4: 400
|
||||
}
|
||||
21 {
|
||||
1 {
|
||||
1: 2638466963214383
|
||||
2: 1520817
|
||||
3: "\350\213\261\345\256\207girl"
|
||||
9 {
|
||||
1: "https://sf3-ttcdn-tos.pstatp.com/img/user-avatar/a15266741ef770c810e18e5e6d073da9~300x300.image"
|
||||
}
|
||||
21 {
|
||||
1: "http://p1-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
1: "http://p3-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
2: "webcast/3_xigua_honor_level.png"
|
||||
3: 16
|
||||
4: 30
|
||||
6: 1
|
||||
7: "sslocal://webcast_webview?url=https%3A%2F%2Fwebcast.ixigua.com%2Ffalcon%2Fwebcast_xigua%2Fpage%2Fhonor_level%2Fuser%2Findex.html&type=fullscreen&hide_nav_bar=1&hide_status_bar=0&__live_platform__=webcast"
|
||||
}
|
||||
22 {
|
||||
1: 60
|
||||
2: 120
|
||||
}
|
||||
23 {
|
||||
6: 3
|
||||
19 {
|
||||
1: "http://p1-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
1: "http://p3-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
2: "webcast/3_xigua_honor_level.png"
|
||||
3: 16
|
||||
4: 30
|
||||
6: 1
|
||||
7: "sslocal://webcast_webview?url=https%3A%2F%2Fwebcast.ixigua.com%2Ffalcon%2Fwebcast_xigua%2Fpage%2Fhonor_level%2Fuser%2Findex.html&type=fullscreen&hide_nav_bar=1&hide_status_bar=0&__live_platform__=webcast"
|
||||
}
|
||||
}
|
||||
24 {
|
||||
1 {
|
||||
4 {
|
||||
1 {
|
||||
1: 0
|
||||
2: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32: ""
|
||||
38: "1520817"
|
||||
46: "MS4wLjABAAAATNQnQSVI6ePiovK48LHJHvefLO_4RAmla1Hkn0GYTWc8oE5u0a0VZnmPM3VKnaSh"
|
||||
54: 3
|
||||
61 {
|
||||
1: "http://p1-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
1: "http://p3-webcast-xgcdn.byteimg.com/img/webcast/3_xigua_honor_level.png~tplv-obj.png"
|
||||
2: "webcast/3_xigua_honor_level.png"
|
||||
3: 16
|
||||
4: 30
|
||||
6: 1
|
||||
7: "sslocal://webcast_webview?url=https%3A%2F%2Fwebcast.ixigua.com%2Ffalcon%2Fwebcast_xigua%2Fpage%2Fhonor_level%2Fuser%2Findex.html&type=fullscreen&hide_nav_bar=1&hide_status_bar=0&__live_platform__=webcast"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3: 6902672961728662285
|
||||
}
|
||||
1 {
|
||||
1: "WebcastMemberMessage"
|
||||
2 {
|
||||
1 {
|
||||
1: "WebcastMemberMessage"
|
||||
2: 6902672961866681091
|
||||
3: 6902629276546566925
|
||||
6: 1
|
||||
8 {
|
||||
1: "live_room_enter_toast"
|
||||
2: "{0:user} \346\235\245\344\272\206{1:string}"
|
||||
3 {
|
||||
1: "#de000000"
|
||||
4: 400
|
||||
}
|
||||
4 {
|
||||
1: 11
|
||||
2 {
|
||||
1: "#61000000"
|
||||
4: 400
|
||||
}
|
||||
21 {
|
||||
1 {
|
||||
1: 1015537608169303
|
||||
3: "\347\224\250\346\210\267525366763846"
|
||||
9 {
|
||||
1: "https://sf1-ttcdn-tos.pstatp.com/img/mosaic-legacy/3791/5070639578~120x256.image"
|
||||
}
|
||||
22 {
|
||||
1: 31
|
||||
}
|
||||
23 {
|
||||
19: ""
|
||||
}
|
||||
24 {
|
||||
1 {
|
||||
4 {
|
||||
1: "\010\000\022\000"
|
||||
}
|
||||
}
|
||||
}
|
||||
32: ""
|
||||
38: "0"
|
||||
46: "MS4wLjABAAAAXDp4yUHFF_sA3Uf9T4OPkW0dk-9SUYxbFNFrs7CcyhAcjYzdsYFYE5vuUdaHbo9R"
|
||||
54: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9: 1
|
||||
10: 1
|
||||
11: 42000
|
||||
}
|
||||
2 {
|
||||
1: 1015537608169303
|
||||
3: "\347\224\250\346\210\267525366763846"
|
||||
9 {
|
||||
1: "https://sf1-ttcdn-tos.pstatp.com/img/mosaic-legacy/3791/5070639578~120x256.image"
|
||||
}
|
||||
22 {
|
||||
1: 31
|
||||
}
|
||||
23 {
|
||||
19: ""
|
||||
}
|
||||
24 {
|
||||
1 {
|
||||
4 {
|
||||
1 {
|
||||
1: 0
|
||||
2: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32: ""
|
||||
38: "0"
|
||||
46: "MS4wLjABAAAAXDp4yUHFF_sA3Uf9T4OPkW0dk-9SUYxbFNFrs7CcyhAcjYzdsYFYE5vuUdaHbo9R"
|
||||
54: 3
|
||||
}
|
||||
10: 1
|
||||
14: "0\344\272\272"
|
||||
18 {
|
||||
1: "live_room_enter_toast"
|
||||
2: "{0:user} \346\235\245\344\272\206{1:string}"
|
||||
3 {
|
||||
1: "#de000000"
|
||||
4: 400
|
||||
}
|
||||
4 {
|
||||
1: 11
|
||||
2 {
|
||||
1: "#61000000"
|
||||
4: 400
|
||||
}
|
||||
21 {
|
||||
1 {
|
||||
1: 1015537608169303
|
||||
3: "\347\224\250\346\210\267525366763846"
|
||||
9 {
|
||||
1: "https://sf1-ttcdn-tos.pstatp.com/img/mosaic-legacy/3791/5070639578~120x256.image"
|
||||
}
|
||||
22 {
|
||||
1: 31
|
||||
}
|
||||
23 {
|
||||
19: ""
|
||||
}
|
||||
24 {
|
||||
1 {
|
||||
4 {
|
||||
1 {
|
||||
1: 0
|
||||
2: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32: ""
|
||||
38: "0"
|
||||
46: "MS4wLjABAAAAXDp4yUHFF_sA3Uf9T4OPkW0dk-9SUYxbFNFrs7CcyhAcjYzdsYFYE5vuUdaHbo9R"
|
||||
54: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3: 6902672961866681091
|
||||
}
|
||||
2: "1607153789923_6902672963397497753_6902669995575083008_1"
|
||||
3: 1000
|
||||
4: 1607153789923
|
||||
5: "fetch_time:1607153789923|start_time:0|fetch_id:6902672959102530453|flag:0|seq:2684|next_cursor:1607153789923_6902672963397497753_6902669995575083008_1"
|
13
LICENSE
Normal file
13
LICENSE
Normal file
@ -0,0 +1,13 @@
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
25
README.md
25
README.md
@ -1,18 +1,31 @@
|
||||
# XiguaLiveDanmakuHelper
|
||||
|
||||
## 现在西瓜视频搜索接口无法使用,建议开发者自己找到自己的用户ID,写死加载即可
|
||||
|
||||
### 因西瓜直播弹幕接口换成了ProtoBuf,已经尝试解析出了部分proto
|
||||
([v7旧版本](https://github.com/q792602257/XiguaLiveDanmakuHelper/tree/v7)仍可用就是带动画的礼物不显示而已)
|
||||
|
||||
### 西瓜直播弹幕助手--控制台版
|
||||
### 从安卓9.4版本后 *(大概是)* 发现需要连接Websocket才能获取弹幕,且又是魔改protobuf(搞不懂),手动断开Websocket后才会轮询请求
|
||||
|
||||
界面版:[q792602257/XiguaDanmakuHelperGUI](https://github.com/q792602257/XiguaDanmakuHelperGUI "C# ver")
|
||||
### ~~西瓜直播弹幕助手--界面版~~
|
||||
|
||||
> 界面版:[q792602257/XiguaDanmakuHelperGUI](https://github.com/q792602257/XiguaDanmakuHelperGUI "C# ver")
|
||||
> #### 该项目已经荒废,除非你知道如何开发,否则不建议使用
|
||||
|
||||
### 西瓜直播弹幕接口```api.py```
|
||||
|
||||
> - 基于安卓8.1.6
|
||||
> - 基于安卓9.6.6(96615)
|
||||
|
||||
### 西瓜直播弹幕助手--礼物端```WinMain.py```
|
||||
|
||||
### <s>计划更新</s>
|
||||
### 西瓜直播弹幕助手--录播端```WebMain.py```
|
||||
|
||||
### 并没有呢,这段时间太忙了
|
||||
> - 能够自动进行ffmpeg转码
|
||||
> - 转码后自动上传至B站
|
||||
> - 顺便还能自己清理录播的文件(移动到一个位置,执行shell命令,上传百度云)
|
||||
> - 把录像文件分一定大小保存(B站有限制,但是不知道是多少)
|
||||
> - 少部分错误包容机制
|
||||
> - 有一个简单的WEB页面,及简单的控制接口
|
||||
|
||||
### ~~计划更新~~
|
||||
|
||||
### 随缘更新
|
||||
|
@ -4,13 +4,11 @@ from XiguaMessage_pb2 import GiftMessage
|
||||
|
||||
|
||||
class Gift:
|
||||
roomID = 0
|
||||
giftList = {}
|
||||
|
||||
def __init__(self, json=None):
|
||||
self.ID = 0
|
||||
self.count = 0
|
||||
self.amount = 0
|
||||
self.user = None
|
||||
self.isFinished = False
|
||||
self.backupName = None
|
||||
@ -31,10 +29,6 @@ class Gift:
|
||||
|
||||
def parse(self, json):
|
||||
self.user = User(json)
|
||||
if "common" in json and json["common"] is not None:
|
||||
if Gift.roomID != int(json["common"]["room_id"]):
|
||||
Gift.roomID = int(json["common"]["room_id"])
|
||||
self.update()
|
||||
if "extra" in json and json["extra"] is not None:
|
||||
if "present_info" in json["extra"] and json["extra"]['present_info'] is not None:
|
||||
self.ID = int(json["extra"]['present_info']['id'])
|
||||
@ -42,21 +36,6 @@ class Gift:
|
||||
elif "present_end_info" in json["extra"] and json["extra"]['present_end_info'] is not None:
|
||||
self.ID = int(json["extra"]['present_end_info']['id'])
|
||||
self.count = json["extra"]['present_end_info']['count']
|
||||
if self.ID != 0 and self.ID in self.giftList:
|
||||
self.amount = self.giftList[self.ID]['diamond_count'] * self.count
|
||||
else:
|
||||
self.update()
|
||||
|
||||
@classmethod
|
||||
def update(cls):
|
||||
p = requests.get("https://i.snssdk.com/videolive/gift/get_gift_list?room_id={roomID}"
|
||||
"&version_code=800&device_platform=android".format(roomID=Gift.roomID))
|
||||
d = p.json()
|
||||
if "gift_info" not in d:
|
||||
print("错误:礼物更新失败")
|
||||
else:
|
||||
for i in d["gift_info"]:
|
||||
cls.addGift(i)
|
||||
|
||||
def isAnimate(self):
|
||||
if self.ID != 0 and self.ID in self.giftList:
|
||||
@ -68,7 +47,8 @@ class Gift:
|
||||
return self.giftList[self.ID]["type"] == 2
|
||||
return False
|
||||
|
||||
def _getGiftName(self):
|
||||
@property
|
||||
def name(self):
|
||||
if self.ID in self.giftList:
|
||||
return self.giftList[self.ID]["name"]
|
||||
elif self.backupName is not None:
|
||||
@ -77,13 +57,13 @@ class Gift:
|
||||
return "未知礼物[{}]".format(self.ID)
|
||||
|
||||
def __str__(self):
|
||||
return "{user} 送出的 {count} 个 {name}".format(user=self.user, count=self.count, name=self._getGiftName())
|
||||
return "{user} 送出的 {count} 个 {name}".format(user=self.user, count=self.count, name=self.name)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __repr__(self):
|
||||
return "西瓜礼物【{}(ID:{})】".format(self._getGiftName(), self.ID)
|
||||
return "西瓜礼物【{}(ID:{})】".format(self.name, self.ID)
|
||||
|
||||
@classmethod
|
||||
def addGift(cls, _gift):
|
||||
|
@ -41,13 +41,17 @@ class User:
|
||||
self.type = json["extra"]["user_room_auth_status"]["user_type"]
|
||||
self.block = json["extra"]["user_room_auth_status"]["is_block"]
|
||||
self.mute = json["extra"]["user_room_auth_status"]["is_silence"]
|
||||
elif "user_info" in json and json["user_info"] is not None:
|
||||
if "user_info" in json and json["user_info"] is not None:
|
||||
self.ID = json['user_info']['user_id']
|
||||
self.name = json['user_info']['name']
|
||||
elif "anchor" in json and json["anchor"] is not None:
|
||||
if "anchor" in json and json["anchor"] is not None:
|
||||
if "user_info" in json["anchor"] and json["anchor"]['user_info'] is not None:
|
||||
self.ID = json["anchor"]['user_info']['user_id']
|
||||
self.name = json["anchor"]['user_info']['name']
|
||||
if "user_id" in json:
|
||||
self.ID = json["user_id"]
|
||||
if "user_name" in json:
|
||||
self.name = json["user_name"]
|
||||
if self.type is None:
|
||||
self.type = 0
|
||||
if isinstance(self.level, str):
|
||||
|
198
WebMain.py
Normal file
198
WebMain.py
Normal file
@ -0,0 +1,198 @@
|
||||
import os
|
||||
from glob import glob
|
||||
from time import sleep
|
||||
from flask_cors import CORS
|
||||
from flask import Flask, jsonify, request, render_template, Response, send_file
|
||||
import Common
|
||||
import threading
|
||||
from liveDownloader import run as RUN
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['JSON_AS_ASCII'] = False
|
||||
CORS(app, supports_credentials=True)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
@app.route("/config", methods=["GET"])
|
||||
def readConfig():
|
||||
config = Common.config.copy()
|
||||
return jsonify(config)
|
||||
|
||||
|
||||
@app.route("/config", methods=["POST"])
|
||||
def writeConfig():
|
||||
# TODO : 完善
|
||||
Common.appendOperation("更新配置")
|
||||
Common.reloadConfig()
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0, "data": request.form})
|
||||
|
||||
|
||||
@app.route("/force/not/upload", methods=["POST"])
|
||||
def toggleForceNotUpload():
|
||||
Common.forceNotUpload = not Common.forceNotUpload
|
||||
Common.appendOperation("将强制不上传的值改为:{}".format(Common.forceNotUpload))
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0, "data": {
|
||||
"forceNotUpload": Common.forceNotUpload,
|
||||
}})
|
||||
|
||||
|
||||
@app.route("/force/not/encode", methods=["POST"])
|
||||
def toggleForceNotEncode():
|
||||
Common.forceNotEncode = not Common.forceNotEncode
|
||||
Common.appendOperation("将强制不编码的值改为:{}".format(Common.forceNotEncode))
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0, "data": {
|
||||
"forceNotEncode": Common.forceNotEncode,
|
||||
}})
|
||||
|
||||
|
||||
@app.route("/force/not/download", methods=["POST"])
|
||||
def toggleForceNotDownload():
|
||||
Common.forceNotDownload = not Common.forceNotDownload
|
||||
Common.appendOperation("将强制不下载的值改为:{}".format(Common.forceNotDownload))
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0, "data": {
|
||||
"forceNotDownload": Common.forceNotDownload,
|
||||
}})
|
||||
|
||||
|
||||
@app.route("/force/not/broadcast", methods=["POST"])
|
||||
def toggleForceNotBroadcast():
|
||||
Common.forceNotBroadcasting = not Common.forceNotBroadcasting
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0, "data": {
|
||||
"forceNotBroadcasting": Common.forceNotBroadcasting,
|
||||
}})
|
||||
|
||||
|
||||
@app.route("/force/start/encode", methods=["POST"])
|
||||
def toggleForceStartEncodeThread():
|
||||
Common.forceStartEncodeThread = True
|
||||
Common.appendOperation("强制运行编码线程")
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0, "data": {
|
||||
}})
|
||||
|
||||
|
||||
@app.route("/force/start/upload", methods=["POST"])
|
||||
def toggleForceStartUploadThread():
|
||||
Common.forceStartUploadThread = True
|
||||
Common.appendOperation("强制运行上传线程")
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0, "data": {
|
||||
}})
|
||||
|
||||
|
||||
@app.route("/force/start/clean", methods=["POST"])
|
||||
def startForceCleanDisk():
|
||||
Common.doClean(True)
|
||||
Common.appendOperation("强制执行清理程序")
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0, "data": {
|
||||
}})
|
||||
|
||||
|
||||
@app.route("/encode/insert", methods=["POST"])
|
||||
def insertEncode():
|
||||
if "filename" in request.form and os.path.exists(request.form["filename"]):
|
||||
Common.appendOperation("添加编码文件:{}".format(request.form["filename"]))
|
||||
Common.encodeQueue.put(request.form["filename"])
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0})
|
||||
else:
|
||||
return jsonify({"message": "no filename specific", "code": 400, "status": 1})
|
||||
|
||||
|
||||
@app.route("/upload/insert", methods=["POST"])
|
||||
def insertUpload():
|
||||
if "filename" in request.form and os.path.exists(request.form["filename"]):
|
||||
Common.appendOperation("添加上传文件:{}".format(request.form["filename"]))
|
||||
Common.uploadQueue.put(request.form["filename"])
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0})
|
||||
else:
|
||||
return jsonify({"message": "no filename specific", "code": 400, "status": 1})
|
||||
|
||||
|
||||
@app.route("/upload/discard", methods=["POST"])
|
||||
def discardUpload():
|
||||
Common.uploadQueue.empty()
|
||||
Common.b.clear()
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0})
|
||||
|
||||
|
||||
@app.route("/upload/finish", methods=["POST"])
|
||||
def finishUpload():
|
||||
Common.appendOperation("设置当前已完成上传")
|
||||
Common.uploadQueue.put(True)
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0})
|
||||
|
||||
|
||||
@app.route("/stats", methods=["GET"])
|
||||
def getAllStats():
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0, "data": Common.collectInfomation()})
|
||||
|
||||
|
||||
@app.route("/stats/device", methods=["GET"])
|
||||
def getDeviceStatus():
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0, "data": {
|
||||
"status": Common.getCurrentStatus(),
|
||||
}})
|
||||
|
||||
|
||||
@app.route("/stats/config", methods=["GET"])
|
||||
def getConfigStats():
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0, "data": {
|
||||
"config": {
|
||||
"forceNotBroadcasting": Common.forceNotBroadcasting,
|
||||
"forceNotDownload": Common.forceNotDownload,
|
||||
"forceNotUpload": Common.forceNotUpload,
|
||||
"forceNotEncode": Common.forceNotEncode,
|
||||
"downloadOnly": Common.config['dlO'],
|
||||
}
|
||||
}})
|
||||
|
||||
|
||||
@app.route("/account/reLogin", methods=["POST"])
|
||||
def accountRelogin():
|
||||
res = Common.loginBilibili(True)
|
||||
return jsonify({"message": "ok", "code": 200, "status": 0, "data": {"result": res}})
|
||||
|
||||
|
||||
@app.route("/files/", methods=["GET"])
|
||||
def fileIndex():
|
||||
a = []
|
||||
for i in (glob("*.mp4") + glob("*.flv")):
|
||||
a.append({
|
||||
"name": i,
|
||||
"size": Common.parseSize(os.path.getsize(i))
|
||||
})
|
||||
return render_template("files.html", files=a)
|
||||
|
||||
|
||||
@app.route("/files/download/<path>", methods=["GET"])
|
||||
def fileDownload(path):
|
||||
if not (".mp4" in path or ".flv" in path):
|
||||
return Response(status=404)
|
||||
if os.path.exists(path):
|
||||
return send_file(path, as_attachment=True)
|
||||
else:
|
||||
return Response(status=404)
|
||||
|
||||
|
||||
def SubThread():
|
||||
t = threading.Thread(target=RUN, args=())
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
while True:
|
||||
if t.is_alive():
|
||||
sleep(240)
|
||||
else:
|
||||
t = threading.Thread(target=RUN, args=())
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
|
||||
|
||||
if not app.debug:
|
||||
p = threading.Thread(target=SubThread)
|
||||
p.setDaemon(True)
|
||||
p.start()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
25
WinMain.py
25
WinMain.py
@ -10,6 +10,7 @@ from Struct.Gift import Gift
|
||||
from Struct.Chat import Chat
|
||||
from Struct.Lottery import Lottery
|
||||
from api import XiGuaLiveApi as Api
|
||||
from api import public_hello
|
||||
import msvcrt
|
||||
import ctypes
|
||||
|
||||
@ -121,10 +122,11 @@ class WinMain(Api):
|
||||
return "人气:{} --弹幕助手 by JerryYan".format(self.roomPopularity)
|
||||
|
||||
def onMessage(self, msg: str):
|
||||
set_cmd_text_color(BACKGROUND_BLACK | FOREGROUND_DARKGRAY)
|
||||
print("消息 : ", msg, end="")
|
||||
resetColor()
|
||||
print()
|
||||
if SHOW_ALL:
|
||||
set_cmd_text_color(BACKGROUND_BLACK | FOREGROUND_DARKGRAY)
|
||||
print("消息 : ", msg, end="")
|
||||
resetColor()
|
||||
print()
|
||||
|
||||
def onJoin(self, user: User):
|
||||
set_cmd_text_color(BACKGROUND_WHITE | FOREGROUND_BLACK)
|
||||
@ -188,16 +190,13 @@ def warning(*args):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
name = "永恒de草薙"
|
||||
resetColor()
|
||||
print("西瓜直播礼物助手 by JerryYan")
|
||||
print("接口版本8.1.6")
|
||||
if len(sys.argv) > 1:
|
||||
name = "97621754276"
|
||||
if len(sys.argv) > 2:
|
||||
if sys.argv[-1] == "a":
|
||||
SHOW_ALL = True
|
||||
name = sys.argv[1]
|
||||
if len(sys.argv) > 2:
|
||||
SHOW_ALL = sys.argv[2] == "a"
|
||||
else:
|
||||
name = readInput("请输入主播用户名,默认为", name, 3)
|
||||
resetColor()
|
||||
public_hello()
|
||||
print("搜索【", name, "】", end="\t", flush=True)
|
||||
api = WinMain(name)
|
||||
if not api.isValidUser:
|
||||
|
@ -4,13 +4,13 @@ block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(['WinMain.py'],
|
||||
pathex=['E:\\XiGuaLiveDanmakuHelper',r'C:\\Program Files (x86)\\Windows Kits\\10\\Redist\\10.0.18362.0\\ucrt\\DLLs\\x86'],
|
||||
pathex=['E:\\XiGuaLiveDanmakuHelper',r'C:\\Program Files (x86)\\Windows Kits\\10\\Redist\\10.0.17763.0\\ucrt\\DLLs\\x86'],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
excludes=["xml", "_tkinter", "pydoc", "lib2to3"],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
|
0
access_token
Normal file
0
access_token
Normal file
227
api.py
227
api.py
@ -5,7 +5,6 @@ from Struct.MemberMsg import MemberMsg
|
||||
from Struct.User import User
|
||||
from Struct.Gift import Gift
|
||||
from Struct.Chat import Chat
|
||||
from Struct.Lottery import Lottery
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
@ -13,28 +12,68 @@ from Xigua_pb2 import XiguaLive
|
||||
from XiguaMessage_pb2 import FansClubMessage, SocialMessage
|
||||
|
||||
DEBUG = False
|
||||
# 自己抓的自己设备的参数,建议开发者自己抓一个长期使用
|
||||
# 如果有大佬破解初次激活设备时的数据也行,可以自己生成一堆用
|
||||
CUSTOM_INFO = {
|
||||
'iid': "3993882704224472",
|
||||
'device_id': "71008241150",
|
||||
'cdid': "f93b3708-3fec-498f-9080-723a5679f4c0",
|
||||
'openudid': "4aeb1e2b627697be",
|
||||
# 'aid': "32", # 是一个不变的值
|
||||
'channel': "xiaomi",
|
||||
'device_brand': "Xiaomi",
|
||||
'device_type': "MI+9",
|
||||
'os_api': "29",
|
||||
'os_version': "10",
|
||||
'abi': "armeabi-v7a",
|
||||
'dpi': "480",
|
||||
'resolution': "1080*2217",
|
||||
'rom_version': "miui_V12_V12.0.6.0.QFACNXM",
|
||||
}
|
||||
VERSION_INFO = {
|
||||
'app_name': "video_article",
|
||||
'version_code': "966",
|
||||
'version_code_full': "96615",
|
||||
'version_name': "9.6.6",
|
||||
'ab_version': "668851,2678488,668858,2678385,668859,2678471,668856,2678470,668855,2678439,668854,994679,"
|
||||
"2678460,2713070,2738381,2744004,668853,2678466,668852,2678435,2625016",
|
||||
'manifest_version_code': "566",
|
||||
'tma_jssdk_version': "2010000",
|
||||
'oaid': "693ea85657ef38ca",
|
||||
}
|
||||
COMMON_GET_PARAM = (
|
||||
"&iid=96159232732&device_id=55714661189&channel=xiaomi&aid=32&app_name=video_article&version_code=816"
|
||||
"&version_name=8.1.6&device_platform=android&ab_version=941090,785218,668858,1046292,1073579,830454,956074,929436,"
|
||||
"797199,1135476,1179370,994679,959010,900042,1113833,668854,1193963,901277,1043330,1038721,994822,1002058,1230687,"
|
||||
"1189797,1143356,1143441,1143501,1143698,1143713,1371009,1243997,1392586,1395695,1395486,1398858,668852,668856,"
|
||||
"668853,1186421,668851,668859,999124,668855,1039075&device_type=MI+8+SE&device_brand=Xiaomi&language=zh"
|
||||
"&os_api=28&os_version=9&openudid=70d6668d41512c39&manifest_version_code=412&update_version_code=81606"
|
||||
"&_rticket={TIMESTAMP:.0f}&cdid_ts={TIMESTAMP:.0f}&fp=a_fake_fp&tma_jssdk_version=1290000"
|
||||
"&cdid=ed4295e8-5d9a-4cb9-b2a2-04009a3baa2d&oaid=a625f466e0975d42")
|
||||
"&iid={iid}&device_id={device_id}&ac=wifi&channel={channel}&aid=32&app_name={app_name}&version_code={version_code}&"
|
||||
"version_name={version_name}&device_platform=android&ab_version={ab_version}&ssmix=a&device_type={device_type}&"
|
||||
"device_brand={device_brand}&language=zh&os_api={os_api}&os_version={os_version}&openudid={openudid}&"
|
||||
"manifest_version_code={manifest_version_code}&resolution={resolution}&dpi={dpi}&"
|
||||
"update_version_code={version_code_full}&_rticket={{TIMESTAMP:.0f}}&"
|
||||
"cdid_ts={{TIMESTAMP:.0f}}&host_abi={abi}&tma_jssdk_version={tma_jssdk_version}&"
|
||||
"rom_version={rom_version}&cdid={cdid}&oaid={oaid}").format_map({**VERSION_INFO, **CUSTOM_INFO})
|
||||
WEBCAST_GET_PARAMS = "webcast_sdk_version=1990&webcast_language=zh&webcast_locale=zh_CN&webcast_gps_access=1"
|
||||
ROOM_ENTER_POST_PARAMS = (
|
||||
"room_id={roomId}&hold_living_room=1&is_login=0&enter_from_uid_by_shared=0&video_id=0&"
|
||||
"scenario=0&enter_type=click&enter_source=click_pgc_WITHIN_pgc-head_portrait&live_room_mode=0")
|
||||
SEARCH_USER_API = (
|
||||
"https://security.snssdk.com/video/app/search/live/?format=json&search_sug=0&forum=0&m_tab=live&is_native_req=0"
|
||||
"&offset=0&from=live&en_qc=1&pd=xigua_live&ssmix=a{COMMON}&keyword={keyword}")
|
||||
USER_INFO_API = "https://is.snssdk.com/video/app/user/home/v7/?to_user_id={userId}{COMMON}"
|
||||
ROOM_INFO_API = ("https://webcast3.ixigua.com/webcast/room/enter/?room_id={roomId}&webcast_sdk_version=1350"
|
||||
"&webcast_language=zh&webcast_locale=zh_CN&pack_level=4{COMMON}")
|
||||
DANMAKU_GET_API = ("https://webcast3.ixigua.com/webcast/room/{roomId}/_fetch_message_polling/?webcast_sdk_version=1350"
|
||||
"&webcast_language=zh&webcast_locale=zh_CN{COMMON}")
|
||||
GIFT_DATA_API = ("https://webcast.ixigua.com/webcast/gift/list/?room_id={roomId}&fetch_giftlist_from=2"
|
||||
"&webcast_sdk_version=1350&webcast_language=zh&webcast_locale=zh_CN{COMMON}")
|
||||
"https://search5-search-lq.ixigua.com/video/app/search/live/?format=json&keyword_type=search_subtab_switch"
|
||||
"&fss=search_subtab_switch&target_channel=video_search&keyword_type=search_subtab_switch&offset=0&count=10"
|
||||
"&search_sug=1&forum=1&is_native_req=0&m_tab=video&pd=user&tab=user&_s_tma=SEARCH_STANDARD.list.fe_get_data"
|
||||
'&_s_page_sub_route=/&_s_ec={{"filterDataType":[],"reserveFilterBar":true}}&__use_xigua_native_bridge_fetch__=1'
|
||||
'&ab_param={{"is_show_filter_feature": 1, "is_hit_new_ui": 1}}'
|
||||
"&search_start_time={TIMESTAMP:.0f}&from=live&en_qc=1&pd=xigua_live&ssmix=a{COMMON}&keyword={keyword}")
|
||||
USER_INFO_API = "https://ib-lq.snssdk.com/video/app/user/userhome/v8/?to_user_id={userId}{COMMON}"
|
||||
ROOM_INFO_API = "https://webcast5-normal-c-lq.ixigua.com/webcast/room/enter/?{WEBCAST}{COMMON}"
|
||||
DANMAKU_GET_API = "https://webcast5-normal-c-lq.ixigua.com/webcast/im/fetch/?{WEBCAST}{COMMON}"
|
||||
GIFT_DATA_API = ("https://webcast5-normal-c-hl.ixigua.com/webcast/gift/list/?room_id={roomId}&to_room_id={roomId}&"
|
||||
"gift_scene=1&fetch_giftlist_from=2¤t_network_quality_info={{}}&"
|
||||
"{WEBCAST}{COMMON}")
|
||||
COMMON_HEADERS = {
|
||||
"sdk-version": '1',
|
||||
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 9) VideoArticle/8.1.6 cronet/TTNetVersion:b97574c0 2019-09-24",
|
||||
"x-vc-bdturing-sdk-version": "2.0.1",
|
||||
"sdk-version": '2',
|
||||
"passport-sdk-version": "21",
|
||||
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 10; MI 9 MIUI/V12.0.6.0.QFACNXM) VideoArticle/9.6.6 "
|
||||
"cronet/TTNetVersion:4b936afe 2021-01-13 QuicVersion:47946d2a 2020-10-14",
|
||||
"X-SS-STUB": "74103A7F40FDC66E0675705DCA2C3A77",
|
||||
"X-SS-DP": "32",
|
||||
"Accept-Encoding": "gzip, deflate"
|
||||
}
|
||||
|
||||
@ -51,6 +90,7 @@ class XiGuaLiveApi:
|
||||
name = "永恒de草薙"
|
||||
self.broadcaster = None
|
||||
self.isValidUser = False
|
||||
self.name = str(name)
|
||||
if type(name) == User:
|
||||
self.broadcaster = name
|
||||
self.name = name.name
|
||||
@ -64,7 +104,6 @@ class XiGuaLiveApi:
|
||||
self._rawRoomInfo = {}
|
||||
self.roomID = 0
|
||||
self.roomPopularity = 0
|
||||
self.lottery = None
|
||||
self.s = requests.session()
|
||||
self.s.headers.update(COMMON_HEADERS)
|
||||
self._updRoomAt = datetime.fromtimestamp(0)
|
||||
@ -86,6 +125,8 @@ class XiGuaLiveApi:
|
||||
self.roomPopularity = _data["data"]["popularity"]
|
||||
|
||||
def getJson(self, url, **kwargs):
|
||||
if "timeout" not in kwargs:
|
||||
kwargs["timeout"] = 10
|
||||
try:
|
||||
p = self.s.get(url, **kwargs)
|
||||
except Exception as e:
|
||||
@ -107,6 +148,8 @@ class XiGuaLiveApi:
|
||||
return None
|
||||
|
||||
def postJson(self, url, data, **kwargs):
|
||||
if "timeout" not in kwargs:
|
||||
kwargs["timeout"] = 10
|
||||
try:
|
||||
p = self.s.post(url, data=data, **kwargs)
|
||||
except Exception as e:
|
||||
@ -114,6 +157,7 @@ class XiGuaLiveApi:
|
||||
if DEBUG:
|
||||
print("POST")
|
||||
print("URL", url)
|
||||
print("DATA", data)
|
||||
print("ERR ", e.__str__())
|
||||
return None
|
||||
try:
|
||||
@ -121,9 +165,11 @@ class XiGuaLiveApi:
|
||||
except Exception as e:
|
||||
print("解析请求失败")
|
||||
if DEBUG:
|
||||
print("GET JSON")
|
||||
print("POST JSON")
|
||||
print("URL", url)
|
||||
print("DATA", data)
|
||||
print("CNT", p.text)
|
||||
print("HDR", p.headers)
|
||||
print("ERR ", e.__str__())
|
||||
return None
|
||||
|
||||
@ -220,13 +266,6 @@ class XiGuaLiveApi:
|
||||
print("消息 :", "主播离开了")
|
||||
self.updRoomInfo()
|
||||
|
||||
def onLottery(self, i: Lottery):
|
||||
"""
|
||||
中奖的内容
|
||||
:param i:
|
||||
"""
|
||||
print("中奖消息 :", i)
|
||||
|
||||
def _checkUsernameIsMatched(self, compare=None):
|
||||
"""
|
||||
验证主播名字是自己想要的那个
|
||||
@ -237,35 +276,41 @@ class XiGuaLiveApi:
|
||||
compare = self.broadcaster
|
||||
if self.name is None or compare is None:
|
||||
return False
|
||||
return self.name == compare.__str__() or compare.__str__() in self.name or self.name in compare.__str__()
|
||||
return self.name == compare.__str__() or compare.__repr__() in self.name or self.name in compare.__repr__()
|
||||
|
||||
def _forceSearchUser(self):
|
||||
"""
|
||||
搜索主播名
|
||||
:return:
|
||||
"""
|
||||
_formatData = {"COMMON": COMMON_GET_PARAM, "TIMESTAMP": time.time() * 1000, "keyword": self.name}
|
||||
_url = SEARCH_USER_API.format_map(_formatData).format_map(_formatData)
|
||||
_formatData = {"TIMESTAMP": time.time() * 1000, "keyword": self.name}
|
||||
_COMMON = COMMON_GET_PARAM.format_map(_formatData)
|
||||
_formatData['COMMON'] = _COMMON
|
||||
_url = SEARCH_USER_API.format_map(_formatData)
|
||||
d = self.getJson(_url)
|
||||
if d is None:
|
||||
print("搜索接口请求失败")
|
||||
return False
|
||||
self.broadcaster = None
|
||||
self.isValidUser = False
|
||||
self.isLive = False
|
||||
if "data" in d and d["data"] is not None:
|
||||
for i in d["data"]:
|
||||
if self.broadcaster is not None:
|
||||
break
|
||||
if i["block_type"] != 0:
|
||||
if i["card_type"] != 3:
|
||||
continue
|
||||
if "cells" not in i or len(i["cells"]) == 0:
|
||||
if "search_data" not in i or len(i["search_data"]) == 0:
|
||||
break
|
||||
for _j in i["cells"]:
|
||||
_user = User(_j)
|
||||
if self._checkUsernameIsMatched(_user):
|
||||
self.isValidUser = True
|
||||
self.broadcaster = _user
|
||||
break
|
||||
for _j in i["search_data"]:
|
||||
if "room" in _j:
|
||||
_user = User(_j["room"])
|
||||
self.roomID = _j["room"]["room_id"]
|
||||
self.isLive = _j["room"]["is_living"]
|
||||
if self._checkUsernameIsMatched(_user):
|
||||
self.isValidUser = True
|
||||
self.broadcaster = _user
|
||||
break
|
||||
self._updRoomAt = datetime.now()
|
||||
return self._updateUserInfo()
|
||||
|
||||
@ -277,41 +322,56 @@ class XiGuaLiveApi:
|
||||
if self.broadcaster is None:
|
||||
self.isValidUser = False
|
||||
return False
|
||||
_formatData = {"COMMON": COMMON_GET_PARAM, "TIMESTAMP": time.time() * 1000, "userId": self.broadcaster.ID}
|
||||
_url = USER_INFO_API.format_map(_formatData).format_map(_formatData)
|
||||
self.isLive = False
|
||||
_formatData = {"TIMESTAMP": time.time() * 1000, "userId": self.broadcaster.ID}
|
||||
_COMMON = COMMON_GET_PARAM.format_map(_formatData)
|
||||
_formatData['COMMON'] = _COMMON
|
||||
_url = USER_INFO_API.format_map(_formatData)
|
||||
d = self.getJson(_url)
|
||||
if d is None:
|
||||
print("获取用户信息失败")
|
||||
return False
|
||||
self.isValidUser = d["status"] == 0
|
||||
if "user_info" not in d and d["user_info"] is None:
|
||||
_d = d.get('data', {})
|
||||
if "user_home_info" not in _d and _d['user_home_info']['user_info'] is None:
|
||||
self.apiChangedError("Api发生改变,请及时联系我", d)
|
||||
return False
|
||||
self.broadcaster = User(d)
|
||||
self._updRoomAt = datetime.now()
|
||||
self.broadcaster = User(_d['user_home_info'])
|
||||
if not self._checkUsernameIsMatched():
|
||||
self.isLive = False
|
||||
return False
|
||||
self.isLive = d["user_info"]["is_living"]
|
||||
self._updRoomAt = datetime.now()
|
||||
self._rawRoomInfo = d["user_info"]['live_info']
|
||||
self.isLive = 'user_live_info_list' in _d
|
||||
if self.isLive and len(_d['user_live_info_list']) != 0:
|
||||
# 既然有长度,默认个0应该没事
|
||||
self._rawRoomInfo = _d['user_live_info_list'][0]['live_info']
|
||||
else:
|
||||
self.isLive = False
|
||||
if self.isLive:
|
||||
self.roomID = d["user_info"]['live_info']['room_id']
|
||||
# 处理抽奖事件
|
||||
l = Lottery(self._rawRoomInfo)
|
||||
if l.isActive:
|
||||
# 因为现在每个房间只能同时开启一个抽奖,所以放一个就行了
|
||||
self.lottery = l
|
||||
return True
|
||||
self.roomID = self._rawRoomInfo['room_id']
|
||||
return self._getRoomInfo(True)
|
||||
return self.isLive
|
||||
|
||||
def _getRoomInfo(self, force=False):
|
||||
if self.roomID == 0:
|
||||
if self.roomID == 0 or not self.roomID:
|
||||
self.isLive = False
|
||||
return False
|
||||
if not force and (self._updRoomAt + timedelta(minutes=10) > datetime.now()):
|
||||
if (self._updRoomAt + timedelta(minutes=3) > datetime.now()) and not force:
|
||||
return self.isLive
|
||||
_formatData = {"COMMON": COMMON_GET_PARAM, "TIMESTAMP": time.time() * 1000, "roomId": self.roomID}
|
||||
_url = ROOM_INFO_API.format_map(_formatData).format_map(_formatData)
|
||||
d = self.getJson(_url)
|
||||
_formatData = {"TIMESTAMP": time.time() * 1000}
|
||||
_COMMON = COMMON_GET_PARAM.format_map(_formatData)
|
||||
_formatData['COMMON'] = _COMMON
|
||||
_formatData['WEBCAST'] = WEBCAST_GET_PARAMS
|
||||
_url = ROOM_INFO_API.format_map(_formatData)
|
||||
_postData = ROOM_ENTER_POST_PARAMS.format_map({'roomId': self.roomID})
|
||||
_headers = {"response-format": "json",
|
||||
'Connection': "keep-alive",
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'Content-Length': str(len(_postData)),
|
||||
**COMMON_HEADERS,
|
||||
}
|
||||
d = self.postJson(_url, _postData, headers=_headers)
|
||||
self.isLive = False
|
||||
if d is None:
|
||||
print("获取房间信息接口请求失败")
|
||||
return False
|
||||
@ -319,10 +379,9 @@ class XiGuaLiveApi:
|
||||
print("接口提示:【{}】".format(d["data"]["message"]))
|
||||
return False
|
||||
self._rawRoomInfo = d["data"]
|
||||
self.isLive = d["data"]["status"] == 2
|
||||
self.isLive = self._rawRoomInfo["status"] == 2
|
||||
self._updRoomAt = datetime.now()
|
||||
self._updateRoomPopularity(d)
|
||||
Gift.roomID = self.roomID
|
||||
return self.isLive
|
||||
|
||||
def updRoomInfo(self, force=False):
|
||||
@ -341,15 +400,15 @@ class XiGuaLiveApi:
|
||||
|
||||
def updGiftInfo(self):
|
||||
self.updRoomInfo()
|
||||
_formatData = {"COMMON": COMMON_GET_PARAM, "TIMESTAMP": time.time() * 1000, "roomId": self.roomID}
|
||||
_url = GIFT_DATA_API.format_map(_formatData).format_map(_formatData)
|
||||
_formatData = {"TIMESTAMP": time.time() * 1000, "roomId": self.roomID}
|
||||
_COMMON = COMMON_GET_PARAM.format_map(_formatData)
|
||||
_formatData['COMMON'] = _COMMON
|
||||
_formatData['WEBCAST'] = WEBCAST_GET_PARAMS
|
||||
_url = GIFT_DATA_API.format_map(_formatData)
|
||||
d = self.getJson(_url)
|
||||
Gift.roomID = self.roomID
|
||||
if d is None or d["status_code"] != 0:
|
||||
Gift.update()
|
||||
elif 'pages' not in d["data"]:
|
||||
Gift.update()
|
||||
else:
|
||||
return "异常"
|
||||
elif 'pages' in d["data"]:
|
||||
for _page in d["data"]['pages']:
|
||||
if 'gifts' in _page:
|
||||
for _gift in _page['gifts']:
|
||||
@ -361,10 +420,17 @@ class XiGuaLiveApi:
|
||||
获取弹幕
|
||||
"""
|
||||
self.updRoomInfo()
|
||||
_formatData = {"COMMON": COMMON_GET_PARAM, "TIMESTAMP": time.time() * 1000, "roomId": self.roomID}
|
||||
_url = DANMAKU_GET_API.format_map(_formatData).format_map(_formatData)
|
||||
p = self.s.post(_url, data="cursor={cursor}&resp_content_type=protobuf&live_id=3&user_id=0&identity=audience"
|
||||
"&internal_ext={ext}".format_map({"cursor": self._cursor, "ext": self._ext}),
|
||||
if not self.isLive:
|
||||
return
|
||||
_formatData = {"TIMESTAMP": time.time() * 1000, "roomId": self.roomID}
|
||||
_COMMON = COMMON_GET_PARAM.format_map(_formatData)
|
||||
_formatData['COMMON'] = _COMMON
|
||||
_formatData['WEBCAST'] = WEBCAST_GET_PARAMS
|
||||
_url = DANMAKU_GET_API.format_map(_formatData)
|
||||
p = self.s.post(_url, data="room_id={roomId}&fetch_rule=0&cursor={cursor}&"
|
||||
"resp_content_type=protobuf&live_id=3&user_id=0&identity=audience&"
|
||||
"last_rtt=85&internal_ext={ext}"
|
||||
.format_map({"roomId":self.roomID, "cursor": self._cursor, "ext": self._ext}),
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"})
|
||||
if p.status_code != 200:
|
||||
return
|
||||
@ -382,6 +448,9 @@ class XiGuaLiveApi:
|
||||
elif _each.method == "WebcastChatMessage":
|
||||
_chat = Chat(_each.raw)
|
||||
self.onChat(_chat)
|
||||
elif _each.method == "WebcastControlMessage":
|
||||
# 下播的时候会有个这个
|
||||
self.onLeave(None)
|
||||
elif _each.method == "WebcastSocialMessage":
|
||||
_socialMessage = SocialMessage()
|
||||
_socialMessage.ParseFromString(_each.raw)
|
||||
@ -398,26 +467,24 @@ class XiGuaLiveApi:
|
||||
self.onMessage(_fansClubMessage.content)
|
||||
else:
|
||||
pass
|
||||
# 更新抽奖信息
|
||||
if self.lottery is not None and self.lottery.ID != 0:
|
||||
self.lottery.update()
|
||||
if self.lottery.isFinished:
|
||||
self.onLottery(self.lottery)
|
||||
self.lottery = None
|
||||
|
||||
@property
|
||||
def updateAt(self):
|
||||
return self._updRoomAt
|
||||
|
||||
|
||||
def public_hello():
|
||||
print("西瓜直播弹幕助手 by JerryYan")
|
||||
print("接口版本:{version_name}({version_code_full})".format_map(VERSION_INFO))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
name = "永恒de草薙"
|
||||
if len(sys.argv) > 2:
|
||||
if sys.argv[-1] == "d":
|
||||
DEBUG = True
|
||||
name = sys.argv[1]
|
||||
print("西瓜直播弹幕助手 by JerryYan")
|
||||
print("接口版本8.1.6")
|
||||
public_hello()
|
||||
print("搜索【", name, "】", end="\t", flush=True)
|
||||
api = XiGuaLiveApi(name)
|
||||
if not api.isValidUser:
|
||||
|
121
bilibili.py
Normal file
121
bilibili.py
Normal file
@ -0,0 +1,121 @@
|
||||
from bilibiliuploader import core, VideoPart
|
||||
|
||||
|
||||
class Bilibili:
|
||||
def __init__(self):
|
||||
self.access_token = ""
|
||||
self.session_id = ""
|
||||
self.user_id = ""
|
||||
self.parts = []
|
||||
|
||||
def login(self):
|
||||
from Common import appendOperation
|
||||
with open("access_token", "r") as f:
|
||||
self.access_token = f.read(64).strip()
|
||||
self.session_id, self.user_id, expires = core.login_by_access_token(self.access_token)
|
||||
appendOperation("B站登录,UID【{}】,过期时间【{}】".format(self.user_id, expires))
|
||||
|
||||
def upload(self,
|
||||
parts,
|
||||
title,
|
||||
tid,
|
||||
tag,
|
||||
desc,
|
||||
source='',
|
||||
cover='',
|
||||
no_reprint=1,
|
||||
):
|
||||
"""
|
||||
|
||||
:param parts: e.g. VideoPart('part path', 'part title', 'part desc'), or [VideoPart(...), VideoPart(...)]
|
||||
:type parts: VideoPart or list<VideoPart>
|
||||
:param title: video's title
|
||||
:type title: str
|
||||
:param tid: video type, see: https://member.bilibili.com/x/web/archive/pre
|
||||
or https://github.com/uupers/BiliSpider/wiki/%E8%A7%86%E9%A2%91%E5%88%86%E5%8C%BA%E5%AF%B9%E5%BA%94%E8%A1%A8
|
||||
:type tid: int
|
||||
:param tag: video's tag
|
||||
:type tag: list<str>
|
||||
:param desc: video's description
|
||||
:type desc: str
|
||||
:param source: (optional) 转载地址
|
||||
:type source: str
|
||||
:param cover: (optional) cover's URL, use method *cover_up* to get
|
||||
:type cover: str
|
||||
:param no_reprint: (optional) 0=可以转载, 1=禁止转载(default)
|
||||
:type no_reprint: int
|
||||
"""
|
||||
self.preUpload(parts)
|
||||
self.finishUpload(title, tid, tag, desc, source, cover, no_reprint)
|
||||
self.clear()
|
||||
|
||||
def preUpload(self, parts, max_retry=5):
|
||||
"""
|
||||
:param max_retry:
|
||||
:param parts: e.g. VideoPart('part path', 'part title', 'part desc'), or [VideoPart(...), VideoPart(...)]
|
||||
:type parts: VideoPart or list<VideoPart>
|
||||
"""
|
||||
from Common import appendUploadStatus, modifyLastUploadStatus, appendError
|
||||
if not isinstance(parts, list):
|
||||
parts = [parts]
|
||||
|
||||
def log_status(video_part: VideoPart, chunks_index: int, chunks_num: int):
|
||||
modifyLastUploadStatus("Uploading >{}< @ {:.2f}%".format(video_part.path, 100.0 * chunks_index / chunks_num))
|
||||
for part in parts:
|
||||
appendUploadStatus("Start Uploading >{}<".format(part.path))
|
||||
status = core.upload_video_part(self.access_token, self.session_id, self.user_id, part, max_retry, cb=log_status)
|
||||
if status:
|
||||
# 上传完毕
|
||||
modifyLastUploadStatus("Upload >{}< Finished;【{}】".format(part.path, part.server_file_name))
|
||||
self.parts.append(part)
|
||||
else:
|
||||
modifyLastUploadStatus("Upload >{}< Failed".format(part.path))
|
||||
|
||||
def finishUpload(self,
|
||||
title,
|
||||
tid,
|
||||
tag,
|
||||
desc,
|
||||
source='',
|
||||
cover='',
|
||||
no_reprint=1,
|
||||
):
|
||||
"""
|
||||
:param title: video's title
|
||||
:type title: str
|
||||
:param tid: video type, see: https://member.bilibili.com/x/web/archive/pre
|
||||
or https://github.com/uupers/BiliSpider/wiki/%E8%A7%86%E9%A2%91%E5%88%86%E5%8C%BA%E5%AF%B9%E5%BA%94%E8%A1%A8
|
||||
:type tid: int
|
||||
:param tag: video's tag
|
||||
:type tag: list<str>
|
||||
:param desc: video's description
|
||||
:type desc: str
|
||||
:param source: (optional) 转载地址
|
||||
:type source: str
|
||||
:param cover: (optional) cover's URL, use method *cover_up* to get
|
||||
:type cover: str
|
||||
:param no_reprint: (optional) 0=可以转载, 1=禁止转载(default)
|
||||
:type no_reprint: int
|
||||
:param copyright: (optional) 0=转载的, 1=自制的(default)
|
||||
:type copyright: int
|
||||
"""
|
||||
from Common import appendUploadStatus, modifyLastUploadStatus, appendError
|
||||
if len(self.parts) == 0:
|
||||
return
|
||||
appendUploadStatus("[{}]投稿中,请稍后".format(title))
|
||||
copyright = 2 if source else 1
|
||||
try:
|
||||
avid, bvid = core.upload(self.access_token, self.session_id, self.user_id, self.parts, copyright,
|
||||
title=title, tid=tid, tag=','.join(tag), desc=desc, source=source, cover=cover, no_reprint=no_reprint)
|
||||
modifyLastUploadStatus("[{}]投稿成功;AVID【{}】,BVID【{}】".format(title, avid, bvid))
|
||||
self.clear()
|
||||
except Exception as e:
|
||||
modifyLastUploadStatus("[{}]投稿失败".format(title))
|
||||
appendError(e)
|
||||
|
||||
|
||||
def reloadFromPrevious(self):
|
||||
...
|
||||
|
||||
def clear(self):
|
||||
self.parts = []
|
4
bilibiliuploader/README.md
Normal file
4
bilibiliuploader/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
修改自
|
||||
https://github.com/FortuneDayssss/BilibiliUploader/
|
||||
|
||||
LICENSE:GPL
|
4
bilibiliuploader/__init__.py
Normal file
4
bilibiliuploader/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .bilibiliuploader import BilibiliUploader
|
||||
from .core import VideoPart
|
||||
|
||||
__version__ = '0.0.6'
|
118
bilibiliuploader/bilibiliuploader.py
Normal file
118
bilibiliuploader/bilibiliuploader.py
Normal file
@ -0,0 +1,118 @@
|
||||
import bilibiliuploader.core as core
|
||||
from bilibiliuploader.util import cipher
|
||||
import json
|
||||
|
||||
|
||||
class BilibiliUploader():
|
||||
def __init__(self):
|
||||
self.access_token = None
|
||||
self.refresh_token = None
|
||||
self.sid = None
|
||||
self.mid = None
|
||||
|
||||
def login(self, username, password):
|
||||
code, self.access_token, self.refresh_token, self.sid, self.mid, _ = core.login(username, password)
|
||||
if code != 0: # success
|
||||
print("login fail, error code = {}".format(code))
|
||||
|
||||
def login_by_access_token(self, access_token, refresh_token=None):
|
||||
self.access_token = access_token
|
||||
self.refresh_token = refresh_token
|
||||
self.sid, self.mid, _ = core.login_by_access_token(access_token)
|
||||
|
||||
def login_by_access_token_file(self, file_name):
|
||||
with open(file_name, "r") as f:
|
||||
login_data = json.loads(f.read())
|
||||
self.access_token = login_data["access_token"]
|
||||
self.refresh_token = login_data["refresh_token"]
|
||||
self.sid, self.mid, _ = core.login_by_access_token(self.access_token)
|
||||
|
||||
def save_login_data(self, file_name=None):
|
||||
login_data = json.dumps(
|
||||
{
|
||||
"access_token": self.access_token,
|
||||
"refresh_token": self.refresh_token
|
||||
}
|
||||
)
|
||||
try:
|
||||
with open(file_name, "w+") as f:
|
||||
f.write(login_data)
|
||||
finally:
|
||||
return login_data
|
||||
|
||||
|
||||
def upload(self,
|
||||
parts,
|
||||
copyright: int,
|
||||
title: str,
|
||||
tid: int,
|
||||
tag: str,
|
||||
desc: str,
|
||||
source: str = '',
|
||||
cover: str = '',
|
||||
no_reprint: int = 0,
|
||||
open_elec: int = 1,
|
||||
max_retry: int = 5,
|
||||
thread_pool_workers: int = 1):
|
||||
return core.upload(self.access_token,
|
||||
self.sid,
|
||||
self.mid,
|
||||
parts,
|
||||
copyright,
|
||||
title,
|
||||
tid,
|
||||
tag,
|
||||
desc,
|
||||
source,
|
||||
cover,
|
||||
no_reprint,
|
||||
open_elec,
|
||||
max_retry,
|
||||
thread_pool_workers)
|
||||
|
||||
def edit(self,
|
||||
avid=None,
|
||||
bvid=None,
|
||||
parts=None,
|
||||
insert_index=None,
|
||||
copyright=None,
|
||||
title=None,
|
||||
tid=None,
|
||||
tag=None,
|
||||
desc=None,
|
||||
source=None,
|
||||
cover=None,
|
||||
no_reprint=None,
|
||||
open_elec=None,
|
||||
max_retry: int = 5,
|
||||
thread_pool_workers: int = 1):
|
||||
|
||||
if not avid and not bvid:
|
||||
print("please provide avid or bvid")
|
||||
return None, None
|
||||
if not avid:
|
||||
avid = cipher.bv2av(bvid)
|
||||
if not isinstance(parts, list):
|
||||
parts = [parts]
|
||||
if type(avid) is str:
|
||||
avid = int(avid)
|
||||
core.edit_videos(
|
||||
self.access_token,
|
||||
self.sid,
|
||||
self.mid,
|
||||
avid,
|
||||
bvid,
|
||||
parts,
|
||||
insert_index,
|
||||
copyright,
|
||||
title,
|
||||
tid,
|
||||
tag,
|
||||
desc,
|
||||
source,
|
||||
cover,
|
||||
no_reprint,
|
||||
open_elec,
|
||||
max_retry,
|
||||
thread_pool_workers
|
||||
)
|
790
bilibiliuploader/core.py
Normal file
790
bilibiliuploader/core.py
Normal file
@ -0,0 +1,790 @@
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from bilibiliuploader.util import cipher as cipher
|
||||
from urllib import parse
|
||||
import os
|
||||
import math
|
||||
import hashlib
|
||||
from bilibiliuploader.util.retry import Retry
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import base64
|
||||
|
||||
# From PC ugc_assisstant
|
||||
# APPKEY = 'aae92bc66f3edfab'
|
||||
# APPSECRET = 'af125a0d5279fd576c1b4418a3e8276d'
|
||||
APPKEY = '1d8b6e7d45233436'
|
||||
APPSECRET = '560c52ccd288fed045859ed18bffd973'
|
||||
LOGIN_APPKEY = '783bbb7264451d82'
|
||||
|
||||
# upload chunk size = 2MB
|
||||
CHUNK_SIZE = 2 * 1024 * 1024
|
||||
|
||||
# captcha
|
||||
CAPTCHA_RECOGNIZE_URL = "NOT SUPPORT"
|
||||
|
||||
|
||||
class VideoPart:
|
||||
"""
|
||||
Video Part of a post.
|
||||
每个对象代表一个分P
|
||||
|
||||
Attributes:
|
||||
path: file path in local file system.
|
||||
title: title of the video part.
|
||||
desc: description of the video part.
|
||||
server_file_name: file name in bilibili server. generated by pre-upload API.
|
||||
"""
|
||||
|
||||
def __init__(self, path, title='', desc='', server_file_name=None):
|
||||
self.path = path
|
||||
self.title = title
|
||||
self.desc = desc
|
||||
self.server_file_name = server_file_name
|
||||
|
||||
def __repr__(self):
|
||||
return '<{clazz}, path: {path}, title: {title}, desc: {desc}, server_file_name:{server_file_name}>' \
|
||||
.format(clazz=self.__class__.__name__,
|
||||
path=self.path,
|
||||
title=self.title,
|
||||
desc=self.desc,
|
||||
server_file_name=self.server_file_name)
|
||||
|
||||
|
||||
def get_key_old(sid=None, jsessionid=None):
|
||||
"""
|
||||
get public key, hash and session id for login.
|
||||
Args:
|
||||
sid: session id. only for captcha login.
|
||||
jsessionid: j-session id. only for captcha login.
|
||||
Returns:
|
||||
hash: salt for password encryption.
|
||||
pubkey: rsa public key for password encryption.
|
||||
sid: session id.
|
||||
"""
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': "application/json, text/javascript, */*; q=0.01"
|
||||
}
|
||||
post_data = {
|
||||
'appkey': APPKEY,
|
||||
'platform': "pc",
|
||||
'ts': str(int(datetime.now().timestamp()))
|
||||
}
|
||||
post_data['sign'] = cipher.sign_dict(post_data, APPSECRET)
|
||||
cookie = {}
|
||||
if sid:
|
||||
cookie['sid'] = sid
|
||||
if jsessionid:
|
||||
cookie['JSESSIONID'] = jsessionid
|
||||
r = requests.post(
|
||||
# "https://passport.bilibili.com/api/oauth2/getKey",
|
||||
"https://passport.bilibili.com/x/passport-login/web/key",
|
||||
headers=headers,
|
||||
data=post_data,
|
||||
cookies=cookie
|
||||
)
|
||||
print(r.content.decode())
|
||||
r_data = r.json()['data']
|
||||
if sid:
|
||||
return r_data['hash'], r_data['key'], sid
|
||||
return r_data['hash'], r_data['key'], r.cookies['sid']
|
||||
|
||||
|
||||
def get_key():
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': "application/json, text/javascript, */*; q=0.01"
|
||||
}
|
||||
params_data = {
|
||||
'appkey': LOGIN_APPKEY,
|
||||
# 'ts': str(int(datetime.now().timestamp()))
|
||||
}
|
||||
params_data['sign'] = cipher.login_sign_dict_bin(params_data)
|
||||
r = requests.get(
|
||||
"https://passport.bilibili.com/x/passport-login/web/key",
|
||||
headers=headers,
|
||||
params=params_data
|
||||
)
|
||||
r_data = r.json()['data']
|
||||
return r_data['hash'], r_data['key'], ''
|
||||
|
||||
|
||||
def get_capcha(sid):
|
||||
headers = {
|
||||
'User-Agent': '',
|
||||
'Accept-Encoding': 'gzip,deflate',
|
||||
}
|
||||
|
||||
params = {
|
||||
'appkey': APPKEY,
|
||||
'platform': 'pc',
|
||||
'ts': str(int(datetime.now().timestamp()))
|
||||
}
|
||||
params['sign'] = cipher.sign_dict(params, APPSECRET)
|
||||
|
||||
r = requests.get(
|
||||
"https://passport.bilibili.com/captcha",
|
||||
headers=headers,
|
||||
params=params,
|
||||
cookies={
|
||||
'sid': sid
|
||||
}
|
||||
)
|
||||
|
||||
print(r.status_code)
|
||||
|
||||
capcha_data = r.content
|
||||
|
||||
return r.cookies['JSESSIONID'], capcha_data
|
||||
|
||||
|
||||
def recognize_captcha(img: bytes):
|
||||
img_base64 = str(base64.b64encode(img), encoding='utf-8')
|
||||
r = requests.post(
|
||||
url=CAPTCHA_RECOGNIZE_URL,
|
||||
data={'image': img_base64}
|
||||
)
|
||||
return r.content.decode()
|
||||
|
||||
|
||||
def login(username, password):
|
||||
"""
|
||||
bilibili login.
|
||||
Args:
|
||||
username: plain text username for bilibili.
|
||||
password: plain text password for bilibili.
|
||||
|
||||
Returns:
|
||||
code: login response code (0: success, -105: captcha error, ...).
|
||||
access_token: token for further operation.
|
||||
refresh_token: token for refresh access_token.
|
||||
sid: session id.
|
||||
mid: member id.
|
||||
expires_in: access token expire time (30 days)
|
||||
"""
|
||||
hash, pubkey, sid = get_key()
|
||||
|
||||
encrypted_password = cipher.encrypt_login_password(password, hash, pubkey)
|
||||
url_encoded_username = parse.quote_plus(username)
|
||||
url_encoded_password = parse.quote_plus(encrypted_password)
|
||||
|
||||
post_data = {
|
||||
'appkey': LOGIN_APPKEY,
|
||||
'password': url_encoded_password,
|
||||
'ts': str(int(datetime.now().timestamp())),
|
||||
'username': url_encoded_username
|
||||
}
|
||||
|
||||
post_data['sign'] = cipher.login_sign_dict_bin(post_data)
|
||||
# avoid multiple url parse
|
||||
post_data['username'] = username
|
||||
post_data['password'] = encrypted_password
|
||||
|
||||
headers = {
|
||||
'Connection': 'keep-alive',
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'User-Agent': '',
|
||||
'Accept-Encoding': 'gzip,deflate',
|
||||
}
|
||||
|
||||
r = requests.post(
|
||||
# "https://passport.bilibili.com/api/v3/oauth2/login",
|
||||
"https://passport.bilibili.com/x/passport-login/oauth2/login",
|
||||
headers=headers,
|
||||
data=post_data,
|
||||
)
|
||||
response = r.json()
|
||||
response_code = response['code']
|
||||
if response_code == 0:
|
||||
login_data = response['data']['token_info']
|
||||
return response_code, login_data['access_token'], login_data['refresh_token'], sid, login_data['mid'], \
|
||||
login_data["expires_in"]
|
||||
elif response_code == -105: # captcha error, retry=5
|
||||
retry_cnt = 5
|
||||
while response_code == -105 and retry_cnt > 0:
|
||||
response_code, access_token, refresh_token, sid, mid, expire_in = login_captcha(username, password, sid)
|
||||
if response_code == 0:
|
||||
return response_code, access_token, refresh_token, sid, mid, expire_in
|
||||
retry_cnt -= 1
|
||||
|
||||
# other error code
|
||||
return response_code, None, None, sid, None, None
|
||||
|
||||
|
||||
def login_captcha(username, password, sid):
|
||||
"""
|
||||
bilibili login with captcha.
|
||||
depend on captcha recognize service, please do not use this as first choice.
|
||||
Args:
|
||||
username: plain text username for bilibili.
|
||||
password: plain text password for bilibili.
|
||||
sid: session id
|
||||
Returns:
|
||||
code: login response code (0: success, -105: captcha error, ...).
|
||||
access_token: token for further operation.
|
||||
refresh_token: token for refresh access_token.
|
||||
sid: session id.
|
||||
mid: member id.
|
||||
expires_in: access token expire time (30 days)
|
||||
"""
|
||||
|
||||
jsessionid, captcha_img = get_capcha(sid)
|
||||
captcha_str = recognize_captcha(captcha_img)
|
||||
|
||||
hash, pubkey, sid = get_key(sid, jsessionid)
|
||||
|
||||
encrypted_password = cipher.encrypt_login_password(password, hash, pubkey)
|
||||
url_encoded_username = parse.quote_plus(username)
|
||||
url_encoded_password = parse.quote_plus(encrypted_password)
|
||||
|
||||
post_data = {
|
||||
'appkey': APPKEY,
|
||||
'captcha': captcha_str,
|
||||
'password': url_encoded_password,
|
||||
'platform': "pc",
|
||||
'ts': str(int(datetime.now().timestamp())),
|
||||
'username': url_encoded_username
|
||||
}
|
||||
|
||||
post_data['sign'] = cipher.sign_dict(post_data, APPSECRET)
|
||||
# avoid multiple url parse
|
||||
post_data['username'] = username
|
||||
post_data['password'] = encrypted_password
|
||||
post_data['captcha'] = captcha_str
|
||||
|
||||
headers = {
|
||||
'Connection': 'keep-alive',
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'User-Agent': '',
|
||||
'Accept-Encoding': 'gzip,deflate',
|
||||
}
|
||||
|
||||
r = requests.post(
|
||||
"https://passport.bilibili.com/api/oauth2/login",
|
||||
headers=headers,
|
||||
data=post_data,
|
||||
cookies={
|
||||
'JSESSIONID': jsessionid,
|
||||
'sid': sid
|
||||
}
|
||||
)
|
||||
response = r.json()
|
||||
if response['code'] == 0:
|
||||
login_data = response['data']
|
||||
return response['code'], login_data['access_token'], login_data['refresh_token'], sid, login_data['mid'], \
|
||||
login_data["expires_in"]
|
||||
else:
|
||||
return response['code'], None, None, sid, None, None
|
||||
|
||||
|
||||
def login_by_access_token(access_token):
|
||||
"""
|
||||
bilibili access token login.
|
||||
Args:
|
||||
access_token: Bilibili access token got by previous username/password login.
|
||||
|
||||
Returns:
|
||||
sid: session id.
|
||||
mid: member id.
|
||||
expires_in: access token expire time
|
||||
"""
|
||||
headers = {
|
||||
'Connection': 'keep-alive',
|
||||
'Accept-Encoding': 'gzip,deflate',
|
||||
'Host': 'passport.bilibili.com',
|
||||
'User-Agent': '',
|
||||
}
|
||||
|
||||
login_params = {
|
||||
'appkey': APPKEY,
|
||||
'access_token': access_token,
|
||||
'platform': "pc",
|
||||
'ts': str(int(datetime.now().timestamp())),
|
||||
}
|
||||
login_params['sign'] = cipher.sign_dict(login_params, APPSECRET)
|
||||
|
||||
r = requests.get(
|
||||
url="https://passport.bilibili.com/api/oauth2/info",
|
||||
headers=headers,
|
||||
params=login_params
|
||||
)
|
||||
|
||||
login_data = r.json()['data']
|
||||
|
||||
return r.cookies['sid'], login_data['mid'], login_data["expires_in"]
|
||||
|
||||
|
||||
def upload_cover(access_token, sid, cover_file_path):
|
||||
with open(cover_file_path, "rb") as f:
|
||||
cover_pic = f.read()
|
||||
|
||||
headers = {
|
||||
'Connection': 'keep-alive',
|
||||
'Host': 'member.bilibili.com',
|
||||
'Accept-Encoding': 'gzip,deflate',
|
||||
'User-Agent': '',
|
||||
}
|
||||
|
||||
params = {
|
||||
"access_key": access_token,
|
||||
}
|
||||
|
||||
params["sign"] = cipher.sign_dict(params, APPSECRET)
|
||||
|
||||
files = {
|
||||
'file': ("cover.png", cover_pic, "Content-Type: image/png"),
|
||||
}
|
||||
|
||||
r = requests.post(
|
||||
"http://member.bilibili.com/x/vu/client/cover/up",
|
||||
headers=headers,
|
||||
params=params,
|
||||
files=files,
|
||||
cookies={
|
||||
'sid': sid
|
||||
},
|
||||
verify=False,
|
||||
)
|
||||
|
||||
return r.json()["data"]["url"]
|
||||
|
||||
|
||||
def upload_chunk(upload_url, server_file_name, local_file_name, chunk_data, chunk_size, chunk_id, chunk_total_num):
|
||||
"""
|
||||
upload video chunk.
|
||||
Args:
|
||||
upload_url: upload url by pre_upload api.
|
||||
server_file_name: file name on server by pre_upload api.
|
||||
local_file_name: video file name in local fs.
|
||||
chunk_data: binary data of video chunk.
|
||||
chunk_size: default of ugc_assisstant is 2M.
|
||||
chunk_id: chunk number.
|
||||
chunk_total_num: total chunk number.
|
||||
|
||||
Returns:
|
||||
True: upload chunk success.
|
||||
False: upload chunk fail.
|
||||
"""
|
||||
print("chunk{}/{}".format(chunk_id, chunk_total_num))
|
||||
print("filename: {}".format(local_file_name))
|
||||
files = {
|
||||
'version': (None, '2.0.0.1054'),
|
||||
'filesize': (None, chunk_size),
|
||||
'chunk': (None, chunk_id),
|
||||
'chunks': (None, chunk_total_num),
|
||||
'md5': (None, cipher.md5_bytes(chunk_data)),
|
||||
'file': (local_file_name, chunk_data, 'application/octet-stream')
|
||||
}
|
||||
|
||||
r = requests.post(
|
||||
url=upload_url,
|
||||
files=files,
|
||||
cookies={
|
||||
'PHPSESSID': server_file_name
|
||||
},
|
||||
)
|
||||
print(r.status_code)
|
||||
print(r.content)
|
||||
|
||||
if r.status_code == 200 and r.json()['OK'] == 1:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def upload_video_part(access_token, sid, mid, video_part: VideoPart, max_retry=5, cb=None):
|
||||
"""
|
||||
upload a video file.
|
||||
Args:
|
||||
access_token: access token generated by login api.
|
||||
sid: session id.
|
||||
mid: member id.
|
||||
video_part: local video file data.
|
||||
max_retry: max retry number for each chunk.
|
||||
cb: 回调
|
||||
|
||||
Returns:
|
||||
status: success or fail.
|
||||
server_file_name: server file name by pre_upload api.
|
||||
"""
|
||||
if cb is None:
|
||||
cb = lambda f, c, t: None
|
||||
if not isinstance(video_part, VideoPart):
|
||||
return False
|
||||
if video_part.server_file_name is not None:
|
||||
return True
|
||||
headers = {
|
||||
'Connection': 'keep-alive',
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'User-Agent': '',
|
||||
'Accept-Encoding': 'gzip,deflate',
|
||||
}
|
||||
|
||||
r = requests.get(
|
||||
"http://member.bilibili.com/preupload?access_key={}&mid={}&profile=ugcfr%2Fpc3".format(access_token, mid),
|
||||
headers=headers,
|
||||
cookies={
|
||||
'sid': sid
|
||||
},
|
||||
verify=False,
|
||||
)
|
||||
|
||||
pre_upload_data = r.json()
|
||||
upload_url = pre_upload_data['url']
|
||||
complete_upload_url = pre_upload_data['complete']
|
||||
server_file_name = pre_upload_data['filename']
|
||||
local_file_name = video_part.path
|
||||
|
||||
file_size = os.path.getsize(local_file_name)
|
||||
chunk_total_num = int(math.ceil(file_size / CHUNK_SIZE))
|
||||
file_hash = hashlib.md5()
|
||||
with open(local_file_name, 'rb') as f:
|
||||
for chunk_id in range(0, chunk_total_num):
|
||||
chunk_data = f.read(CHUNK_SIZE)
|
||||
cb(video_part, chunk_id, chunk_total_num)
|
||||
status = Retry(max_retry=max_retry, success_return_value=True).run(
|
||||
upload_chunk,
|
||||
upload_url,
|
||||
server_file_name,
|
||||
os.path.basename(local_file_name),
|
||||
chunk_data,
|
||||
CHUNK_SIZE,
|
||||
chunk_id,
|
||||
chunk_total_num
|
||||
)
|
||||
|
||||
if not status:
|
||||
return False
|
||||
file_hash.update(chunk_data)
|
||||
print(file_hash.hexdigest())
|
||||
|
||||
# complete upload
|
||||
post_data = {
|
||||
'chunks': chunk_total_num,
|
||||
'filesize': file_size,
|
||||
'md5': file_hash.hexdigest(),
|
||||
'name': os.path.basename(local_file_name),
|
||||
'version': '2.0.0.1054',
|
||||
}
|
||||
|
||||
r = requests.post(
|
||||
url=complete_upload_url,
|
||||
data=post_data,
|
||||
headers=headers,
|
||||
)
|
||||
print(r.status_code)
|
||||
print(r.content)
|
||||
|
||||
video_part.server_file_name = server_file_name
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def upload(access_token,
|
||||
sid,
|
||||
mid,
|
||||
parts,
|
||||
copyright: int,
|
||||
title: str,
|
||||
tid: int,
|
||||
tag: str,
|
||||
desc: str,
|
||||
source: str = '',
|
||||
cover: str = '',
|
||||
no_reprint: int = 0,
|
||||
open_elec: int = 1,
|
||||
max_retry: int = 5,
|
||||
thread_pool_workers: int = 1):
|
||||
"""
|
||||
upload video.
|
||||
|
||||
Args:
|
||||
access_token: oauth2 access token.
|
||||
sid: session id.
|
||||
mid: member id.
|
||||
parts: VideoPart list.
|
||||
copyright: 原创/转载.
|
||||
title: 投稿标题.
|
||||
tid: 分区id.
|
||||
tag: 标签.
|
||||
desc: 投稿简介.
|
||||
source: 转载地址.
|
||||
cover: 封面图片文件路径.
|
||||
no_reprint: 可否转载.
|
||||
open_elec: 充电.
|
||||
max_retry: max retry time for each chunk.
|
||||
thread_pool_workers: max upload threads.
|
||||
|
||||
Returns:
|
||||
(aid, bvid)
|
||||
aid: av号
|
||||
bvid: bv号
|
||||
"""
|
||||
if not isinstance(parts, list):
|
||||
parts = [parts]
|
||||
|
||||
status = True
|
||||
with ThreadPoolExecutor(max_workers=thread_pool_workers) as tpe:
|
||||
t_list = []
|
||||
for video_part in parts:
|
||||
print("upload {} added in pool".format(video_part.title))
|
||||
t_obj = tpe.submit(upload_video_part, access_token, sid, mid, video_part, max_retry)
|
||||
t_obj.video_part = video_part
|
||||
t_list.append(t_obj)
|
||||
|
||||
for t_obj in as_completed(t_list):
|
||||
status = status and t_obj.result()
|
||||
print("video part {} finished, status: {}".format(t_obj.video_part.title, t_obj.result()))
|
||||
if not status:
|
||||
print("upload failed")
|
||||
return None, None
|
||||
|
||||
# cover
|
||||
if os.path.isfile(cover):
|
||||
try:
|
||||
cover = upload_cover(access_token, sid, cover)
|
||||
except:
|
||||
cover = ''
|
||||
else:
|
||||
cover = ''
|
||||
|
||||
# submit
|
||||
headers = {
|
||||
'Connection': 'keep-alive',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': '',
|
||||
}
|
||||
post_data = {
|
||||
'build': 1054,
|
||||
'copyright': copyright,
|
||||
'cover': cover,
|
||||
'desc': desc,
|
||||
'no_reprint': no_reprint,
|
||||
'open_elec': open_elec,
|
||||
'source': source,
|
||||
'tag': tag,
|
||||
'tid': tid,
|
||||
'title': title,
|
||||
'videos': []
|
||||
}
|
||||
for video_part in parts:
|
||||
post_data['videos'].append({
|
||||
"desc": video_part.desc,
|
||||
"filename": video_part.server_file_name,
|
||||
"title": video_part.title
|
||||
})
|
||||
|
||||
params = {
|
||||
'access_key': access_token,
|
||||
}
|
||||
params['sign'] = cipher.sign_dict(params, APPSECRET)
|
||||
r = requests.post(
|
||||
url="http://member.bilibili.com/x/vu/client/add",
|
||||
params=params,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
cookies={
|
||||
'sid': sid
|
||||
},
|
||||
json=post_data,
|
||||
)
|
||||
|
||||
print("submit")
|
||||
print(r.status_code)
|
||||
print(r.content.decode())
|
||||
|
||||
data = r.json()["data"]
|
||||
return data["aid"], data["bvid"]
|
||||
|
||||
|
||||
def get_post_data(access_token, sid, avid):
|
||||
headers = {
|
||||
'Connection': 'keep-alive',
|
||||
'Host': 'member.bilibili.com',
|
||||
'Accept-Encoding': 'gzip,deflate',
|
||||
'User-Agent': '',
|
||||
}
|
||||
|
||||
params = {
|
||||
"access_key": access_token,
|
||||
"aid": avid,
|
||||
"build": "1054"
|
||||
}
|
||||
|
||||
params["sign"] = cipher.sign_dict(params, APPSECRET)
|
||||
|
||||
r = requests.get(
|
||||
url="http://member.bilibili.com/x/client/archive/view",
|
||||
headers=headers,
|
||||
params=params,
|
||||
cookies={
|
||||
'sid': sid
|
||||
}
|
||||
)
|
||||
|
||||
return r.json()["data"]
|
||||
|
||||
|
||||
def edit_videos(
|
||||
access_token,
|
||||
sid,
|
||||
mid,
|
||||
avid=None,
|
||||
bvid=None,
|
||||
parts=None,
|
||||
insert_index=None,
|
||||
copyright=None,
|
||||
title=None,
|
||||
tid=None,
|
||||
tag=None,
|
||||
desc=None,
|
||||
source=None,
|
||||
cover=None,
|
||||
no_reprint=None,
|
||||
open_elec=None,
|
||||
max_retry: int = 5,
|
||||
thread_pool_workers: int = 1):
|
||||
"""
|
||||
insert videos into existed post.
|
||||
|
||||
Args:
|
||||
access_token: oauth2 access token.
|
||||
sid: session id.
|
||||
mid: member id.
|
||||
avid: av number,
|
||||
bvid: bv string,
|
||||
parts: VideoPart list.
|
||||
insert_index: new video index.
|
||||
copyright: 原创/转载.
|
||||
title: 投稿标题.
|
||||
tid: 分区id.
|
||||
tag: 标签.
|
||||
desc: 投稿简介.
|
||||
source: 转载地址.
|
||||
cover: cover url.
|
||||
no_reprint: 可否转载.
|
||||
open_elec: 充电.
|
||||
max_retry: max retry time for each chunk.
|
||||
thread_pool_workers: max upload threads.
|
||||
|
||||
Returns:
|
||||
(aid, bvid)
|
||||
aid: av号
|
||||
bvid: bv号
|
||||
"""
|
||||
if not avid and not bvid:
|
||||
print("please provide avid or bvid")
|
||||
return None, None
|
||||
if not avid:
|
||||
avid = cipher.bv2av(bvid)
|
||||
if not isinstance(parts, list):
|
||||
parts = [parts]
|
||||
if type(avid) is str:
|
||||
avid = int(avid)
|
||||
|
||||
post_video_data = get_post_data(access_token, sid, avid)
|
||||
|
||||
status = True
|
||||
with ThreadPoolExecutor(max_workers=thread_pool_workers) as tpe:
|
||||
t_list = []
|
||||
for video_part in parts:
|
||||
print("upload {} added in pool".format(video_part.title))
|
||||
t_obj = tpe.submit(upload_video_part, access_token, sid, mid, video_part, max_retry)
|
||||
t_obj.video_part = video_part
|
||||
t_list.append(t_obj)
|
||||
|
||||
for t_obj in as_completed(t_list):
|
||||
status = status and t_obj.result()
|
||||
print("video part {} finished, status: {}".format(t_obj.video_part.title, t_obj.result()))
|
||||
if not status:
|
||||
print("upload failed")
|
||||
return None, None
|
||||
|
||||
headers = {
|
||||
'Connection': 'keep-alive',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': '',
|
||||
}
|
||||
submit_data = {
|
||||
'aid': avid,
|
||||
'build': 1054,
|
||||
'copyright': post_video_data["archive"]["copyright"],
|
||||
'cover': post_video_data["archive"]["cover"],
|
||||
'desc': post_video_data["archive"]["desc"],
|
||||
'no_reprint': post_video_data["archive"]["no_reprint"],
|
||||
'open_elec': post_video_data["archive_elec"]["state"], # open_elec not tested
|
||||
'source': post_video_data["archive"]["source"],
|
||||
'tag': post_video_data["archive"]["tag"],
|
||||
'tid': post_video_data["archive"]["tid"],
|
||||
'title': post_video_data["archive"]["title"],
|
||||
'videos': post_video_data["videos"]
|
||||
}
|
||||
|
||||
# cover
|
||||
if os.path.isfile(cover):
|
||||
try:
|
||||
cover = upload_cover(access_token, sid, cover)
|
||||
except:
|
||||
cover = ''
|
||||
else:
|
||||
cover = ''
|
||||
|
||||
# edit archive data
|
||||
if copyright:
|
||||
submit_data["copyright"] = copyright
|
||||
if title:
|
||||
submit_data["title"] = title
|
||||
if tid:
|
||||
submit_data["tid"] = tid
|
||||
if tag:
|
||||
submit_data["tag"] = tag
|
||||
if desc:
|
||||
submit_data["desc"] = desc
|
||||
if source:
|
||||
submit_data["source"] = source
|
||||
if cover:
|
||||
submit_data["cover"] = cover
|
||||
if no_reprint:
|
||||
submit_data["no_reprint"] = no_reprint
|
||||
if open_elec:
|
||||
submit_data["open_elec"] = open_elec
|
||||
|
||||
if type(insert_index) is int:
|
||||
for i, video_part in enumerate(parts):
|
||||
submit_data['videos'].insert(insert_index + i, {
|
||||
"desc": video_part.desc,
|
||||
"filename": video_part.server_file_name,
|
||||
"title": video_part.title
|
||||
})
|
||||
elif insert_index is None:
|
||||
for video_part in parts:
|
||||
submit_data['videos'].append({
|
||||
"desc": video_part.desc,
|
||||
"filename": video_part.server_file_name,
|
||||
"title": video_part.title
|
||||
})
|
||||
else:
|
||||
print("wrong insert index")
|
||||
return None, None
|
||||
|
||||
params = {
|
||||
'access_key': access_token,
|
||||
}
|
||||
params['sign'] = cipher.sign_dict(params, APPSECRET)
|
||||
r = requests.post(
|
||||
url="http://member.bilibili.com/x/vu/client/edit",
|
||||
params=params,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
cookies={
|
||||
'sid': sid
|
||||
},
|
||||
json=submit_data,
|
||||
)
|
||||
|
||||
print("edit submit")
|
||||
print(r.status_code)
|
||||
print(r.content.decode())
|
||||
|
||||
data = r.json()["data"]
|
||||
return data["aid"], data["bvid"]
|
1
bilibiliuploader/util/__init__.py
Normal file
1
bilibiliuploader/util/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .cipher import *
|
119
bilibiliuploader/util/cipher.py
Normal file
119
bilibiliuploader/util/cipher.py
Normal file
@ -0,0 +1,119 @@
|
||||
import hashlib
|
||||
import rsa
|
||||
import base64
|
||||
import subprocess
|
||||
import platform
|
||||
import os.path
|
||||
|
||||
|
||||
def md5(data: str):
|
||||
"""
|
||||
generate md5 hash of utf-8 encoded string.
|
||||
"""
|
||||
return hashlib.md5(data.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def md5_bytes(data: bytes):
|
||||
"""
|
||||
generate md5 hash of binary.
|
||||
"""
|
||||
return hashlib.md5(data).hexdigest()
|
||||
|
||||
|
||||
def sign_str(data: str, app_secret: str):
|
||||
"""
|
||||
sign a string of request parameters
|
||||
Args:
|
||||
data: string of request parameters, must be sorted by key before input.
|
||||
app_secret: a secret string coupled with app_key.
|
||||
|
||||
Returns:
|
||||
A hash string. len=32
|
||||
"""
|
||||
return md5(data + app_secret)
|
||||
|
||||
|
||||
def sign_dict(data: dict, app_secret: str):
|
||||
"""
|
||||
sign a dictionary of request parameters
|
||||
Args:
|
||||
data: dictionary of request parameters.
|
||||
app_secret: a secret string coupled with app_key.
|
||||
|
||||
Returns:
|
||||
A hash string. len=32
|
||||
"""
|
||||
data_str = []
|
||||
keys = list(data.keys())
|
||||
keys.sort()
|
||||
for key in keys:
|
||||
data_str.append("{}={}".format(key, data[key]))
|
||||
data_str = "&".join(data_str)
|
||||
data_str = data_str + app_secret
|
||||
return md5(data_str)
|
||||
|
||||
|
||||
def login_sign_dict_bin(data: dict):
|
||||
data_str = []
|
||||
keys = list(data.keys())
|
||||
keys.sort()
|
||||
for key in keys:
|
||||
data_str.append("{}={}".format(key, data[key]))
|
||||
data_str = "&".join(data_str)
|
||||
package_directory = os.path.dirname(os.path.abspath(__file__))
|
||||
if platform.system().lower() == 'windows':
|
||||
print(data_str)
|
||||
print(subprocess.Popen([os.path.join(package_directory, "sign.exe"), data_str], stdout=subprocess.PIPE).communicate()[0].decode().strip())
|
||||
|
||||
return subprocess.Popen([os.path.join(package_directory, "sign.exe"), data_str], stdout=subprocess.PIPE).communicate()[0].decode().strip()
|
||||
if platform.system().lower() == 'linux':
|
||||
return subprocess.Popen([os.path.join(package_directory, "sign.out"), data_str], stdout=subprocess.PIPE).communicate()[0].decode().strip()
|
||||
raise Exception("Operating System is not supported.")
|
||||
|
||||
|
||||
def encrypt_login_password(password, hash, pubkey):
|
||||
"""
|
||||
encrypt password for login api.
|
||||
Args:
|
||||
password: plain text of user password.
|
||||
hash: hash provided by /api/oauth2/getKey.
|
||||
pubkey: public key provided by /api/oauth2/getKey.
|
||||
|
||||
Returns:
|
||||
An encrypted cipher of password.
|
||||
"""
|
||||
return base64.b64encode(rsa.encrypt(
|
||||
(hash + password).encode('utf-8'),
|
||||
rsa.PublicKey.load_pkcs1_openssl_pem(pubkey.encode()),
|
||||
))
|
||||
|
||||
|
||||
def av2bv(av: int):
|
||||
table = 'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF'
|
||||
tr = {}
|
||||
for i in range(58):
|
||||
tr[table[i]] = i
|
||||
s = [11, 10, 3, 8, 4, 6]
|
||||
xor = 177451812
|
||||
add = 8728348608
|
||||
|
||||
av = (av ^ xor) + add
|
||||
r = list('BV1 4 1 7 ')
|
||||
for i in range(6):
|
||||
r[s[i]] = table[av // 58 ** i % 58]
|
||||
return ''.join(r)
|
||||
|
||||
|
||||
def bv2av(bv: str):
|
||||
table = 'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF'
|
||||
tr = {}
|
||||
for i in range(58):
|
||||
tr[table[i]] = i
|
||||
s = [11, 10, 3, 8, 4, 6]
|
||||
xor = 177451812
|
||||
add = 8728348608
|
||||
|
||||
r = 0
|
||||
for i in range(6):
|
||||
r += tr[bv[s[i]]] * 58 ** i
|
||||
return (r - add) ^ xor
|
18
bilibiliuploader/util/retry.py
Normal file
18
bilibiliuploader/util/retry.py
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
|
||||
class Retry:
|
||||
def __init__(self, max_retry, success_return_value):
|
||||
self.max_retry = max_retry
|
||||
self.success_return_value = success_return_value
|
||||
|
||||
def run(self, func, *args, **kwargs):
|
||||
status = False
|
||||
for i in range(0, self.max_retry):
|
||||
try:
|
||||
return_value = func(*args, **kwargs)
|
||||
except Exception:
|
||||
return_value = not self.success_return_value
|
||||
if return_value == self.success_return_value:
|
||||
status = True
|
||||
break
|
||||
return status
|
BIN
bilibiliuploader/util/sign.exe
Normal file
BIN
bilibiliuploader/util/sign.exe
Normal file
Binary file not shown.
BIN
bilibiliuploader/util/sign.out
Normal file
BIN
bilibiliuploader/util/sign.out
Normal file
Binary file not shown.
153
liveDownloader.py
Normal file
153
liveDownloader.py
Normal file
@ -0,0 +1,153 @@
|
||||
import time
|
||||
from datetime import datetime
|
||||
import threading
|
||||
import Common
|
||||
import os
|
||||
import requests
|
||||
|
||||
session = requests.session()
|
||||
|
||||
|
||||
def download():
|
||||
while Common.api.isLive and not Common.forceNotDownload:
|
||||
if not Common.streamUrl:
|
||||
Common.appendError("Download with No StreamUrl Specific")
|
||||
break
|
||||
path = datetime.strftime(datetime.now(), "%Y%m%d_%H%M.flv")
|
||||
base_path = Common.config["path"]
|
||||
if not os.path.isdir(base_path):
|
||||
os.makedirs(base_path)
|
||||
try:
|
||||
p = session.get(Common.streamUrl, stream=True, timeout=3)
|
||||
p.raise_for_status()
|
||||
except Exception as e:
|
||||
Common.appendError("Download >{}< with Exception [{}]".format(path,e.__str__()))
|
||||
break
|
||||
Common.api.initSave(os.path.join(base_path, path)+".xml")
|
||||
Common.appendDownloadStatus("Download >{}< Start".format(path))
|
||||
f = open(os.path.join(base_path, path), "wb")
|
||||
_size = 0
|
||||
try:
|
||||
for T in p.iter_content(chunk_size=Common.config["c_s"]):
|
||||
if Common.forceNotDownload:
|
||||
Common.modifyLastDownloadStatus("Force Stop Download".format(path))
|
||||
return
|
||||
f.write(T)
|
||||
_size += len(T)
|
||||
Common.modifyLastDownloadStatus(
|
||||
"Downloading >{}< @ {:.2f}%".format(path, 100.0 * _size / Common.config["p_s"]))
|
||||
if _size > Common.config["p_s"] and not Common.config["dlO"]:
|
||||
Common.modifyLastDownloadStatus("Download >{}< Exceed MaxSize".format(path))
|
||||
break
|
||||
Common.modifyLastDownloadStatus("Download >{}< Finished".format(path))
|
||||
except Exception as e:
|
||||
Common.appendError("Download >{}< With Exception {}".format(path, e.__str__()))
|
||||
Common.api.updRoomInfo(True)
|
||||
finally:
|
||||
f.close()
|
||||
if os.path.getsize(os.path.join(base_path, path)) < Common.config["i_s"]:
|
||||
Common.modifyLastDownloadStatus("Downloaded File >{}< is too small, will ignore it".format(path))
|
||||
else:
|
||||
Common.encodeQueue.put(os.path.join(base_path, path))
|
||||
Common.doUpdatePlaylist()
|
||||
Common.api.updRoomInfo(True)
|
||||
|
||||
|
||||
def encode():
|
||||
Common.appendEncodeStatus("Encode Daemon Starting")
|
||||
while True:
|
||||
i = Common.encodeQueue.get()
|
||||
Common.encodeVideo(i)
|
||||
|
||||
|
||||
def upload():
|
||||
date = datetime.strftime(datetime.now(), Common.config["sdf"])
|
||||
Common.appendUploadStatus("Upload Daemon Starting")
|
||||
i = Common.uploadQueue.get()
|
||||
while True:
|
||||
if i is True:
|
||||
Common.publishVideo(date)
|
||||
break
|
||||
try:
|
||||
Common.uploadVideo(i)
|
||||
except Exception as e:
|
||||
Common.appendError(e.__str__())
|
||||
continue
|
||||
finally:
|
||||
time.sleep(90)
|
||||
i = Common.uploadQueue.get()
|
||||
Common.appendUploadStatus("Upload Daemon Quiting")
|
||||
|
||||
|
||||
t = threading.Thread(target=download, args=())
|
||||
ut = threading.Thread(target=upload, args=())
|
||||
et = threading.Thread(target=encode, args=())
|
||||
|
||||
|
||||
def awakeEncode():
|
||||
global et
|
||||
if et.is_alive():
|
||||
return True
|
||||
et = threading.Thread(target=encode, args=())
|
||||
et.setDaemon(True)
|
||||
et.start()
|
||||
return False
|
||||
|
||||
|
||||
def awakeDownload():
|
||||
global t
|
||||
if t.is_alive():
|
||||
return True
|
||||
t = threading.Thread(target=download, args=())
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
Common.api.updRoomInfo()
|
||||
return False
|
||||
|
||||
|
||||
def awakeUpload():
|
||||
global ut
|
||||
if ut.is_alive():
|
||||
return True
|
||||
ut = threading.Thread(target=upload, args=())
|
||||
ut.setDaemon(True)
|
||||
ut.start()
|
||||
return False
|
||||
|
||||
|
||||
def run():
|
||||
Common.refreshDownloader()
|
||||
if not Common.api.isValidUser:
|
||||
Common.appendError("[{}]用户未找到".format(Common.api.name))
|
||||
return
|
||||
while True:
|
||||
if Common.api.isLive and not Common.forceNotBroadcasting:
|
||||
if not Common.forceNotDownload:
|
||||
awakeDownload()
|
||||
if not Common.forceNotUpload:
|
||||
awakeUpload()
|
||||
if not Common.forceNotEncode:
|
||||
awakeEncode()
|
||||
try:
|
||||
Common.api.getDanmaku()
|
||||
except Exception as e:
|
||||
Common.appendError(e.__str__())
|
||||
finally:
|
||||
time.sleep(1)
|
||||
else:
|
||||
try:
|
||||
Common.api.updRoomInfo()
|
||||
except Exception as e:
|
||||
Common.appendError(e.__str__())
|
||||
Common.refreshDownloader()
|
||||
if not Common.api.broadcaster:
|
||||
Common.refreshDownloader()
|
||||
if Common.forceStartEncodeThread:
|
||||
awakeEncode()
|
||||
Common.forceStartEncodeThread = False
|
||||
if Common.forceStartUploadThread:
|
||||
awakeUpload()
|
||||
Common.forceStartUploadThread = False
|
||||
if Common.doDelay():
|
||||
Common.uploadQueue.put(True)
|
||||
time.sleep(5)
|
12
requirements.txt
Normal file
12
requirements.txt
Normal file
@ -0,0 +1,12 @@
|
||||
psutil>=5.9.0
|
||||
certifi>=2020.4.5.1
|
||||
chardet>=3.0.4
|
||||
idna>=2.9
|
||||
pyasn1>=0.4.8
|
||||
requests>=2.23.0
|
||||
rsa>=4.0
|
||||
urllib3>=1.25.9
|
||||
flask>=2.0.2
|
||||
flask_cors>=3.0.10
|
||||
protobuf>=3.19.3
|
||||
python-dotenv>=0.19.2
|
25
static/device.js
Normal file
25
static/device.js
Normal file
@ -0,0 +1,25 @@
|
||||
function deviceUpdate(){
|
||||
$.ajax(
|
||||
"/stats/device",
|
||||
{
|
||||
success: function (res){
|
||||
$("#memTotal").text(res.data.status.memTotal)
|
||||
$("#memUsed").text(res.data.status.memUsed)
|
||||
$("#memUsage").text(res.data.status.memUsage)
|
||||
$("#diskTotal").text(res.data.status.diskTotal)
|
||||
$("#diskUsed").text(res.data.status.diskUsed)
|
||||
$("#diskUsage").text(res.data.status.diskUsage)
|
||||
$("#cpu").text(res.data.status.cpu)
|
||||
$("#memUsageP").val(res.data.status.memUsage)
|
||||
$("#diskUsageP").val(res.data.status.diskUsage)
|
||||
$("#cpuP").val(res.data.status.cpu)
|
||||
$("#inSpeed").text(res.data.status.inSpeed)
|
||||
$("#outSpeed").text(res.data.status.outSpeed)
|
||||
$("#doCleanTime").text(res.data.status.doCleanTime)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
deviceUpdate()
|
||||
setInterval(deviceUpdate,2000)
|
59
static/index.js
Normal file
59
static/index.js
Normal file
@ -0,0 +1,59 @@
|
||||
function taskUpdate(){
|
||||
$.ajax(
|
||||
"/stats",
|
||||
{
|
||||
success: function (res){
|
||||
$("#broadcaster").text(res.data.broadcast.broadcaster)
|
||||
$("#isBroadcasting").text(res.data.broadcast.isBroadcasting)
|
||||
$("#streamUrl").text(res.data.broadcast.streamUrl)
|
||||
$("#delayTime").text(res.data.broadcast.delayTime)
|
||||
$("#forceNotBroadcasting").text(res.data.config.forceNotBroadcasting)
|
||||
$("#forceNotDownload").text(res.data.config.forceNotDownload)
|
||||
$("#forceNotUpload").text(res.data.config.forceNotUpload)
|
||||
$("#forceNotEncode").text(res.data.config.forceNotEncode)
|
||||
$("#downloadOnly").text(res.data.config.downloadOnly)
|
||||
$("#updateTime").text(res.data.broadcast.updateTime)
|
||||
$("#encodeQueueSize").text(res.data.encodeQueueSize)
|
||||
$("#uploadQueueSize").text(res.data.uploadQueueSize)
|
||||
$("#download").html(function(){
|
||||
var ret = ""
|
||||
res.data.download.reverse().forEach(function(obj){
|
||||
ret += "<tr><td class='time'>" + obj.datetime + "</td><td>" + obj.message + "</td></tr>"
|
||||
})
|
||||
return "<table>" + ret + "</table>"
|
||||
})
|
||||
$("#encode").html(function(){
|
||||
var ret = ""
|
||||
res.data.encode.reverse().forEach(function(obj){
|
||||
ret += "<tr><td class='time'>" + obj.datetime + "</td><td>" + obj.message + "</td></tr>"
|
||||
})
|
||||
return "<table>" + ret + "</table>"
|
||||
})
|
||||
$("#upload").html(function(){
|
||||
var ret = ""
|
||||
res.data.upload.reverse().forEach(function(obj){
|
||||
ret += "<tr><td class='time'>" + obj.datetime + "</td><td>" + obj.message + "</td></tr>"
|
||||
})
|
||||
return "<table>" + ret + "</table>"
|
||||
})
|
||||
$("#error").html(function(){
|
||||
var ret = ""
|
||||
res.data.error.reverse().forEach(function(obj){
|
||||
ret += "<tr><td class='time'>" + obj.datetime + "</td><td>" + obj.message + "</td></tr>"
|
||||
})
|
||||
return "<table>" + ret + "</table>"
|
||||
})
|
||||
$("#operation").html(function(){
|
||||
var ret = ""
|
||||
res.data.operation.reverse().forEach(function(obj){
|
||||
ret += "<tr><td class='time'>" + obj.datetime + "</td><td>" + obj.message + "</td></tr>"
|
||||
})
|
||||
return "<table>" + ret + "</table>"
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
taskUpdate()
|
||||
setInterval(taskUpdate,8000)
|
30
templates/device.html
Normal file
30
templates/device.html
Normal file
@ -0,0 +1,30 @@
|
||||
<h1>机器状态</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<td class='title'>CPU使用率</td>
|
||||
<td><progress id="cpuP" max="100" value="0"></progress></td>
|
||||
<td><span id="cpu"></span>%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class='title'>内存使用率</td>
|
||||
<td><progress id="memUsageP" max="100" value="0"></progress></td>
|
||||
<td><span id="memUsed"></span>/<span id="memTotal"></span>(<span id="memUsage"></span>%)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class='title'>磁盘使用率</td>
|
||||
<td><progress id="diskUsageP" max="100" value="0"></progress></td>
|
||||
<td><span id="diskUsed"></span>/<span id="diskTotal"></span>(<span id="diskUsage"></span>%)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class='title'>网络速率</td>
|
||||
<td>↓ <span id="inSpeed"></span>/s</td>
|
||||
<td>↑ <span id="outSpeed"></span>/s</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class='title'>文件清理</td>
|
||||
<td></td>
|
||||
<td>@ <span id="doCleanTime"></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
<script src="/static/device.js"></script>
|
||||
|
26
templates/files.html
Normal file
26
templates/files.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh_CN">
|
||||
<head>
|
||||
<title>文件</title>
|
||||
{% include 'head.html' %}
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<h1>所有录像文件</h1>
|
||||
<p>部分录像文件已转移至百度云,请在<a href="https://pan.baidu.com/s/1ECnwiHnsm-3dSXNJGWlR2g">这里</a>下载 提取码: ddxt</p>
|
||||
<table>
|
||||
<tr>
|
||||
<td>文件名</td><td>文件大小</td><td>链接</td>
|
||||
</tr>
|
||||
{%for i in files %}
|
||||
<tr>
|
||||
<td>{{i.name}}</td><td>{{i.size}}</td><td><a href="/files/download/{{i.name}}">下载文件</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<hr/>
|
||||
<h3><a href="/">录播信息页</a></h3>
|
||||
{% include 'device.html' %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
13
templates/head.html
Normal file
13
templates/head.html
Normal file
@ -0,0 +1,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<script src="https://cdn.staticfile.org/jquery/3.3.1/jquery.min.js"></script>
|
||||
<style>
|
||||
td{
|
||||
border: solid 1px lightgray;
|
||||
}
|
||||
.title{
|
||||
width: 6em;
|
||||
}
|
||||
.time{
|
||||
width: 10em;
|
||||
}
|
||||
</style>
|
86
templates/index.html
Normal file
86
templates/index.html
Normal file
@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh_CN">
|
||||
<head>
|
||||
<title>录播</title>
|
||||
{% include 'head.html' %}
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<h1>基本信息</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<td>主播名</td>
|
||||
<td><span id="broadcaster"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>是否正在直播</td>
|
||||
<td><span id="isBroadcasting"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>直播视频流地址</td>
|
||||
<td><span id="streamUrl"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>信息更新时间</td>
|
||||
<td><span id="updateTime"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>延迟投稿时间</td>
|
||||
<td><span id="delayTime"></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr/>
|
||||
<h1>特殊设置</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<td>是否设置强制认为不直播</td>
|
||||
<td><span id="forceNotBroadcasting"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>是否设置强制不下载</td>
|
||||
<td><span id="forceNotDownload"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>是否设置强制不上传</td>
|
||||
<td><span id="forceNotUpload"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>是否设置强制不转码</td>
|
||||
<td><span id="forceNotEncode"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>是否设置为仅下载(不上传不转码)</td>
|
||||
<td><span id="downloadOnly"></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr/>
|
||||
<h1>当前状态</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<td class='title'>下载日志</td>
|
||||
<td><span id="download"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class='title'>转码日志<br>队列<span id="encodeQueueSize"></span></td>
|
||||
<td><span id="encode"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class='title'>上传日志<br>队列<span id="uploadQueueSize"></span></td>
|
||||
<td><span id="upload"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class='title'>错误日志</td>
|
||||
<td><span id="error"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class='title'>操作日志</td>
|
||||
<td><span id="operation"></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr/>
|
||||
<h3><a href="/files/">所有录播文件</a></h3>
|
||||
{% include 'device.html' %}
|
||||
</div>
|
||||
<script src="../static/index.js"></script>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user