You've already forked my-video-workflow
							
							初次提交
This commit is contained in:
		
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
.idea
 | 
			
		||||
*.flv
 | 
			
		||||
*.xml
 | 
			
		||||
*.db
 | 
			
		||||
*.ini
 | 
			
		||||
							
								
								
									
										30
									
								
								app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
from flask import Flask
 | 
			
		||||
from config import load_config, WEB_HOST, WEB_PORT
 | 
			
		||||
from controller.view.main_blueprint import blueprint as view_main_blueprint
 | 
			
		||||
from controller.api.config_blueprint import blueprint as api_config_blueprint
 | 
			
		||||
from controller.api.collector_blueprint import blueprint as api_collector_blueprint
 | 
			
		||||
from controller.api.bilirecorder_blueprint import blueprint as api_bilirecorder_blueprint
 | 
			
		||||
from controller.api.workflow_blueprint import blueprint as api_workflow_blueprint
 | 
			
		||||
from model import db
 | 
			
		||||
 | 
			
		||||
app = Flask(__name__)
 | 
			
		||||
app.config['JSON_AS_ASCII'] = False
 | 
			
		||||
# CORS(app, supports_credentials=True)
 | 
			
		||||
app.debug = True
 | 
			
		||||
load_config()
 | 
			
		||||
app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///database.db?check_same_thread=False"
 | 
			
		||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
 | 
			
		||||
app.config['SQLALCHEMY_ECHO'] = True
 | 
			
		||||
db.init_app(app)
 | 
			
		||||
 | 
			
		||||
app.register_blueprint(view_main_blueprint)
 | 
			
		||||
app.register_blueprint(api_config_blueprint)
 | 
			
		||||
app.register_blueprint(api_collector_blueprint)
 | 
			
		||||
app.register_blueprint(api_bilirecorder_blueprint)
 | 
			
		||||
app.register_blueprint(api_workflow_blueprint)
 | 
			
		||||
with app.app_context():
 | 
			
		||||
    # db.drop_all(app=app)
 | 
			
		||||
    db.create_all(app=app)
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    app.run(WEB_HOST, WEB_PORT)
 | 
			
		||||
							
								
								
									
										105
									
								
								config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								config.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,105 @@
 | 
			
		||||
import configparser
 | 
			
		||||
import os.path
 | 
			
		||||
 | 
			
		||||
# [danmaku]
 | 
			
		||||
# exec
 | 
			
		||||
DANMAKU_FACTORY_EXEC = "DanmakuFactory"
 | 
			
		||||
# speed
 | 
			
		||||
DANMAKU_SPEED = 12
 | 
			
		||||
# font
 | 
			
		||||
DEFAULT_FONT_NAME = "Sarasa Term SC"
 | 
			
		||||
# resolution
 | 
			
		||||
VIDEO_RESOLUTION = "1280x720"
 | 
			
		||||
# [ffmpeg]
 | 
			
		||||
# exec
 | 
			
		||||
FFMPEG_EXEC = "ffmpeg"
 | 
			
		||||
# gpu
 | 
			
		||||
FFMPEG_USE_GPU = False
 | 
			
		||||
# bitrate
 | 
			
		||||
VIDEO_BITRATE = "2.5M"
 | 
			
		||||
# [video]
 | 
			
		||||
# title
 | 
			
		||||
VIDEO_TITLE = "【永恒de草薙直播录播】直播于 {}"
 | 
			
		||||
# [recorder]
 | 
			
		||||
# bili_dir
 | 
			
		||||
BILILIVE_RECORDER_DIRECTORY = "./"
 | 
			
		||||
# xigua_dir
 | 
			
		||||
XIGUALIVE_RECORDER_DIRECTORY = "./"
 | 
			
		||||
# [web]
 | 
			
		||||
# host
 | 
			
		||||
WEB_HOST = "0.0.0.0"
 | 
			
		||||
# port
 | 
			
		||||
WEB_PORT = 5000
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def load_config():
 | 
			
		||||
    if not os.path.exists("config.ini"):
 | 
			
		||||
        write_config()
 | 
			
		||||
        return False
 | 
			
		||||
    config = configparser.ConfigParser()
 | 
			
		||||
    if config.has_section("danmaku"):
 | 
			
		||||
        section = config['danmaku']
 | 
			
		||||
        global DANMAKU_FACTORY_EXEC, DANMAKU_SPEED, DEFAULT_FONT_NAME, VIDEO_RESOLUTION
 | 
			
		||||
        DANMAKU_FACTORY_EXEC = section.get('exec', DANMAKU_FACTORY_EXEC)
 | 
			
		||||
        DANMAKU_SPEED = section.getfloat('speed', DANMAKU_SPEED)
 | 
			
		||||
        DEFAULT_FONT_NAME = section.get('font', DEFAULT_FONT_NAME)
 | 
			
		||||
        VIDEO_RESOLUTION = section.get('resolution', VIDEO_RESOLUTION)
 | 
			
		||||
    if config.has_section("video"):
 | 
			
		||||
        section = config['video']
 | 
			
		||||
        global VIDEO_TITLE
 | 
			
		||||
        VIDEO_TITLE = section.get('title', VIDEO_TITLE)
 | 
			
		||||
    if config.has_section("ffmpeg"):
 | 
			
		||||
        section = config['ffmpeg']
 | 
			
		||||
        global FFMPEG_EXEC, FFMPEG_USE_GPU, VIDEO_BITRATE
 | 
			
		||||
        FFMPEG_EXEC = section.get('exec', FFMPEG_EXEC)
 | 
			
		||||
        FFMPEG_USE_GPU = section.getboolean('gpu', FFMPEG_USE_GPU)
 | 
			
		||||
        VIDEO_BITRATE = section.get('bitrate', VIDEO_BITRATE)
 | 
			
		||||
    if config.has_section("recorder"):
 | 
			
		||||
        global BILILIVE_RECORDER_DIRECTORY, XIGUALIVE_RECORDER_DIRECTORY
 | 
			
		||||
        section = config['recorder']
 | 
			
		||||
        BILILIVE_RECORDER_DIRECTORY = section.get('bili_dir', BILILIVE_RECORDER_DIRECTORY)
 | 
			
		||||
        XIGUALIVE_RECORDER_DIRECTORY = section.get('xigua_dir', XIGUALIVE_RECORDER_DIRECTORY)
 | 
			
		||||
    if config.has_section("web"):
 | 
			
		||||
        global WEB_HOST, WEB_PORT
 | 
			
		||||
        section = config['web']
 | 
			
		||||
        WEB_HOST = section.get('host', WEB_HOST)
 | 
			
		||||
        WEB_PORT = section.getint('port', WEB_PORT)
 | 
			
		||||
    return True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_config():
 | 
			
		||||
    config = {
 | 
			
		||||
        'danmaku': {
 | 
			
		||||
            'exec': DANMAKU_FACTORY_EXEC,
 | 
			
		||||
            'speed': DANMAKU_SPEED,
 | 
			
		||||
            'font': DEFAULT_FONT_NAME,
 | 
			
		||||
            'resolution': VIDEO_RESOLUTION,
 | 
			
		||||
        },
 | 
			
		||||
        'video': {
 | 
			
		||||
            'title': VIDEO_TITLE,
 | 
			
		||||
        },
 | 
			
		||||
        'ffmpeg': {
 | 
			
		||||
            'exec': FFMPEG_EXEC,
 | 
			
		||||
            'gpu': FFMPEG_USE_GPU,
 | 
			
		||||
            'bitrate': VIDEO_BITRATE,
 | 
			
		||||
        },
 | 
			
		||||
        'recorder': {
 | 
			
		||||
            'bili_dir': BILILIVE_RECORDER_DIRECTORY,
 | 
			
		||||
            'xigua_dir': XIGUALIVE_RECORDER_DIRECTORY,
 | 
			
		||||
        },
 | 
			
		||||
        'web': {
 | 
			
		||||
            'host': WEB_HOST,
 | 
			
		||||
            'port': WEB_PORT,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def write_config():
 | 
			
		||||
    config = configparser.ConfigParser()
 | 
			
		||||
    _config = get_config()
 | 
			
		||||
    for _i in _config:
 | 
			
		||||
        config[_i] = _config[_i]
 | 
			
		||||
    with open("config.ini", "w", encoding="utf-8") as f:
 | 
			
		||||
        config.write(f)
 | 
			
		||||
    return True
 | 
			
		||||
							
								
								
									
										134
									
								
								controller/api/bilirecorder_blueprint.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								controller/api/bilirecorder_blueprint.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
			
		||||
import os.path
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from glob import glob
 | 
			
		||||
from flask import Blueprint, jsonify, request, current_app
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from config import BILILIVE_RECORDER_DIRECTORY, VIDEO_TITLE
 | 
			
		||||
from model import db
 | 
			
		||||
from model.DanmakuClip import DanmakuClip
 | 
			
		||||
from model.VideoClip import VideoClip
 | 
			
		||||
from model.Workflow import Workflow
 | 
			
		||||
 | 
			
		||||
blueprint = Blueprint("api_bilirecorder", __name__, url_prefix="/api/bilirecorder")
 | 
			
		||||
 | 
			
		||||
bili_record_workflow_item: Optional[Workflow] = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def clear_item():
 | 
			
		||||
    global bili_record_workflow_item
 | 
			
		||||
    bili_record_workflow_item = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def commit_item():
 | 
			
		||||
    global bili_record_workflow_item
 | 
			
		||||
    if bili_record_workflow_item is None:
 | 
			
		||||
        return
 | 
			
		||||
    bili_record_workflow_item.calculate_start_time()
 | 
			
		||||
    db.session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def safe_create_item():
 | 
			
		||||
    global bili_record_workflow_item
 | 
			
		||||
    if bili_record_workflow_item is None:
 | 
			
		||||
        bili_record_workflow_item = Workflow.query.filter(
 | 
			
		||||
            Workflow.update_time > (datetime.now() - timedelta(hours=8)),
 | 
			
		||||
            Workflow.automatic == 1
 | 
			
		||||
        ).first()
 | 
			
		||||
        if bili_record_workflow_item is None:
 | 
			
		||||
            bili_record_workflow_item = Workflow()
 | 
			
		||||
    else:
 | 
			
		||||
        if bili_record_workflow_item is not None and bili_record_workflow_item.id is not None:
 | 
			
		||||
            bili_record_workflow_item.editing = False
 | 
			
		||||
            commit_item()
 | 
			
		||||
            bili_record_workflow_item = Workflow()
 | 
			
		||||
    if bili_record_workflow_item is None:
 | 
			
		||||
        bili_record_workflow_item = Workflow()
 | 
			
		||||
    bili_record_workflow_item.name = VIDEO_TITLE.format(datetime.now().strftime("%Y%m%d"))
 | 
			
		||||
    bili_record_workflow_item.automatic = True
 | 
			
		||||
    bili_record_workflow_item.editing = True
 | 
			
		||||
    db.session.commit()
 | 
			
		||||
    return bili_record_workflow_item
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def safe_get_item() -> Workflow:
 | 
			
		||||
    global bili_record_workflow_item
 | 
			
		||||
    if bili_record_workflow_item is None:
 | 
			
		||||
        return safe_create_item()
 | 
			
		||||
    return bili_record_workflow_item
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def collect_danmaku_files(workflow: Optional[Workflow]):
 | 
			
		||||
    if workflow is None:
 | 
			
		||||
        return
 | 
			
		||||
    clip: VideoClip
 | 
			
		||||
    for clip in workflow.video_clips:
 | 
			
		||||
        full_path = clip.full_path
 | 
			
		||||
        pre_file_name = os.path.splitext(full_path)[0]
 | 
			
		||||
        # 理论上也只有一个结果
 | 
			
		||||
        for danmaku_file in glob("{}*xml".format(pre_file_name)):
 | 
			
		||||
            if os.path.exists(danmaku_file):
 | 
			
		||||
                relpath = os.path.relpath(danmaku_file, BILILIVE_RECORDER_DIRECTORY)
 | 
			
		||||
                # 确认是否已经添加
 | 
			
		||||
                already_add = False
 | 
			
		||||
                for danmaku_clip in workflow.danmaku_clips:
 | 
			
		||||
                    if danmaku_clip.file == relpath and danmaku_clip.base_path == BILILIVE_RECORDER_DIRECTORY:
 | 
			
		||||
                        already_add = True
 | 
			
		||||
                        break
 | 
			
		||||
                if not already_add:
 | 
			
		||||
                    danmaku = DanmakuClip()
 | 
			
		||||
                    danmaku.file = relpath
 | 
			
		||||
                    danmaku.base_path = BILILIVE_RECORDER_DIRECTORY
 | 
			
		||||
                    danmaku.offset = 0
 | 
			
		||||
                    danmaku.workflow = workflow
 | 
			
		||||
                    db.session.add(danmaku)
 | 
			
		||||
                    workflow.danmaku_clips.append(danmaku)
 | 
			
		||||
    commit_item()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@blueprint.post("/")
 | 
			
		||||
def bilirecorder_event():
 | 
			
		||||
    payload = request.json
 | 
			
		||||
    current_app.logger.debug(payload)
 | 
			
		||||
    if 'EventType' not in payload:
 | 
			
		||||
        response = jsonify({
 | 
			
		||||
            'error': "异常",
 | 
			
		||||
            'payload': payload,
 | 
			
		||||
        })
 | 
			
		||||
        response.status_code = 403
 | 
			
		||||
        return response
 | 
			
		||||
 | 
			
		||||
    if payload['EventType'] == "SessionStarted":
 | 
			
		||||
        # 录制开始
 | 
			
		||||
        safe_create_item()
 | 
			
		||||
    elif payload['EventType'] == "SessionEnded":
 | 
			
		||||
        # 录制结束
 | 
			
		||||
        item = safe_get_item()
 | 
			
		||||
        item.editing = False
 | 
			
		||||
        commit_item()
 | 
			
		||||
        clear_item()
 | 
			
		||||
        return jsonify(item.to_dict())
 | 
			
		||||
    elif payload['EventType'] == "FileClosed":
 | 
			
		||||
        # 文件关闭
 | 
			
		||||
        item = safe_get_item()
 | 
			
		||||
        event_data = payload.get("EventData", {})
 | 
			
		||||
        video_file = event_data.get("RelativePath", None)
 | 
			
		||||
        # 判断是否重复
 | 
			
		||||
        already_add = False
 | 
			
		||||
        for clip in item.video_clips:
 | 
			
		||||
            if video_file == clip.file and clip.base_path == BILILIVE_RECORDER_DIRECTORY:
 | 
			
		||||
                already_add = True
 | 
			
		||||
                break
 | 
			
		||||
        if not already_add:
 | 
			
		||||
            video_clip = VideoClip()
 | 
			
		||||
            video_clip.file = video_file
 | 
			
		||||
            video_clip.base_path = BILILIVE_RECORDER_DIRECTORY
 | 
			
		||||
            video_clip.duration = event_data.get("Duration", 0)
 | 
			
		||||
            item.video_clips.append(video_clip)
 | 
			
		||||
            commit_item()
 | 
			
		||||
        collect_danmaku_files(item)
 | 
			
		||||
        return jsonify(item.to_dict())
 | 
			
		||||
    commit_item()
 | 
			
		||||
    item = safe_get_item()
 | 
			
		||||
    return jsonify(item.to_dict())
 | 
			
		||||
							
								
								
									
										20
									
								
								controller/api/collector_blueprint.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								controller/api/collector_blueprint.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
import platform
 | 
			
		||||
 | 
			
		||||
from flask import Blueprint, jsonify
 | 
			
		||||
from util.system import check_exec
 | 
			
		||||
from config import DANMAKU_FACTORY_EXEC, FFMPEG_EXEC
 | 
			
		||||
 | 
			
		||||
blueprint = Blueprint("api_collector", __name__, url_prefix="/api/collector")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@blueprint.get("/")
 | 
			
		||||
def collect_basic_status():
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'exec': {
 | 
			
		||||
            'ffmpeg': check_exec(FFMPEG_EXEC),
 | 
			
		||||
            'danmaku': check_exec(DANMAKU_FACTORY_EXEC),
 | 
			
		||||
        },
 | 
			
		||||
        'system': {
 | 
			
		||||
            'os': platform.system(),
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
							
								
								
									
										29
									
								
								controller/api/config_blueprint.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								controller/api/config_blueprint.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
from flask import Blueprint, jsonify, request
 | 
			
		||||
 | 
			
		||||
from config import get_config, write_config, load_config
 | 
			
		||||
 | 
			
		||||
blueprint = Blueprint("api_config", __name__, url_prefix="/api/config")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@blueprint.get("/")
 | 
			
		||||
def get_global_config():
 | 
			
		||||
    return jsonify(get_config())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@blueprint.put("/")
 | 
			
		||||
def modify_global_config():
 | 
			
		||||
    return jsonify(request.json)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@blueprint.post("/write")
 | 
			
		||||
def write_global_config():
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        "result": write_config()
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@blueprint.post("/load")
 | 
			
		||||
def load_global_config():
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        "result": load_config()
 | 
			
		||||
    })
 | 
			
		||||
							
								
								
									
										32
									
								
								controller/api/workflow_blueprint.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								controller/api/workflow_blueprint.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
from flask import Blueprint, jsonify
 | 
			
		||||
 | 
			
		||||
from model.Workflow import Workflow
 | 
			
		||||
from model import db
 | 
			
		||||
 | 
			
		||||
blueprint = Blueprint("api_workflow", __name__, url_prefix="/api/workflow")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@blueprint.get("/")
 | 
			
		||||
def get_workflow_list():
 | 
			
		||||
    workflows = Workflow.query.all()
 | 
			
		||||
    return jsonify([d.to_dict() for d in workflows])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@blueprint.get("/<int:workflow_id>")
 | 
			
		||||
def get_workflow_info(workflow_id):
 | 
			
		||||
    workflow = Workflow.get(workflow_id)
 | 
			
		||||
    return jsonify(workflow)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@blueprint.put("/<int:workflow_id>/done")
 | 
			
		||||
def done_editing(workflow_id):
 | 
			
		||||
    workflow = Workflow.get(workflow_id)
 | 
			
		||||
    return jsonify(workflow.to_dict())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@blueprint.post("/<int:workflow_id>/queue")
 | 
			
		||||
def add_to_queue(workflow_id):
 | 
			
		||||
    # JOB_QUEUE.put(workflow_item)
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'id': workflow_id,
 | 
			
		||||
    })
 | 
			
		||||
							
								
								
									
										8
									
								
								controller/view/main_blueprint.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								controller/view/main_blueprint.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
from flask import Blueprint, render_template
 | 
			
		||||
 | 
			
		||||
blueprint = Blueprint("view_main", __name__, url_prefix="/")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@blueprint.route("/")
 | 
			
		||||
def main_page():
 | 
			
		||||
    return render_template("index.html")
 | 
			
		||||
							
								
								
									
										5
									
								
								entity/DanmakuFile.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								entity/DanmakuFile.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
from entity.File import File
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DanmakuFile(File):
 | 
			
		||||
    ...
 | 
			
		||||
							
								
								
									
										25
									
								
								entity/File.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								entity/File.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
import os
 | 
			
		||||
from os import PathLike
 | 
			
		||||
from typing import Union, Optional
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class File(object):
 | 
			
		||||
    base_path: Optional[Union[os.PathLike[str], str]]
 | 
			
		||||
    file: Union[PathLike[str], str]
 | 
			
		||||
 | 
			
		||||
    def __init__(self, file: Union[os.PathLike[str], str], base_path: Optional[Union[os.PathLike[str], str]] = None):
 | 
			
		||||
        self.file = file
 | 
			
		||||
        self.base_path = base_path
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def full_path(self) -> str:
 | 
			
		||||
        if self.base_path is None or len(self.base_path.strip()) == 0:
 | 
			
		||||
            return self.file
 | 
			
		||||
        else:
 | 
			
		||||
            return os.path.join(self.base_path, self.file)
 | 
			
		||||
 | 
			
		||||
    def abs_path(self) -> str:
 | 
			
		||||
        return os.path.abspath(self.full_path)
 | 
			
		||||
 | 
			
		||||
    def exist(self) -> bool:
 | 
			
		||||
        return os.path.exists(self.full_path)
 | 
			
		||||
							
								
								
									
										18
									
								
								entity/VideoClip.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								entity/VideoClip.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from entity.File import File
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VideoClip(File):
 | 
			
		||||
    duration: Optional[float]
 | 
			
		||||
 | 
			
		||||
    def __init__(self, file, base_path):
 | 
			
		||||
        super(VideoClip, self).__init__(file, base_path)
 | 
			
		||||
        self.duration = None
 | 
			
		||||
 | 
			
		||||
    def set_duration(self, duration: Optional[float]):
 | 
			
		||||
        self.duration = duration
 | 
			
		||||
 | 
			
		||||
    def evaluate_duration(self):
 | 
			
		||||
        if self.duration is None or self.duration < 0:
 | 
			
		||||
            ...
 | 
			
		||||
							
								
								
									
										5
									
								
								entity/VideoPart.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								entity/VideoPart.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
from entity.File import File
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VideoPart(File):
 | 
			
		||||
    ...
 | 
			
		||||
							
								
								
									
										118
									
								
								entity/WorkflowItem.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								entity/WorkflowItem.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,118 @@
 | 
			
		||||
import os
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from hashlib import md5
 | 
			
		||||
from typing import Union, Optional
 | 
			
		||||
 | 
			
		||||
from config import VIDEO_TITLE
 | 
			
		||||
from entity.DanmakuFile import DanmakuFile
 | 
			
		||||
from entity.VideoClip import VideoClip
 | 
			
		||||
from entity.VideoPart import VideoPart
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WorkflowItem(object):
 | 
			
		||||
    automatic: bool
 | 
			
		||||
    finished: bool
 | 
			
		||||
    conflict: bool
 | 
			
		||||
    editing: bool
 | 
			
		||||
    error: bool
 | 
			
		||||
    delay_time: int
 | 
			
		||||
    mode: int
 | 
			
		||||
    videoParts: list[VideoPart]
 | 
			
		||||
    videos: list[VideoClip]
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.automatic = False
 | 
			
		||||
        self.finished = False
 | 
			
		||||
        self.conflict = False
 | 
			
		||||
        self.editing = False
 | 
			
		||||
        self.error = False
 | 
			
		||||
        self.title = VIDEO_TITLE
 | 
			
		||||
        self.create_time = datetime.now()
 | 
			
		||||
        self.delay_time = 0
 | 
			
		||||
        self.mode = WorkflowModeEnum.MANUAL
 | 
			
		||||
        self.danmakus = []
 | 
			
		||||
        self.subtitleFiles = []
 | 
			
		||||
        self.videos = []
 | 
			
		||||
        self.videoParts = []
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def id(self):
 | 
			
		||||
        return md5(self.name.encode("utf-8")).hexdigest()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def name(self):
 | 
			
		||||
        return self.title.format(self.create_time.strftime("%Y%m%d"))
 | 
			
		||||
 | 
			
		||||
    def mode_set(self, mode: int):
 | 
			
		||||
        self.mode = mode
 | 
			
		||||
 | 
			
		||||
    def mode_add(self, mode: int):
 | 
			
		||||
        self.mode = self.mode | mode
 | 
			
		||||
 | 
			
		||||
    def mode_del(self, mode: int):
 | 
			
		||||
        self.mode = self.mode ^ mode
 | 
			
		||||
 | 
			
		||||
    def mode_has(self, mode: int) -> bool:
 | 
			
		||||
        return self.mode & mode == mode
 | 
			
		||||
 | 
			
		||||
    def mode_add_merge(self):
 | 
			
		||||
        return self.mode_add(WorkflowModeEnum.MERGE)
 | 
			
		||||
 | 
			
		||||
    def mode_del_merge(self):
 | 
			
		||||
        return self.mode_del(WorkflowModeEnum.MERGE)
 | 
			
		||||
 | 
			
		||||
    def mode_has_merge(self) -> bool:
 | 
			
		||||
        return self.mode_has(WorkflowModeEnum.MERGE)
 | 
			
		||||
 | 
			
		||||
    def mode_add_danmaku(self):
 | 
			
		||||
        return self.mode_add(WorkflowModeEnum.DANMAKU)
 | 
			
		||||
 | 
			
		||||
    def mode_del_danmaku(self):
 | 
			
		||||
        return self.mode_del(WorkflowModeEnum.DANMAKU)
 | 
			
		||||
 | 
			
		||||
    def mode_has_danmaku(self) -> bool:
 | 
			
		||||
        return self.mode_has(WorkflowModeEnum.DANMAKU)
 | 
			
		||||
 | 
			
		||||
    def mode_add_encode(self):
 | 
			
		||||
        return self.mode_add(WorkflowModeEnum.ENCODE)
 | 
			
		||||
 | 
			
		||||
    def mode_del_encode(self):
 | 
			
		||||
        return self.mode_del(WorkflowModeEnum.ENCODE)
 | 
			
		||||
 | 
			
		||||
    def mode_has_encode(self) -> bool:
 | 
			
		||||
        return self.mode_has(WorkflowModeEnum.ENCODE)
 | 
			
		||||
 | 
			
		||||
    def mode_add_split(self):
 | 
			
		||||
        return self.mode_add(WorkflowModeEnum.SPLIT)
 | 
			
		||||
 | 
			
		||||
    def mode_del_split(self):
 | 
			
		||||
        return self.mode_del(WorkflowModeEnum.SPLIT)
 | 
			
		||||
 | 
			
		||||
    def mode_has_split(self) -> bool:
 | 
			
		||||
        return self.mode_has(WorkflowModeEnum.SPLIT)
 | 
			
		||||
 | 
			
		||||
    def add_video(self, video_file: Union[VideoClip, os.PathLike[str], str], base_path: Optional[Union[os.PathLike[str], str]] = None) -> VideoClip:
 | 
			
		||||
        if isinstance(video_file, VideoClip):
 | 
			
		||||
            self.videos.append(video_file)
 | 
			
		||||
        else:
 | 
			
		||||
            video_file = VideoClip(video_file, base_path)
 | 
			
		||||
            self.videos.append(video_file)
 | 
			
		||||
        return video_file
 | 
			
		||||
 | 
			
		||||
    def add_danmaku(self, danmaku_file: Union[DanmakuFile, os.PathLike[str], str], base_path: Optional[Union[os.PathLike[str], str]] = None) -> DanmakuFile:
 | 
			
		||||
        if isinstance(danmaku_file, DanmakuFile):
 | 
			
		||||
            self.danmakus.append(danmaku_file)
 | 
			
		||||
        else:
 | 
			
		||||
            danmaku_file = DanmakuFile(danmaku_file, base_path)
 | 
			
		||||
            self.danmakus.append(danmaku_file)
 | 
			
		||||
        return danmaku_file
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WorkflowModeEnum:
 | 
			
		||||
    MERGE = 1 << 4
 | 
			
		||||
    DANMAKU = 1 << 3
 | 
			
		||||
    ENCODE = 1 << 2
 | 
			
		||||
    SPLIT = 1 << 1
 | 
			
		||||
    DANMAKU_DUAL = DANMAKU | ENCODE | SPLIT
 | 
			
		||||
    DANMAKU_SINGLE = DANMAKU | ENCODE | SPLIT
 | 
			
		||||
    MANUAL = 0
 | 
			
		||||
							
								
								
									
										6
									
								
								exception/danmaku.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								exception/danmaku.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
class NoDanmakuException(Exception):
 | 
			
		||||
    ...
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DanmakuFormatErrorException(Exception):
 | 
			
		||||
    ...
 | 
			
		||||
							
								
								
									
										20
									
								
								model/DanmakuClip.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								model/DanmakuClip.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
from . import db
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DanmakuClip(db.Model):
 | 
			
		||||
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
 | 
			
		||||
    base_path = db.Column(db.String(255))
 | 
			
		||||
    file = db.Column(db.String(255))
 | 
			
		||||
    subtitle_file = db.Column(db.String(255))
 | 
			
		||||
    offset = db.Column(db.Float, nullable=False, default=0)
 | 
			
		||||
    workflow_id = db.Column(db.Integer, db.ForeignKey('workflow.id'))
 | 
			
		||||
    workflow = db.relationship("Workflow", backref=db.backref("danmaku_clips", lazy="dynamic"))
 | 
			
		||||
 | 
			
		||||
    def to_json(self):
 | 
			
		||||
        return {
 | 
			
		||||
            "id": self.id,
 | 
			
		||||
            "base_path": self.base_path,
 | 
			
		||||
            "file": self.file,
 | 
			
		||||
            "subtitle_file": self.subtitle_file,
 | 
			
		||||
            "offset": self.offset,
 | 
			
		||||
        }
 | 
			
		||||
							
								
								
									
										25
									
								
								model/Posting.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								model/Posting.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
from model import db
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Posting(db.Model):
 | 
			
		||||
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
 | 
			
		||||
    bvid = db.Column(db.String(255))
 | 
			
		||||
    name = db.Column(db.String(255))
 | 
			
		||||
    description = db.Column(db.Text)
 | 
			
		||||
    state = db.Column(db.SmallInteger, nullable=False, default=0)
 | 
			
		||||
    create_time = db.Column(db.DateTime, nullable=False, default=datetime.now)
 | 
			
		||||
    update_time = db.Column(db.DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
 | 
			
		||||
 | 
			
		||||
    def to_json(self):
 | 
			
		||||
        return {
 | 
			
		||||
            "id": self.id,
 | 
			
		||||
            "bvid": self.bvid,
 | 
			
		||||
            "name": self.name,
 | 
			
		||||
            "description": self.description,
 | 
			
		||||
            "state": self.state,
 | 
			
		||||
            "create_time": self.create_time.strftime("%Y/%m/%d %H:%M:%S") if self.create_time else None,
 | 
			
		||||
            "update_time": self.update_time.strftime("%Y/%m/%d %H:%M:%S") if self.update_time else None,
 | 
			
		||||
            "video_parts": [i.to_json() for i in self.video_parts.all()]
 | 
			
		||||
        }
 | 
			
		||||
							
								
								
									
										29
									
								
								model/VideoClip.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								model/VideoClip.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
import os.path
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
 | 
			
		||||
from . import db
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from .Workflow import Workflow
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VideoClip(db.Model):
 | 
			
		||||
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
 | 
			
		||||
    base_path = db.Column(db.String(255))
 | 
			
		||||
    file = db.Column(db.String(255))
 | 
			
		||||
    duration = db.Column(db.Float, nullable=False, default=0)
 | 
			
		||||
    offset = db.Column(db.Float, nullable=False, default=0)
 | 
			
		||||
    workflow_id = db.Column(db.Integer, db.ForeignKey('workflow.id'))
 | 
			
		||||
    workflow: "Workflow" = db.relationship("Workflow", uselist=False, backref=db.backref("video_clips"))
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def full_path(self):
 | 
			
		||||
        return os.path.abspath(os.path.join(self.base_path, self.file))
 | 
			
		||||
 | 
			
		||||
    def to_json(self):
 | 
			
		||||
        return {
 | 
			
		||||
            "id": self.id,
 | 
			
		||||
            "base_path": self.base_path,
 | 
			
		||||
            "file": self.file,
 | 
			
		||||
            "duration": self.duration,
 | 
			
		||||
            "offset": self.offset,
 | 
			
		||||
        }
 | 
			
		||||
							
								
								
									
										29
									
								
								model/VideoPart.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								model/VideoPart.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
from . import db
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VideoPart(db.Model):
 | 
			
		||||
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
 | 
			
		||||
    base_path = db.Column(db.String(255), nullable=False)
 | 
			
		||||
    file = db.Column(db.String(255), nullable=False)
 | 
			
		||||
    remote_id = db.Column(db.String(255))
 | 
			
		||||
    name = db.Column(db.String(255))
 | 
			
		||||
    state = db.Column(db.SmallInteger, nullable=False, default=0)
 | 
			
		||||
 | 
			
		||||
    workflow_id = db.Column(db.Integer, db.ForeignKey('workflow.id'))
 | 
			
		||||
    workflow = db.relationship("Workflow", uselist=False, backref=db.backref("video_parts", lazy="dynamic"))
 | 
			
		||||
 | 
			
		||||
    posting_id = db.Column(db.Integer, db.ForeignKey('posting.id'))
 | 
			
		||||
    posting = db.relationship("Posting", backref=db.backref("video_parts", lazy="dynamic"), lazy=False)
 | 
			
		||||
 | 
			
		||||
    def to_json(self):
 | 
			
		||||
        return {
 | 
			
		||||
            "id": self.id,
 | 
			
		||||
            "base_path": self.base_path,
 | 
			
		||||
            "file": self.file,
 | 
			
		||||
            "duration": self.duration,
 | 
			
		||||
            "remote_id": self.remote_id,
 | 
			
		||||
            "name": self.name,
 | 
			
		||||
            "state": self.state,
 | 
			
		||||
        }
 | 
			
		||||
							
								
								
									
										46
									
								
								model/Workflow.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								model/Workflow.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
 | 
			
		||||
from . import db
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from .VideoClip import VideoClip
 | 
			
		||||
    from .VideoPart import VideoPart
 | 
			
		||||
    from .DanmakuClip import DanmakuClip
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Workflow(db.Model):
 | 
			
		||||
    video_clips: list["VideoClip"]
 | 
			
		||||
    danmaku_clips: list["DanmakuClip"]
 | 
			
		||||
    video_parts: list["VideoPart"]
 | 
			
		||||
 | 
			
		||||
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
 | 
			
		||||
    name = db.Column(db.String(255), nullable=False)
 | 
			
		||||
    state = db.Column(db.SmallInteger, nullable=False, default=0)
 | 
			
		||||
    """0未开始,1弹幕处理,2完成,3弹幕压制,4完成,5切割视频,6完成,9全部完成"""
 | 
			
		||||
    create_time = db.Column(db.DateTime, nullable=False, default=datetime.now)
 | 
			
		||||
    update_time = db.Column(db.DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
 | 
			
		||||
    automatic = db.Column(db.Boolean, default=False, nullable=False)
 | 
			
		||||
    editing = db.Column(db.Boolean, default=True, nullable=False)
 | 
			
		||||
    start_after_time = db.Column(db.DateTime, default=None)
 | 
			
		||||
 | 
			
		||||
    def calculate_start_time(self):
 | 
			
		||||
        if self.editing:
 | 
			
		||||
            self.start_after_time = None
 | 
			
		||||
        if not self.automatic:
 | 
			
		||||
            self.start_after_time = self.start_after_time
 | 
			
		||||
        else:
 | 
			
		||||
            self.start_after_time = datetime.now() + timedelta(minutes=30)
 | 
			
		||||
 | 
			
		||||
    def to_dict(self):
 | 
			
		||||
        return {
 | 
			
		||||
            "id": self.id,
 | 
			
		||||
            "name": self.name,
 | 
			
		||||
            "state": self.state,
 | 
			
		||||
            "create_time": self.create_time.strftime("%Y/%m/%d %H:%M:%S") if self.create_time else None,
 | 
			
		||||
            "update_time": self.update_time.strftime("%Y/%m/%d %H:%M:%S") if self.update_time else None,
 | 
			
		||||
            "automatic": self.automatic,
 | 
			
		||||
            "editing": self.editing,
 | 
			
		||||
            "start_after_time": self.start_after_time.strftime("%Y/%m/%d %H:%M:%S") if self.start_after_time else None,
 | 
			
		||||
            "video_clips": [i.to_json() for i in self.video_clips],
 | 
			
		||||
            "danmaku_clips": [i.to_json() for i in self.danmaku_clips],
 | 
			
		||||
        }
 | 
			
		||||
							
								
								
									
										3
									
								
								model/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								model/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
from flask_sqlalchemy import SQLAlchemy
 | 
			
		||||
 | 
			
		||||
db = SQLAlchemy()
 | 
			
		||||
							
								
								
									
										131
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,131 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="zh">
 | 
			
		||||
<head>
 | 
			
		||||
    <title>首页</title>
 | 
			
		||||
    {% include 'layout/head.html' %}
 | 
			
		||||
    <style>
 | 
			
		||||
        table {
 | 
			
		||||
            border: 1px solid black;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        table tr > td {
 | 
			
		||||
            padding: 0;
 | 
			
		||||
            margin: 0;
 | 
			
		||||
            border: 1px solid black;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        td.success {
 | 
			
		||||
            background: green;
 | 
			
		||||
            color: white;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        td.warning {
 | 
			
		||||
            background: orangered;
 | 
			
		||||
            color: white;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        #app[v-cloak] {
 | 
			
		||||
            display: none;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
{% raw %}
 | 
			
		||||
<div id="app" v-cloak>
 | 
			
		||||
    <h1>TEST</h1>
 | 
			
		||||
    <hr>
 | 
			
		||||
    <h2>当前状态</h2>
 | 
			
		||||
    <table class="current-status-table">
 | 
			
		||||
        <tbody>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <td>系统</td>
 | 
			
		||||
            <td>{{ collector.basic.system.os }}</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <td>FFMPEG状态</td>
 | 
			
		||||
            <td :class="collector.basic.exec.ffmpeg ? 'success' : 'warning'"></td>
 | 
			
		||||
        </tr>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <td>弹幕工具状态</td>
 | 
			
		||||
            <td :class="collector.basic.exec.danmaku ? 'success' : 'warning'"></td>
 | 
			
		||||
        </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
    <h2>配置状态</h2>
 | 
			
		||||
    <table class="current-config">
 | 
			
		||||
        <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <td colspan="2">FFMPEG</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <td>命令</td>
 | 
			
		||||
            <td>{{ config.ffmpeg.exec }}</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <td>GPU使用</td>
 | 
			
		||||
            <td :class="{warning: !config.ffmpeg.gpu, success: config.ffmpeg.gpu}"></td>
 | 
			
		||||
        </tr>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <td>视频比特率</td>
 | 
			
		||||
            <td>{{ config.ffmpeg.bitrate }}</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
</div>
 | 
			
		||||
{% endraw %}
 | 
			
		||||
</body>
 | 
			
		||||
<script>
 | 
			
		||||
    Vue.devtools = true
 | 
			
		||||
    Vue.createApp({
 | 
			
		||||
        data() {
 | 
			
		||||
            return {
 | 
			
		||||
                collector: {
 | 
			
		||||
                    basic: {
 | 
			
		||||
                        exec: {
 | 
			
		||||
                            ffmpeg: false,
 | 
			
		||||
                            danmaku: false,
 | 
			
		||||
                        },
 | 
			
		||||
                        system: {
 | 
			
		||||
                            os: "",
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                config: {
 | 
			
		||||
                    danmaku: {
 | 
			
		||||
                        exec: "",
 | 
			
		||||
                        speed: 0,
 | 
			
		||||
                        font: "",
 | 
			
		||||
                        resolution: "",
 | 
			
		||||
                    },
 | 
			
		||||
                    video: {
 | 
			
		||||
                        title: "",
 | 
			
		||||
                    },
 | 
			
		||||
                    ffmpeg: {
 | 
			
		||||
                        exec: "",
 | 
			
		||||
                        gpu: false,
 | 
			
		||||
                        bitrate: "",
 | 
			
		||||
                    },
 | 
			
		||||
                    web: {
 | 
			
		||||
                        host: "",
 | 
			
		||||
                        port: 0,
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        created() {
 | 
			
		||||
            axios({
 | 
			
		||||
                url: "/api/collector/"
 | 
			
		||||
            }).then((response) => {
 | 
			
		||||
                this.collector.basic = response.data;
 | 
			
		||||
            })
 | 
			
		||||
            axios({
 | 
			
		||||
                url: "/api/config/"
 | 
			
		||||
            }).then((response) => {
 | 
			
		||||
                this.config = response.data;
 | 
			
		||||
            })
 | 
			
		||||
        },
 | 
			
		||||
    }).mount("#app")
 | 
			
		||||
</script>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										0
									
								
								templates/layout/foot.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								templates/layout/foot.html
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										4
									
								
								templates/layout/head.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								templates/layout/head.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
<meta charset="UTF-8">
 | 
			
		||||
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/modern-normalize/1.1.0/modern-normalize.css" type="text/css" rel="stylesheet" />
 | 
			
		||||
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/3.2.31/vue.global.js" type="application/javascript"></script>
 | 
			
		||||
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-y/axios/0.26.0/axios.js" type="application/javascript"></script>
 | 
			
		||||
							
								
								
									
										0
									
								
								util/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								util/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										6
									
								
								util/file.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								util/file.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_file_exist(file):
 | 
			
		||||
    if not os.path.isfile(file):
 | 
			
		||||
        raise FileNotFoundError("文件不存在:%s" % file)
 | 
			
		||||
							
								
								
									
										29
									
								
								util/system.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								util/system.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
import os
 | 
			
		||||
import platform
 | 
			
		||||
import subprocess
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_exec(name: Union[os.PathLike[str], str]) -> bool:
 | 
			
		||||
    if is_windows():
 | 
			
		||||
        check_process = subprocess.Popen([
 | 
			
		||||
            "where.exe", name
 | 
			
		||||
        ], stdout=subprocess.PIPE)
 | 
			
		||||
        check_process.wait()
 | 
			
		||||
        return len(check_process.stdout.readlines()) > 0
 | 
			
		||||
    elif is_linux():
 | 
			
		||||
        check_process = subprocess.Popen([
 | 
			
		||||
            "which", name
 | 
			
		||||
        ])
 | 
			
		||||
        check_process.wait()
 | 
			
		||||
        return check_process.returncode == 0
 | 
			
		||||
    else:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_windows() -> bool:
 | 
			
		||||
    return platform.system().lower() == "windows"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_linux() -> bool:
 | 
			
		||||
    return platform.system().lower() == "linux"
 | 
			
		||||
							
								
								
									
										0
									
								
								workflow/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								workflow/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										61
									
								
								workflow/danmaku.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								workflow/danmaku.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
import datetime
 | 
			
		||||
import os
 | 
			
		||||
import argparse
 | 
			
		||||
import subprocess
 | 
			
		||||
from hashlib import md5
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
from bs4 import BeautifulSoup
 | 
			
		||||
 | 
			
		||||
from config import DANMAKU_FACTORY_EXEC, VIDEO_RESOLUTION, DANMAKU_SPEED, DEFAULT_FONT_NAME
 | 
			
		||||
from exception.danmaku import NoDanmakuException, DanmakuFormatErrorException
 | 
			
		||||
from util.file import check_file_exist
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_file_start(file: Union[os.PathLike[str], str]) -> float:
 | 
			
		||||
    with open(file, "r", encoding="utf-8") as f:
 | 
			
		||||
        soup = BeautifulSoup("".join(f.readlines()), "lxml")
 | 
			
		||||
        danmaku_item = soup.find("d")
 | 
			
		||||
        if danmaku_item is None:
 | 
			
		||||
            # 没有弹幕?
 | 
			
		||||
            raise NoDanmakuException()
 | 
			
		||||
        danmaku_info = danmaku_item["p"]
 | 
			
		||||
        split_info = danmaku_info.split(",")
 | 
			
		||||
        if len(split_info) < 5:
 | 
			
		||||
            raise DanmakuFormatErrorException()
 | 
			
		||||
        bias_sec = float(split_info[0])
 | 
			
		||||
        bias_ts_ms = int(split_info[4])
 | 
			
		||||
        return bias_ts_ms / 1000 - bias_sec
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def diff_danmaku_files(base_file: Union[os.PathLike[str], str], file: Union[os.PathLike[str], str]) -> float:
 | 
			
		||||
    return get_file_start(file) - get_file_start(base_file)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def danmaku_to_subtitle(file: Union[os.PathLike[str], str], time_shift: float):
 | 
			
		||||
    new_subtitle_name = md5(file).digest().hex() + ".ass"
 | 
			
		||||
    process = subprocess.Popen((
 | 
			
		||||
        DANMAKU_FACTORY_EXEC,
 | 
			
		||||
        "-r", str(VIDEO_RESOLUTION), "-s", str(DANMAKU_SPEED), "-f", "5",
 | 
			
		||||
        "-S", "40", "-N", str(DEFAULT_FONT_NAME), "--showmsgbox", "FALSE",
 | 
			
		||||
        "-O", "255", "-L", "1", "-D", "0",
 | 
			
		||||
        "-o", "ass", new_subtitle_name, "-i", file, "-t", str(time_shift)
 | 
			
		||||
    ))
 | 
			
		||||
    process.wait()
 | 
			
		||||
    return new_subtitle_name
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    parser = argparse.ArgumentParser()
 | 
			
		||||
    parser.add_argument("base", help="以此为标准")
 | 
			
		||||
    parser.add_argument("file", nargs="+", help="需要对齐的文件")
 | 
			
		||||
    args = parser.parse_args()
 | 
			
		||||
    check_file_exist(args.base)
 | 
			
		||||
    base_start_ts = get_file_start(args.base)
 | 
			
		||||
    base_start = datetime.datetime.fromtimestamp(base_start_ts)
 | 
			
		||||
    print("[+] 基准文件[{:s}],开始时间:{:.3f},时间:{}".format(args.base, base_start_ts, base_start))
 | 
			
		||||
    for _file in args.file:
 | 
			
		||||
        check_file_exist(_file)
 | 
			
		||||
        file_start_ts = get_file_start(_file)
 | 
			
		||||
        diff_sec = file_start_ts - base_start_ts
 | 
			
		||||
        print("[+] 待调整文件[{:s}],开始时间:{:.3f},偏差:{:.3f}".format(_file, file_start_ts, diff_sec))
 | 
			
		||||
							
								
								
									
										13
									
								
								workflow/queues.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								workflow/queues.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
from multiprocessing import Queue
 | 
			
		||||
 | 
			
		||||
from dto.DanmakuJobItem import DanmakuJobItem
 | 
			
		||||
from dto.EncodeJobItem import EncodeJobItem
 | 
			
		||||
from dto.SplitJobItem import SplitJobItem
 | 
			
		||||
from dto.TypeJobResult import TypeJobResult
 | 
			
		||||
from entity.WorkflowItem import WorkflowItem
 | 
			
		||||
 | 
			
		||||
JOB_EVENT_QUEUE: "Queue[TypeJobResult]" = Queue()
 | 
			
		||||
JOB_QUEUE: "Queue[WorkflowItem]" = Queue()
 | 
			
		||||
DANMAKU_PROCESSING_QUEUE: "Queue[DanmakuJobItem]" = Queue()
 | 
			
		||||
VIDEO_ENCODING_QUEUE: "Queue[EncodeJobItem]" = Queue()
 | 
			
		||||
VIDEO_SPLITING_QUEUE: "Queue[SplitJobItem]" = Queue()
 | 
			
		||||
							
								
								
									
										16
									
								
								workflow/video.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								workflow/video.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
import re
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_video_real_duration(filename):
 | 
			
		||||
    ffmpeg_process = subprocess.Popen([
 | 
			
		||||
        "ffmpeg", "-hide_banner", "-i", filename, "-c", "copy", "-f", "null", "-"
 | 
			
		||||
    ], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 | 
			
		||||
    result = "0:0:0.0"
 | 
			
		||||
    for line in ffmpeg_process.stderr.readlines():
 | 
			
		||||
        match_result = re.findall("(?<= time=).+?(?= )", line.decode())
 | 
			
		||||
        if len(match_result) == 0:
 | 
			
		||||
            continue
 | 
			
		||||
        result = match_result.pop()
 | 
			
		||||
    return result
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user