Files
youtube-dl/lib/support/expand/ffmpeg.py
flaskfarm dd32e8a04e update
2022-10-22 23:10:07 +09:00

452 lines
19 KiB
Python

import enum
import os
import platform
import re
import shutil
import subprocess
import threading
import time
import traceback
from datetime import datetime
from support import SupportFile, SupportSubprocess, SupportUtil, logger
class SupportFfmpeg(object):
__instance_list = []
__ffmpeg_path = None
__idx = 1
total_callback_function = None
temp_path = None
@classmethod
def initialize(cls, __ffmpeg_path, temp_path, total_callback_function, max_pf_count=-1):
cls.__ffmpeg_path = __ffmpeg_path
cls.temp_path = temp_path
cls.total_callback_function = total_callback_function
cls.max_pf_count = max_pf_count
# retry : 재시도 횟수
# max_error_packet_count : 이 숫자 초과시 중단
# where : 호출 모듈
def __init__(self, url, filename, save_path=None, max_pf_count=None, headers=None, timeout_minute=60, proxy=None, callback_id=None, callback_function=None):
self.__idx = str(SupportFfmpeg.__idx)
SupportFfmpeg.__idx += 1
self.url = url
self.filename = filename
self.save_path = save_path
self.max_pf_count = max_pf_count
self.headers = headers
self.timeout_minute = int(timeout_minute)
self.proxy = proxy
self.callback_id = callback_id
if callback_id == None:
self.callback_id = str(self.__idx)
self.callback_function = callback_function
self.temp_fullpath = os.path.join(self.temp_path, filename)
self.save_fullpath = os.path.join(self.save_path, filename)
self.thread = None
self.process = None
self.log_thread = None
self.status = SupportFfmpeg.Status.READY
self.duration = 0
self.duration_str = ''
self.current_duration = 0
self.percent = 0
#self.log = []
self.current_pf_count = 0
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 = ''
SupportFfmpeg.__instance_list.append(self)
if len(SupportFfmpeg.__instance_list) > 30:
for instance in SupportFfmpeg.__instance_list:
if instance.thread is None and instance.status != SupportFfmpeg.Status.READY:
SupportFfmpeg.__instance_list.remove(instance)
break
else:
logger.debug('remove fail %s %s', instance.thread, self.status)
def start(self):
self.thread = threading.Thread(target=self.thread_fuction, args=())
self.thread.start()
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 = SupportFfmpeg.Status.USER_STOP
self.kill()
logger.warning('stop')
except Exception as e:
logger.error(f'Exception:{str(e)}')
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 e:
logger.error(f'Exception:{str(e)}')
logger.error(traceback.format_exc())
def thread_fuction(self):
try:
header_count = 0
if self.proxy is None:
if self.headers is None:
command = [self.__ffmpeg_path, '-y', '-i', self.url, '-c', 'copy', '-bsf:a', 'aac_adtstoasc']
else:
headers_command = []
tmp = ""
for key, value in self.headers.items():
if key.lower() == 'user-agent':
headers_command.append('-user_agent')
headers_command.append(f"{value}")
pass
else:
#headers_command.append('-headers')
if platform.system() == 'Windows':
tmp += f'{key}:{value}\r\n'
header_count += 1
else:
#tmp.append(f'{key}:{value}')
tmp += f'{key}:{value}\r\n'
if len(tmp) > 0:
headers_command.append('-headers')
headers_command.append(f'{tmp}')
command = [self.__ffmpeg_path, '-y'] + headers_command + ['-i', self.url, '-c', 'copy', '-bsf:a', 'aac_adtstoasc']
else:
command = [self.__ffmpeg_path, '-y', '-http_proxy', self.proxy, '-i', self.url, '-c', 'copy', '-bsf:a', 'aac_adtstoasc']
if platform.system() == 'Windows':
now = str(datetime.now()).replace(':', '').replace('-', '').replace(' ', '-')
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)
try:
#logger.debug(' '.join(command))
if os.path.exists(self.temp_fullpath):
for f in SupportFfmpeg.__instance_list:
if f.__idx != self.__idx and f.temp_fullpath == self.temp_fullpath and f.status in [SupportFfmpeg.Status.DOWNLOADING, SupportFfmpeg.Status.READY]:
self.status = SupportFfmpeg.Status.ALREADY_DOWNLOADING
return
except:
pass
#logger.error(' '.join(command))
command = SupportSubprocess.command_for_windows(command)
if platform.system() == 'Windows' and header_count > 1:
if os.environ.get('FF'):
from framework import F
batfilepath = os.path.join(F.config['path_data'], 'tmp', f"{time.time()}.bat")
else:
batfilepath = f"{time.time()}.bat"
tmp = command.replace('\r\n', '!CRLF!')
text = f"""setlocal enabledelayedexpansion
SET CRLF=^
{tmp}"""
SupportFile.write_file(batfilepath, text)
command = batfilepath
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, encoding='utf8')
self.status = SupportFfmpeg.Status.READY
self.log_thread = threading.Thread(target=self.log_thread_fuction, args=())
self.log_thread.start()
self.start_event.wait(timeout=60)
if self.log_thread is None:
if self.status == SupportFfmpeg.Status.READY:
self.status = SupportFfmpeg.Status.ERROR
self.kill()
elif self.status == SupportFfmpeg.Status.READY:
self.status = SupportFfmpeg.Status.ERROR
self.kill()
else:
process_ret = self.process.wait(timeout=60*self.timeout_minute)
if process_ret is None: # timeout
if self.status != SupportFfmpeg.Status.COMPLETED and self.status != SupportFfmpeg.Status.USER_STOP and self.status != SupportFfmpeg.Status.PF_STOP:
self.status = SupportFfmpeg.Status.TIME_OVER
self.kill()
else:
if self.status == SupportFfmpeg.Status.DOWNLOADING:
self.status = SupportFfmpeg.Status.FORCE_STOP
self.end_time = datetime.now()
self.download_time = self.end_time - self.start_time
try:
if self.status == SupportFfmpeg.Status.COMPLETED:
if self.save_fullpath != self.temp_fullpath:
if os.path.exists(self.save_fullpath):
os.remove(self.save_fullpath)
if platform.system() != 'Windows':
os.system('chmod 777 "%s"' % self.temp_fullpath)
shutil.move(self.temp_fullpath, self.save_fullpath)
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
except Exception as e:
logger.error(f'Exception:{str(e)}')
logger.error(traceback.format_exc())
try:
self.status = SupportFfmpeg.Status.EXCEPTION
arg = {'type':'last', 'status':self.status, 'data' : self.get_data()}
self.send_to_listener(**arg)
except:
pass
def log_thread_fuction(self):
with self.process.stdout:
for line in iter(self.process.stdout.readline, ''):
line = line.strip()
#logger.error(line)
try:
if self.status == SupportFfmpeg.Status.READY:
if line.find('Server returned 404 Not Found') != -1 or line.find('Unknown error') != -1:
self.status = SupportFfmpeg.Status.WRONG_URL
self.start_event.set()
elif line.find('No such file or directory') != -1:
self.status = SupportFfmpeg.Status.WRONG_DIRECTORY
self.start_event.set()
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
if match:
self.status = SupportFfmpeg.Status.READY
arg = {'type':'status_change', 'status':self.status, 'data' : self.get_data()}
self.send_to_listener(**arg)
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.status = SupportFfmpeg.Status.DOWNLOADING
arg = {'type':'status_change', 'status':self.status, 'data' : self.get_data()}
self.send_to_listener(**arg)
self.start_event.set()
elif self.status == SupportFfmpeg.Status.DOWNLOADING:
if line.find('PES packet size mismatch') != -1:
self.current_pf_count += 1
if self.current_pf_count > self.max_pf_count:
self.status = SupportFfmpeg.Status.PF_STOP
self.kill()
continue
if line.find('HTTP error 403 Forbidden') != -1:
self.status = SupportFfmpeg.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
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 = SupportFfmpeg.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 e:
logger.error(f'Exception:{str(e)}')
logger.error(traceback.format_exc())
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,
'callback_id' : self.callback_id,
'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 == SupportFfmpeg.Status.COMPLETED:
data['filesize'] = self.filesize
data['filesize_str'] = SupportUtil.sizeof_fmt(self.filesize)
if self.download_time.seconds != 0:
data['download_speed'] = SupportUtil.sizeof_fmt(self.filesize/self.download_time.seconds, suffix='Bytes/Second')
else:
data['download_speed'] = '0Bytes/Second'
return data
def send_to_listener(self, **arg):
if self.total_callback_function != None:
self.total_callback_function(**arg)
if self.callback_function is not None and self.callback_function != self.total_callback_function:
arg['callback_id'] = self.callback_id
self.callback_function(**arg)
@classmethod
def stop_by_idx(cls, idx):
try:
for __instance in SupportFfmpeg.__instance_list:
if __instance.__idx == idx:
__instance.stop()
break
except Exception as e:
logger.error(f'Exception:{str(e)}')
logger.error(traceback.format_exc())
@classmethod
def get_instance_by_idx(cls, idx):
try:
for __instance in SupportFfmpeg.__instance_list:
if __instance.__idx == idx:
return __instance
except Exception as e:
logger.error(f'Exception:{str(e)}')
logger.error(traceback.format_exc())
@classmethod
def get_instance_by_callback_id(cls, callback_id):
try:
for __instance in SupportFfmpeg.__instance_list:
if __instance.callback_id == callback_id:
return __instance
except Exception as e:
logger.error(f'Exception:{str(e)}')
logger.error(traceback.format_exc())
@classmethod
def all_stop(cls):
try:
for __instance in SupportFfmpeg.__instance_list:
__instance.stop()
except Exception as e:
logger.error(f'Exception:{str(e)}')
logger.error(traceback.format_exc())
@classmethod
def get_list(cls):
return cls.__instance_list
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 = ['준비', 'URL에러', '폴더에러', '실패(Exception)', '실패(에러)', '다운로드중', '사용자중지', '완료', '시간초과', 'PF중지', '강제중지',
'403에러', '임시파일이 이미 있음']
return kor[int(self)]
def __repr__(self):
return self.name
@staticmethod
def get_instance(value):
tmp = [
SupportFfmpeg.Status.READY,
SupportFfmpeg.Status.WRONG_URL,
SupportFfmpeg.Status.WRONG_DIRECTORY,
SupportFfmpeg.Status.EXCEPTION,
SupportFfmpeg.Status.ERROR,
SupportFfmpeg.Status.DOWNLOADING,
SupportFfmpeg.Status.USER_STOP,
SupportFfmpeg.Status.COMPLETED,
SupportFfmpeg.Status.TIME_OVER,
SupportFfmpeg.Status.PF_STOP,
SupportFfmpeg.Status.FORCE_STOP,
SupportFfmpeg.Status.HTTP_FORBIDDEN,
SupportFfmpeg.Status.ALREADY_DOWNLOADING ]
return tmp[value]