Merge branch 'with_api'

# Conflicts:
#	README.md
This commit is contained in:
Jerry Yan 2019-05-14 20:41:29 +08:00
commit a6aafd0ef2
22 changed files with 1175 additions and 176 deletions

322
Common.py Normal file
View File

@ -0,0 +1,322 @@
import os
import queue
from datetime import datetime
from glob import glob
import psutil
from api import XiGuaLiveApi
import json
import threading
from bypy import ByPy
_config_fp = open("config.json", "r", encoding="utf8")
config = json.load(_config_fp)
_config_fp.close()
bypy = ByPy()
doCleanTime = datetime.now()
_clean_flag = None
network = {
"currentTime": datetime.now(),
"out": {
"currentByte": psutil.net_io_counters().bytes_sent,
},
"in": {
"currentByte": psutil.net_io_counters().bytes_recv,
}
}
def updateNetwork():
global network
network = {
"currentTime": datetime.now(),
"out": {
"currentByte": psutil.net_io_counters().bytes_sent,
},
"in": {
"currentByte": psutil.net_io_counters().bytes_recv,
}
}
def getTimeDelta(a, b):
sec = (a - b).seconds
ms = (a - b).microseconds
return sec+(ms/100000.0)
def _doClean(_force=False):
global doCleanTime, _clean_flag
_disk = psutil.disk_usage(".")
if (_disk.percent > config["max"] and getTimeDelta(datetime.now(), doCleanTime) > 7200) or _force:
doCleanTime = datetime.now()
_list = sorted(glob("*.flv"), key=lambda x: datetime.utcfromtimestamp(os.path.getmtime(x)))
for _i in _list:
if not os.path.exists(_i):
break
doCleanTime = datetime.now()
if (datetime.now() - datetime.utcfromtimestamp(os.path.getmtime(_i))).days >= config["exp"]:
_clean_flag = True
if config["dow"] == "bypy":
_res = bypy.upload(_i)
if _res == 0:
os.remove(_i)
else:
os.system(config["dow"])
else:
break
doCleanTime = datetime.now()
_clean_flag = False
def doClean(_force=False):
if _clean_flag:
appendError("doClean request on cleaning, will ignore it")
return
p = threading.Thread(target=_doClean, args=(_force,))
p.setDaemon(True)
p.start()
def getCurrentStatus():
_disk = psutil.disk_usage(".")
_mem = psutil.virtual_memory()
_delta= getTimeDelta(datetime.now(),network["currentTime"])
_net = psutil.net_io_counters()
if 60 > _delta > 0:
_inSpeed = (_net.bytes_recv - network["in"]["currentByte"]) / _delta
_outSpeed = (_net.bytes_sent - network["out"]["currentByte"]) / _delta
else:
_outSpeed = 0
_inSpeed = 0
updateNetwork()
if getTimeDelta(datetime.now(), doCleanTime) > 3600:
doClean()
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),
"fileExpire": config["exp"],
}
def reloadConfig():
global config, _config_fp
_config_fp = open("config.json", "r", encoding="utf8")
config = json.load(_config_fp)
_config_fp.close()
dt_format = "%Y/%m/%d %H:%M:%S"
broadcaster = ""
streamUrl = ""
isBroadcasting = False
updateTime = ""
forceNotDownload = False
forceNotBroadcasting = False
forceNotUpload = False
forceNotEncode = False
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"]:]
class downloader(XiGuaLiveApi):
files = []
playlist = None
def updRoomInfo(self):
global broadcaster, isBroadcasting, updateTime, forceNotBroadcasting, forceNotDownload
super(downloader, self).updRoomInfo()
updateTime = datetime.strftime(datetime.now(), dt_format)
broadcaster = self.roomLiver
isBroadcasting = self.isLive
if self.isLive:
self.updPlayList()
else:
forceNotDownload = False
forceNotBroadcasting = False
self.playlist = False
self.files = []
def updPlayList(self):
global streamUrl
if self.isLive:
if "stream_url" in self._rawRoomInfo:
if self.playlist is None:
self.playlist = False
else:
self.playlist = self._rawRoomInfo["stream_url"]["flv_pull_url"]
self.playlist = self.playlist.replace("_uhd", "").replace("_sd", "").replace("_ld", "")
streamUrl = self.playlist
def onLike(self, user):
pass
def onAd(self, i):
pass
def onChat(self, chat):
pass
def onEnter(self, msg):
pass
def onJoin(self, user):
pass
def onLeave(self, json):
self.updRoomInfo()
def onMessage(self, msg):
pass
def onPresent(self, gift):
pass
def onPresentEnd(self, gift):
pass
def onSubscribe(self, user):
pass
api = downloader(config["l_u"])
def refreshDownloader():
global api
api = downloader(config["l_u"])

70
CursesDownload.py Normal file
View File

@ -0,0 +1,70 @@
import curses
import Common
widths = [
(126, 1), (159, 0), (687, 1), (710, 0), (711, 1),
(727, 0), (733, 1), (879, 0), (1154, 1), (1161, 0),
(4347, 1), (4447, 2), (7467, 1), (7521, 0), (8369, 1),
(8426, 0), (9000, 1), (9002, 2), (11021, 1), (12350, 2),
(12351, 1), (12438, 2), (12442, 0), (19893, 2), (19967, 1),
(55203, 2), (63743, 1), (64106, 2), (65039, 1), (65059, 0),
(65131, 2), (65279, 1), (65376, 2), (65500, 1), (65510, 2),
(120831, 1), (262141, 2), (1114109, 1),
]
def get_width(o):
global widths
if o == 0xe or o == 0xf:
return 0
for num, wid in widths:
if o <= num:
return wid
return 1
def c_print(handle, y, x, string, style=curses.A_NORMAL):
if type(string) != str:
string = str(string)
for _i in string:
_w = get_width(ord(_i))
if(_w>1):
handle.addch(y, x+1, " ", style)
handle.addch(y, x, ord(_i), style)
x += _w
def render(screen):
_style = curses.A_DIM
if Common.api.isLive:
_style = curses.A_BOLD | curses.A_BLINK | curses.A_ITALIC | curses.A_UNDERLINE
c_print(screen, 1, 3, Common.api.roomLiver, _style)
screen.refresh()
def main(stdscr):
global screen
screen = stdscr.subwin(23, 79, 0, 0)
screen.timeout(2000)
screen.box()
screen.hline(2, 1, curses.ACS_HLINE, 77)
c_print(screen, 1, 2, " "*45 + " 西瓜录播助手 -- by JerryYan ", curses.A_STANDOUT)
render(screen)
while True:
c = stdscr.getch()
if c == ord("q"):
break
elif c == ord("f"):
render(screen)
stdscr = curses.initscr()
curses.noecho()
curses.cbreak()
stdscr.keypad(1)
curses.wrapper(main)
stdscr.keypad(0)
curses.echo()
curses.nocbreak()
curses.endwin()

107
CursesMain.py Normal file
View File

@ -0,0 +1,107 @@
import curses
from Struct.Chat import Chat
from Struct.Gift import Gift
from Struct.MemberMsg import MemberMsg
from Struct.User import User
from api import XiGuaLiveApi
class Api(XiGuaLiveApi):
danmakuList = []
def onAd(self, i):
pass
def onChat(self, chat: Chat):
self.danmakuList.append(str(chat))
def onLike(self, user: User):
pass
def onEnter(self, msg: MemberMsg):
pass
def onJoin(self, user: User):
self.danmakuList.append(str(user))
def onSubscribe(self, user: User):
self.danmakuList.append(str(user))
def onPresent(self, gift: Gift):
pass
def onPresentEnd(self, gift: Gift):
self.danmakuList.append(str(gift))
api = Api()
widths = [
(126, 1), (159, 0), (687, 1), (710, 0), (711, 1),
(727, 0), (733, 1), (879, 0), (1154, 1), (1161, 0),
(4347, 1), (4447, 2), (7467, 1), (7521, 0), (8369, 1),
(8426, 0), (9000, 1), (9002, 2), (11021, 1), (12350, 2),
(12351, 1), (12438, 2), (12442, 0), (19893, 2), (19967, 1),
(55203, 2), (63743, 1), (64106, 2), (65039, 1), (65059, 0),
(65131, 2), (65279, 1), (65376, 2), (65500, 1), (65510, 2),
(120831, 1), (262141, 2), (1114109, 1),
]
def get_width(o):
global widths
if o == 0xe or o == 0xf:
return 0
for num, wid in widths:
if o <= num:
return wid
return 1
def c_print(handle, y, x, string, style=curses.A_NORMAL):
if type(string) != str:
string = str(string)
for _i in string:
_w = get_width(ord(_i))
if(_w>1):
handle.addch(y, x+1, " ", style)
if _i != " " or style!=curses.A_NORMAL:
handle.addch(y, x, ord(_i), style)
else:
handle.addch(y, x, 0, style)
x += _w
def render(screen):
screen.erase()
screen.box()
screen.hline(2, 1, curses.ACS_HLINE, 77)
c_print(screen, 1, 2, " "*45 + " 西瓜弹幕助手 -- by JerryYan ", curses.A_STANDOUT)
_style = curses.A_DIM
if api.isLive:
_style = curses.A_BOLD | curses.A_BLINK | curses.A_ITALIC
c_print(screen, 1, 3, api.roomLiver, _style)
_y = 3
api.getDanmaku()
for i in api.danmakuList[-10:]:
c_print(screen, _y, 2, i)
_y += 1
screen.move(0,0)
screen.refresh()
def main(stdscr):
global screen
screen = stdscr.subwin(23, 79, 0, 0)
screen.timeout(2000)
render(screen)
while True:
c = screen.getch()
if c == ord("q"):
break
render(screen)
stdscr = curses.initscr()
curses.noecho()
curses.cbreak()
stdscr.keypad(1)
curses.wrapper(main)
stdscr.keypad(0)
curses.echo()
curses.nocbreak()
curses.endwin()

8
Model/DataBase.py Normal file
View File

@ -0,0 +1,8 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URL"] = "sqlite://data.db"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
db = SQLAlchemy(app)

10
Model/Files.py Normal file
View File

@ -0,0 +1,10 @@
from datetime import datetime
from sqlalchemy import func
from .DataBase import db
class Files(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
filename = db.Column(db.String(50))
is_upload = db.Column(db.Integer(1), server_default=0, default=0)
create_time = db.Column(db.TIMESTAMP, server_default=func.now(), default=datetime.now())

View File

@ -5,17 +5,12 @@
### 西瓜直播弹幕助手--录播端```WebMain.py```
- 能够自动进行ffmpeg转码
- 转码后自动上传至B站
- 顺便还能自己清理录播的文件移动到一个位置执行shell命令上传百度云
- 把录像文件分一定大小保存B站有限制但是不知道是多少
- 少部分错误包容机制
- 有一个简单的WEB页面及简单的控制接口
> - 能够自动进行ffmpeg转码
> - 转码后自动上传至B站
> - 顺便还能自己清理录播的文件移动到一个位置执行shell命令上传百度云
> - 把录像文件分一定大小保存B站有限制但是不知道是多少
> - 少部分错误包容机制
> - 有一个简单的WEB页面及简单的控制接口
### 西瓜直播弹幕助手--礼物端```WinMain.py```

View File

@ -4,9 +4,9 @@ from .Lottery import Lottery
class Chat:
content: str =""
user: User=None
filterString:list = ["",]
content =""
user=None
filterString = ["",]
isFiltered = False
def __init__(self, json=None, lottery:Lottery = None):

View File

@ -3,12 +3,12 @@ from .User import User
class Gift:
ID:int = 0
count:int = 0
roomID:int = 0
giftList:dict = {}
amount:int = 0
user:User = None
ID = 0
count = 0
roomID = 0
giftList = {}
amount = 0
user = None
def __init__(self, json=None):
if json:

View File

@ -5,14 +5,14 @@ from .LuckyUser import LuckyUser
class Lottery:
ID: int = 0
ID = 0
isActive = False
content = ""
isFinished = False
luckyUsers = []
joinedUserCount = 0
prizeName = ""
finish:int = 0
finish = 0
def __init__(self, json=None):
if json:

View File

@ -2,9 +2,9 @@ from .User import User
class MemberMsg:
type:int = 0
content:str = ""
user:User = None
type = 0
content = ""
user = None
def __init__(self, json=None):
if json:

View File

@ -1,11 +1,11 @@
class User:
ID: int = 0
name: str = ""
brand: str = ""
level: int = 0
type: int = 0
block: bool = False
mute: bool = False
ID = 0
name = ""
brand = ""
level = 0
type = 0
block = False
mute = False
def __init__(self, json=None):
if json:
@ -47,8 +47,7 @@ class User:
else:
if self.type != 0:
return "[{}{}]{}".format(self.brand, self.level, self.name)
return "<{}{}>{}".format(self.brand,self.level,self.name)
return "<{}{}>{}".format(self.brand, self.level, self.name)
def __unicode__(self):
return self.__str__()

260
WebMain.py Normal file
View File

@ -0,0 +1,260 @@
import os
from glob import glob
from time import sleep
from flask_cors import CORS
from flask import Flask, jsonify, request, redirect, render_template, Response
import Common
import threading
from liveDownloader import run as RUN
app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False
CORS(app, supports_credentials=True)
# url_for('static', filename='index.html')
# url_for('static', filename='index.js')
@app.route("/")
def index():
return render_template("index.html")
@app.route("/config", methods=["GET"])
def readConfig():
config = Common.config.copy()
config.pop("b_p")
config.pop("mv")
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/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":{
"download":Common.downloadStatus,
"encode": Common.encodeStatus,
"encodeQueueSize": Common.encodeQueue.qsize(),
"upload": Common.uploadStatus,
"uploadQueueSize": Common.uploadQueue.qsize(),
"error": Common.errors,
"operation": Common.operations,
"broadcast": {
"broadcaster": Common.broadcaster.__str__(),
"isBroadcasting": Common.isBroadcasting,
"streamUrl": Common.streamUrl,
"updateTime": Common.updateTime
},
"config": {
"forceNotBroadcasting": Common.forceNotBroadcasting,
"forceNotDownload": Common.forceNotDownload,
"forceNotUpload": Common.forceNotUpload,
"forceNotEncode": Common.forceNotEncode,
},
}})
@app.route("/stats/device", methods=["GET"])
def getDeviceStatus():
return jsonify({"message":"ok","code":200,"status":0,"data":{
"status": Common.getCurrentStatus(),
}})
@app.route("/stats/broadcast", methods=["GET"])
def getBroadcastStats():
return jsonify({"message":"ok","code":200,"status":0,"data":{
"broadcast": {
"broadcaster": Common.broadcaster.__str__(),
"isBroadcasting": Common.isBroadcasting,
"streamUrl": Common.streamUrl,
"updateTime": Common.updateTime
}
}})
@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,
}
}})
@app.route("/stats/download", methods=["GET"])
def getDownloadStats():
return jsonify({"message":"ok","code":200,"status":0,"data":{
"download":Common.downloadStatus,
}})
@app.route("/stats/encode", methods=["GET"])
def getEncodeStats():
return jsonify({"message":"ok","code":200,"status":0,"data":{
"encode": Common.encodeStatus,
"encodeQueueSize": Common.encodeQueue.qsize(),
}})
@app.route("/stats/upload", methods=["GET"])
def getUploadStats():
return jsonify({"message":"ok","code":200,"status":0,"data":{
"upload": Common.uploadStatus,
"uploadQueueSize": Common.uploadQueue.qsize(),
}})
@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):
def generate(file, offset=0):
with open(file, "rb") as f:
f.seek(offset)
for row in f:
yield row
if os.path.exists(path):
if "RANGE" in request.headers:
offset = int(request.headers["RANGE"].replace("=","-").split("-")[1].strip())
code = 206
else:
offset = 0
code = 200
return Response(generate(path, offset),
status=code,
mimetype='application/octet-stream',
headers={
"Content-Length": os.path.getsize(path),
"Content-Range": "bytes {}-{}/{}".format(offset,os.path.getsize(path)-1,os.path.getsize(path)),
"Accept-Ranges": "bytes",
"Range": "bytes",
})
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()

View File

@ -185,10 +185,14 @@ if __name__ == "__main__":
else:
name = readInput("请输入主播用户名,默认为", name, 3)
api = WinMain(name)
print("进入", api.roomLiver, "的直播间")
if not api.isValidRoom:
input("房间不存在")
while not api.isValidRoom:
set_cmd_text_color(FOREGROUND_RED)
print("未找到对应房间或未开播等待1分钟后重试")
resetColor()
time.sleep(60)
api.updRoomInfo()
sys.exit()
print("进入", api.roomLiver, "的直播间")
os.system("title {}".format(api.getTitle()))
print("=" * 30)
while True:

27
api.py
View File

@ -12,21 +12,21 @@ import time
s = requests.Session()
DEBUG: bool = False
DEBUG = False
class XiGuaLiveApi:
isLive: bool = False
isValidRoom: bool = False
isLive = False
isValidRoom = False
_rawRoomInfo = {}
name: str = ""
roomID: int = 0
roomTitle: str = ""
roomLiver: User = None
roomPopularity: int = 0
_cursor:str = "0"
_updRoomCount:int = 0
lottery:Lottery = None
name = ""
roomID = 0
roomTitle = ""
roomLiver = None
roomPopularity = 0
_cursor = "0"
_updRoomCount = 0
lottery = None
def __init__(self, name: str = "永恒de草薙"):
self.name = name
@ -161,9 +161,12 @@ class XiGuaLiveApi:
if "room" not in d and d["room"] is None:
self.apiChangedError("Api发生改变请及时联系我", d)
return False
self.roomLiver = User(d)
if self.name not in str(self.roomLiver):
self.isLive = False
return False
self._rawRoomInfo = d["room"]
self.isLive = d["room"]["status"] == 2
self.roomLiver = User(d)
self.roomTitle = d["room"]["title"]
self.roomPopularity = d["room"]["user_count"]
l = Lottery(d)

View File

@ -4,7 +4,8 @@ import os
import re
import json as JSON
from datetime import datetime
from time import sleep
import Common
import rsa
import math
import base64
@ -25,6 +26,7 @@ class Bilibili:
self.files = []
self.videos = []
self.session = requests.session()
self.session.keep_alive = False
if cookie:
self.session.headers["cookie"] = cookie
self.csrf = re.search('bili_jct=(.*?);', cookie).group(1)
@ -188,14 +190,13 @@ class Bilibili:
"""
self.preUpload(parts)
self.finishUpload(title, tid, tag, desc, source, cover, no_reprint)
self.clean()
self.clear()
def preUpload(self, parts):
"""
:param parts: e.g. VideoPart('part path', 'part title', 'part desc'), or [VideoPart(...), VideoPart(...)]
:type parts: VideoPart or list<VideoPart>
"""
self.session.headers['Content-Type'] = 'application/json; charset=utf-8'
if not isinstance(parts, list):
parts = [parts]
@ -204,6 +205,7 @@ class Bilibili:
filepath = part.path
filename = os.path.basename(filepath)
filesize = os.path.getsize(filepath)
Common.appendUploadStatus("Upload >{}< Started".format(filepath))
self.files.append(part)
r = self.session.get('https://member.bilibili.com/preupload?'
'os=upos&upcdn=ws&name={name}&size={size}&r=upos&profile=ugcupos%2Fyb&ssl=0'
@ -238,12 +240,13 @@ class Bilibili:
# {"upload_id":"72eb747b9650b8c7995fdb0efbdc2bb6","key":"\/i181012ws2wg1tb7tjzswk2voxrwlk1u.mp4","OK":1,"bucket":"ugc"}
json = r.json()
upload_id = json['upload_id']
with open(filepath, 'rb') as f:
chunks_num = math.ceil(filesize / chunk_size)
chunks_index = 0
chunks_data = f.read(chunk_size)
Common.modifyLastUploadStatus("Uploading >{}< @ {:.2f}%".format(filepath, 100.0 * chunks_index / chunks_num))
while True:
_d = datetime.now()
if not chunks_data:
break
r = self.session.put('https:{endpoint}/{upos_uri}?'
@ -263,10 +266,11 @@ class Bilibili:
)
if r.status_code != 200:
continue
print('{} : UPLOAD {}/{}'.format(datetime.strftime(datetime.now(), "%y%m%d %H%M"), chunks_index,
chunks_num), r.text)
chunks_data = f.read(chunk_size)
chunks_index += 1 # start with 0
Common.modifyLastUploadStatus("Uploading >{}< @ {:.2f}%".format(filepath, 100.0*chunks_index/chunks_num))
if (datetime.now()-_d).seconds < 2:
sleep(1)
# NOT DELETE! Refer to https://github.com/comwrg/bilibiliupload/issues/15#issuecomment-424379769
self.session.post('https:{endpoint}/{upos_uri}?'
@ -282,6 +286,7 @@ class Bilibili:
self.videos.append({'filename': upos_uri.replace('upos://ugc/', '').split('.')[0],
'title': part.title,
'desc': part.desc})
Common.modifyLastUploadStatus("Upload >{}< Finished".format(filepath))
__f = open("uploaded.json","w")
JSON.dump(self.videos, __f)
@ -314,6 +319,7 @@ class Bilibili:
"""
if len(self.videos) == 0:
return
Common.appendUploadStatus("[{}]投稿中,请稍后".format(title))
self.session.headers['Content-Type'] = 'application/json; charset=utf-8'
copyright = 2 if source else 1
r = self.session.post('https://member.bilibili.com/x/vu/web/add?csrf=' + self.csrf,
@ -330,21 +336,21 @@ class Bilibili:
"order_id": 0,
"videos": self.videos}
)
print(r.text)
Common.modifyLastUploadStatus("[{}] Published | Result : {}".format(title, r.text))
def reloadFromPrevious(self):
if os.path.exists("uploaded.json"):
__f = open("uploaded.json", "r")
try:
self.videos = JSON.load(__f)
print("RELOAD Success")
Common.appendUploadStatus("RELOAD SUCCESS")
except:
print("RELOAD Failed")
Common.appendUploadStatus("RELOAD Failed")
self.videos = []
__f.close()
os.remove("uploaded.json")
else:
print("RELOAD Failed")
Common.appendUploadStatus("RELOAD Failed")
self.videos = []
def clear(self):

View File

@ -2,71 +2,14 @@ import shutil
import sys
import time
from datetime import datetime
import queue
import threading
from config import config
from api import XiGuaLiveApi
from bilibili import *
import Common
import os
import requests
q = queue.Queue()
base_uri = ""
isEncode = False
isDownload = False
uq = queue.Queue()
eq = queue.Queue()
class downloader(XiGuaLiveApi):
files = []
playlist: str = None
def updRoomInfo(self):
super(downloader, self).updRoomInfo()
if self.isLive:
self.updPlayList()
else:
print("未开播,等待开播")
self.files = []
def updPlayList(self):
if self.isLive:
if "stream_url" in self._rawRoomInfo:
if self.playlist is None:
self.apiChangedError("无法获取直播链接")
self.playlist = False
else:
self.playlist = self._rawRoomInfo["stream_url"]["flv_pull_url"]
self.playlist = self.playlist.replace("_uhd", "").replace("_sd", "").replace("_ld", "")
def onLike(self, user):
pass
def onAd(self, i):
pass
def onChat(self, chat):
pass
def onEnter(self, msg):
pass
def onJoin(self, user):
pass
def onLeave(self, json):
self.updRoomInfo()
def onMessage(self, msg):
pass
def onPresent(self, gift):
pass
def onPresentEnd(self, gift):
pass
def onSubscribe(self, user):
pass
def download(url):
@ -74,100 +17,129 @@ def download(url):
path = datetime.strftime(datetime.now(), "%Y%m%d_%H%M.flv")
p = requests.get(url, stream=True)
if p.status_code != 200:
print("{} : Download Response 404 ,will stop looping".format(datetime.strftime(datetime.now(), "%y%m%d %H%M")))
Common.appendDownloadStatus("Download with Response 404, maybe broadcaster is not broadcasting")
return True
isDownload = True
print("{} : Download {}".format(datetime.strftime(datetime.now(), "%y%m%d %H%M"), path))
Common.appendDownloadStatus("Download >{}< Start".format(path))
f = open(path, "wb")
try:
for t in p.iter_content(chunk_size=64 * 1024):
if t:
f.write(t)
if os.path.getsize(path) > 1024 * 1024 * 1024 * 1.5:
else:
raise Exception("`t` is not valid")
_size = os.path.getsize(path)
Common.modifyLastDownloadStatus("Downloading >{}< @ {:.2f}%".format(path, 100.0 * _size/Common.config["p_s"]))
if _size > Common.config["p_s"] or Common.forceNotDownload:
Common.modifyLastDownloadStatus("Download >{}< Exceed MaxSize".format(path))
break
print("{} : Download Quiting".format(datetime.strftime(datetime.now(), "%y%m%d %H%M")))
except Exception as e:
print("{} : Download Quiting With Exception {}".format(datetime.strftime(datetime.now(), "%y%m%d %H%M"),
e.__str__()))
Common.appendError("Download >{}< With Exception {}".format(path, e.__str__()))
f.close()
isDownload = False
if os.path.getsize(path) == 0:
Common.modifyLastDownloadStatus("Download >{}< Finished".format(path))
if os.path.getsize(path) < 1024 * 1024:
Common.modifyLastDownloadStatus("Downloaded File >{}< is too small, will ignore it".format(path))
os.remove(path)
return False
eq.put(path)
download(url)
if Common.forceNotDownload:
Common.modifyLastDownloadStatus("设置了不下载,所以[{}]不会下载".format(path))
return
else:
Common.encodeQueue.put(path)
download(url)
def encode():
global isEncode
Common.appendEncodeStatus("Encode Daemon Starting")
while True:
i = eq.get()
isEncode = False
i = Common.encodeQueue.get()
if Common.forceNotEncode:
Common.appendEncodeStatus("设置了不编码,所以[{}]不会编码".format(i))
Common.uploadQueue.put(i)
continue
if os.path.exists(i):
isEncode = True
os.system("ffmpeg -i {} -c:v copy -c:a copy -f mp4 {}".format(i, i[:13] + ".mp4"))
uq.put(i[:13] + ".mp4")
if config["mv"]:
shutil.move(i, config["mtd"])
elif config["del"]:
os.remove(i)
isEncode = False
if os.path.getsize(i) < 8 * 1024 * 1024:
Common.appendEncodeStatus("Encoded File >{}< is too small, will ignore it".format(i))
continue
Common.appendEncodeStatus("Encoding >{}< Start".format(i))
os.system("ffmpeg -i {} -c:v copy -c:a copy -f mp4 {} -y".format(i, i[:13] + ".mp4"))
Common.uploadQueue.put(i[:13] + ".mp4")
Common.modifyLastEncodeStatus("Encode >{}< Finished".format(i))
def upload(date=datetime.strftime(datetime.now(), "%Y_%m_%d")):
print("{} : Upload Daemon Starting".format(datetime.strftime(datetime.now(), "%y%m%d %H%M")))
i = uq.get()
Common.appendUploadStatus("Upload Daemon Starting")
i = Common.uploadQueue.get()
while True:
Common.doClean()
if Common.forceNotUpload:
if isinstance(i, bool):
Common.appendUploadStatus("设置了不上传,不会发布了")
return
Common.appendUploadStatus("设置了不上传,所以[{}]不会上传了".format(i))
i = Common.uploadQueue.get()
continue
if isinstance(i, bool):
print("{} : Upload Daemon Receive Command {}"
.format(datetime.strftime(datetime.now(), "%y%m%d %H%M"), i))
if i is True:
print("自动投稿中,请稍后")
b.finishUpload(config["t_t"].format(date), 17, config["tag"], config["des"],
source=config["src"], no_reprint=0)
b.finishUpload(Common.config["t_t"].format(date), 17, Common.config["tag"], Common.config["des"],
source=Common.config["src"], no_reprint=0)
b.clear()
break
print("{} : Upload {}".format(datetime.strftime(datetime.now(), "%y%m%d %H%M"), i))
if not os.path.exists(i):
print("{} : Upload File Not Exist {}".format(datetime.strftime(datetime.now(), "%y%m%d %H%M"), i))
i = uq.get()
Common.appendError("Upload File Not Exist {}".format(i))
i = Common.uploadQueue.get()
continue
try:
b.preUpload(VideoPart(i, os.path.basename(i)))
except:
except Exception as e:
Common.appendError(e.__str__())
continue
os.remove(i)
i = uq.get()
print("{} : Upload Daemon Quiting".format(datetime.strftime(datetime.now(), "%y%m%d %H%M")))
if not Common.forceNotEncode:
os.remove(i)
i = Common.uploadQueue.get()
Common.appendUploadStatus("Upload Daemon Quiting")
b = Bilibili()
b.login(config["b_u"], config["b_p"])
b.login(Common.config["b_u"], Common.config["b_p"])
if __name__ == "__main__":
name = config["l_u"]
print("西瓜直播录播助手 by JerryYan")
api = downloader(name)
print("进入", api.roomLiver, "的直播间")
if not api.isValidRoom:
input("房间不存在")
sys.exit()
print("=" * 30)
d = datetime.strftime(datetime.now(), "%Y_%m_%d")
et = threading.Thread(target=encode, args=())
et.setDaemon(True)
et.start()
def run():
global isEncode, isDownload, et
Common.refreshDownloader()
if not Common.api.isValidRoom:
Common.appendError("[{}]房间未找到".format(Common.config["l_u"]))
return
d = None
t = threading.Thread(target=download)
ut = threading.Thread(target=upload, args=(d,))
et = threading.Thread(target=encode, args=())
et.setDaemon(True)
et.start()
_count = 0
_count_error = 0
while True:
if api.isLive:
if Common.api.isLive and not Common.forceNotBroadcasting:
if d is None:
d = datetime.strftime(datetime.now(), "%Y_%m_%d")
if not t.is_alive():
if not t.is_alive() and not Common.forceNotDownload:
try:
Common.api.updRoomInfo()
_count = 0
_count_error = 0
except Exception as e:
Common.appendError(e.__str__())
continue
_count_error += 1
_preT = api.playlist
_preT = Common.api.playlist
if not _preT:
Common.api.updRoomInfo()
continue
t = threading.Thread(target=download, args=(_preT,))
t.setDaemon(True)
t.start()
@ -181,31 +153,45 @@ if __name__ == "__main__":
et.start()
if _count % 15 == 0:
try:
api.updRoomInfo()
Common.api.updRoomInfo()
_count = 0
_count_error = 0
except Exception as e:
print(e.__str__())
Common.appendError(e.__str__())
time.sleep(20)
_count_error += 1
continue
if _count_error > 15:
api.isLive = False
Common.api.isLive = False
_count += 1
time.sleep(20)
else:
if d is not None:
d = None
if not isEncode and not isDownload:
uq.put(True)
Common.uploadQueue.put(True)
isEncode = True
isDownload = True
del config
from config import config
# print("主播未开播等待1分钟后重试")
time.sleep(60)
try:
api.updRoomInfo()
Common.api.updRoomInfo()
_count_error = 0
except Exception as e:
print(e.__str__())
Common.appendError(e.__str__())
Common.refreshDownloader()
if not Common.api.roomLiver:
Common.refreshDownloader()
if Common.forceStartEncodeThread:
if not et.is_alive():
et = threading.Thread(target=encode, args=())
et.setDaemon(True)
et.start()
Common.forceStartEncodeThread = False
if Common.forceStartUploadThread:
if not ut.is_alive():
d = datetime.strftime(datetime.now(), "%Y_%m_%d")
ut = threading.Thread(target=upload, args=(d,))
ut.setDaemon(True)
ut.start()
Common.forceStartUploadThread = False

26
static/device.js Normal file
View File

@ -0,0 +1,26 @@
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)
$("#fileExpire").text(res.data.status.fileExpire)
}
}
)
}
deviceUpdate()
setInterval(deviceUpdate,4000)

57
static/index.js Normal file
View File

@ -0,0 +1,57 @@
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)
$("#forceNotBroadcasting").text(res.data.config.forceNotBroadcasting)
$("#forceNotDownload").text(res.data.config.forceNotDownload)
$("#forceNotUpload").text(res.data.config.forceNotUpload)
$("#forceNotEncode").text(res.data.config.forceNotEncode)
$("#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,10000)

29
templates/device.html Normal file
View File

@ -0,0 +1,29 @@
<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>清理<span id="fileExpire"></span>天前的文件</td>
<td>@ <span id="doCleanTime"></span></td>
</tr>
</table>
<script src="/static/device.js"></script>

26
templates/files.html Normal file
View 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
View File

@ -0,0 +1,13 @@
<meta charset="UTF-8">
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<style>
td{
border: solid 1px lightgray;
}
.title{
width: 6em;
}
.time{
width: 10em;
}
</style>

78
templates/index.html Normal file
View File

@ -0,0 +1,78 @@
<!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>
</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>
</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>