또 많은 수정을 했슴.
This commit is contained in:
@@ -205,6 +205,10 @@ class FfmpegQueue(object):
|
||||
logger.info(f"save_path: {dirname}, filename: {filename}")
|
||||
logger.info(f"headers: {_headers}")
|
||||
|
||||
# 자막 URL 로그
|
||||
vtt_url = getattr(entity, 'vtt', None)
|
||||
logger.info(f"Subtitle URL (vtt): {vtt_url}")
|
||||
|
||||
# 터미널에서 수동 테스트용 ffmpeg 명령어
|
||||
output_file = os.path.join(dirname, filename)
|
||||
referer = _headers.get("Referer", "") if _headers else ""
|
||||
@@ -214,33 +218,51 @@ class FfmpegQueue(object):
|
||||
logger.info(ffmpeg_cmd)
|
||||
logger.info(f"=== END COMMAND ===")
|
||||
|
||||
# m3u8 URL인 경우 커스텀 HLS 다운로더 사용 (ffmpeg 8.0 .jpg 확장자 문제 우회)
|
||||
# m3u8 URL인 경우 다운로드 방법 설정에 따라 분기
|
||||
if video_url.endswith('.m3u8'):
|
||||
logger.info("Using custom HLS downloader for m3u8 URL...")
|
||||
from .hls_downloader import HlsDownloader
|
||||
# 다운로드 방법 설정 확인
|
||||
download_method = P.ModelSetting.get(f"{self.name}_download_method")
|
||||
logger.info(f"Download method: {download_method}")
|
||||
|
||||
# 다운로드 시작 전 카운트 증가
|
||||
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 run_download(downloader_self, entity_ref, output_file_ref, headers_ref, method):
|
||||
def progress_callback(percent, current, total, speed="", elapsed=""):
|
||||
entity_ref.ffmpeg_status = 5 # DOWNLOADING
|
||||
entity_ref.ffmpeg_status_kor = f"다운로드중 ({current}/{total})"
|
||||
if method == "ytdlp":
|
||||
entity_ref.ffmpeg_status_kor = f"다운로드중 (yt-dlp) {percent}%"
|
||||
else:
|
||||
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
|
||||
)
|
||||
if method == "ytdlp":
|
||||
# yt-dlp 사용
|
||||
from .ytdlp_downloader import YtdlpDownloader
|
||||
logger.info("Using yt-dlp downloader...")
|
||||
downloader = YtdlpDownloader(
|
||||
url=video_url,
|
||||
output_path=output_file_ref,
|
||||
headers=headers_ref,
|
||||
callback=progress_callback
|
||||
)
|
||||
else:
|
||||
# 기본: HLS 다운로더 사용
|
||||
from .hls_downloader import HlsDownloader
|
||||
logger.info("Using custom HLS downloader for m3u8 URL...")
|
||||
downloader = HlsDownloader(
|
||||
m3u8_url=video_url,
|
||||
output_path=output_file_ref,
|
||||
headers=headers_ref,
|
||||
callback=progress_callback
|
||||
)
|
||||
|
||||
success, message = hls_downloader.download()
|
||||
success, message = downloader.download()
|
||||
|
||||
# 다운로드 완료 후 카운트 감소
|
||||
downloader_self.current_ffmpeg_count -= 1
|
||||
@@ -252,17 +274,75 @@ class FfmpegQueue(object):
|
||||
entity_ref.ffmpeg_percent = 100
|
||||
entity_ref.download_completed()
|
||||
entity_ref.refresh_status()
|
||||
logger.info(f"HLS download completed: {output_file_ref}")
|
||||
logger.info(f"Download completed: {output_file_ref}")
|
||||
|
||||
# 자막 파일 다운로드 (vtt_url이 있는 경우)
|
||||
vtt_url = getattr(entity_ref, 'vtt', None)
|
||||
if vtt_url:
|
||||
try:
|
||||
import requests
|
||||
# 자막 파일 경로 생성 (비디오 파일명.srt)
|
||||
video_basename = os.path.splitext(output_file_ref)[0]
|
||||
srt_path = video_basename + ".srt"
|
||||
|
||||
logger.info(f"Downloading subtitle from: {vtt_url}")
|
||||
sub_response = requests.get(vtt_url, headers=headers_ref, timeout=30)
|
||||
|
||||
if sub_response.status_code == 200:
|
||||
vtt_content = sub_response.text
|
||||
|
||||
# VTT를 SRT로 변환 (간단한 변환)
|
||||
srt_content = vtt_content
|
||||
if vtt_content.startswith("WEBVTT"):
|
||||
# WEBVTT 헤더 제거
|
||||
lines = vtt_content.split("\n")
|
||||
srt_lines = []
|
||||
cue_index = 1
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i].strip()
|
||||
# WEBVTT, NOTE, STYLE 등 메타데이터 스킵
|
||||
if line.startswith("WEBVTT") or line.startswith("NOTE") or line.startswith("STYLE"):
|
||||
i += 1
|
||||
continue
|
||||
# 빈 줄 스킵
|
||||
if not line:
|
||||
i += 1
|
||||
continue
|
||||
# 타임코드 라인 (00:00:00.000 --> 00:00:00.000)
|
||||
if "-->" in line:
|
||||
# VTT 타임코드를 SRT 형식으로 변환 (. -> ,)
|
||||
srt_timecode = line.replace(".", ",")
|
||||
srt_lines.append(str(cue_index))
|
||||
srt_lines.append(srt_timecode)
|
||||
cue_index += 1
|
||||
i += 1
|
||||
# 자막 텍스트 읽기
|
||||
while i < len(lines) and lines[i].strip():
|
||||
srt_lines.append(lines[i].rstrip())
|
||||
i += 1
|
||||
srt_lines.append("")
|
||||
else:
|
||||
i += 1
|
||||
srt_content = "\n".join(srt_lines)
|
||||
|
||||
with open(srt_path, "w", encoding="utf-8") as f:
|
||||
f.write(srt_content)
|
||||
logger.info(f"Subtitle saved: {srt_path}")
|
||||
else:
|
||||
logger.warning(f"Subtitle download failed: HTTP {sub_response.status_code}")
|
||||
except Exception as sub_err:
|
||||
logger.error(f"Subtitle download error: {sub_err}")
|
||||
else:
|
||||
entity_ref.ffmpeg_status = -1
|
||||
entity_ref.ffmpeg_status_kor = f"실패: {message}"
|
||||
entity_ref.refresh_status()
|
||||
logger.error(f"HLS download failed: {message}")
|
||||
logger.error(f"Download failed: {message}")
|
||||
|
||||
# 스레드 시작
|
||||
download_thread = threading.Thread(
|
||||
target=run_hls_download,
|
||||
args=(self, entity, output_file, _headers)
|
||||
target=run_download,
|
||||
args=(self, entity, output_file, _headers, download_method)
|
||||
)
|
||||
download_thread.daemon = True
|
||||
download_thread.start()
|
||||
@@ -443,14 +523,20 @@ class FfmpegQueue(object):
|
||||
|
||||
def add_queue(self, entity):
|
||||
try:
|
||||
# entity = QueueEntity.create(info)
|
||||
# if entity is not None:
|
||||
# LogicQueue.download_queue.put(entity)
|
||||
# return True
|
||||
entity.entity_id = self.static_index
|
||||
self.static_index += 1
|
||||
self.entity_list.append(entity)
|
||||
self.download_queue.put(entity)
|
||||
|
||||
# 소켓IO로 추가 이벤트 전송
|
||||
try:
|
||||
from framework import socketio
|
||||
namespace = f"/{self.P.package_name}/{self.name}/queue"
|
||||
socketio.emit("add", entity.as_dict(), namespace=namespace)
|
||||
logger.debug(f"Emitted 'add' event for entity {entity.entity_id}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Socket emit error (non-critical): {e}")
|
||||
|
||||
return True
|
||||
except Exception as exception:
|
||||
self.P.logger.error("Exception:%s", exception)
|
||||
@@ -528,7 +614,7 @@ class FfmpegQueue(object):
|
||||
|
||||
def get_entity_list(self):
|
||||
ret = []
|
||||
P.logger.debug(self)
|
||||
#P.logger.debug(self)
|
||||
for x in self.entity_list:
|
||||
tmp = x.as_dict()
|
||||
ret.append(tmp)
|
||||
|
||||
157
lib/ytdlp_downloader.py
Normal file
157
lib/ytdlp_downloader.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
yt-dlp Downloader for linkkf
|
||||
- Uses yt-dlp as Python module or subprocess
|
||||
- Same interface as HlsDownloader for easy switching
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import re
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class YtdlpDownloader:
|
||||
"""yt-dlp 기반 다운로더"""
|
||||
|
||||
def __init__(self, url, output_path, headers=None, callback=None):
|
||||
self.url = url
|
||||
self.output_path = output_path
|
||||
self.headers = headers or {}
|
||||
self.callback = callback # 진행 상황 콜백
|
||||
self.cancelled = False
|
||||
self.process = None
|
||||
self.error_output = [] # 에러 메시지 저장
|
||||
|
||||
# 속도 및 시간 계산용
|
||||
self.start_time = None
|
||||
self.current_speed = ""
|
||||
self.elapsed_time = ""
|
||||
self.percent = 0
|
||||
|
||||
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 format_speed(self, bytes_per_sec):
|
||||
"""속도를 읽기 좋은 형식으로 변환"""
|
||||
if bytes_per_sec is None:
|
||||
return ""
|
||||
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 download(self):
|
||||
"""yt-dlp Python 모듈로 다운로드 수행"""
|
||||
try:
|
||||
import yt_dlp
|
||||
except ImportError:
|
||||
return False, "yt-dlp를 찾을 수 없습니다. pip install yt-dlp 로 설치해주세요."
|
||||
|
||||
try:
|
||||
self.start_time = time.time()
|
||||
|
||||
# 출력 디렉토리 생성
|
||||
output_dir = os.path.dirname(self.output_path)
|
||||
if output_dir and not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
# 진행률 콜백
|
||||
def progress_hook(d):
|
||||
if self.cancelled:
|
||||
raise Exception("Cancelled")
|
||||
|
||||
if d['status'] == 'downloading':
|
||||
# 진행률 추출
|
||||
total = d.get('total_bytes') or d.get('total_bytes_estimate') or 0
|
||||
downloaded = d.get('downloaded_bytes', 0)
|
||||
speed = d.get('speed', 0)
|
||||
|
||||
if total > 0:
|
||||
self.percent = (downloaded / total) * 100
|
||||
|
||||
self.current_speed = self.format_speed(speed) if speed else ""
|
||||
|
||||
if self.start_time:
|
||||
elapsed = time.time() - self.start_time
|
||||
self.elapsed_time = self.format_time(elapsed)
|
||||
|
||||
# 콜백 호출
|
||||
if self.callback:
|
||||
self.callback(
|
||||
percent=int(self.percent),
|
||||
current=int(self.percent),
|
||||
total=100,
|
||||
speed=self.current_speed,
|
||||
elapsed=self.elapsed_time
|
||||
)
|
||||
|
||||
elif d['status'] == 'finished':
|
||||
logger.info(f"yt-dlp download finished: {d.get('filename', '')}")
|
||||
|
||||
# yt-dlp 옵션 설정
|
||||
ydl_opts = {
|
||||
'outtmpl': self.output_path,
|
||||
'progress_hooks': [progress_hook],
|
||||
'quiet': False,
|
||||
'no_warnings': False,
|
||||
'noprogress': False,
|
||||
}
|
||||
|
||||
# 헤더 추가
|
||||
http_headers = {}
|
||||
if self.headers:
|
||||
if self.headers.get('Referer'):
|
||||
http_headers['Referer'] = self.headers['Referer']
|
||||
if self.headers.get('User-Agent'):
|
||||
http_headers['User-Agent'] = self.headers['User-Agent']
|
||||
|
||||
if http_headers:
|
||||
ydl_opts['http_headers'] = http_headers
|
||||
|
||||
logger.info(f"yt-dlp downloading: {self.url}")
|
||||
logger.info(f"Output path: {self.output_path}")
|
||||
logger.info(f"Headers: {http_headers}")
|
||||
|
||||
# 다운로드 실행
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([self.url])
|
||||
|
||||
# 파일 존재 확인
|
||||
if os.path.exists(self.output_path):
|
||||
return True, "Download completed"
|
||||
else:
|
||||
# yt-dlp가 확장자를 변경했을 수 있음
|
||||
base_name = os.path.splitext(self.output_path)[0]
|
||||
for ext in ['.mp4', '.mkv', '.webm', '.ts']:
|
||||
possible_path = base_name + ext
|
||||
if os.path.exists(possible_path):
|
||||
if possible_path != self.output_path:
|
||||
os.rename(possible_path, self.output_path)
|
||||
return True, "Download completed"
|
||||
|
||||
return False, "Output file not found"
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"yt-dlp download error: {error_msg}")
|
||||
return False, f"yt-dlp 실패: {error_msg}"
|
||||
|
||||
def cancel(self):
|
||||
"""다운로드 취소"""
|
||||
self.cancelled = True
|
||||
Reference in New Issue
Block a user