많은 수정중

This commit is contained in:
2025-12-27 16:45:13 +09:00
parent d756fa6b72
commit 92e23896bf
4 changed files with 682 additions and 97 deletions

View File

@@ -43,6 +43,8 @@ class FfmpegQueueEntity(abc.ABCMeta("ABC", (object,), {"__slots__": ()})):
self.filepath = None
self.quality = None
self.headers = None
self.current_speed = "" # 다운로드 속도
self.download_time = "" # 경과 시간
# FfmpegQueueEntity.static_index += 1
# FfmpegQueueEntity.entity_list.append(self)
@@ -194,24 +196,99 @@ class FfmpegQueue(object):
# SupportFfmpeg 초기화
self.support_init()
# entity.headers가 있으면 우선 사용, 없으면 caller.headers 사용
_headers = entity.headers
if self.caller is not None:
if _headers is None and self.caller is not None:
_headers = self.caller.headers
logger.info(f"Starting ffmpeg download - video_url: {video_url}")
logger.info(f"save_path: {dirname}, filename: {filename}")
logger.info(f"headers: {_headers}")
# 터미널에서 수동 테스트용 ffmpeg 명령어
output_file = os.path.join(dirname, filename)
referer = _headers.get("Referer", "") if _headers else ""
user_agent = _headers.get("User-Agent", "") if _headers else ""
ffmpeg_cmd = f'ffmpeg -headers "Referer: {referer}\\r\\nUser-Agent: {user_agent}\\r\\n" -i "{video_url}" -c copy "{output_file}"'
logger.info(f"=== MANUAL TEST COMMAND ===")
logger.info(ffmpeg_cmd)
logger.info(f"=== END COMMAND ===")
# m3u8 URL인 경우 커스텀 HLS 다운로더 사용 (ffmpeg 8.0 .jpg 확장자 문제 우회)
if video_url.endswith('.m3u8'):
logger.info("Using custom HLS downloader for m3u8 URL...")
from .hls_downloader import HlsDownloader
# 다운로드 시작 전 카운트 증가
self.current_ffmpeg_count += 1
logger.info(f"Download started, current_ffmpeg_count: {self.current_ffmpeg_count}/{self.max_ffmpeg_count}")
# 별도 스레드에서 다운로드 실행 (동시 다운로드 지원)
def run_hls_download(downloader_self, entity_ref, output_file_ref, headers_ref):
def progress_callback(percent, current, total, speed="", elapsed=""):
entity_ref.ffmpeg_status = 5 # DOWNLOADING
entity_ref.ffmpeg_status_kor = f"다운로드중 ({current}/{total})"
entity_ref.ffmpeg_percent = percent
entity_ref.current_speed = speed
entity_ref.download_time = elapsed
entity_ref.refresh_status()
hls_downloader = HlsDownloader(
m3u8_url=video_url,
output_path=output_file_ref,
headers=headers_ref,
callback=progress_callback
)
success, message = hls_downloader.download()
# 다운로드 완료 후 카운트 감소
downloader_self.current_ffmpeg_count -= 1
logger.info(f"Download finished, current_ffmpeg_count: {downloader_self.current_ffmpeg_count}/{downloader_self.max_ffmpeg_count}")
if success:
entity_ref.ffmpeg_status = 7 # COMPLETED
entity_ref.ffmpeg_status_kor = "완료"
entity_ref.ffmpeg_percent = 100
entity_ref.download_completed()
entity_ref.refresh_status()
logger.info(f"HLS download completed: {output_file_ref}")
else:
entity_ref.ffmpeg_status = -1
entity_ref.ffmpeg_status_kor = f"실패: {message}"
entity_ref.refresh_status()
logger.error(f"HLS download failed: {message}")
# 스레드 시작
download_thread = threading.Thread(
target=run_hls_download,
args=(self, entity, output_file, _headers)
)
download_thread.daemon = True
download_thread.start()
self.download_queue.task_done()
else:
# 일반 URL은 기존 SupportFfmpeg 사용 (비동기 방식)
self.current_ffmpeg_count += 1
ffmpeg = SupportFfmpeg(
url=video_url,
filename=filename,
callback_function=self.callback_function,
headers=_headers,
max_pf_count=0,
save_path=ToolUtil.make_path(dirname),
timeout_minute=60,
)
#
# todo: 임시로 start() 중지
logger.info("Calling ffmpeg.start()...")
ffmpeg.start()
logger.info("ffmpeg.start() returned")
self.download_queue.task_done()
ffmpeg = SupportFfmpeg(
url=video_url,
filename=filename,
callback_function=self.callback_function,
headers=_headers,
max_pf_count=0,
save_path=ToolUtil.make_path(dirname),
timeout_minute=60,
)
#
# todo: 임시로 start() 중지
ffmpeg.start()
self.current_ffmpeg_count += 1
self.download_queue.task_done()
except Exception as exception:
self.P.logger.error("Exception:%s", exception)
@@ -236,19 +313,19 @@ class FfmpegQueue(object):
+ args["data"]["save_fullpath"],
"url": "/ffmpeg/download/list",
}
socketio.emit("notify", data, namespace="/framework", broadcast=True)
socketio.emit("notify", data, namespace="/framework")
refresh_type = "add"
elif args["type"] == "last":
if args["status"] == SupportFfmpeg.Status.WRONG_URL:
data = {"type": "warning", "msg": "잘못된 URL입니다"}
socketio.emit("notify", data, namespace="/framework", broadcast=True)
socketio.emit("notify", data, namespace="/framework")
refresh_type = "add"
elif args["status"] == SupportFfmpeg.Status.WRONG_DIRECTORY:
data = {
"type": "warning",
"msg": "잘못된 디렉토리입니다.<br>" + args["data"]["save_fullpath"],
}
socketio.emit("notify", data, namespace="/framework", broadcast=True)
socketio.emit("notify", data, namespace="/framework")
refresh_type = "add"
elif (
args["status"] == SupportFfmpeg.Status.ERROR
@@ -258,7 +335,7 @@ class FfmpegQueue(object):
"type": "warning",
"msg": "다운로드 시작 실패.<br>" + args["data"]["save_fullpath"],
}
socketio.emit("notify", data, namespace="/framework", broadcast=True)
socketio.emit("notify", data, namespace="/framework")
refresh_type = "add"
elif args["status"] == SupportFfmpeg.Status.USER_STOP:
data = {
@@ -266,7 +343,7 @@ class FfmpegQueue(object):
"msg": "다운로드가 중지 되었습니다.<br>" + args["data"]["save_fullpath"],
"url": "/ffmpeg/download/list",
}
socketio.emit("notify", data, namespace="/framework", broadcast=True)
socketio.emit("notify", data, namespace="/framework")
refresh_type = "last"
elif args["status"] == SupportFfmpeg.Status.COMPLETED:
print("print():: ffmpeg download completed..")
@@ -278,7 +355,7 @@ class FfmpegQueue(object):
"url": "/ffmpeg/download/list",
}
socketio.emit("notify", data, namespace="/framework", broadcast=True)
socketio.emit("notify", data, namespace="/framework")
refresh_type = "last"
elif args["status"] == SupportFfmpeg.Status.TIME_OVER:
data = {
@@ -286,7 +363,7 @@ class FfmpegQueue(object):
"msg": "시간초과로 중단 되었습니다.<br>" + args["data"]["save_fullpath"],
"url": "/ffmpeg/download/list",
}
socketio.emit("notify", data, namespace="/framework", broadcast=True)
socketio.emit("notify", data, namespace="/framework")
refresh_type = "last"
elif args["status"] == SupportFfmpeg.Status.PF_STOP:
data = {
@@ -294,7 +371,7 @@ class FfmpegQueue(object):
"msg": "PF초과로 중단 되었습니다.<br>" + args["data"]["save_fullpath"],
"url": "/ffmpeg/download/list",
}
socketio.emit("notify", data, namespace="/framework", broadcast=True)
socketio.emit("notify", data, namespace="/framework")
refresh_type = "last"
elif args["status"] == SupportFfmpeg.Status.FORCE_STOP:
data = {
@@ -302,7 +379,7 @@ class FfmpegQueue(object):
"msg": "강제 중단 되었습니다.<br>" + args["data"]["save_fullpath"],
"url": "/ffmpeg/download/list",
}
socketio.emit("notify", data, namespace="/framework", broadcast=True)
socketio.emit("notify", data, namespace="/framework")
refresh_type = "last"
elif args["status"] == SupportFfmpeg.Status.HTTP_FORBIDDEN:
data = {
@@ -310,7 +387,7 @@ class FfmpegQueue(object):
"msg": "403에러로 중단 되었습니다.<br>" + args["data"]["save_fullpath"],
"url": "/ffmpeg/download/list",
}
socketio.emit("notify", data, namespace="/framework", broadcast=True)
socketio.emit("notify", data, namespace="/framework")
refresh_type = "last"
elif args["status"] == SupportFfmpeg.Status.ALREADY_DOWNLOADING:
data = {
@@ -318,14 +395,17 @@ class FfmpegQueue(object):
"msg": "임시파일폴더에 파일이 있습니다.<br>" + args["data"]["temp_fullpath"],
"url": "/ffmpeg/download/list",
}
socketio.emit("notify", data, namespace="/framework", broadcast=True)
socketio.emit("notify", data, namespace="/framework")
refresh_type = "last"
elif args["type"] == "normal":
if args["status"] == SupportFfmpeg.Status.DOWNLOADING:
refresh_type = "status"
# P.logger.info(refresh_type)
# Todo:
self.caller.socketio_callback(refresh_type, args["data"])
if self.caller is not None:
self.caller.socketio_callback(refresh_type, args["data"])
else:
logger.warning("caller is None, cannot send socketio_callback")
# def ffmpeg_listener(self, **arg):
# import ffmpeg

172
lib/hls_downloader.py Normal file
View File

@@ -0,0 +1,172 @@
"""
Custom HLS Downloader for linkkf
- Handles .jpg extension segments that ffmpeg 8.0 rejects
- Downloads segments individually and concatenates them
"""
import os
import requests
import tempfile
import subprocess
import time
from urllib.parse import urljoin
class HlsDownloader:
"""HLS 다운로더 - .jpg 확장자 세그먼트 지원"""
def __init__(self, m3u8_url, output_path, headers=None, callback=None):
self.m3u8_url = m3u8_url
self.output_path = output_path
self.headers = headers or {}
self.callback = callback # 진행 상황 콜백
self.segments = []
self.total_segments = 0
self.downloaded_segments = 0
self.cancelled = False
# 속도 및 시간 계산용
self.start_time = None
self.total_bytes = 0
self.last_speed_update_time = None
self.last_bytes = 0
self.current_speed = 0 # bytes per second
def parse_m3u8(self):
"""m3u8 파일 파싱"""
response = requests.get(self.m3u8_url, headers=self.headers, timeout=30)
content = response.text
base_url = self.m3u8_url.rsplit('/', 1)[0] + '/'
self.segments = []
for line in content.strip().split('\n'):
line = line.strip()
if line and not line.startswith('#'):
# 상대 경로면 절대 경로로 변환
if not line.startswith('http'):
segment_url = urljoin(base_url, line)
else:
segment_url = line
self.segments.append(segment_url)
self.total_segments = len(self.segments)
return self.total_segments
def format_speed(self, bytes_per_sec):
"""속도를 읽기 좋은 형식으로 변환"""
if bytes_per_sec < 1024:
return f"{bytes_per_sec:.0f} B/s"
elif bytes_per_sec < 1024 * 1024:
return f"{bytes_per_sec / 1024:.1f} KB/s"
else:
return f"{bytes_per_sec / (1024 * 1024):.2f} MB/s"
def format_time(self, seconds):
"""시간을 읽기 좋은 형식으로 변환"""
seconds = int(seconds)
if seconds < 60:
return f"{seconds}"
elif seconds < 3600:
mins = seconds // 60
secs = seconds % 60
return f"{mins}{secs}"
else:
hours = seconds // 3600
mins = (seconds % 3600) // 60
return f"{hours}시간 {mins}"
def download(self):
"""세그먼트 다운로드 및 합치기"""
try:
# m3u8 파싱
self.parse_m3u8()
if not self.segments:
return False, "No segments found in m3u8"
self.start_time = time.time()
self.last_speed_update_time = self.start_time
# 임시 디렉토리에 세그먼트 저장
with tempfile.TemporaryDirectory() as temp_dir:
segment_files = []
for i, segment_url in enumerate(self.segments):
if self.cancelled:
return False, "Cancelled"
# 세그먼트 다운로드
segment_path = os.path.join(temp_dir, f"segment_{i:05d}.ts")
try:
response = requests.get(segment_url, headers=self.headers, timeout=60)
response.raise_for_status()
segment_data = response.content
with open(segment_path, 'wb') as f:
f.write(segment_data)
segment_files.append(segment_path)
self.downloaded_segments = i + 1
self.total_bytes += len(segment_data)
# 속도 계산 (1초마다 갱신)
current_time = time.time()
time_diff = current_time - self.last_speed_update_time
if time_diff >= 1.0:
bytes_diff = self.total_bytes - self.last_bytes
self.current_speed = bytes_diff / time_diff
self.last_speed_update_time = current_time
self.last_bytes = self.total_bytes
# 경과 시간 계산
elapsed_time = current_time - self.start_time
# 콜백 호출 (진행 상황 업데이트)
if self.callback:
percent = int((self.downloaded_segments / self.total_segments) * 100)
self.callback(
percent=percent,
current=self.downloaded_segments,
total=self.total_segments,
speed=self.format_speed(self.current_speed),
elapsed=self.format_time(elapsed_time)
)
except Exception as e:
return False, f"Failed to download segment {i}: {e}"
# 세그먼트 합치기 (concat 파일 생성)
concat_file = os.path.join(temp_dir, "concat.txt")
with open(concat_file, 'w') as f:
for seg_file in segment_files:
f.write(f"file '{seg_file}'\n")
# 출력 디렉토리 생성
output_dir = os.path.dirname(self.output_path)
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir)
# ffmpeg로 합치기
cmd = [
'ffmpeg', '-y',
'-f', 'concat',
'-safe', '0',
'-i', concat_file,
'-c', 'copy',
self.output_path
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
if result.returncode != 0:
return False, f"FFmpeg concat failed: {result.stderr}"
return True, "Download completed"
except Exception as e:
return False, f"Download error: {e}"
def cancel(self):
"""다운로드 취소"""
self.cancelled = True