feat: Add subtitle download and VTT to SRT conversion, refactor Linkkf queue to class-level, improve video URL extraction, and introduce downloader factory.
This commit is contained in:
@@ -177,6 +177,24 @@ class FfmpegQueue(object):
|
||||
if entity.cancel:
|
||||
continue
|
||||
|
||||
# [Lazy Extraction] 다운로드 시작 전 무거운 분석 로직 수행
|
||||
try:
|
||||
entity.ffmpeg_status = 1 # ANALYZING
|
||||
entity.ffmpeg_status_kor = "분석 중"
|
||||
entity.refresh_status()
|
||||
|
||||
if hasattr(entity, 'prepare_extra'):
|
||||
logger.info(f"Starting background extraction: {entity.info.get('title')}")
|
||||
entity.prepare_extra()
|
||||
logger.info(f"Extraction finished for: {entity.info.get('title')}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to prepare entity: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
entity.ffmpeg_status = -1
|
||||
entity.ffmpeg_status_kor = "분석 실패"
|
||||
entity.refresh_status()
|
||||
continue
|
||||
|
||||
# from .logic_ani24 import LogicAni24
|
||||
# entity.url = LogicAni24.get_video_url(entity.info['code'])
|
||||
video_url = entity.get_video_url()
|
||||
@@ -258,226 +276,96 @@ class FfmpegQueue(object):
|
||||
logger.info(ffmpeg_cmd)
|
||||
logger.info(f"=== END COMMAND ===")
|
||||
|
||||
# m3u8 URL인 경우 다운로드 방법 설정에 따라 분기
|
||||
if video_url.endswith('.m3u8') or 'master.txt' in video_url or 'gcdn.app' in video_url:
|
||||
# 다운로드 방법 및 스레드 설정 확인
|
||||
download_method = P.ModelSetting.get(f"{self.name}_download_method")
|
||||
download_threads = P.ModelSetting.get_int(f"{self.name}_download_threads")
|
||||
if not download_threads:
|
||||
download_threads = 16
|
||||
# 다운로드 시작 전 카운트 증가
|
||||
self.current_ffmpeg_count += 1
|
||||
logger.info(f"Download started, current_ffmpeg_count: {self.current_ffmpeg_count}/{self.max_ffmpeg_count}")
|
||||
|
||||
# 별도 스레드에서 다운로드 실행 (동시 다운로드 지원)
|
||||
def run_download(downloader_self, entity_ref, output_file_ref):
|
||||
method = P.ModelSetting.get(f"{downloader_self.name}_download_method")
|
||||
|
||||
# cdndania.com 감지 로직 제거 - 이제 설정에서 직접 선택
|
||||
# 사용자가 ohli24_download_method 설정에서 cdndania 선택 가능
|
||||
# if getattr(entity, 'need_special_downloader', False) or 'cdndania.com' in video_url or 'michealcdn.com' in video_url:
|
||||
# logger.info(f"Detected special CDN requirement - using Optimized CdndaniaDownloader")
|
||||
# download_method = "cdndania"
|
||||
pass # 이제 설정값(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_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
|
||||
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()
|
||||
|
||||
if method == "cdndania":
|
||||
# cdndania.com 전용 다운로더 사용 (curl_cffi 세션 기반)
|
||||
from .cdndania_downloader import CdndaniaDownloader
|
||||
logger.info("Using CdndaniaDownloader (curl_cffi session-based)...")
|
||||
# 엔티티에서 원본 iframe_src 가져오기
|
||||
_iframe_src = getattr(entity_ref, 'iframe_src', None)
|
||||
if not _iframe_src:
|
||||
# 폴백: headers의 Referer에서 가져오기
|
||||
_iframe_src = getattr(entity_ref, 'headers', {}).get('Referer', video_url)
|
||||
# 슬롯 조기 반환을 위한 콜백
|
||||
slot_released = [False]
|
||||
def release_slot():
|
||||
if not slot_released[0]:
|
||||
downloader_self.current_ffmpeg_count -= 1
|
||||
slot_released[0] = True
|
||||
logger.info(f"Download slot released early (Network finished), current_ffmpeg_count: {downloader_self.current_ffmpeg_count}/{downloader_self.max_ffmpeg_count}")
|
||||
|
||||
logger.info(f"CdndaniaDownloader iframe_src: {_iframe_src}")
|
||||
downloader = CdndaniaDownloader(
|
||||
iframe_src=_iframe_src,
|
||||
output_path=output_file_ref,
|
||||
referer_url="https://ani.ohli24.com/",
|
||||
callback=progress_callback,
|
||||
proxy=_proxy,
|
||||
threads=download_threads,
|
||||
on_download_finished=release_slot # 조기 반환 콜백 전달
|
||||
)
|
||||
elif method == "ytdlp" or method == "aria2c":
|
||||
# yt-dlp 사용 (aria2c 옵션 포함)
|
||||
# yt-dlp는 내부적으로 병합 과정을 포함하므로 조기 반환이 어려울 수 있음 (추후 지원 고려)
|
||||
slot_released = [False]
|
||||
from .ytdlp_downloader import YtdlpDownloader
|
||||
logger.info(f"Using yt-dlp downloader (method={method})...")
|
||||
# 엔티티에서 쿠키 파일 가져오기 (있는 경우)
|
||||
_cookies_file = getattr(entity_ref, 'cookies_file', None)
|
||||
downloader = YtdlpDownloader(
|
||||
url=video_url,
|
||||
output_path=output_file_ref,
|
||||
headers=headers_ref,
|
||||
callback=progress_callback,
|
||||
proxy=_proxy,
|
||||
cookies_file=_cookies_file,
|
||||
use_aria2c=(method == "aria2c"),
|
||||
threads=download_threads
|
||||
)
|
||||
def progress_callback(percent, current, total, speed="", elapsed=""):
|
||||
entity_ref.ffmpeg_status = 5 # DOWNLOADING
|
||||
if method in ["ytdlp", "aria2c"]:
|
||||
entity_ref.ffmpeg_status_kor = f"다운로드중 (yt-dlp) {percent}%"
|
||||
elif method in ["ffmpeg", "normal"]:
|
||||
# SupportFfmpeg handles its own kor status via listener
|
||||
pass
|
||||
else:
|
||||
slot_released = [False]
|
||||
# 기본: 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,
|
||||
proxy=_proxy
|
||||
)
|
||||
entity_ref.ffmpeg_status_kor = f"다운로드중 ({percent}%)"
|
||||
|
||||
# 다운로더 인스턴스를 entity에 저장 (취소 시 사용)
|
||||
entity_ref.downloader = downloader
|
||||
entity_ref.ffmpeg_percent = percent
|
||||
entity_ref.current_speed = speed
|
||||
entity_ref.download_time = elapsed
|
||||
entity_ref.refresh_status()
|
||||
|
||||
# Factory를 통해 다운로더 인스턴스 획득
|
||||
downloader = entity_ref.get_downloader(
|
||||
video_url=video_url,
|
||||
output_file=output_file_ref,
|
||||
callback=progress_callback,
|
||||
callback_function=downloader_self.callback_function
|
||||
)
|
||||
|
||||
if not downloader:
|
||||
logger.error(f"Failed to create downloader for method: {method}")
|
||||
downloader_self.current_ffmpeg_count -= 1
|
||||
entity_ref.ffmpeg_status = 4 # ERROR
|
||||
entity_ref.ffmpeg_status_kor = "다운로더 생성 실패"
|
||||
entity_ref.refresh_status()
|
||||
return
|
||||
|
||||
entity_ref.downloader = downloader
|
||||
|
||||
# 조기 취소 체크
|
||||
if entity_ref.cancel:
|
||||
downloader.cancel()
|
||||
entity_ref.ffmpeg_status_kor = "취소됨"
|
||||
entity_ref.refresh_status()
|
||||
downloader_self.current_ffmpeg_count -= 1
|
||||
return
|
||||
|
||||
# 다운로드 실행 (blocking)
|
||||
logger.info(f"Executing downloader[{method}] for {output_file_ref}")
|
||||
success = downloader.download()
|
||||
|
||||
# 슬롯 반환
|
||||
downloader_self.current_ffmpeg_count -= 1
|
||||
logger.info(f"Download finished ({'SUCCESS' if success else 'FAILED'}), slot released. count: {downloader_self.current_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()
|
||||
|
||||
# cancel 상태 체크
|
||||
# 자막 다운로드 (vtt_url이 있는 경우)
|
||||
vtt_url = getattr(entity_ref, 'vtt', None)
|
||||
if vtt_url:
|
||||
from .util import Util
|
||||
Util.download_subtitle(vtt_url, output_file_ref, headers=entity_ref.headers)
|
||||
else:
|
||||
# 취소 혹은 실패 처리
|
||||
if entity_ref.cancel:
|
||||
downloader.cancel()
|
||||
entity_ref.ffmpeg_status = -1
|
||||
entity_ref.ffmpeg_status_kor = "취소됨"
|
||||
entity_ref.refresh_status()
|
||||
if not slot_released[0]:
|
||||
downloader_self.current_ffmpeg_count -= 1
|
||||
return
|
||||
|
||||
success, message = downloader.download()
|
||||
|
||||
# 다운로드 완료 후 카운트 감소 (이미 반환되었으면 스킵)
|
||||
if not slot_released[0]:
|
||||
downloader_self.current_ffmpeg_count -= 1
|
||||
logger.info(f"Download finished (Slot released normally), 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"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}")
|
||||
logger.info(f"Download cancelled by user: {output_file_ref}")
|
||||
else:
|
||||
# 취소된 경우와 실패를 구분
|
||||
if entity_ref.cancel or "Cancelled" in message:
|
||||
entity_ref.ffmpeg_status = -1
|
||||
entity_ref.ffmpeg_status_kor = "취소됨"
|
||||
entity_ref.ffmpeg_percent = 0
|
||||
logger.info(f"Download cancelled: {output_file_ref}")
|
||||
else:
|
||||
entity_ref.ffmpeg_status = -1
|
||||
entity_ref.ffmpeg_status_kor = f"실패"
|
||||
logger.error(f"Download failed: {message}")
|
||||
entity_ref.refresh_status()
|
||||
|
||||
# 스레드 시작
|
||||
download_thread = threading.Thread(
|
||||
target=run_download,
|
||||
args=(self, entity, output_file, _headers, download_method)
|
||||
)
|
||||
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,
|
||||
proxy=_proxy,
|
||||
)
|
||||
#
|
||||
# todo: 임시로 start() 중지
|
||||
logger.info("Calling ffmpeg.start()...")
|
||||
ffmpeg.start()
|
||||
logger.info("ffmpeg.start() returned")
|
||||
|
||||
self.download_queue.task_done()
|
||||
entity_ref.ffmpeg_status = -1
|
||||
entity_ref.ffmpeg_status_kor = "실패"
|
||||
logger.error(f"Download failed: {output_file_ref}")
|
||||
entity_ref.refresh_status()
|
||||
|
||||
# 스레드 시작
|
||||
download_thread = threading.Thread(
|
||||
target=run_download,
|
||||
args=(self, entity, output_file)
|
||||
)
|
||||
download_thread.daemon = True
|
||||
download_thread.start()
|
||||
|
||||
self.download_queue.task_done()
|
||||
|
||||
|
||||
except Exception as exception:
|
||||
@@ -538,7 +426,7 @@ class FfmpegQueue(object):
|
||||
elif args["status"] == SupportFfmpeg.Status.COMPLETED:
|
||||
print("print():: ffmpeg download completed..")
|
||||
logger.debug("ffmpeg download completed......")
|
||||
entity.download_completed()
|
||||
# entity.download_completed() # Removed! Handled in run_download thread
|
||||
data = {
|
||||
"type": "success",
|
||||
"msg": "다운로드가 완료 되었습니다.<br>" + args["data"]["save_fullpath"],
|
||||
|
||||
Reference in New Issue
Block a user