Compare commits

..

6 Commits

18 changed files with 1264 additions and 164 deletions

3
.gitignore vendored
View File

@ -12,5 +12,4 @@ build/
dist/ dist/
access_token access_token
winsw.* winsw.*
*.json *.json
*.log

2
app.py
View File

@ -11,7 +11,6 @@ from controller.api.posting_blueprint import blueprint as api_posting_blueprint
from controller.api.video_part_blueprint import blueprint as api_video_part_blueprint from controller.api.video_part_blueprint import blueprint as api_video_part_blueprint
from controller.api.biliuploader_blueprint import blueprint as api_biliuploader_blueprint from controller.api.biliuploader_blueprint import blueprint as api_biliuploader_blueprint
from model import db from model import db
from workflow.bilibili import Bilibili
app = Flask(__name__) app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False app.config['JSON_AS_ASCII'] = False
@ -36,5 +35,4 @@ with app.app_context():
db.create_all(app=app) db.create_all(app=app)
if __name__ == '__main__': if __name__ == '__main__':
Bilibili().start()
app.run() app.run()

View File

@ -22,15 +22,18 @@ DANMAKU_OPACITY = 100
# [ffmpeg] # [ffmpeg]
# exec # exec
FFMPEG_EXEC = "ffmpeg" FFMPEG_EXEC = "ffmpeg"
# [handbrake] # hevc
# exec FFMPEG_USE_HEVC = False
HANDBRAKE_EXEC = "HandBrakeCli" # nvidia_gpu
# preset_file FFMPEG_USE_NVIDIA_GPU = False
HANDBRAKE_PRESET_FILE = "handbrake.json" # intel_gpu
# preset FFMPEG_USE_INTEL_GPU = False
HANDBRAKE_PRESET = "NvEnc" # vaapi
# encopt FFMPEG_USE_VAAPI = False
HANDBRAKE_ENCOPT = "" # crf
VIDEO_CRF = 28
# gop
VIDEO_GOP = 60
# [video] # [video]
# enabled # enabled
VIDEO_ENABLED = False VIDEO_ENABLED = False
@ -90,19 +93,19 @@ def load_config():
if config.has_section("clip"): if config.has_section("clip"):
section = config['clip'] section = config['clip']
global VIDEO_CLIP_EACH_SEC, VIDEO_CLIP_OVERFLOW_SEC global VIDEO_CLIP_EACH_SEC, VIDEO_CLIP_OVERFLOW_SEC
VIDEO_CLIP_EACH_SEC = section.getint('each_sec', VIDEO_CLIP_EACH_SEC) VIDEO_CLIP_EACH_SEC = section.getfloat('each_sec', VIDEO_CLIP_EACH_SEC)
VIDEO_CLIP_OVERFLOW_SEC = section.getint('overflow_sec', VIDEO_CLIP_OVERFLOW_SEC) VIDEO_CLIP_OVERFLOW_SEC = section.getfloat('overflow_sec', VIDEO_CLIP_OVERFLOW_SEC)
if config.has_section("ffmpeg"): if config.has_section("ffmpeg"):
section = config['ffmpeg'] section = config['ffmpeg']
global FFMPEG_EXEC global FFMPEG_EXEC, FFMPEG_USE_HEVC, FFMPEG_USE_NVIDIA_GPU, FFMPEG_USE_INTEL_GPU, VIDEO_CRF, \
VIDEO_GOP, FFMPEG_USE_VAAPI
FFMPEG_EXEC = section.get('exec', FFMPEG_EXEC) FFMPEG_EXEC = section.get('exec', FFMPEG_EXEC)
if config.has_section("handbrake"): FFMPEG_USE_HEVC = section.getboolean('hevc', FFMPEG_USE_HEVC)
section = config['handbrake'] FFMPEG_USE_NVIDIA_GPU = section.getboolean('nvidia_gpu', FFMPEG_USE_NVIDIA_GPU)
global HANDBRAKE_EXEC, HANDBRAKE_PRESET_FILE, HANDBRAKE_PRESET, HANDBRAKE_ENCOPT FFMPEG_USE_INTEL_GPU = section.getboolean('intel_gpu', FFMPEG_USE_INTEL_GPU)
HANDBRAKE_EXEC = section.get('exec', HANDBRAKE_EXEC) FFMPEG_USE_VAAPI = section.getboolean('vaapi', FFMPEG_USE_VAAPI)
HANDBRAKE_PRESET_FILE = section.get('preset_file', HANDBRAKE_PRESET_FILE) VIDEO_CRF = section.getfloat('crf', VIDEO_CRF)
HANDBRAKE_PRESET = section.get('preset', HANDBRAKE_PRESET) VIDEO_GOP = section.getfloat('gop', VIDEO_GOP)
HANDBRAKE_ENCOPT = section.get('encopt', HANDBRAKE_ENCOPT)
if config.has_section("recorder"): if config.has_section("recorder"):
global BILILIVE_RECORDER_DIRECTORY, XIGUALIVE_RECORDER_DIRECTORY, VIDEO_OUTPUT_DIR global BILILIVE_RECORDER_DIRECTORY, XIGUALIVE_RECORDER_DIRECTORY, VIDEO_OUTPUT_DIR
section = config['recorder'] section = config['recorder']
@ -137,12 +140,12 @@ def get_config():
}, },
'ffmpeg': { 'ffmpeg': {
'exec': FFMPEG_EXEC, 'exec': FFMPEG_EXEC,
}, 'hevc': FFMPEG_USE_HEVC,
'handbrake': { 'nvidia_gpu': FFMPEG_USE_NVIDIA_GPU,
'exec': HANDBRAKE_EXEC, 'intel_gpu': FFMPEG_USE_INTEL_GPU,
'preset_file': HANDBRAKE_PRESET_FILE, 'vaapi': FFMPEG_USE_VAAPI,
'preset': HANDBRAKE_PRESET, 'crf': VIDEO_CRF,
'encopt': HANDBRAKE_ENCOPT, 'gop': VIDEO_GOP,
}, },
'recorder': { 'recorder': {
'bili_dir': BILILIVE_RECORDER_DIRECTORY, 'bili_dir': BILILIVE_RECORDER_DIRECTORY,

View File

@ -6,15 +6,18 @@ from typing import Optional
from flask import Blueprint, jsonify, request, current_app from flask import Blueprint, jsonify, request, current_app
from config import BILILIVE_RECORDER_DIRECTORY, VIDEO_TITLE, XIGUALIVE_RECORDER_DIRECTORY from config import BILILIVE_RECORDER_DIRECTORY, VIDEO_TITLE, XIGUALIVE_RECORDER_DIRECTORY, VIDEO_DESC, \
VIDEO_TAGS, VIDEO_TID, VIDEO_ENABLED
from exception.danmaku import DanmakuException from exception.danmaku import DanmakuException
from model import db from model import db
from model.DanmakuClip import DanmakuClip from model.DanmakuClip import DanmakuClip
from model.VideoClip import VideoClip from model.VideoClip import VideoClip
from model.Workflow import Workflow from model.Workflow import Workflow
from workflow.bilibili import ENCODING_QUEUE, IS_LIVING from workflow.bilibili import IS_LIVING, IS_UPLOADING, INSTANCE as bilibili_instance, IS_ENCODING
from workflow.bilibili import VideoPart
from workflow.danmaku import get_file_start from workflow.danmaku import get_file_start
from workflow.video import get_video_real_duration, duration_str_to_float from workflow.video import get_video_real_duration, duration_str_to_float
from workflow.worker import do_workflow
blueprint = Blueprint("api_bilirecorder", __name__, url_prefix="/api/bilirecorder") blueprint = Blueprint("api_bilirecorder", __name__, url_prefix="/api/bilirecorder")
@ -32,13 +35,50 @@ def auto_submit_task():
if len(bili_record_workflow_item.video_clips) == 0: if len(bili_record_workflow_item.video_clips) == 0:
print("[!]Auto Submit Fail: No Video Clips") print("[!]Auto Submit Fail: No Video Clips")
return return
if VIDEO_ENABLED:
bilibili_instance.login()
video_title = bili_record_workflow_item.name
_future = None
for video_clip in bili_record_workflow_item.video_clips: for video_clip in bili_record_workflow_item.video_clips:
if len(video_clip.danmaku_clips) > 0: if len(video_clip.danmaku_clips) > 0:
print("[+]Workflow:", bili_record_workflow_item.id, "; Video:", video_clip.full_path) print("[+]Workflow:", bili_record_workflow_item.id, "; Video:", video_clip.full_path)
ENCODING_QUEUE.put(bili_record_workflow_item) _started = True
IS_ENCODING.set()
_future = pool.submit(
do_workflow,
video_clip.full_path,
video_clip.danmaku_clips[0].full_path,
*[clip.full_path for clip in video_clip.danmaku_clips[1:]]
)
clear_item() clear_item()
def _clear_encode_flag_callback(_f: "Future"):
IS_ENCODING.clear()
_future.add_done_callback(_clear_encode_flag_callback)
if VIDEO_ENABLED:
def _encode_finish_callback(_f: "Future"):
_result = _f.result()
if _result:
# start uploading
bilibili_instance.pre_upload(
parts=[VideoPart(os.path.join(_item['base_path'], _item['file']), _item['file'])
for _item in _result],
max_retry=10
)
_future.add_done_callback(_encode_finish_callback)
else: else:
print("[-]Workflow:", bili_record_workflow_item.id, "; Video:", video_clip.full_path, "; No Danmaku") print("[-]Workflow:", bili_record_workflow_item.id, "; Video:", video_clip.full_path, "; No Danmaku")
if VIDEO_ENABLED and _future is not None:
def _on_upload_finish(_f: "Future"):
if IS_UPLOADING.is_set() or IS_LIVING.is_set() or IS_ENCODING.is_set():
return
bilibili_instance.finish_upload(
title=video_title,
desc=VIDEO_DESC,
tid=VIDEO_TID,
tag=VIDEO_TAGS,
no_reprint=0)
_future.add_done_callback(_on_upload_finish)
def clear_item(): def clear_item():
@ -69,7 +109,6 @@ def safe_create_item():
commit_item() commit_item()
auto_submit_task() auto_submit_task()
bili_record_workflow_item = Workflow() bili_record_workflow_item = Workflow()
bili_record_workflow_item.name = VIDEO_TITLE.format(datetime.utcnow().strftime("%Y%m%d"))
else: else:
bili_record_workflow_item.name = VIDEO_TITLE.format(datetime.utcnow().strftime("%Y%m%d")) bili_record_workflow_item.name = VIDEO_TITLE.format(datetime.utcnow().strftime("%Y%m%d"))
bili_record_workflow_item.automatic = True bili_record_workflow_item.automatic = True

View File

@ -2,3 +2,23 @@ from flask import Blueprint, jsonify
from workflow.bilibili import INSTANCE as BILIBILI_INSTANCE from workflow.bilibili import INSTANCE as BILIBILI_INSTANCE
blueprint = Blueprint("api_biliuploader", __name__, url_prefix="/api/biliuploader") blueprint = Blueprint("api_biliuploader", __name__, url_prefix="/api/biliuploader")
@blueprint.get("/")
def get_login_info():
return jsonify({
"mid": BILIBILI_INSTANCE.user_id,
"expires": BILIBILI_INSTANCE.expires,
"login_at": BILIBILI_INSTANCE.login_time,
})
@blueprint.post("/")
def do_login():
BILIBILI_INSTANCE.login()
return get_login_info()
@blueprint.post("/finish")
def finish_uploading():
BILIBILI_INSTANCE.finish_upload()

View File

@ -4,8 +4,7 @@ import platform
import psutil import psutil
from flask import Blueprint, jsonify from flask import Blueprint, jsonify
from config import DANMAKU_EXEC, FFMPEG_EXEC, BILILIVE_RECORDER_DIRECTORY, XIGUALIVE_RECORDER_DIRECTORY, \ from config import DANMAKU_EXEC, FFMPEG_EXEC, BILILIVE_RECORDER_DIRECTORY, XIGUALIVE_RECORDER_DIRECTORY, VIDEO_OUTPUT_DIR
VIDEO_OUTPUT_DIR, HANDBRAKE_EXEC
from util.system import check_exec from util.system import check_exec
from workflow.bilibili import IS_LIVING, IS_UPLOADING, IS_ENCODING from workflow.bilibili import IS_LIVING, IS_UPLOADING, IS_ENCODING
@ -61,7 +60,6 @@ def collect_basic_status():
}, },
'exec': { 'exec': {
'ffmpeg': check_exec(FFMPEG_EXEC), 'ffmpeg': check_exec(FFMPEG_EXEC),
'handbrake': check_exec(HANDBRAKE_EXEC),
'danmaku': check_exec(DANMAKU_EXEC), 'danmaku': check_exec(DANMAKU_EXEC),
}, },
'system': { 'system': {

View File

@ -50,10 +50,6 @@
<td>FFMPEG状态</td> <td>FFMPEG状态</td>
<td :class="collector.basic.exec.ffmpeg ? 'success' : 'warning'"></td> <td :class="collector.basic.exec.ffmpeg ? 'success' : 'warning'"></td>
</tr> </tr>
<tr>
<td>HANDBRAKE状态</td>
<td :class="collector.basic.exec.handbrake ? 'success' : 'warning'"></td>
</tr>
<tr> <tr>
<td>弹幕工具状态</td> <td>弹幕工具状态</td>
<td :class="collector.basic.exec.danmaku ? 'success' : 'warning'"></td> <td :class="collector.basic.exec.danmaku ? 'success' : 'warning'"></td>
@ -88,30 +84,29 @@
<td>命令</td> <td>命令</td>
<td>{{ config.ffmpeg.exec }}</td> <td>{{ config.ffmpeg.exec }}</td>
</tr> </tr>
</tbody>
</table>
<table class="current-config">
<thead>
<tr class="table-header">
<td colspan="2">HANDBRAKE</td>
</tr>
</thead>
<tbody>
<tr> <tr>
<td>命令</td> <td>HEVC</td>
<td>{{ config.handbrake.exec }}</td> <td :class="{warning: !config.ffmpeg.hevc, success: config.ffmpeg.hevc}"></td>
</tr> </tr>
<tr> <tr>
<td>预设文件</td> <td>VAAPI</td>
<td>{{ config.handbrake.preset_file }}</td> <td :class="{warning: !config.ffmpeg.vaapi, success: config.ffmpeg.vaapi}"></td>
</tr> </tr>
<tr> <tr>
<td>预设使用</td> <td>嘤伟达GPU</td>
<td>{{ config.handbrake.preset }}</td> <td :class="{warning: !config.ffmpeg.nvidia_gpu, success: config.ffmpeg.nvidia_gpu}"></td>
</tr> </tr>
<tr> <tr>
<td>编码器参数</td> <td>嘤特尔GPU</td>
<td>{{ config.handbrake.encopt }}</td> <td :class="{warning: !config.ffmpeg.intel_gpu, success: config.ffmpeg.intel_gpu}"></td>
</tr>
<tr>
<td>视频CRF</td>
<td>{{ config.ffmpeg.crf }}</td>
</tr>
<tr>
<td>GOP</td>
<td>{{ config.ffmpeg.gop }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -264,7 +259,6 @@
basic: { basic: {
exec: { exec: {
ffmpeg: false, ffmpeg: false,
handbrake: false,
danmaku: false, danmaku: false,
}, },
system: { system: {
@ -330,12 +324,12 @@
}, },
ffmpeg: { ffmpeg: {
exec: "", exec: "",
}, hevc: false,
handbrake: { nvidia_gpu: false,
exec: "", intel_gpu: false,
preset_file: "", vaapi: false,
preset: "", crf: "",
encopt: "", gop: "",
}, },
recorder: { recorder: {
bili_dir: "", bili_dir: "",

View File

@ -1,7 +1,7 @@
import logging import logging
LOGGER = logging.getLogger("WORKFLOW") LOGGER = logging.getLogger("WORKFLOW")
_ch = logging.FileHandler("workflow.log", "w", encoding="UTF-8", delay=False) _ch = logging.StreamHandler()
_ch.setLevel(logging.DEBUG) _ch.setLevel(logging.DEBUG)
LOGGER.setLevel(logging.DEBUG) LOGGER.setLevel(logging.DEBUG)
LOGGER.addHandler(_ch) LOGGER.addHandler(_ch)

View File

@ -1,58 +1,143 @@
import threading import threading
from queue import SimpleQueue as Queue from datetime import datetime
from time import sleep
from model.Workflow import Workflow
from workflow.video import quick_split_video
from workflow.worker import do_workflow
from . import LOGGER from . import LOGGER
from .bilibiliupload import core, VideoPart
IS_LIVING = threading.Event() IS_LIVING = threading.Event()
IS_ENCODING = threading.Event() IS_ENCODING = threading.Event()
IS_UPLOADING = threading.Event() IS_UPLOADING = threading.Event()
ENCODING_QUEUE: "Queue[Workflow]" = Queue()
class Bilibili(threading.Thread): class Bilibili:
def __init__(self):
def __init__(self) -> None: self.access_token = ""
super().__init__() self.session_id = ""
self.user_id = ""
self.expires = 0
self.login_time = None
self.parts = [] self.parts = []
def run(self) -> None: def login(self):
while True: with open("access_token", "r") as f:
if ENCODING_QUEUE.empty(): self.access_token = f.read(64).strip()
sleep(5) self.session_id, self.user_id, self.expires = core.login_by_access_token(self.access_token)
if len(self.parts) > 0 and not IS_UPLOADING.is_set(): self.login_time = datetime.now()
self.do_upload() LOGGER.info("B站登录UID【{}】,过期时间【{}".format(self.user_id, self.expires))
else:
workflow_item = ENCODING_QUEUE.get()
LOGGER.info("收到工作流请求ID:【{}".format(workflow_item.id))
for video_clip in workflow_item.video_clips:
IS_ENCODING.set()
try:
LOGGER.info("工作流视频ID{}】,路径:【{}".format(video_clip.id, video_clip.full_path))
if len(video_clip.danmaku_clips) < 1:
_parts = quick_split_video(video_clip.full_path)
else:
_parts = do_workflow(video_clip.full_path, video_clip.danmaku_clips[0].full_path)
LOGGER.info("工作流视频压制完成:结果:【{}".format(_parts))
for _part in _parts:
self.parts.append(_part)
except:
LOGGER.error("压制异常工作流视频ID{}】,路径:【{}".format(video_clip.id, video_clip.full_path))
finally:
IS_ENCODING.clear()
def do_upload(self): def upload(self,
LOGGER.info("尝试投稿:内容【{}".format(self.parts)) 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.pre_upload(parts)
self.finish_upload(title, tid, tag, desc, source, cover, no_reprint)
self.clear() self.clear()
def pre_upload(self, parts: "VideoPart", 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>
"""
if not isinstance(parts, list):
parts = [parts]
IS_UPLOADING.set()
for part in parts:
if isinstance(part, str):
part = VideoPart(part)
LOGGER.info("Start Uploading >{}<".format(part.path))
status = core.upload_video_part(self.access_token, self.session_id, self.user_id, part, max_retry)
if status:
# 上传完毕
LOGGER.info("Upload >{}< Finished{}".format(part.path, part.server_file_name))
self.parts.append(part)
else:
LOGGER.warn("Upload >{}< Failed".format(part.path))
IS_UPLOADING.clear()
def finish_upload(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: 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
"""
if len(self.parts) == 0:
return
if IS_ENCODING.is_set():
LOGGER.info("[{}]仍在压制,取消发布".format(title))
return
if IS_LIVING.is_set():
LOGGER.info("[{}]仍在直播,取消发布".format(title))
return
if IS_UPLOADING.is_set():
LOGGER.info("[{}]仍在上传,取消发布".format(title))
return
LOGGER.info("[{}]投稿中,请稍后".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=tag, desc=desc, source=source, cover=cover, no_reprint=no_reprint)
LOGGER.info("[{}]投稿成功AVID【{}BVID【{}".format(title, avid, bvid))
self.clear()
except Exception as e:
LOGGER.error("[{}]投稿失败".format(title), exc_info=e)
def reloadFromPrevious(self):
...
def clear(self): def clear(self):
self.parts = [] self.parts = []
INSTANCE = Bilibili() INSTANCE = Bilibili()

View File

@ -0,0 +1,4 @@
修改自
[BilibiliUploader](https://github.com/FortuneDayssss/BilibiliUploader/)
LICENSEGPL

View File

@ -0,0 +1,4 @@
from .bilibiliuploader import BilibiliUploader
from .core import VideoPart
__version__ = '0.0.6'

View File

@ -0,0 +1,91 @@
from .core import login_by_access_token, upload, edit_videos
from .util import cipher
class BilibiliUploader():
def __init__(self):
self.access_token = None
self.refresh_token = None
self.sid = None
self.mid = None
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, _ = login_by_access_token(access_token)
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 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)
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
)

View File

@ -0,0 +1,641 @@
import requests
from datetime import datetime
from .util import cipher as cipher
import os
import math
import hashlib
from .util.retry import Retry
from concurrent.futures import ThreadPoolExecutor, as_completed
# From PC ugc_assisstant
# APPKEY = 'aae92bc66f3edfab'
# APPSECRET = 'af125a0d5279fd576c1b4418a3e8276d'
APPKEY = '1d8b6e7d45233436'
APPSECRET = '560c52ccd288fed045859ed18bffd973'
LOGIN_APPKEY = '783bbb7264451d82'
# upload chunk size = 2MB
CHUNK_SIZE = 2 * 1024 * 1024
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 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("filename: {}".format(local_file_name), "chunk{}/{}".format(chunk_id, chunk_total_num))
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
},
)
r.raise_for_status()
if r.status_code == 200 and r.json().get("OK", 0) == 1:
return True
else:
print(r.status_code)
print(r.content)
return False
def upload_video_part(access_token, sid, mid, video_part: VideoPart, max_retry=5):
"""
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.
Returns:
status: success or fail.
server_file_name: server file name by pre_upload api.
"""
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)
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"]

View File

@ -0,0 +1 @@
from .cipher import *

View 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

View File

@ -0,0 +1,21 @@
from time import sleep
class Retry:
def __init__(self, max_retry, success_return_value, sleep_sec = 60):
self.max_retry = max_retry
self.success_return_value = success_return_value
self.sleep_sec = 60
def run(self, func, *args, **kwargs):
status = False
for i in range(0, self.max_retry):
try:
return_value = func(*args, **kwargs)
except Exception:
sleep(self.sleep_sec)
continue
if return_value == self.success_return_value:
status = True
break
return status

View File

@ -1,12 +1,11 @@
import json
import os import os
import subprocess import subprocess
import warnings
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import IO from typing import IO
from config import VIDEO_CLIP_EACH_SEC, VIDEO_CLIP_OVERFLOW_SEC, VIDEO_OUTPUT_DIR, \ from config import FFMPEG_EXEC, FFMPEG_USE_HEVC, VIDEO_CRF, FFMPEG_USE_NVIDIA_GPU, VIDEO_CLIP_EACH_SEC, \
FFMPEG_EXEC, HANDBRAKE_EXEC, HANDBRAKE_PRESET_FILE, HANDBRAKE_PRESET, HANDBRAKE_ENCOPT VIDEO_CLIP_OVERFLOW_SEC, \
FFMPEG_USE_INTEL_GPU, VIDEO_OUTPUT_DIR, VIDEO_GOP, FFMPEG_USE_VAAPI
from . import LOGGER from . import LOGGER
@ -26,65 +25,143 @@ def get_video_real_duration(filename):
def encode_video_with_subtitles(orig_filename: str, subtitles: list[str], base_ts: float): def encode_video_with_subtitles(orig_filename: str, subtitles: list[str], base_ts: float):
create_dt = datetime.fromtimestamp(base_ts) new_filename = base_ts_to_filename(base_ts, False)
current_dt = (create_dt).strftime("%Y%m%d_%H%M_") new_fullpath = os.path.join(VIDEO_OUTPUT_DIR, new_filename)
process = get_encode_process_use_handbrake(orig_filename, if FFMPEG_USE_HEVC:
subtitles, if FFMPEG_USE_NVIDIA_GPU:
os.path.join(VIDEO_OUTPUT_DIR, "{}.mp4".format(current_dt))) process = get_encode_hevc_process_use_nvenc(orig_filename, subtitles, new_fullpath)
handle_handbrake_output(process.stdout) elif FFMPEG_USE_VAAPI:
process = get_encode_hevc_process_use_vaapi(orig_filename, subtitles, new_fullpath)
elif FFMPEG_USE_INTEL_GPU:
process = get_encode_hevc_process_use_intel(orig_filename, subtitles, new_fullpath)
else:
process = get_encode_hevc_process_use_cpu(orig_filename, subtitles, new_fullpath)
else:
if FFMPEG_USE_NVIDIA_GPU:
process = get_encode_process_use_nvenc(orig_filename, subtitles, new_fullpath)
elif FFMPEG_USE_VAAPI:
process = get_encode_process_use_vaapi(orig_filename, subtitles, new_fullpath)
elif FFMPEG_USE_INTEL_GPU:
process = get_encode_process_use_intel(orig_filename, subtitles, new_fullpath)
else:
process = get_encode_process_use_cpu(orig_filename, subtitles, new_fullpath)
handle_ffmpeg_output(process.stdout)
process.wait() process.wait()
return [{ return [new_fullpath]
"base_path": VIDEO_OUTPUT_DIR,
"file": "{}.mp4".format(current_dt),
}]
def get_encode_process_use_handbrake(orig_filename: str, subtitles: list[str], new_filename: str): def get_encode_process_use_nvenc(orig_filename: str, subtitles: list[str], new_filename: str):
print("[+]Use HandBrakeCli") print("[+]Use Nvidia NvEnc Acceleration")
encode_process = subprocess.Popen([ encode_process = subprocess.Popen([
HANDBRAKE_EXEC, *_common_handbrake_setting(), FFMPEG_EXEC, *_common_ffmpeg_setting(),
"--preset-import-file", HANDBRAKE_PRESET_FILE, "--preset", HANDBRAKE_PRESET, "-i", orig_filename, "-vf",
"-i", orig_filename, "-x", HANDBRAKE_ENCOPT, ",".join("subtitles=%s" % i for i in subtitles) + ",hwupload_cuda",
"--ssa-file", ",".join(i for i in subtitles), "-c:v", "h264_nvenc", "-preset:v", "p7",
"--ssa-burn", ",".join("%d" % (i+1) for i in range(len(subtitles))), *_common_ffmpeg_params(),
"-o", # "-t", "10",
new_filename new_filename
], stdout=subprocess.PIPE) ], stdout=subprocess.PIPE)
return encode_process return encode_process
def get_encode_process_use_intel(orig_filename: str, subtitles: list[str], new_filename: str):
print("[+]Use Intel QSV Acceleration")
encode_process = subprocess.Popen([
FFMPEG_EXEC, *_common_ffmpeg_setting(),
"-hwaccel", "qsv", "-i", orig_filename, "-vf",
",".join("subtitles=%s" % i for i in subtitles),
"-c:v", "h264_qsv",
*_common_ffmpeg_params(),
# "-t", "10",
new_filename
], stdout=subprocess.PIPE)
return encode_process
def handle_handbrake_output(stdout: IO[bytes]):
out_time = "0:0:0.0" def get_encode_process_use_vaapi(orig_filename: str, subtitles: list[str], new_filename: str):
speed = "0" print("[+]Use VAAPI Acceleration")
if stdout is None: encode_process = subprocess.Popen([
print("[!]STDOUT is null") FFMPEG_EXEC, *_common_ffmpeg_setting(),
return "-hwaccel", "vaapi", "-hwaccel_output_format", "vaapi", "-i", orig_filename, "-vf",
json_body = "" "hwmap=mode=read+write+direct,format=nv12," +
json_start = False "".join("subtitles=%s," % i for i in subtitles) + "hwmap",
_i = 0 "-c:v", "h264_vaapi",
while True: *_common_ffmpeg_params(),
line = stdout.readline() # "-t", "10",
if line == b"": new_filename
break ], stdout=subprocess.PIPE)
if json_start: return encode_process
json_body += line.strip().decode("UTF-8")
if line.startswith(b"}"):
json_start = False def get_encode_process_use_cpu(orig_filename: str, subtitles: list[str], new_filename: str):
status_payload = json.loads(json_body) print("[+]Use CPU Encode")
if status_payload["State"] == "WORKING": encode_process = subprocess.Popen([
out_time = "ETA: {Hours:02d}:{Minutes:02d}:{Seconds:02d}".format_map(status_payload["Working"]) FFMPEG_EXEC, *_common_ffmpeg_setting(),
speed = "{Rate:.2f}FPS".format_map(status_payload["Working"]) "-i", orig_filename, "-vf",
_i += 1 ",".join("subtitles=%s" % i for i in subtitles),
if _i % 300 == 150: "-c:v", "h264",
LOGGER.debug("[>]Speed:{}@{}".format(out_time, speed)) *_common_ffmpeg_params(),
elif status_payload["State"] == "WORKDONE": # "-t", "10",
break new_filename
continue ], stdout=subprocess.PIPE)
if line.startswith(b"Progress:"): return encode_process
json_start = True
json_body = "{"
LOGGER.debug("[ ]Speed:{}@{}".format(out_time, speed)) def get_encode_hevc_process_use_nvenc(orig_filename: str, subtitles: list[str], new_filename: str):
print("[+]Use Nvidia NvEnc Acceleration")
encode_process = subprocess.Popen([
FFMPEG_EXEC, *_common_ffmpeg_setting(),
"-i", orig_filename, "-vf",
"".join("subtitles=%s," % i for i in subtitles) + "hwupload_cuda",
"-c:v", "hevc_nvenc", "-preset:v", "p7",
*_common_ffmpeg_params(),
# "-t", "10",
new_filename
], stdout=subprocess.PIPE)
return encode_process
def get_encode_hevc_process_use_vaapi(orig_filename: str, subtitles: list[str], new_filename: str):
print("[+]Use VAAPI Acceleration")
encode_process = subprocess.Popen([
FFMPEG_EXEC, *_common_ffmpeg_setting(),
"-hwaccel", "vaapi", "-hwaccel_output_format", "vaapi", "-i", orig_filename, "-vf",
"hwmap=mode=read+write+direct,format=nv12," +
"".join("subtitles=%s," % i for i in subtitles) + "hwmap",
"-c:v", "hevc_vaapi",
*_common_ffmpeg_params(),
# "-t", "10",
new_filename
], stdout=subprocess.PIPE)
return encode_process
def get_encode_hevc_process_use_intel(orig_filename: str, subtitles: list[str], new_filename: str):
print("[+]Use Intel QSV Acceleration")
encode_process = subprocess.Popen([
FFMPEG_EXEC, *_common_ffmpeg_setting(),
"-hwaccel", "qsv", "-i", orig_filename, "-vf",
",".join("subtitles=%s" % i for i in subtitles),
"-c:v", "hevc_qsv",
*_common_ffmpeg_params(),
# "-t", "10",
new_filename
], stdout=subprocess.PIPE)
return encode_process
def get_encode_hevc_process_use_cpu(orig_filename: str, subtitles: list[str], new_filename: str):
print("[+]Use CPU Encode")
encode_process = subprocess.Popen([
FFMPEG_EXEC, *_common_ffmpeg_setting(),
"-i", orig_filename, "-vf",
",".join("subtitles=%s" % i for i in subtitles),
"-c:v", "hevc",
*_common_ffmpeg_params(),
# "-t", "10",
new_filename
], stdout=subprocess.PIPE)
return encode_process
def handle_ffmpeg_output(stdout: IO[bytes]) -> str: def handle_ffmpeg_output(stdout: IO[bytes]) -> str:
@ -118,12 +195,11 @@ def duration_str_to_float(duration_str) -> float:
def quick_split_video(file): def quick_split_video(file):
warnings.warn("已过时", DeprecationWarning)
if not os.path.isfile(file): if not os.path.isfile(file):
raise FileNotFoundError(file) raise FileNotFoundError(file)
file_name = os.path.split(file)[-1] file_name = os.path.split(file)[-1]
_create_dt = os.path.splitext(file_name)[0] _create_dt = os.path.splitext(file_name)[0]
create_dt = datetime.strptime(_create_dt[:13], "%Y%m%d_%H%M") create_dt = datetime.strptime(_create_dt, "%Y%m%d_%H%M")
_duration_str = get_video_real_duration(file) _duration_str = get_video_real_duration(file)
duration = duration_str_to_float(_duration_str) duration = duration_str_to_float(_duration_str)
current_sec = 0 current_sec = 0
@ -159,9 +235,10 @@ def _common_ffmpeg_setting():
) )
def _common_handbrake_setting(): def _common_ffmpeg_params():
return ( return (
"--json", "-vsync", "1", "-async", "1", "-avoid_negative_ts", "1",
"--crop-mode", "none", "--no-comb-detect", "--no-bwdif", "--no-decomb", "--no-detelecine", "--no-hqdn3d", "-f", "mp4", "-c:a", "aac",
"--no-nlmeans", "--no-chroma-smooth", "--no-unsharp", "--no-lapsharp", "--no-deblock", "--align-av" "-crf", str(VIDEO_CRF), "-g:v", str(VIDEO_GOP),
"-fflags", "+genpts", "-shortest"
) )

View File

@ -13,7 +13,7 @@ def do_workflow(video_file, danmaku_base_file, *danmaku_files):
start_ts = get_file_start(danmaku_base_file) start_ts = get_file_start(danmaku_base_file)
except DanmakuException: except DanmakuException:
print("基准弹幕文件异常,跳过") print("基准弹幕文件异常,跳过")
return [] return
result.append(danmaku_to_subtitle(danmaku_base_file, 0)) result.append(danmaku_to_subtitle(danmaku_base_file, 0))
for danmaku_file in danmaku_files: for danmaku_file in danmaku_files:
try: try:
@ -26,9 +26,15 @@ def do_workflow(video_file, danmaku_base_file, *danmaku_files):
print("弹幕文件", danmaku_file, "异常") print("弹幕文件", danmaku_file, "异常")
continue continue
print(result) print(result)
_video_parts = encode_video_with_subtitles(video_file, result, start_ts) file_need_split = encode_video_with_subtitles(video_file, result, start_ts)
_video_parts = []
for file in file_need_split:
_video_parts += quick_split_video(file)
# clean files # clean files
for file in result: for file in result:
if os.path.isfile(file): if os.path.isfile(file):
os.remove(file) os.remove(file)
for file in file_need_split:
if os.path.isfile(file):
os.remove(file)
return _video_parts return _video_parts