diff --git a/Common.py b/Common.py new file mode 100644 index 0000000..265d968 --- /dev/null +++ b/Common.py @@ -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"]) diff --git a/CursesDownload.py b/CursesDownload.py new file mode 100644 index 0000000..ba31106 --- /dev/null +++ b/CursesDownload.py @@ -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() + diff --git a/CursesMain.py b/CursesMain.py new file mode 100644 index 0000000..4a1f513 --- /dev/null +++ b/CursesMain.py @@ -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() + diff --git a/Model/DataBase.py b/Model/DataBase.py new file mode 100644 index 0000000..de475fd --- /dev/null +++ b/Model/DataBase.py @@ -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) \ No newline at end of file diff --git a/Model/Files.py b/Model/Files.py new file mode 100644 index 0000000..3b6d595 --- /dev/null +++ b/Model/Files.py @@ -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()) diff --git a/README.md b/README.md index aabc22a..e4374b4 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,12 @@ ### 西瓜直播弹幕助手--录播端```WebMain.py``` -- 能够自动进行ffmpeg转码 - -- 转码后自动上传至B站 - -- 顺便还能自己清理录播的文件(移动到一个位置,执行shell命令,上传百度云) - -- 把录像文件分一定大小保存(B站有限制,但是不知道是多少) - -- 少部分错误包容机制 - -- 有一个简单的WEB页面,及简单的控制接口 +> - 能够自动进行ffmpeg转码 +> - 转码后自动上传至B站 +> - 顺便还能自己清理录播的文件(移动到一个位置,执行shell命令,上传百度云) +> - 把录像文件分一定大小保存(B站有限制,但是不知道是多少) +> - 少部分错误包容机制 +> - 有一个简单的WEB页面,及简单的控制接口 ### 西瓜直播弹幕助手--礼物端```WinMain.py``` diff --git a/Struct/Chat.py b/Struct/Chat.py index 729ee78..31269c6 100644 --- a/Struct/Chat.py +++ b/Struct/Chat.py @@ -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): diff --git a/Struct/Gift.py b/Struct/Gift.py index c856b28..64d11bb 100644 --- a/Struct/Gift.py +++ b/Struct/Gift.py @@ -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: diff --git a/Struct/Lottery.py b/Struct/Lottery.py index d2b5ae1..e2d9eb8 100644 --- a/Struct/Lottery.py +++ b/Struct/Lottery.py @@ -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: diff --git a/Struct/MemberMsg.py b/Struct/MemberMsg.py index 404a522..2dcdeea 100644 --- a/Struct/MemberMsg.py +++ b/Struct/MemberMsg.py @@ -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: diff --git a/Struct/User.py b/Struct/User.py index f2253f3..628a234 100644 --- a/Struct/User.py +++ b/Struct/User.py @@ -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__() - diff --git a/WebMain.py b/WebMain.py new file mode 100644 index 0000000..70b9481 --- /dev/null +++ b/WebMain.py @@ -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/", 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() diff --git a/WinMain.py b/WinMain.py index c5a7011..294e088 100644 --- a/WinMain.py +++ b/WinMain.py @@ -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: diff --git a/api.py b/api.py index fd272bb..d644e5f 100644 --- a/api.py +++ b/api.py @@ -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) diff --git a/bilibili.py b/bilibili.py index 76a55b0..000158e 100644 --- a/bilibili.py +++ b/bilibili.py @@ -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 """ - 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): diff --git a/liveDownloader.py b/liveDownloader.py index 4c03283..ff6a461 100644 --- a/liveDownloader.py +++ b/liveDownloader.py @@ -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 diff --git a/static/device.js b/static/device.js new file mode 100644 index 0000000..2d311b8 --- /dev/null +++ b/static/device.js @@ -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) diff --git a/static/index.js b/static/index.js new file mode 100644 index 0000000..2d54541 --- /dev/null +++ b/static/index.js @@ -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 += "" + obj.datetime + "" + obj.message + "" + }) + return "" + ret + "
" + }) + $("#encode").html(function(){ + var ret = "" + res.data.encode.reverse().forEach(function(obj){ + ret += "" + obj.datetime + "" + obj.message + "" + }) + return "" + ret + "
" + }) + $("#upload").html(function(){ + var ret = "" + res.data.upload.reverse().forEach(function(obj){ + ret += "" + obj.datetime + "" + obj.message + "" + }) + return "" + ret + "
" + }) + $("#error").html(function(){ + var ret = "" + res.data.error.reverse().forEach(function(obj){ + ret += "" + obj.datetime + "" + obj.message + "" + }) + return "" + ret + "
" + }) + $("#operation").html(function(){ + var ret = "" + res.data.operation.reverse().forEach(function(obj){ + ret += "" + obj.datetime + "" + obj.message + "" + }) + return "" + ret + "
" + }) + } + } + ) +} + +taskUpdate() +setInterval(taskUpdate,10000) diff --git a/templates/device.html b/templates/device.html new file mode 100644 index 0000000..a10011a --- /dev/null +++ b/templates/device.html @@ -0,0 +1,29 @@ +

机器状态

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
CPU使用率%
内存使用率/(%)
磁盘使用率/(%)
网络速率/s/s
文件清理清理天前的文件@
+ diff --git a/templates/files.html b/templates/files.html new file mode 100644 index 0000000..40221cb --- /dev/null +++ b/templates/files.html @@ -0,0 +1,26 @@ + + + + 文件 + {% include 'head.html' %} + + +
+

所有录像文件

+

部分录像文件已转移至百度云,请在这里下载 提取码: ddxt

+ + + + + {%for i in files %} + + + + {% endfor %} +
文件名文件大小链接
{{i.name}}{{i.size}}下载文件
+
+

录播信息页

+ {% include 'device.html' %} +
+ + \ No newline at end of file diff --git a/templates/head.html b/templates/head.html new file mode 100644 index 0000000..c9f310b --- /dev/null +++ b/templates/head.html @@ -0,0 +1,13 @@ + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..0c350ad --- /dev/null +++ b/templates/index.html @@ -0,0 +1,78 @@ + + + + 录播 + {% include 'head.html' %} + + +
+

基本信息

+ + + + + + + + + + + + + + + + + +
主播名
是否正在直播
直播视频流地址
信息更新时间
+
+

特殊设置

+ + + + + + + + + + + + + + + + + +
是否设置强制认为不直播
是否设置强制不下载
是否设置强制不上传
是否设置强制不转码
+
+

当前状态

+ + + + + + + + + + + + + + + + + + + + + +
下载日志
转码日志
队列
上传日志
队列
错误日志
操作日志
+
+

所有录播文件

+ {% include 'device.html' %} +
+ + + \ No newline at end of file