많은 수정중
This commit is contained in:
@@ -43,6 +43,8 @@ class FfmpegQueueEntity(abc.ABCMeta("ABC", (object,), {"__slots__": ()})):
|
|||||||
self.filepath = None
|
self.filepath = None
|
||||||
self.quality = None
|
self.quality = None
|
||||||
self.headers = None
|
self.headers = None
|
||||||
|
self.current_speed = "" # 다운로드 속도
|
||||||
|
self.download_time = "" # 경과 시간
|
||||||
# FfmpegQueueEntity.static_index += 1
|
# FfmpegQueueEntity.static_index += 1
|
||||||
# FfmpegQueueEntity.entity_list.append(self)
|
# FfmpegQueueEntity.entity_list.append(self)
|
||||||
|
|
||||||
@@ -194,24 +196,99 @@ class FfmpegQueue(object):
|
|||||||
|
|
||||||
# SupportFfmpeg 초기화
|
# SupportFfmpeg 초기화
|
||||||
self.support_init()
|
self.support_init()
|
||||||
|
# entity.headers가 있으면 우선 사용, 없으면 caller.headers 사용
|
||||||
_headers = entity.headers
|
_headers = entity.headers
|
||||||
if self.caller is not None:
|
if _headers is None and self.caller is not None:
|
||||||
_headers = self.caller.headers
|
_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:
|
except Exception as exception:
|
||||||
self.P.logger.error("Exception:%s", exception)
|
self.P.logger.error("Exception:%s", exception)
|
||||||
@@ -236,19 +313,19 @@ class FfmpegQueue(object):
|
|||||||
+ args["data"]["save_fullpath"],
|
+ args["data"]["save_fullpath"],
|
||||||
"url": "/ffmpeg/download/list",
|
"url": "/ffmpeg/download/list",
|
||||||
}
|
}
|
||||||
socketio.emit("notify", data, namespace="/framework", broadcast=True)
|
socketio.emit("notify", data, namespace="/framework")
|
||||||
refresh_type = "add"
|
refresh_type = "add"
|
||||||
elif args["type"] == "last":
|
elif args["type"] == "last":
|
||||||
if args["status"] == SupportFfmpeg.Status.WRONG_URL:
|
if args["status"] == SupportFfmpeg.Status.WRONG_URL:
|
||||||
data = {"type": "warning", "msg": "잘못된 URL입니다"}
|
data = {"type": "warning", "msg": "잘못된 URL입니다"}
|
||||||
socketio.emit("notify", data, namespace="/framework", broadcast=True)
|
socketio.emit("notify", data, namespace="/framework")
|
||||||
refresh_type = "add"
|
refresh_type = "add"
|
||||||
elif args["status"] == SupportFfmpeg.Status.WRONG_DIRECTORY:
|
elif args["status"] == SupportFfmpeg.Status.WRONG_DIRECTORY:
|
||||||
data = {
|
data = {
|
||||||
"type": "warning",
|
"type": "warning",
|
||||||
"msg": "잘못된 디렉토리입니다.<br>" + args["data"]["save_fullpath"],
|
"msg": "잘못된 디렉토리입니다.<br>" + args["data"]["save_fullpath"],
|
||||||
}
|
}
|
||||||
socketio.emit("notify", data, namespace="/framework", broadcast=True)
|
socketio.emit("notify", data, namespace="/framework")
|
||||||
refresh_type = "add"
|
refresh_type = "add"
|
||||||
elif (
|
elif (
|
||||||
args["status"] == SupportFfmpeg.Status.ERROR
|
args["status"] == SupportFfmpeg.Status.ERROR
|
||||||
@@ -258,7 +335,7 @@ class FfmpegQueue(object):
|
|||||||
"type": "warning",
|
"type": "warning",
|
||||||
"msg": "다운로드 시작 실패.<br>" + args["data"]["save_fullpath"],
|
"msg": "다운로드 시작 실패.<br>" + args["data"]["save_fullpath"],
|
||||||
}
|
}
|
||||||
socketio.emit("notify", data, namespace="/framework", broadcast=True)
|
socketio.emit("notify", data, namespace="/framework")
|
||||||
refresh_type = "add"
|
refresh_type = "add"
|
||||||
elif args["status"] == SupportFfmpeg.Status.USER_STOP:
|
elif args["status"] == SupportFfmpeg.Status.USER_STOP:
|
||||||
data = {
|
data = {
|
||||||
@@ -266,7 +343,7 @@ class FfmpegQueue(object):
|
|||||||
"msg": "다운로드가 중지 되었습니다.<br>" + args["data"]["save_fullpath"],
|
"msg": "다운로드가 중지 되었습니다.<br>" + args["data"]["save_fullpath"],
|
||||||
"url": "/ffmpeg/download/list",
|
"url": "/ffmpeg/download/list",
|
||||||
}
|
}
|
||||||
socketio.emit("notify", data, namespace="/framework", broadcast=True)
|
socketio.emit("notify", data, namespace="/framework")
|
||||||
refresh_type = "last"
|
refresh_type = "last"
|
||||||
elif args["status"] == SupportFfmpeg.Status.COMPLETED:
|
elif args["status"] == SupportFfmpeg.Status.COMPLETED:
|
||||||
print("print():: ffmpeg download completed..")
|
print("print():: ffmpeg download completed..")
|
||||||
@@ -278,7 +355,7 @@ class FfmpegQueue(object):
|
|||||||
"url": "/ffmpeg/download/list",
|
"url": "/ffmpeg/download/list",
|
||||||
}
|
}
|
||||||
|
|
||||||
socketio.emit("notify", data, namespace="/framework", broadcast=True)
|
socketio.emit("notify", data, namespace="/framework")
|
||||||
refresh_type = "last"
|
refresh_type = "last"
|
||||||
elif args["status"] == SupportFfmpeg.Status.TIME_OVER:
|
elif args["status"] == SupportFfmpeg.Status.TIME_OVER:
|
||||||
data = {
|
data = {
|
||||||
@@ -286,7 +363,7 @@ class FfmpegQueue(object):
|
|||||||
"msg": "시간초과로 중단 되었습니다.<br>" + args["data"]["save_fullpath"],
|
"msg": "시간초과로 중단 되었습니다.<br>" + args["data"]["save_fullpath"],
|
||||||
"url": "/ffmpeg/download/list",
|
"url": "/ffmpeg/download/list",
|
||||||
}
|
}
|
||||||
socketio.emit("notify", data, namespace="/framework", broadcast=True)
|
socketio.emit("notify", data, namespace="/framework")
|
||||||
refresh_type = "last"
|
refresh_type = "last"
|
||||||
elif args["status"] == SupportFfmpeg.Status.PF_STOP:
|
elif args["status"] == SupportFfmpeg.Status.PF_STOP:
|
||||||
data = {
|
data = {
|
||||||
@@ -294,7 +371,7 @@ class FfmpegQueue(object):
|
|||||||
"msg": "PF초과로 중단 되었습니다.<br>" + args["data"]["save_fullpath"],
|
"msg": "PF초과로 중단 되었습니다.<br>" + args["data"]["save_fullpath"],
|
||||||
"url": "/ffmpeg/download/list",
|
"url": "/ffmpeg/download/list",
|
||||||
}
|
}
|
||||||
socketio.emit("notify", data, namespace="/framework", broadcast=True)
|
socketio.emit("notify", data, namespace="/framework")
|
||||||
refresh_type = "last"
|
refresh_type = "last"
|
||||||
elif args["status"] == SupportFfmpeg.Status.FORCE_STOP:
|
elif args["status"] == SupportFfmpeg.Status.FORCE_STOP:
|
||||||
data = {
|
data = {
|
||||||
@@ -302,7 +379,7 @@ class FfmpegQueue(object):
|
|||||||
"msg": "강제 중단 되었습니다.<br>" + args["data"]["save_fullpath"],
|
"msg": "강제 중단 되었습니다.<br>" + args["data"]["save_fullpath"],
|
||||||
"url": "/ffmpeg/download/list",
|
"url": "/ffmpeg/download/list",
|
||||||
}
|
}
|
||||||
socketio.emit("notify", data, namespace="/framework", broadcast=True)
|
socketio.emit("notify", data, namespace="/framework")
|
||||||
refresh_type = "last"
|
refresh_type = "last"
|
||||||
elif args["status"] == SupportFfmpeg.Status.HTTP_FORBIDDEN:
|
elif args["status"] == SupportFfmpeg.Status.HTTP_FORBIDDEN:
|
||||||
data = {
|
data = {
|
||||||
@@ -310,7 +387,7 @@ class FfmpegQueue(object):
|
|||||||
"msg": "403에러로 중단 되었습니다.<br>" + args["data"]["save_fullpath"],
|
"msg": "403에러로 중단 되었습니다.<br>" + args["data"]["save_fullpath"],
|
||||||
"url": "/ffmpeg/download/list",
|
"url": "/ffmpeg/download/list",
|
||||||
}
|
}
|
||||||
socketio.emit("notify", data, namespace="/framework", broadcast=True)
|
socketio.emit("notify", data, namespace="/framework")
|
||||||
refresh_type = "last"
|
refresh_type = "last"
|
||||||
elif args["status"] == SupportFfmpeg.Status.ALREADY_DOWNLOADING:
|
elif args["status"] == SupportFfmpeg.Status.ALREADY_DOWNLOADING:
|
||||||
data = {
|
data = {
|
||||||
@@ -318,14 +395,17 @@ class FfmpegQueue(object):
|
|||||||
"msg": "임시파일폴더에 파일이 있습니다.<br>" + args["data"]["temp_fullpath"],
|
"msg": "임시파일폴더에 파일이 있습니다.<br>" + args["data"]["temp_fullpath"],
|
||||||
"url": "/ffmpeg/download/list",
|
"url": "/ffmpeg/download/list",
|
||||||
}
|
}
|
||||||
socketio.emit("notify", data, namespace="/framework", broadcast=True)
|
socketio.emit("notify", data, namespace="/framework")
|
||||||
refresh_type = "last"
|
refresh_type = "last"
|
||||||
elif args["type"] == "normal":
|
elif args["type"] == "normal":
|
||||||
if args["status"] == SupportFfmpeg.Status.DOWNLOADING:
|
if args["status"] == SupportFfmpeg.Status.DOWNLOADING:
|
||||||
refresh_type = "status"
|
refresh_type = "status"
|
||||||
# P.logger.info(refresh_type)
|
# P.logger.info(refresh_type)
|
||||||
# Todo:
|
# 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):
|
# def ffmpeg_listener(self, **arg):
|
||||||
# import ffmpeg
|
# import ffmpeg
|
||||||
|
|||||||
172
lib/hls_downloader.py
Normal file
172
lib/hls_downloader.py
Normal 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
|
||||||
364
mod_linkkf.py
364
mod_linkkf.py
@@ -199,28 +199,141 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif sub == "add_queue":
|
elif sub == "add_queue":
|
||||||
|
logger.info("========= add_queue START =========")
|
||||||
logger.debug("linkkf add_queue routine ===============")
|
logger.debug("linkkf add_queue routine ===============")
|
||||||
ret = {}
|
ret = {}
|
||||||
info = json.loads(request.form["data"])
|
try:
|
||||||
logger.info(f"info:: {info}")
|
form_data = request.form.get("data")
|
||||||
ret["ret"] = self.add(info)
|
if not form_data:
|
||||||
|
logger.error(f"No data in form. Form keys: {list(request.form.keys())}")
|
||||||
|
ret["ret"] = "error"
|
||||||
|
ret["log"] = "No data received"
|
||||||
|
return jsonify(ret)
|
||||||
|
info = json.loads(form_data)
|
||||||
|
logger.info(f"info:: {info}")
|
||||||
|
ret["ret"] = self.add(info)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"add_queue error: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
ret["ret"] = "error"
|
||||||
|
ret["log"] = str(e)
|
||||||
return jsonify(ret)
|
return jsonify(ret)
|
||||||
elif sub == "entity_list":
|
elif sub == "entity_list":
|
||||||
pass
|
ret = {"list": self.queue.get_entity_list() if self.queue else []}
|
||||||
|
return jsonify(ret)
|
||||||
elif sub == "queue_command":
|
elif sub == "queue_command":
|
||||||
pass
|
cmd = request.form.get("cmd", "")
|
||||||
|
entity_id = request.form.get("entity_id", "")
|
||||||
|
if self.queue:
|
||||||
|
ret = self.queue.command(cmd, int(entity_id) if entity_id else 0)
|
||||||
|
else:
|
||||||
|
ret = {"ret": "error", "log": "Queue not initialized"}
|
||||||
|
return jsonify(ret)
|
||||||
elif sub == "add_queue_checked_list":
|
elif sub == "add_queue_checked_list":
|
||||||
pass
|
return jsonify({"ret": "not_implemented"})
|
||||||
elif sub == "web_list":
|
elif sub == "web_list":
|
||||||
pass
|
return jsonify({"ret": "not_implemented"})
|
||||||
elif sub == "db_remove":
|
elif sub == "db_remove":
|
||||||
pass
|
return jsonify({"ret": "not_implemented"})
|
||||||
elif sub == "add_whitelist":
|
elif sub == "add_whitelist":
|
||||||
pass
|
return jsonify({"ret": "not_implemented"})
|
||||||
|
elif sub == "command":
|
||||||
|
# command = queue_command와 동일
|
||||||
|
cmd = request.form.get("cmd", "")
|
||||||
|
entity_id = request.form.get("entity_id", "")
|
||||||
|
|
||||||
|
logger.debug(f"command endpoint - cmd: {cmd}, entity_id: {entity_id}")
|
||||||
|
|
||||||
|
# list 명령 처리
|
||||||
|
if cmd == "list":
|
||||||
|
if self.queue:
|
||||||
|
return jsonify(self.queue.get_entity_list())
|
||||||
|
else:
|
||||||
|
return jsonify([])
|
||||||
|
|
||||||
|
# 기타 명령 처리
|
||||||
|
if self.queue:
|
||||||
|
ret = self.queue.command(cmd, int(entity_id) if entity_id else 0)
|
||||||
|
if ret is None:
|
||||||
|
ret = {"ret": "success"}
|
||||||
|
else:
|
||||||
|
ret = {"ret": "error", "log": "Queue not initialized"}
|
||||||
|
return jsonify(ret)
|
||||||
|
|
||||||
|
# 매치되는 sub가 없는 경우 기본 응답
|
||||||
|
return jsonify({"ret": "error", "log": f"Unknown sub: {sub}"})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
P.logger.error(f"Exception: {str(e)}")
|
P.logger.error(f"Exception: {str(e)}")
|
||||||
P.logger.error(traceback.format_exc())
|
P.logger.error(traceback.format_exc())
|
||||||
|
return jsonify({"ret": "error", "log": str(e)})
|
||||||
|
|
||||||
|
def process_command(self, command, arg1, arg2, arg3, req):
|
||||||
|
"""
|
||||||
|
FlaskFarm 프레임워크가 /command 엔드포인트에서 호출하는 함수
|
||||||
|
queue 페이지에서 list, stop 등의 명령을 처리
|
||||||
|
"""
|
||||||
|
ret = {"ret": "success"}
|
||||||
|
logger.debug(f"process_command - command: {command}, arg1: {arg1}")
|
||||||
|
|
||||||
|
if command == "list":
|
||||||
|
# 큐 목록 반환
|
||||||
|
if self.queue:
|
||||||
|
ret = [x for x in self.queue.get_entity_list()]
|
||||||
|
else:
|
||||||
|
ret = []
|
||||||
|
return jsonify(ret)
|
||||||
|
|
||||||
|
elif command == "stop":
|
||||||
|
# 다운로드 중지
|
||||||
|
if self.queue and arg1:
|
||||||
|
try:
|
||||||
|
entity_id = int(arg1)
|
||||||
|
result = self.queue.command("stop", entity_id)
|
||||||
|
if result:
|
||||||
|
ret = result
|
||||||
|
except Exception as e:
|
||||||
|
ret = {"ret": "error", "log": str(e)}
|
||||||
|
return jsonify(ret)
|
||||||
|
|
||||||
|
elif command == "queue_list":
|
||||||
|
# 대기 큐 목록
|
||||||
|
if self.queue:
|
||||||
|
ret = [x for x in self.queue.get_entity_list()]
|
||||||
|
else:
|
||||||
|
ret = []
|
||||||
|
return jsonify(ret)
|
||||||
|
|
||||||
|
# 기본 응답
|
||||||
|
return jsonify(ret)
|
||||||
|
|
||||||
|
def socketio_callback(self, refresh_type, data):
|
||||||
|
"""
|
||||||
|
socketio를 통해 클라이언트에 상태 업데이트 전송
|
||||||
|
refresh_type: 'add', 'status', 'last' 등
|
||||||
|
data: entity.as_dict() 데이터
|
||||||
|
"""
|
||||||
|
logger.info(f">>> socketio_callback called: {refresh_type}, {data.get('percent', 'N/A')}%")
|
||||||
|
try:
|
||||||
|
from flaskfarm.lib.framework.init_main import socketio
|
||||||
|
|
||||||
|
# FlaskFarm의 기존 패턴: /framework namespace로 emit
|
||||||
|
# queue 페이지의 소켓이 이 메시지를 받아서 처리
|
||||||
|
namespace = f"/{P.package_name}/{self.name}/queue"
|
||||||
|
|
||||||
|
# 먼저 queue에 직접 emit (기존 방식)
|
||||||
|
socketio.emit(refresh_type, data, namespace=namespace)
|
||||||
|
|
||||||
|
# /framework namespace로도 notify 이벤트 전송
|
||||||
|
notify_data = {
|
||||||
|
"type": "success",
|
||||||
|
"msg": f"다운로드중 {data.get('percent', 0)}% - {data.get('filename', '')}",
|
||||||
|
}
|
||||||
|
socketio.emit("notify", notify_data, namespace="/framework")
|
||||||
|
logger.info(f">>> socketio.emit completed to /framework")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"socketio_callback error: {e}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_html(url, cached=False):
|
def get_html(url, cached=False):
|
||||||
@@ -338,7 +451,7 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
ret["log"] = str(e)
|
ret["log"] = str(e)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def setting_save_after(self):
|
def setting_save_after(self, change_list=None):
|
||||||
if self.queue.get_max_ffmpeg_count() != P.ModelSetting.get_int(
|
if self.queue.get_max_ffmpeg_count() != P.ModelSetting.get_int(
|
||||||
"linkkf_max_ffmpeg_process_count"
|
"linkkf_max_ffmpeg_process_count"
|
||||||
):
|
):
|
||||||
@@ -346,6 +459,79 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
P.ModelSetting.get_int("linkkf_max_ffmpeg_process_count")
|
P.ModelSetting.get_int("linkkf_max_ffmpeg_process_count")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extract_video_url_from_playid(playid_url):
|
||||||
|
"""
|
||||||
|
linkkf.live의 playid URL에서 실제 비디오 URL(m3u8)을 추출합니다.
|
||||||
|
|
||||||
|
예시:
|
||||||
|
- playid_url: https://linkkf.live/playid/403116/?server=12&slug=11
|
||||||
|
- iframe: https://play.sub3.top/r2/play.php?id=n8&url=403116s11
|
||||||
|
- m3u8: https://n8.hlz3.top/403116s11/index.m3u8
|
||||||
|
"""
|
||||||
|
video_url = None
|
||||||
|
referer_url = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Extracting video URL from: {playid_url}")
|
||||||
|
|
||||||
|
# Step 1: playid 페이지에서 iframe src 추출
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
||||||
|
"Referer": "https://linkkf.live/"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(playid_url, headers=headers, timeout=15)
|
||||||
|
html_content = response.text
|
||||||
|
|
||||||
|
soup = BeautifulSoup(html_content, "html.parser")
|
||||||
|
|
||||||
|
# iframe 찾기 (id="video-player-iframe" 또는 play.sub3.top 포함)
|
||||||
|
iframe = soup.select_one("iframe#video-player-iframe")
|
||||||
|
if not iframe:
|
||||||
|
iframe = soup.select_one("iframe[src*='play.sub']")
|
||||||
|
if not iframe:
|
||||||
|
iframe = soup.select_one("iframe")
|
||||||
|
|
||||||
|
if iframe and iframe.get("src"):
|
||||||
|
iframe_src = iframe.get("src")
|
||||||
|
logger.info(f"Found iframe: {iframe_src}")
|
||||||
|
|
||||||
|
# Step 2: iframe 페이지에서 m3u8 URL 추출
|
||||||
|
iframe_headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
||||||
|
"Referer": playid_url
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe_response = requests.get(iframe_src, headers=iframe_headers, timeout=15)
|
||||||
|
iframe_content = iframe_response.text
|
||||||
|
|
||||||
|
# m3u8 URL 패턴 찾기
|
||||||
|
# 예: url: 'https://n8.hlz3.top/403116s11/index.m3u8'
|
||||||
|
m3u8_pattern = re.compile(r"url:\s*['\"]([^'\"]*\.m3u8)['\"]")
|
||||||
|
m3u8_match = m3u8_pattern.search(iframe_content)
|
||||||
|
|
||||||
|
if m3u8_match:
|
||||||
|
video_url = m3u8_match.group(1)
|
||||||
|
logger.info(f"Found m3u8 URL: {video_url}")
|
||||||
|
else:
|
||||||
|
# 대안 패턴: source src
|
||||||
|
source_pattern = re.compile(r"<source[^>]+src=['\"]([^'\"]+)['\"]")
|
||||||
|
source_match = source_pattern.search(iframe_content)
|
||||||
|
if source_match:
|
||||||
|
video_url = source_match.group(1)
|
||||||
|
logger.info(f"Found source URL: {video_url}")
|
||||||
|
|
||||||
|
referer_url = iframe_src
|
||||||
|
else:
|
||||||
|
logger.warning("No iframe found in playid page")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error extracting video URL: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
return video_url, referer_url
|
||||||
|
|
||||||
def get_video_url_from_url(url, url2):
|
def get_video_url_from_url(url, url2):
|
||||||
video_url = None
|
video_url = None
|
||||||
referer_url = None
|
referer_url = None
|
||||||
@@ -921,8 +1107,11 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
# 화면 표시용 title은 "01화" 형태
|
# 화면 표시용 title은 "01화" 형태
|
||||||
ep_title = f"{ep_name}화"
|
ep_title = f"{ep_name}화"
|
||||||
|
|
||||||
|
# 에피소드별 고유 ID 생성 (프로그램코드 + 에피소드번호)
|
||||||
|
episode_unique_id = data["code"] + ep_name.zfill(4)
|
||||||
|
|
||||||
entity = {
|
entity = {
|
||||||
"_id": data["code"],
|
"_id": episode_unique_id, # 에피소드별 고유 ID
|
||||||
"program_code": data["code"],
|
"program_code": data["code"],
|
||||||
"program_title": data["title"],
|
"program_title": data["title"],
|
||||||
"save_folder": Util.change_text_for_use_filename(data["save_folder"]),
|
"save_folder": Util.change_text_for_use_filename(data["save_folder"]),
|
||||||
@@ -930,14 +1119,17 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
"season": data["season"],
|
"season": data["season"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# 에피소드 코드 생성
|
# 에피소드 코드 = _id와 동일
|
||||||
entity["code"] = data["code"] + ep_name.zfill(4)
|
entity["code"] = episode_unique_id
|
||||||
|
|
||||||
# URL 생성: playid/{code}/?server=12&slug={slug} 형태
|
# URL 생성: playid/{code}/?server=12&slug={slug} 형태
|
||||||
entity["url"] = f"https://linkkf.live/playid/{code}/?server=12&slug={ep_slug}"
|
entity["url"] = f"https://linkkf.live/playid/{code}/?server=12&slug={ep_slug}"
|
||||||
|
|
||||||
# 저장 경로 설정
|
# 저장 경로 설정
|
||||||
tmp_save_path = P.ModelSetting.get("linkkf_download_path")
|
tmp_save_path = P.ModelSetting.get("linkkf_download_path")
|
||||||
|
if not tmp_save_path:
|
||||||
|
tmp_save_path = "/tmp/anime_downloads"
|
||||||
|
|
||||||
if P.ModelSetting.get("linkkf_auto_make_folder") == "True":
|
if P.ModelSetting.get("linkkf_auto_make_folder") == "True":
|
||||||
program_path = os.path.join(tmp_save_path, entity["save_folder"])
|
program_path = os.path.join(tmp_save_path, entity["save_folder"])
|
||||||
entity["save_path"] = program_path
|
entity["save_path"] = program_path
|
||||||
@@ -945,6 +1137,9 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
entity["save_path"] = os.path.join(
|
entity["save_path"] = os.path.join(
|
||||||
entity["save_path"], "Season %s" % int(entity["season"])
|
entity["save_path"], "Season %s" % int(entity["season"])
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# 기본 경로 설정
|
||||||
|
entity["save_path"] = tmp_save_path
|
||||||
|
|
||||||
entity["image"] = data["poster_url"]
|
entity["image"] = data["poster_url"]
|
||||||
# filename 생성 시 숫자만 전달 ("01화" 아님)
|
# filename 생성 시 숫자만 전달 ("01화" 아님)
|
||||||
@@ -1123,10 +1318,28 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
def add(self, episode_info):
|
def add(self, episode_info):
|
||||||
|
# 큐가 초기화되지 않았으면 초기화
|
||||||
|
if self.queue is None:
|
||||||
|
logger.warning("Queue is None in add(), initializing...")
|
||||||
|
try:
|
||||||
|
self.queue = FfmpegQueue(
|
||||||
|
P, P.ModelSetting.get_int("linkkf_max_ffmpeg_process_count"), "linkkf", caller=self
|
||||||
|
)
|
||||||
|
self.queue.queue_start()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to initialize queue: {e}")
|
||||||
|
return "queue_init_error"
|
||||||
|
|
||||||
|
# 큐 상태 로깅
|
||||||
|
queue_len = len(self.queue.entity_list) if self.queue else 0
|
||||||
|
logger.info(f"add() called - Queue length: {queue_len}, episode _id: {episode_info.get('_id')}")
|
||||||
|
|
||||||
if self.is_exist(episode_info):
|
if self.is_exist(episode_info):
|
||||||
|
logger.info(f"is_exist returned True for _id: {episode_info.get('_id')}")
|
||||||
return "queue_exist"
|
return "queue_exist"
|
||||||
else:
|
else:
|
||||||
db_entity = ModelLinkkfItem.get_by_linkkf_id(episode_info["_id"])
|
db_entity = ModelLinkkfItem.get_by_linkkf_id(episode_info["_id"])
|
||||||
|
logger.info(f"db_entity: {db_entity}")
|
||||||
|
|
||||||
logger.debug("db_entity:::> %s", db_entity)
|
logger.debug("db_entity:::> %s", db_entity)
|
||||||
# logger.debug("db_entity.status ::: %s", db_entity.status)
|
# logger.debug("db_entity.status ::: %s", db_entity.status)
|
||||||
@@ -1150,28 +1363,25 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
# ret = {'ret': 'success'}
|
# ret = {'ret': 'success'}
|
||||||
# ret['json'] = ffmpeg.start()
|
# ret['json'] = ffmpeg.start()
|
||||||
return "enqueue_db_append"
|
return "enqueue_db_append"
|
||||||
elif db_entity.status != "completed":
|
elif db_entity.get("status") != "completed" if isinstance(db_entity, dict) else db_entity.status != "completed":
|
||||||
entity = LinkkfQueueEntity(P, self, episode_info)
|
# DB에 있지만 완료되지 않은 경우도 큐에 추가
|
||||||
|
status = db_entity.get("status") if isinstance(db_entity, dict) else db_entity.status
|
||||||
logger.debug("entity:::> %s", entity.as_dict())
|
logger.info(f"db_entity status: {status}, adding to queue")
|
||||||
|
|
||||||
# P.logger.debug(F.config['path_data'])
|
try:
|
||||||
# P.logger.debug(self.headers)
|
logger.info("Creating LinkkfQueueEntity...")
|
||||||
|
entity = LinkkfQueueEntity(P, self, episode_info)
|
||||||
filename = os.path.basename(entity.filepath)
|
logger.info(f"LinkkfQueueEntity created, url: {entity.url}, filepath: {entity.filepath}")
|
||||||
ffmpeg = SupportFfmpeg(
|
logger.debug("entity:::> %s", entity.as_dict())
|
||||||
entity.url,
|
|
||||||
entity.filename,
|
logger.info(f"Adding to queue, queue length before: {len(self.queue.entity_list)}")
|
||||||
callback_function=self.callback_function,
|
result = self.queue.add_queue(entity)
|
||||||
max_pf_count=0,
|
logger.info(f"add_queue result: {result}, queue length after: {len(self.queue.entity_list)}")
|
||||||
save_path=entity.savepath,
|
except Exception as e:
|
||||||
timeout_minute=60,
|
logger.error(f"Error creating entity or adding to queue: {e}")
|
||||||
headers=self.headers,
|
logger.error(traceback.format_exc())
|
||||||
)
|
return "entity_creation_error"
|
||||||
ret = {"ret": "success"}
|
|
||||||
ret["json"] = ffmpeg.start()
|
|
||||||
|
|
||||||
# self.queue.add_queue(entity)
|
|
||||||
return "enqueue_db_exist"
|
return "enqueue_db_exist"
|
||||||
else:
|
else:
|
||||||
return "db_completed"
|
return "db_completed"
|
||||||
@@ -1183,6 +1393,9 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
# return True
|
# return True
|
||||||
|
|
||||||
def is_exist(self, info):
|
def is_exist(self, info):
|
||||||
|
if self.queue is None:
|
||||||
|
return False
|
||||||
|
|
||||||
for _ in self.queue.entity_list:
|
for _ in self.queue.entity_list:
|
||||||
if _.info["_id"] == info["_id"]:
|
if _.info["_id"] == info["_id"]:
|
||||||
return True
|
return True
|
||||||
@@ -1193,7 +1406,7 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
logger.debug("%s plugin_load", P.package_name)
|
logger.debug("%s plugin_load", P.package_name)
|
||||||
# old version
|
# old version
|
||||||
self.queue = FfmpegQueue(
|
self.queue = FfmpegQueue(
|
||||||
P, P.ModelSetting.get_int("linkkf_max_ffmpeg_process_count")
|
P, P.ModelSetting.get_int("linkkf_max_ffmpeg_process_count"), "linkkf", caller=self
|
||||||
)
|
)
|
||||||
self.current_data = None
|
self.current_data = None
|
||||||
self.queue.queue_start()
|
self.queue.queue_start()
|
||||||
@@ -1248,20 +1461,64 @@ class LinkkfQueueEntity(FfmpegQueueEntity):
|
|||||||
self.headers = None
|
self.headers = None
|
||||||
|
|
||||||
# info에서 필요한 정보 설정
|
# info에서 필요한 정보 설정
|
||||||
self.url = info.get("url", "")
|
playid_url = info.get("url", "")
|
||||||
self.filename = info.get("filename", "")
|
self.filename = info.get("filename", "")
|
||||||
self.filepath = info.get("filename", "")
|
|
||||||
self.savepath = info.get("save_path", "")
|
|
||||||
self.quality = info.get("quality", "720p")
|
self.quality = info.get("quality", "720p")
|
||||||
self.season = info.get("season", "1")
|
self.season = info.get("season", "1")
|
||||||
self.content_title = info.get("program_title", "")
|
self.content_title = info.get("program_title", "")
|
||||||
|
self.savepath = info.get("save_path", "")
|
||||||
|
|
||||||
# make_episode_info는 비디오 URL 추출이 필요할 때만 호출
|
# savepath가 비어있으면 기본값 설정
|
||||||
# 현재는 바로 다운로드 큐에 추가하므로 주석 처리
|
if not self.savepath:
|
||||||
# self.make_episode_info()
|
default_path = P.ModelSetting.get("linkkf_download_path")
|
||||||
|
logger.info(f"[DEBUG] linkkf_download_path from DB: '{default_path}'")
|
||||||
|
logger.info(f"[DEBUG] info save_path: '{info.get('save_path', 'NOT SET')}'")
|
||||||
|
logger.info(f"[DEBUG] info save_folder: '{info.get('save_folder', 'NOT SET')}'")
|
||||||
|
|
||||||
|
if default_path:
|
||||||
|
save_folder = info.get("save_folder", "Unknown")
|
||||||
|
self.savepath = os.path.join(default_path, save_folder)
|
||||||
|
else:
|
||||||
|
self.savepath = "/tmp/anime_downloads"
|
||||||
|
logger.info(f"[DEBUG] Final savepath set to: '{self.savepath}'")
|
||||||
|
|
||||||
|
# filepath = savepath + filename (전체 경로)
|
||||||
|
self.filepath = os.path.join(self.savepath, self.filename) if self.filename else self.savepath
|
||||||
|
logger.info(f"[DEBUG] filepath set to: '{self.filepath}'")
|
||||||
|
|
||||||
|
# playid URL에서 실제 비디오 URL 추출
|
||||||
|
try:
|
||||||
|
video_url, referer_url = LogicLinkkf.extract_video_url_from_playid(playid_url)
|
||||||
|
|
||||||
|
if video_url:
|
||||||
|
self.url = video_url
|
||||||
|
# HLS 다운로드를 위한 헤더 설정
|
||||||
|
self.headers = {
|
||||||
|
"Referer": referer_url or "https://linkkf.live/",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
|
||||||
|
}
|
||||||
|
logger.info(f"Video URL extracted: {self.url}")
|
||||||
|
else:
|
||||||
|
# 추출 실패 시 원본 URL 사용 (fallback)
|
||||||
|
self.url = playid_url
|
||||||
|
logger.warning(f"Failed to extract video URL, using playid URL: {playid_url}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Exception in video URL extraction: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
self.url = playid_url
|
||||||
|
|
||||||
def refresh_status(self):
|
def refresh_status(self):
|
||||||
self.module_logic.socketio_callback("status", self.as_dict())
|
try:
|
||||||
|
# from framework import socketio (FlaskFarm 표준 방식)
|
||||||
|
from framework import socketio
|
||||||
|
|
||||||
|
data = self.as_dict()
|
||||||
|
|
||||||
|
# /framework namespace로 linkkf_status 이벤트 전송
|
||||||
|
socketio.emit("linkkf_status", data, namespace="/framework")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"refresh_status error: {e}")
|
||||||
|
|
||||||
def info_dict(self, tmp):
|
def info_dict(self, tmp):
|
||||||
# logger.debug('self.info::> %s', self.info)
|
# logger.debug('self.info::> %s', self.info)
|
||||||
@@ -1272,6 +1529,27 @@ class LinkkfQueueEntity(FfmpegQueueEntity):
|
|||||||
tmp["content_title"] = self.content_title
|
tmp["content_title"] = self.content_title
|
||||||
tmp["linkkf_info"] = self.info
|
tmp["linkkf_info"] = self.info
|
||||||
tmp["epi_queue"] = self.epi_queue
|
tmp["epi_queue"] = self.epi_queue
|
||||||
|
|
||||||
|
# 템플릿이 기대하는 필드들 추가
|
||||||
|
tmp["idx"] = self.entity_id
|
||||||
|
tmp["callback_id"] = f"linkkf_{self.entity_id}"
|
||||||
|
tmp["start_time"] = self.created_time.strftime("%m-%d %H:%M") if hasattr(self, 'created_time') and self.created_time and hasattr(self.created_time, 'strftime') else (self.created_time if self.created_time else "")
|
||||||
|
tmp["status_kor"] = self.ffmpeg_status_kor if self.ffmpeg_status_kor else "대기중"
|
||||||
|
tmp["percent"] = self.ffmpeg_percent if self.ffmpeg_percent else 0
|
||||||
|
tmp["duration_str"] = ""
|
||||||
|
tmp["current_pf_count"] = 0
|
||||||
|
tmp["current_speed"] = self.current_speed if hasattr(self, 'current_speed') and self.current_speed else ""
|
||||||
|
tmp["download_time"] = self.download_time if hasattr(self, 'download_time') and self.download_time else ""
|
||||||
|
tmp["status_str"] = "WAITING" if not self.ffmpeg_status else ("DOWNLOADING" if self.ffmpeg_status == 5 else "COMPLETED" if self.ffmpeg_status == 7 else "WAITING")
|
||||||
|
tmp["temp_fullpath"] = ""
|
||||||
|
tmp["save_fullpath"] = self.filepath if self.filepath else ""
|
||||||
|
tmp["duration"] = ""
|
||||||
|
tmp["current_duration"] = ""
|
||||||
|
tmp["current_bitrate"] = ""
|
||||||
|
tmp["end_time"] = ""
|
||||||
|
tmp["max_pf_count"] = 0
|
||||||
|
tmp["exist"] = False
|
||||||
|
|
||||||
return tmp
|
return tmp
|
||||||
|
|
||||||
def make_episode_info(self):
|
def make_episode_info(self):
|
||||||
|
|||||||
@@ -24,38 +24,93 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
var socket = io.connect(window.location.href);
|
console.log('Queue page loaded, connecting to sockets...');
|
||||||
|
|
||||||
|
var protocol = location.protocol;
|
||||||
|
var socketUrl = protocol + "//" + document.domain + ":" + location.port;
|
||||||
|
|
||||||
|
// Queue 페이지 전용 namespace 연결
|
||||||
|
var queueSocket = null;
|
||||||
|
try {
|
||||||
|
queueSocket = io.connect(socketUrl + '/anime_downloader/linkkf/queue');
|
||||||
|
console.log('Queue socket connecting to:', socketUrl + '/anime_downloader/linkkf/queue');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Queue socket connect error:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// /framework namespace 연결 (FlaskFarm 기본 알림용)
|
||||||
|
var frameworkSocket = null;
|
||||||
|
try {
|
||||||
|
frameworkSocket = io.connect(socketUrl + '/framework');
|
||||||
|
console.log('Framework socket connecting to:', socketUrl + '/framework');
|
||||||
|
|
||||||
|
frameworkSocket.on('connect', function() {
|
||||||
|
console.log('Framework socket connected!');
|
||||||
|
});
|
||||||
|
|
||||||
|
// linkkf_status 이벤트로 다운로드 상태 업데이트 수신
|
||||||
|
frameworkSocket.on('linkkf_status', function(data) {
|
||||||
|
console.log('linkkf_status received:', data.percent + '%');
|
||||||
|
status_html(data);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Framework socket connect error:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue socket으로 직접 이벤트 받기
|
||||||
|
var socket = queueSocket;
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
socket.on('connect', function() {
|
||||||
|
console.log('Socket connected! socket.id:', socket.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', function() {
|
||||||
|
console.log('Socket disconnected!');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('connect_error', function(err) {
|
||||||
|
console.log('Socket connect error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 모든 이벤트 수신 테스트
|
||||||
|
socket.onAny(function(event, ...args) {
|
||||||
|
console.log('Socket event received:', event, args);
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('on_start', function(data){
|
socket.on('on_start', function(data){
|
||||||
document.getElementById("log").innerHTML += data.data;
|
console.log('on_start received:', data);
|
||||||
document.getElementById("log").scrollTop = document.getElementById("log").scrollHeight;
|
document.getElementById("log").innerHTML += data.data;
|
||||||
document.getElementById("log").style.visibility = 'visible';
|
document.getElementById("log").scrollTop = document.getElementById("log").scrollHeight;
|
||||||
$('#loading').hide();
|
document.getElementById("log").style.visibility = 'visible';
|
||||||
});
|
$('#loading').hide();
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('add', function(data){
|
socket.on('add', function(data){
|
||||||
str = make_item(data);
|
str = make_item(data);
|
||||||
if (current_data == null || current_data.length == 0) {
|
if (current_data == null || current_data.length == 0) {
|
||||||
current_data = Array();
|
current_data = Array();
|
||||||
$("#list").html(str);
|
$("#list").html(str);
|
||||||
} else {
|
} else {
|
||||||
$("#list").html($("#list").html() + str);
|
$("#list").html($("#list").html() + str);
|
||||||
}
|
}
|
||||||
current_data.push(data);
|
current_data.push(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('status_change', function(data) {
|
socket.on('status_change', function(data) {
|
||||||
button_html(data);
|
button_html(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('status', function(data){
|
socket.on('status', function(data){
|
||||||
status_html(data);
|
console.log('status received:', data);
|
||||||
});
|
status_html(data);
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('last', function(data){
|
socket.on('last', function(data){
|
||||||
status_html(data);
|
status_html(data);
|
||||||
button_html(data);
|
button_html(data);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
globalSendCommand('list', null, null, null, function(data) {
|
globalSendCommand('list', null, null, null, function(data) {
|
||||||
current_data = data;
|
current_data = data;
|
||||||
|
|||||||
Reference in New Issue
Block a user