first commit

This commit is contained in:
2022-04-21 19:23:01 +09:00
commit cfd562fd13
152 changed files with 54937 additions and 0 deletions

3
plugin/ffmpeg/__init__.py Executable file
View File

@@ -0,0 +1,3 @@
from .plugin import blueprint, menu, plugin_load, plugin_unload, streaming_kill, get_video_info
from .logic import Status, Logic
from .interface_program_ffmpeg import Ffmpeg

View File

@@ -0,0 +1,446 @@
# -*- coding: utf-8 -*-
#########################################################
# python
import os
import traceback
import threading
import subprocess
import platform
import re
from datetime import datetime
from pytz import timezone
# third-party
import requests
#from flask import Blueprint, request, Response, send_file, render_template, redirect, jsonify
# sjva 공용
from framework.logger import get_logger
from framework.util import Util
from framework import app
# 패키지
from ffmpeg.logic import Logic, Status
from ffmpeg.model import ModelSetting
# 로그
package_name = __name__.split('.')[0]
logger = get_logger(package_name)
#########################################################
class Ffmpeg(object):
instance_list = []
idx = 1
# retry : 재시도 횟수
# max_error_packet_count : 이 숫자 초과시 중단
# where : 호출 모듈
def __init__(self, url, filename, plugin_id=None, listener=None, max_pf_count=None, call_plugin=None, temp_path=None, save_path=None, proxy=None, headers=None):
self.thread = None
self.url = url
self.filename = filename
self.plugin_id = plugin_id
self.listener = listener
self.max_pf_count = int(ModelSetting.query.filter_by(key='max_pf_count').first().value if max_pf_count is None else max_pf_count)
self.call_plugin = call_plugin
self.process = None
self.temp_path = ModelSetting.query.filter_by(key='temp_path').first().value if temp_path is None else temp_path
self.save_path = ModelSetting.query.filter_by(key='save_path').first().value if save_path is None else save_path
self.proxy = proxy
self.temp_fullpath = os.path.join(self.temp_path, filename)
self.save_fullpath = os.path.join(self.save_path, filename)
self.log_thread = None
self.status = Status.READY
self.duration = 0
self.duration_str = ''
self.current_duration = 0
self.percent = 0
#self.log = []
self.current_pf_count = 0
self.idx = str(Ffmpeg.idx)
Ffmpeg.idx += 1
self.current_bitrate = ''
self.current_speed = ''
self.start_time = None
self.end_time = None
self.download_time = None
self.start_event = threading.Event()
self.exist = False
self.filesize = 0
self.filesize_str = ''
self.download_speed = ''
self.headers = headers
Ffmpeg.instance_list.append(self)
#logger.debug('Ffmpeg.instance_list LEN:%s', len(Ffmpeg.instance_list))
if len(Ffmpeg.instance_list) > 30:
for f in Ffmpeg.instance_list:
if f.thread is None and f.status != Status.READY:
Ffmpeg.instance_list.remove(f)
break
else:
logger.debug('remove fail %s %s', f.thread, self.status)
def start(self):
self.thread = threading.Thread(target=self.thread_fuction, args=())
self.thread.start()
#self.start_event.wait(timeout=10)
self.start_time = datetime.now()
return self.get_data()
def start_and_wait(self):
self.start()
self.thread.join(timeout=60*70)
def stop(self):
try:
self.status = Status.USER_STOP
self.kill()
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
def kill(self):
try:
if self.process is not None and self.process.poll() is None:
import psutil
process = psutil.Process(self.process.pid)
for proc in process.children(recursive=True):
proc.kill()
process.kill()
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
def thread_fuction(self):
try:
import system
user = str(system.ModelSetting.get('sjva_me_user_id'))
try:
from framework.common.util import AESCipher
user = AESCipher.encrypt(user)
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
if self.proxy is None:
if self.headers is None:
command = [ModelSetting.get('ffmpeg_path'), '-y', '-i', self.url, '-c', 'copy', '-bsf:a', 'aac_adtstoasc', '-metadata', 'network=%s' % user]
else:
headers_command = []
for key, value in self.headers.items():
if key.lower() == 'user-agent':
headers_command.append('-user_agent')
headers_command.append(value)
pass
else:
headers_command.append('-headers')
#headers_command.append('\'%s: "%s"\''%(key,value))
if platform.system() == 'Windows':
headers_command.append('\'%s:%s\''%(key,value))
else:
headers_command.append(f'{key}:{value}')
command = [ModelSetting.get('ffmpeg_path'), '-y'] + headers_command + ['-i', self.url, '-c', 'copy', '-bsf:a', 'aac_adtstoasc', '-metadata', 'network=%s' % user]
else:
command = [ModelSetting.get('ffmpeg_path'), '-y', '-http_proxy', self.proxy, '-i', self.url, '-c', 'copy', '-bsf:a', 'aac_adtstoasc', '-metadata', 'network=%s' % user]
#command = [ModelSetting.get('ffmpeg_path'), '-y', '-i', self.url, '-c', 'copy', '-bsf:a', 'aac_adtstoasc']
if platform.system() == 'Windows':
#if self.call_plugin.startswith('pooq'):
now = str(datetime.now()).replace(':', '').replace('-', '').replace(' ', '-')
#filename = ('%s' % now).split('.')[0] + '.mp4'
#동시에 요청시 구분이 안됨. 2019-10-11
filename = ('%s' % now) + '.mp4'
self.temp_fullpath = os.path.join(self.temp_path, filename)
command.append(self.temp_fullpath)
#else:
# command.append(self.temp_fullpath.encode('cp949'))
else:
command.append(self.temp_fullpath)
try:
logger.debug(' '.join(command))
#logger.debug(command)
if os.path.exists(self.temp_fullpath):
for f in Ffmpeg.instance_list:
if f.idx != self.idx and f.temp_fullpath == self.temp_fullpath and f.status in [Status.DOWNLOADING, Status.READY]:
self.status = Status.ALREADY_DOWNLOADING
#logger.debug('temp_fullpath ALREADY_DOWNLOADING')
return
except:
pass
#self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, bufsize=1)
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, encoding='utf8')
self.status = Status.READY
self.log_thread = threading.Thread(target=self.log_thread_fuction, args=())
self.log_thread.start()
self.start_event.wait(timeout=60)
#logger.debug('start_event awake.. ')
if self.log_thread is None:
#logger.debug('log_thread is none')
#logger.debug(self.log)
if self.status == Status.READY:
self.status = Status.ERROR
self.kill()
elif self.status == Status.READY:
#logger.debug('still status.ready kill')
#logger.debug(self.log)
self.status = Status.ERROR
self.kill()
else:
#logger.debug('normally process wait()')
#process_ret = self.process.wait(timeout=60*60)
process_ret = self.process.wait(timeout=60*ModelSetting.get_int('timeout_minute'))
#logger.debug('process_ret :%s' % process_ret)
if process_ret is None: # timeout
if self.status != Status.COMPLETED and self.status != Status.USER_STOP and self.status != Status.PF_STOP:
self.status = Status.TIME_OVER
self.kill()
else:
#logger.debug('process end')
if self.status == Status.DOWNLOADING:
self.status = Status.FORCE_STOP
self.end_time = datetime.now()
self.download_time = self.end_time - self.start_time
try:
if self.status == Status.COMPLETED:
if self.save_fullpath != self.temp_fullpath:
if os.path.exists(self.save_fullpath):
os.remove(self.save_fullpath)
#if app.config['config']['is_py2']:
# os.chmod(self.temp_fullpath, 0777)
#else:
# os.chmod(self.temp_fullpath, 777)
os.system('chmod 777 "%s"' % self.temp_fullpath)
# 2020-03-19 씹힌다 ㅠ
#import framework.common.celery as celery_task
#celery_task.move(self.temp_fullpath, self.save_fullpath)
import shutil
shutil.move(self.temp_fullpath, self.save_fullpath)
# 2019-05-16
#os.chmod(self.save_fullpath, 0777)
self.filesize = os.stat(self.save_fullpath).st_size
else:
if os.path.exists(self.temp_fullpath):
os.remove(self.temp_fullpath)
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
arg = {'type':'last', 'status':self.status, 'data' : self.get_data()}
self.send_to_listener(**arg)
self.process = None
self.thread = None
#파일처리
#logger.debug('ffmpeg thread end')
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
try:
self.status = Status.EXCEPTION
arg = {'type':'last', 'status':self.status, 'data' : self.get_data()}
self.send_to_listener(**arg)
except:
pass
#self.start_event.set()
def log_thread_fuction(self):
with self.process.stdout:
iter_arg = b'' if app.config['config']['is_py2'] else ''
for line in iter(self.process.stdout.readline, iter_arg):
try:
#logger.debug(line)
#logger.debug('XXXXXXXXXXXXXX %s', self.status)
#self.log.append(line.strip())
#arg = {'type':'log', 'status':self.status, 'data' : {'idx':self.idx, 'line':line}}
#self.send_to_listener(**arg)
if self.status == Status.READY:
if line.find('Server returned 404 Not Found') != -1 or line.find('Unknown error') != -1:
self.status = Status.WRONG_URL
self.start_event.set()
#logger.debug('start_event set by 404 not found')
elif line.find('No such file or directory') != -1:
self.status = Status.WRONG_DIRECTORY
self.start_event.set()
#logger.debug('start_event set by WRONG_DIR')
else:
match = re.compile(r'Duration\:\s(\d{2})\:(\d{2})\:(\d{2})\.(\d{2})\,\sstart').search(line)
if match:
self.duration_str = '%s:%s:%s' % ( match.group(1), match.group(2), match.group(3))
self.duration = int(match.group(4))
self.duration += int(match.group(3)) * 100
self.duration += int(match.group(2)) * 100 * 60
self.duration += int(match.group(1)) * 100 * 60 * 60
#logger.debug('Duration : %s', self.duration)
if match:
self.status = Status.READY
arg = {'type':'status_change', 'status':self.status, 'data' : self.get_data()}
self.send_to_listener(**arg)
continue
#if self.listener is not None:
# #arg = {'status':self.status, 'duration' : self.duration}
# arg = {'status':self.status, 'data' : self.get_data()}
# self.listener(**arg)
#
#self.status = 'DOWNLOADING'
match = re.compile(r'time\=(\d{2})\:(\d{2})\:(\d{2})\.(\d{2})\sbitrate\=\s*(?P<bitrate>\d+).*?[$|\s](\s?speed\=\s*(?P<speed>.*?)x)?').search(line)
if match:
self.status = Status.DOWNLOADING
arg = {'type':'status_change', 'status':self.status, 'data' : self.get_data()}
self.send_to_listener(**arg)
self.start_event.set()
#logger.debug('start_event set by DOWNLOADING')
elif self.status == Status.DOWNLOADING:
if line.find('PES packet size mismatch') != -1:
self.current_pf_count += 1
if self.current_pf_count > self.max_pf_count:
#logger.debug('%s : PF_STOP!', self.idx)
self.status = Status.PF_STOP
self.kill()
continue
if line.find('HTTP error 403 Forbidden') != -1:
#logger.debug('HTTP error 403 Forbidden')
self.status = Status.HTTP_FORBIDDEN
self.kill()
continue
match = re.compile(r'time\=(\d{2})\:(\d{2})\:(\d{2})\.(\d{2})\sbitrate\=\s*(?P<bitrate>\d+).*?[$|\s](\s?speed\=\s*(?P<speed>.*?)x)?').search(line)
if match:
self.current_duration = int(match.group(4))
self.current_duration += int(match.group(3)) * 100
self.current_duration += int(match.group(2)) * 100 * 60
self.current_duration += int(match.group(1)) * 100 * 60 * 60
try:
self.percent = int(self.current_duration * 100 / self.duration)
except: pass
#logger.debug('Current Duration : %s %s %s%%', self.duration, self.current_duration, self.percent)
self.current_bitrate = match.group('bitrate')
self.current_speed = match.group('speed')
self.download_time = datetime.now() - self.start_time
arg = {'type':'normal', 'status':self.status, 'data' : self.get_data()}
self.send_to_listener(**arg)
continue
match = re.compile(r'video\:\d*kB\saudio\:\d*kB').search(line)
if match:
self.status = Status.COMPLETED
self.end_time = datetime.now()
self.download_time = self.end_time - self.start_time
self.percent = 100
arg = {'type':'status_change', 'status':self.status, 'data' : self.get_data()}
self.send_to_listener(**arg)
continue
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
#logger.debug('ffmpeg log thread end')
self.start_event.set()
self.log_thread = None
def get_data(self):
data = {
'url' : self.url,
'filename' : self.filename,
'max_pf_count' : self.max_pf_count,
'call_plugin' : self.call_plugin,
'temp_path' : self.temp_path,
'save_path' : self.save_path,
'temp_fullpath' : self.temp_fullpath,
'save_fullpath' : self.save_fullpath,
'status' : int(self.status),
'status_str' : self.status.name,
'status_kor' : str(self.status),
'duration' : self.duration,
'duration_str' : self.duration_str,
'current_duration' : self.current_duration,
'percent' : self.percent,
'current_pf_count' : self.current_pf_count,
'idx' : self.idx,
#'log' : self.log,
'current_bitrate' : self.current_bitrate,
'current_speed' : self.current_speed,
'start_time' : '' if self.start_time is None else str(self.start_time).split('.')[0][5:],
'end_time' : '' if self.end_time is None else str(self.end_time).split('.')[0][5:],
'download_time' : '' if self.download_time is None else '%02d:%02d' % (self.download_time.seconds/60, self.download_time.seconds%60),
'exist' : os.path.exists(self.save_fullpath),
}
if self.status == Status.COMPLETED:
data['filesize'] = self.filesize
data['filesize_str'] = Util.sizeof_fmt(self.filesize)
data['download_speed'] = Util.sizeof_fmt(self.filesize/self.download_time.seconds, suffix='Bytes/Second')
return data
def send_to_listener(self, **arg):
Logic.ffmpeg_listener(**arg)
if self.listener is not None:
arg['plugin_id'] = self.plugin_id
self.listener(**arg)
@staticmethod
def get_version():
try:
command = u'%s -version' % (ModelSetting.get('ffmpeg_path'))
command = command.split(' ')
#logger.debug(command)
if app.config['config']['is_py2']:
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, bufsize=1)
else:
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, encoding='utf8')
iter_arg = b'' if app.config['config']['is_py2'] else ''
ret = []
with process.stdout:
for line in iter(process.stdout.readline, iter_arg):
ret.append(line)
process.wait() # wait for the subprocess to exit
return ret
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
@staticmethod
def stop_by_idx(idx):
try:
for f in Ffmpeg.instance_list:
if f.idx == idx:
f.stop()
break
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
@staticmethod
def ffmpeg_by_idx(idx):
try:
for f in Ffmpeg.instance_list:
if f.idx == idx:
return f
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
@staticmethod
def get_ffmpeg_by_caller(caller, caller_id):
try:
for f in Ffmpeg.instance_list:
if f.plugin_id == caller_id and f.call_plugin == caller:
return f
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
@staticmethod
def plugin_unload():
try:
for f in Ffmpeg.instance_list:
f.stop()
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())

178
plugin/ffmpeg/logic.py Executable file
View File

@@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
#########################################################
# python
import os
from datetime import datetime
import traceback
import logging
import subprocess
import time
import re
import threading
import json
import enum
import platform
# third-party
from sqlalchemy import desc
from sqlalchemy import or_, and_, func, not_
# sjva 공용
from framework.logger import get_logger
from framework import app, db, scheduler, path_app_root, socketio, path_data
from framework.job import Job
from framework.util import Util
from system.logic import SystemLogic
# 패키지
from .model import ModelSetting
# 로그
package_name = __name__.split('.')[0]
logger = get_logger(package_name)
#########################################################
class Status(enum.Enum):
READY = 0
WRONG_URL = 1
WRONG_DIRECTORY = 2
EXCEPTION = 3
ERROR = 4
HTTP_FORBIDDEN = 11
DOWNLOADING = 5
USER_STOP = 6
COMPLETED = 7
TIME_OVER = 8
PF_STOP = 9
FORCE_STOP = 10 #강제중단
ALREADY_DOWNLOADING = 12 #이미 목록에 있고 다운로드중
def __int__(self):
return self.value
def __str__(self):
kor = [r'준비', r'URL에러', r'폴더에러', r'실패(Exception)', r'실패(에러)', r'다운로드중', r'사용자중지', r'완료', r'시간초과', r'PF중지', r'강제중지',
r'403에러', r'임시파일이 이미 있음']
return kor[int(self)]
def __repr__(self):
return self.name
@staticmethod
def get_instance(value):
tmp = [ Status.READY, Status.WRONG_URL, Status.WRONG_DIRECTORY, Status.EXCEPTION, Status.ERROR,
Status.DOWNLOADING, Status.USER_STOP, Status.COMPLETED, Status.TIME_OVER, Status.PF_STOP,
Status.FORCE_STOP, Status.HTTP_FORBIDDEN, Status.ALREADY_DOWNLOADING ]
return tmp[value]
class Logic(object):
db_default = {
'temp_path' : os.path.join(path_data, 'tmp'),
'save_path' : os.path.join(path_data, 'download'),
'max_pf_count' : '0',
'if_fail_remove_tmp_file' : 'True',
'timeout_minute' : '60',
'ffmpeg_path' : 'ffmpeg' if platform.system() != 'Windows' else os.path.join(path_data, 'bin', 'ffmpeg.exe'),
}
@staticmethod
def db_init():
try:
for key, value in Logic.db_default.items():
if db.session.query(ModelSetting).filter_by(key=key).count() == 0:
db.session.add(ModelSetting(key, value))
db.session.commit()
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
@staticmethod
def plugin_load():
try:
Logic.db_init()
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
@staticmethod
def plugin_unload():
try:
from ffmpeg.interface_program_ffmpeg import Ffmpeg
Ffmpeg.plugin_unload()
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
@staticmethod
def ffmpeg_listener(**args):
#logger.debug('ffmpeg_listener : %s', args)
#logger.debug('ffmpeg_listener : type:%s status:%s', args['type'], args['status'])
# status_change ready, downloading, completed
refresh_type = None
if args['type'] == 'status_change':
if args['status'] == Status.DOWNLOADING:
refresh_type = 'status_change'
elif args['status'] == Status.COMPLETED:
refresh_type = 'status_change'
elif args['status'] == Status.READY:
data = {'type':'info', 'msg' : u'다운로드중 Duration(%s)' % args['data']['duration_str'] + '<br>' + args['data']['save_fullpath'], 'url':'/ffmpeg/list'}
socketio.emit("notify", data, namespace='/framework', broadcast=True)
# 1번에서 리스트화면, 2번에서 추가시 1번도 추가되도록
refresh_type = 'add'
elif args['type'] == 'last':
if args['status'] == Status.WRONG_URL:
data = {'type':'warning', 'msg' : u'잘못된 URL입니다'}
socketio.emit("notify", data, namespace='/framework', broadcast=True)
refresh_type = 'add'
elif args['status'] == Status.WRONG_DIRECTORY:
data = {'type':'warning', 'msg' : u'잘못된 디렉토리입니다.<br>' + args['data']['save_fullpath']}
socketio.emit("notify", data, namespace='/framework', broadcast=True)
refresh_type = 'add'
elif args['status'] == Status.ERROR or args['status'] == Status.EXCEPTION:
data = {'type':'warning', 'msg' : u'다운로드 시작 실패.<br>' + args['data']['save_fullpath']}
socketio.emit("notify", data, namespace='/framework', broadcast=True)
refresh_type = 'add'
elif args['status'] == Status.USER_STOP:
data = {'type':'warning', 'msg' : u'다운로드가 중지 되었습니다.<br>' + args['data']['save_fullpath'], 'url':'/ffmpeg/list'}
socketio.emit("notify", data, namespace='/framework', broadcast=True)
refresh_type = 'last'
elif args['status'] == Status.COMPLETED:
data = {'type':'success', 'msg' : u'다운로드가 완료 되었습니다.<br>' + args['data']['save_fullpath'], 'url':'/ffmpeg/list'}
socketio.emit("notify", data, namespace='/framework', broadcast=True)
refresh_type = 'last'
elif args['status'] == Status.TIME_OVER:
data = {'type':'warning', 'msg' : u'시간초과로 중단 되었습니다.<br>' + args['data']['save_fullpath'], 'url':'/ffmpeg/list'}
socketio.emit("notify", data, namespace='/framework', broadcast=True)
refresh_type = 'last'
elif args['status'] == Status.PF_STOP:
data = {'type':'warning', 'msg' : u'PF초과로 중단 되었습니다.<br>' + args['data']['save_fullpath'], 'url':'/ffmpeg/list'}
socketio.emit("notify", data, namespace='/framework', broadcast=True)
refresh_type = 'last'
elif args['status'] == Status.FORCE_STOP:
data = {'type':'warning', 'msg' : u'강제 중단 되었습니다.<br>' + args['data']['save_fullpath'], 'url':'/ffmpeg/list'}
socketio.emit("notify", data, namespace='/framework', broadcast=True)
refresh_type = 'last'
elif args['status'] == Status.HTTP_FORBIDDEN:
data = {'type':'warning', 'msg' : u'403에러로 중단 되었습니다.<br>' + args['data']['save_fullpath'], 'url':'/ffmpeg/list'}
socketio.emit("notify", data, namespace='/framework', broadcast=True)
refresh_type = 'last'
elif args['status'] == Status.ALREADY_DOWNLOADING:
data = {'type':'warning', 'msg' : u'임시파일폴더에 파일이 있습니다.<br>' + args['data']['temp_fullpath'], 'url':'/ffmpeg/list'}
socketio.emit("notify", data, namespace='/framework', broadcast=True)
refresh_type = 'last'
#elif args['type'] == 'log':
# socketio.emit("log", args['data'], namespace='/%s' % package_name, broadcast=True)
elif args['type'] == 'normal':
if args['status'] == Status.DOWNLOADING:
refresh_type = 'status'
if refresh_type is not None:
socketio.emit(refresh_type, args['data'], namespace='/%s' % package_name, broadcast=True)

23
plugin/ffmpeg/model.py Executable file
View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
#########################################################
# python
import os
import traceback
from datetime import datetime
import json
# third-party
from sqlalchemy import or_, and_, func, not_
from sqlalchemy.orm import backref
# sjva 공용
from framework import db, app, path_data
from framework.util import Util
# 패키지
from .plugin import logger, package_name
#########################################################
app.config['SQLALCHEMY_BINDS'][package_name] = 'sqlite:///%s' % (os.path.join(path_data, 'db', '%s.db' % package_name))
from plugin import get_model_setting
ModelSetting = get_model_setting(package_name, logger)

323
plugin/ffmpeg/plugin.py Executable file
View File

@@ -0,0 +1,323 @@
# -*- coding: utf-8 -*-
#########################################################
# python
import os
import traceback
import time
from datetime import datetime
from pytz import timezone
# third-party
import requests
from flask import Blueprint, request, Response, send_file, render_template, redirect, jsonify, session, send_from_directory, stream_with_context
from flask_socketio import SocketIO, emit, send
from flask_login import login_user, logout_user, current_user, login_required
# sjva 공용
from framework import app, db, scheduler, path_app_root, socketio, path_data
from framework.logger import get_logger
from framework.util import Util
# 패키지
package_name = __name__.split('.')[0]
logger = get_logger(package_name)
from ffmpeg.logic import Logic
from ffmpeg.model import ModelSetting
from ffmpeg.interface_program_ffmpeg import Ffmpeg
from system.model import ModelSetting as SystemModelSetting
#########################################################
#########################################################
# 플러그인 공용
#########################################################
blueprint = Blueprint(package_name, package_name, url_prefix='/%s' % package_name, template_folder='templates')
menu = {
'main' : [package_name, u'FFMPEG'],
'sub' : [
['setting', u'설정'], ['download', u'다운로드'], ['list', u'목록'], ['log', u'로그'],
]
}
def plugin_load():
Logic.plugin_load()
def plugin_unload():
Logic.plugin_unload()
streaming_kill()
def streaming_kill():
logger.debug('streaming_kill...')
global process_list
try:
for p in process_list:
if p is not None and p.poll() is None:
import psutil
process = psutil.Process(p.pid)
for proc in process.children(recursive=True):
proc.kill()
process.kill()
except Exception as e:
logger.error('Exception:%s', e)
logger.error(traceback.format_exc())
#########################################################
# WEB Menu
#########################################################
@blueprint.route('/')
def home():
return redirect('/%s/list' % package_name)
@blueprint.route('/<sub>')
@login_required
def detail(sub):
arg = ModelSetting.to_dict()
if sub == 'setting':
return render_template('{package_name}_{sub}.html'.format(package_name=package_name, sub=sub), arg=arg)
elif sub == 'download':
now = str(datetime.now(timezone('Asia/Seoul'))).replace(':', '').replace('-', '').replace(' ', '-')
arg['temp_filename'] = ('%s' % now).split('.')[0] + '.mp4'
return render_template('{package_name}_{sub}.html'.format(package_name=package_name, sub=sub), arg=arg)
elif sub == 'list':
return render_template('{package_name}_{sub}.html'.format(package_name=package_name, sub=sub), arg=arg)
elif sub == 'log':
return render_template('log.html', package=package_name)
return render_template('sample.html', title='%s - %s' % (package_name, sub))
#########################################################
# For UI
#########################################################
@blueprint.route('/ajax/<sub>', methods=['GET', 'POST'])
@login_required
def ajax(sub):
try:
if sub == 'setting_save':
ret = ModelSetting.setting_save(request)
return jsonify(ret)
elif sub == 'ffmpeg_version':
ret = Ffmpeg.get_version()
return jsonify(ret)
elif sub == 'download':
url = request.form['url']
filename = request.form['filename']
ffmpeg = Ffmpeg(url, filename, call_plugin=package_name)
data = ffmpeg.start()
return jsonify([])
elif sub == 'stop':
idx = request.form['idx']
Ffmpeg.stop_by_idx(idx)
return jsonify([])
elif sub == 'play':
idx = request.form['idx']
ffmpeg = Ffmpeg.ffmpeg_by_idx(idx)
tmp = ffmpeg.save_fullpath.replace(path_app_root, '')
tmp = tmp.replace('\\', '/')
logger.debug('play : %s', tmp)
#return redirect('/open_file%s', tmp)
#return send_from_directory('', tmp[1:])
return jsonify(tmp)
elif sub == 'list':
ret = []
for ffmpeg in Ffmpeg.instance_list:
ret.append(ffmpeg.get_data())
return jsonify(ret)
elif sub == 'streaming_kill':
streaming_kill()
return jsonify('')
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
#http://192.168.0.11:9999/ffmpeg/api/download?url=https%3a%2f%2fani24zo.com%2fani%2fdownload.php%3fid%3d38912&filename=test.mp4&id=0&caller=ani24&save_path=D:\
#http://192.168.0.11:9999/ffmpeg/api/status?id=0&caller=ani24
#http://192.168.0.11:9999/ffmpeg/api/stop?id=0&caller=ani24
@blueprint.route('/api/<sub>', methods=['GET', 'POST'])
def api(sub):
sjva_token = request.args.get('token')
if sjva_token != SystemModelSetting.get('unique'):
ret = {}
ret['ret'] = 'wrong_token'
return jsonify(ret)
if sub == 'download':
ret = {}
try:
max_pf_count = ModelSetting.get('max_pf_count')
url = request.args.get('url')
filename = request.args.get('filename')
caller_id = request.args.get('id')
package_name = request.args.get('caller')
save_path = request.args.get('save_path')
if save_path is None:
save_path = ModelSetting.get('save_path')
else:
if not os.path.exists(save_path):
os.makedirs(save_path)
logger.debug('url : %s', url)
logger.debug('filename : %s', filename)
logger.debug('caller_id : %s', caller_id)
logger.debug('caller : %s', package_name)
logger.debug('save_path : %s', save_path)
f = Ffmpeg(url, filename, plugin_id=caller_id, listener=None, max_pf_count=max_pf_count, call_plugin=package_name, save_path=save_path)
f.start()
ret['ret'] = 'success'
ret['data'] = f.get_data()
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
ret['ret'] = 'exception'
ret['log'] = traceback.format_exc()
return jsonify(ret)
elif sub == 'stop':
ret = {}
try:
caller_id = request.args.get('id')
package_name = request.args.get('caller')
f = Ffmpeg.get_ffmpeg_by_caller(package_name, caller_id)
Ffmpeg.stop_by_idx(f.idx)
ret['ret'] = 'success'
ret['data'] = f.get_data()
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
ret['ret'] = 'exception'
ret['log'] = traceback.format_exc()
return jsonify(ret)
elif sub == 'status':
ret = {}
try:
caller_id = request.args.get('id')
package_name = request.args.get('caller')
f = Ffmpeg.get_ffmpeg_by_caller(package_name, caller_id)
ret['ret'] = 'success'
ret['data'] = f.get_data()
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
ret['ret'] = 'exception'
ret['log'] = traceback.format_exc()
return jsonify(ret)
@socketio.on('connect', namespace='/%s' % package_name)
def connect():
logger.debug('ffmpeg socketio connect')
@socketio.on('disconnect', namespace='/%s' % package_name)
def disconnect():
logger.debug('ffmpeg socketio disconnect')
process_list = []
@blueprint.route('/streaming', methods=['GET'])
def streaming():
mode = request.args.get('mode')
if mode == 'file':
try:
import subprocess
filename = request.args.get('value')
if filename.endswith('mp4'):
"""
output = os.path.join(path_data, 'tmp', 'index.m3u8')
#ffmpeg_command = ['ffmpeg', "-loglevel", "quiet", "-i", filename, '-ss', '00:00:03', '-t', '00:03:00', "-vcodec", 'libx264', '-vf', 'scale=160:-1', '-qscale:v', '1', '-acodec', 'aac', '-qscale:a', '1', '-f', 'mp4', output, '-y']
ffmpeg_command = ['ffmpeg', "-loglevel", "quiet", "-i", filename, '-ss', '00:00:03', '-t', '00:03:00', "-start_number", '0', '-vf', 'scale=320:-1', '-hls_list_size', '0', '-hls_time', '10', '-f', 'hls', output, '-y']
logger.warning(' '.join(ffmpeg_command))
#subprocess.check_output(ffmpeg_command)
subprocess.Popen(ffmpeg_command)
time.sleep(5)
filename = output
"""
url = '/open_file%s' % filename
logger.debug(url)
return redirect(url)
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
def generate():
startTime = time.time()
buffer = []
sentBurst = False
path_ffmpeg = 'ffmpeg'
#filename = '/home/coder/project/SJ/mnt/soju6janm/AV/censored/library2/vr/C/CBIKMV/CBIKMV-093/cbikmv-093cd1.mp4'
#filename = '/home/coder/project/SJ/mnt/soju6janw/1.mp4'
#ffmpeg_command = [path_ffmpeg, "-loglevel", "quiet", "-i", filename, "-c:v", "copy", "-c:a", "aac", "-b:a", "128k", "-f", "mpegts", "-tune", "zerolatency", "pipe:stdout"]
ffmpeg_command = [path_ffmpeg, "-loglevel", "quiet", "-i", filename, '-ss', '00:00:03', '-t', '00:03:00', "-vcodec", "libvpx", '-vf', 'scale=320:-1', "-qmin", "0", "-qmax", "50", "-crf", "50", "-b:v", "0.1M", '-acodec', 'libvorbis', '-f', 'webm', "pipe:stdout"]
#ffmpeg_command = [path_ffmpeg, "-loglevel", "quiet", "-i", filename, "-vcodec", "libtheora", '-vf', 'scale=320:-1', "-qscale:v", '1', '-acodec', 'libvorbis', '-qscale:a', '1', '-f', 'ogv', "pipe:stdout"]
#ffmpeg_command = [path_ffmpeg, "-loglevel", "quiet", "-i", filename, "-vcodec", 'libx264', '-acodec', 'aac ', '-f', 'mp4', "pipe:stdout"]
logger.debug(' '.join(ffmpeg_command))
#logger.debug('command : %s', ffmpeg_command)
#process = subprocess.Popen(ffmpeg_command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, bufsize = -1)
process = subprocess.Popen(ffmpeg_command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, encoding='utf8')
global process_list
process_list.append(process)
while True:
#if time.time() - startTime > 120:
# break
line = process.stdout.read(1024)
buffer.append(line)
if sentBurst is False and time.time() > startTime + 1 and len(buffer) > 0:
sentBurst = True
for i in range(0, len(buffer) - 2):
yield buffer.pop(0)
elif time.time() > startTime + 1 and len(buffer) > 0:
yield buffer.pop(0)
process.poll()
if isinstance(process.returncode, int):
if process.returncode > 0:
logger.debug('FFmpeg Error :%s', process.returncode)
break
if process is not None and process.poll() is None:
process.kill()
return Response(stream_with_context(generate()), mimetype = "video/MP2T")
def get_video_info(filepath):
try:
from system.logic_command import SystemLogicCommand
command = ['ffprobe', '-v', 'error', '-print_format', 'json', '-show_format', '-show_streams', "%s" % filepath]
ret = SystemLogicCommand.execute_command_return(command, format='json')
return ret
except Exception as exception:
logger.error('Exception:%s', exception)
logger.error(traceback.format_exc())
"""
ffmpeg version 3.4.8-0ubuntu0.2 Copyright (c) 2000-2020 the FFmpeg developers
built with gcc 7 (Ubuntu 7.5.0-3ubuntu1~18.04)
configuration: --prefix=/usr --extra-version=0ubuntu0.2 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --enable-gpl --disable-stripping --enable-avresample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librubberband --enable-librsvg --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-omx --enable-openal --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libopencv --enable-libx264 --enable-shared
libavutil 55. 78.100 / 55. 78.100
libavcodec 57.107.100 / 57.107.100
libavformat 57. 83.100 / 57. 83.100
libavdevice 57. 10.100 / 57. 10.100
libavfilter 6.107.100 / 6.107.100
libavresample 3. 7. 0 / 3. 7. 0
libswscale 4. 8.100 / 4. 8.100
libswresample 2. 9.100 / 2. 9.100
libpostproc 54. 7.100 / 54. 7.100
Hyper fast Audio and Video encoder
usage: ffmpeg [options] [[infile options] -i infile]... {[outfile options] outfile}...
"""

View File

@@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block content %}
<div>
{{ macros.setting_input_text('url', 'URL', placeholder='http:// 주소', desc='비디오 파일 주소 or m3u8 주소') }}
{{ macros.setting_input_text('filename', '파일명', value=arg['temp_filename']) }}
{{ macros.setting_button([['download_start', '다운로드']]) }}
</div>
<script type="text/javascript">
var package_name = "{{arg['package_name'] }}";
$(document).ready(function(){
});
//다운로드시작
$("#download_start").click(function(e) {
e.preventDefault();
if ($("#url").val().startsWith('http') == false) {
$.notify('<strong>URL을 입력하세요.</strong>', {
type: 'warning'
});
return;
}
$.ajax({
url: '/ffmpeg/ajax/download',
type: "POST",
cache: false,
data: {url: $("#url").val(), filename: $("#filename").val()},
dataType: "json",
success: function (data) {
$.notify('<strong>분석중..</strong>', {
type: 'info'
});
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,237 @@
{% extends "base.html" %}
{% block content %}
<!--<div class="d-inline-block"></div>-->
<!--<div id="accordion">-->
<style>
.row>div { padding-top: 3px; padding-bottom:3px; }
.row { align-items: center; word-break:break-all;}
.row>div:nth-child(odd) { align-items: right; text-align: right; }
.row>div:nth-child(even) { align-items: left; text-align: left; }
</style>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.4.6/socket.io.js"></script>
<table id="result_table" class="table table-sm tableRowHover">
<thead>
<tr>
<th style="width:5%">IDX</th>
<th style="width:8%">Plugin</th>
<th style="width:10%">시작시간</th>
<th style="width:20%">파일명</th>
<th style="width:8%">상태</th>
<th style="width:15%">진행률</th>
<th style="width:5%">길이</th>
<th style="width:5%">PF</th>
<th style="width:8%">배속</th>
<th style="width:8%">진행시간</th>
<th style="width:8%">Action</th>
</tr>
</thead>
<tbody id="list" >
</tbody>
</table>
<script type="text/javascript">
$(document).ready(function(){
var package_name = 'ffmpeg';
var protocol = window.location.protocol;
var socket = io.connect(protocol + "//" + document.domain + ":" + location.port + "/" + package_name)
//var socket = io.connect("http://" + document.domain + ":" + location.port + "/ffmpeg", {transports:['websocket']}, {'force new connection': true});
//socket.emit("start");
socket.on('add', function(data){
str = make_item(data)
document.getElementById("list").innerHTML += str;
//document.getElementById("log_"+data.idx).scrollTop = document.getElementById("log_"+data.idx).scrollHeight ;
});
// 화면을 켜놓고, 스케쥴러에 의해 시작될때 add전에 로그가온다.
/*
socket.on('log', function(data){
if (document.getElementById("log_"+data.idx) != null) {
document.getElementById("log_"+data.idx).innerHTML += data.line;
document.getElementById("log_"+data.idx).scrollTop = document.getElementById("log_"+data.idx).scrollHeight;
}
});
*/
socket.on('status_change', function(data) {
button_html(data);
});
socket.on('status', function(data){
status_html(data);
});
socket.on('last', function(data){
status_html(data);
button_html(data);
});
$.ajax({
url: '/' + package_name + '/ajax/list',
type: "POST",
cache: false,
data:{},
dataType: "json",
success: function (data) {
document.getElementById("list").innerHTML = '';
str = ''
for(i in data) {
str += make_item(data[i])
}
document.getElementById("list").innerHTML = str;
}
});
$("body").on('click', '#stop', function(e){
e.stopPropagation();
e.preventDefault();
var idx = $(this).data('idx')
$.ajax({
url: '/ffmpeg/ajax/stop',
type: "POST",
cache: false,
data:{ idx : idx},
dataType: "json",
success: function (data) {
}
});
});
$("body").on('click', '#play', function(e){
e.preventDefault();
e.stopPropagation();
var idx = $(this).data('idx')
$.ajax({
url: '/ffmpeg/ajax/play',
type: "POST",
cache: false,
data:{ idx : idx},
dataType: "json",
success: function (data) {
console.log(data);
if (data.startsWith('/')) {
var url = '/open_file' + data;
} else {
var url = '/open_file/' + data;
}
window.open(url, "_blank");
}
});
});
});
function make_item(data) {
//if (document.getElementById("log_"+data.idx) != null) return;
str = '<tr style="cursor: pointer;" data-toggle="collapse" data-target="#collapse_'+ data.idx + '" aria-expanded="true" >';
str += '<td scope="col" style="width:5%">'+ data.idx + '</td>';
str += '<td scope="col" style="width:8%">'+ data.call_plugin + '</td>';
str += '<td scope="col" style="width:10%">'+ data.start_time + '</td>';
str += '<td scope="col" style="width:20%">'+ data.filename + '</td>';
str += '<td id="status_'+data.idx+'" scope="col" style="width:8%">'+ data.status_kor + '</td>';
var visi = 'hidden';
if (parseInt(data.percent) > 0) {
visi = 'visible';
}
str += '<td scope="col" style="width:20%"><div class="progress"><div id="progress_'+data.idx+'" class="progress-bar" style="visibility: '+visi+'; width:'+data.percent+'%">'+data.percent +'%</div></div></td>';
str += '<td scope="col" style="width:5%">'+ data.duration_str + '</td>';
str += '<td id="current_pf_count_'+data.idx+'" scope="col" style="width:5%">'+ data.current_pf_count + '</td>';
str += '<td id="current_speed_'+data.idx+'" scope="col" style="width:8%">'+ data.current_speed + '</td>';
str += '<td id="download_time_'+data.idx+'" scope="col" style="width:8%">'+ data.download_time + '</td>';
str += '<td id="button_'+data.idx+'" scope="col" style="width:8%" class="tableRowHoverOff">';
if (data.status_str == 'DOWNLOADING') {
str += '<button id="stop" class="align-middle btn btn-outline-danger btn-sm" data-idx="'+data.idx+'">중지</button>';
} else if (data.status_str == 'COMPLETED' && data.exist) {
str += '<button id="play" class="align-middle btn btn-outline-info btn-sm" data-idx="'+data.idx+'">재생</button>';
}
str += '</td>'
str += '</tr>'
str += '<tr class="collapse tableRowHoverOff" style="cursor: pointer;" id="collapse_' + data.idx + '">';
str += '<td colspan="11">';
str += '<div id="detail_'+data.idx+'">';
str += get_detail(data);
str += '</div>';
/*
str += '<div><textarea class="form-control" id="log_'+data.idx+'" rows="20">';
for (var i = 0; i < data.log.length; i++) {
str += data.log[i] + '\n';
}
str += '</textarea></div>';
*/
str += '</td>';
str += '</tr>'
return str
}
function info_html($left, $right) {
var str = '<div class="row">' +
'<div class="col-sm-2">' +
'<strong>'+ $left +'</strong>' +
'</div>' +
'<div class="col-sm-10">' +
'<div class="input-group col-sm-9">' +
'<span class="text-left" style="padding-left:10px; padding-top:3px">';
if ($left == 'URL') {
str += '<a href="/hls?url=' + $right + '" target="_blank">';
}
str += $right;
if ($left == 'URL') {
str += '</a>';
}
str += '</span>' +
'</div>' +
'</div>' +
'</div>';
return str;
}
function get_detail(data) {
var str = info_html('URL', data.url);
str += info_html('임시경로', data.temp_fullpath);
str += info_html('저장경로', data.save_fullpath);
str += info_html('진행률(current/total)', data.percent+ '% (' + data.current_duration + ' / ' + data.duration + ')');
str += info_html('현재 비트레이트', data.current_bitrate);
str += info_html('종료시간', data.end_time);
str += info_html('허용 Packet Fail 수', data.max_pf_count);
str += info_html('파일 Exist', data.exist);
if (data.status_str == 'COMPLETED') {
str += info_html('파일 크기', data.filesize_str);
str += info_html('다운 속도', data.download_speed);
}
return str;
}
function button_html(data) {
if (data.status_str == 'DOWNLOADING') {
document.getElementById("button_" + data.idx).innerHTML =
'<button id="stop" class="align-middle btn btn-outline-danger btn-sm" data-idx="'+data.idx+'">중지</button>';
} else if (data.status_str == 'COMPLETED' && data.exist) {
document.getElementById("button_" + data.idx).innerHTML = '<button id="play" class="align-middle btn btn-outline-info btn-sm" data-idx="'+data.idx+'">재생</button>';
} else {
document.getElementById("button_" + data.idx).innerHTML = '';
}
}
function status_html(data) {
var progress = document.getElementById("progress_" + data.idx);
progress.style.width = data.percent+ '%';
progress.innerHTML = data.percent+ '%';
progress.style.visibility = 'visible';
document.getElementById("status_" + data.idx).innerHTML = data.status_kor;
document.getElementById("current_pf_count_" + data.idx).innerHTML = data.current_pf_count;
document.getElementById("current_speed_" + data.idx).innerHTML = data.current_speed;
document.getElementById("download_time_" + data.idx).innerHTML = data.download_time;
//document.getElementById("log").innerHTML += str + '\r\n';
document.getElementById("detail_" + data.idx).innerHTML = get_detail(data);
}
</script>
{% endblock %}

View File

@@ -0,0 +1,79 @@
{% extends "base.html" %}
{% block content %}
<div>
{{ macros.m_button_group([['global_setting_save_btn', '설정 저장']])}}
{{ macros.m_row_start('5') }}
{{ macros.m_row_end() }}
<nav>
{{ macros.m_tab_head_start() }}
{{ macros.m_tab_head2('normal', '기본', true) }}
{{ macros.m_tab_head_end() }}
</nav >
<form id='setting' name='setting'>
<div class="tab-content" id="nav-tabContent">
{{ macros.m_tab_content_start('normal', true) }}
{{ macros.setting_input_text_and_buttons('ffmpeg_path', 'FFMPEG 경로', [['ffmpeg_version', '버전확인'], ['select_binary_path_btn', '파일 선택']], value=arg['ffmpeg_path']) }}
{{ macros.setting_input_text_and_buttons('temp_path', '임시 폴더', [['select_temp_path_btn', '경로 선택']], value=arg['temp_path'], desc=['다운로드 파일이 임시로 저장될 폴더 입니다.']) }}
{{ macros.setting_input_text_and_buttons('save_path', '저장 폴더', [['select_save_path_btn', '경로 선택']], value=arg['save_path'], placeholder='저장 폴더 경로', desc='정상적으로 완료된 파일이 이동할 폴더 입니다.') }}
{{ macros.setting_input_int('max_pf_count', '허용 Packet Fail 수', value=arg['max_pf_count'], min='0', placeholder='0', desc=['이 값보다 Packet Fail 횟수가 더 많으면 실패처리 합니다.', '0일 경우 Packet Fail이 발생하면 바로 실패처리.']) }}
{{ macros.setting_checkbox('if_fail_remove_tmp_file', '임시 파일', arg['if_fail_remove_tmp_file'], desc='On : 실패시 임시 파일 자동 삭제') }}
{{ macros.setting_input_int('timeout_minute', '타임아웃 시간', value=arg['timeout_minute'], desc=['이 시간 안에 완료가 되지 않으면 시간초과 에러를 발생합니다.', '속도가 느린 경우 값을 올려 설정하세요. 분 단위']) }}
{{ macros.m_tab_content_end() }}
</div>
</form>
</div>
<script type="text/javascript">
var package_name = "{{arg['package_name'] }}";
$(document).ready(function(){
});
//버전
$("#ffmpeg_version").click(function(e) {
e.preventDefault();
$.ajax({
url: '/ffmpeg/ajax/ffmpeg_version',
type: "POST",
cache: false,
data:{ },
dataType: "json",
success: function (list) {
var str = '';
for (var i = 0; i < list.length; i++) {
str += "<div>" + list[i] + "</div>";
}
document.getElementById("modal_title").innerHTML = "ffmpeg -version";
document.getElementById("modal_body").innerHTML = str;
$("#large_modal").modal();
}
});
});
$("body").on('click', '#select_temp_path_btn', function(e){
e.preventDefault();
m_select_local_file_modal("임시 저장 경로 선택", $('#temp_path').val().trim(), true, function(result){
document.getElementById("temp_path").value = result;
});
});
$("body").on('click', '#select_save_path_btn', function(e){
e.preventDefault();
m_select_local_file_modal("저장 경로 선택", $('#save_path').val().trim(), true, function(result){
document.getElementById("save_path").value = result;
});
});
$("body").on('click', '#select_binary_path_btn', function(e){
e.preventDefault();
m_select_local_file_modal("실행 파일 선택", '/', false, function(result){
document.getElementById("ffmpeg_path").value = result;
});
});
</script>
{% endblock %}