또 많은 수정을 했슴.
This commit is contained in:
@@ -205,6 +205,10 @@ class FfmpegQueue(object):
|
|||||||
logger.info(f"save_path: {dirname}, filename: {filename}")
|
logger.info(f"save_path: {dirname}, filename: {filename}")
|
||||||
logger.info(f"headers: {_headers}")
|
logger.info(f"headers: {_headers}")
|
||||||
|
|
||||||
|
# 자막 URL 로그
|
||||||
|
vtt_url = getattr(entity, 'vtt', None)
|
||||||
|
logger.info(f"Subtitle URL (vtt): {vtt_url}")
|
||||||
|
|
||||||
# 터미널에서 수동 테스트용 ffmpeg 명령어
|
# 터미널에서 수동 테스트용 ffmpeg 명령어
|
||||||
output_file = os.path.join(dirname, filename)
|
output_file = os.path.join(dirname, filename)
|
||||||
referer = _headers.get("Referer", "") if _headers else ""
|
referer = _headers.get("Referer", "") if _headers else ""
|
||||||
@@ -214,33 +218,51 @@ class FfmpegQueue(object):
|
|||||||
logger.info(ffmpeg_cmd)
|
logger.info(ffmpeg_cmd)
|
||||||
logger.info(f"=== END COMMAND ===")
|
logger.info(f"=== END COMMAND ===")
|
||||||
|
|
||||||
# m3u8 URL인 경우 커스텀 HLS 다운로더 사용 (ffmpeg 8.0 .jpg 확장자 문제 우회)
|
# m3u8 URL인 경우 다운로드 방법 설정에 따라 분기
|
||||||
if video_url.endswith('.m3u8'):
|
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
|
self.current_ffmpeg_count += 1
|
||||||
logger.info(f"Download started, current_ffmpeg_count: {self.current_ffmpeg_count}/{self.max_ffmpeg_count}")
|
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=""):
|
def progress_callback(percent, current, total, speed="", elapsed=""):
|
||||||
entity_ref.ffmpeg_status = 5 # DOWNLOADING
|
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.ffmpeg_percent = percent
|
||||||
entity_ref.current_speed = speed
|
entity_ref.current_speed = speed
|
||||||
entity_ref.download_time = elapsed
|
entity_ref.download_time = elapsed
|
||||||
entity_ref.refresh_status()
|
entity_ref.refresh_status()
|
||||||
|
|
||||||
hls_downloader = HlsDownloader(
|
if method == "ytdlp":
|
||||||
m3u8_url=video_url,
|
# yt-dlp 사용
|
||||||
output_path=output_file_ref,
|
from .ytdlp_downloader import YtdlpDownloader
|
||||||
headers=headers_ref,
|
logger.info("Using yt-dlp downloader...")
|
||||||
callback=progress_callback
|
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
|
downloader_self.current_ffmpeg_count -= 1
|
||||||
@@ -252,17 +274,75 @@ class FfmpegQueue(object):
|
|||||||
entity_ref.ffmpeg_percent = 100
|
entity_ref.ffmpeg_percent = 100
|
||||||
entity_ref.download_completed()
|
entity_ref.download_completed()
|
||||||
entity_ref.refresh_status()
|
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:
|
else:
|
||||||
entity_ref.ffmpeg_status = -1
|
entity_ref.ffmpeg_status = -1
|
||||||
entity_ref.ffmpeg_status_kor = f"실패: {message}"
|
entity_ref.ffmpeg_status_kor = f"실패: {message}"
|
||||||
entity_ref.refresh_status()
|
entity_ref.refresh_status()
|
||||||
logger.error(f"HLS download failed: {message}")
|
logger.error(f"Download failed: {message}")
|
||||||
|
|
||||||
# 스레드 시작
|
# 스레드 시작
|
||||||
download_thread = threading.Thread(
|
download_thread = threading.Thread(
|
||||||
target=run_hls_download,
|
target=run_download,
|
||||||
args=(self, entity, output_file, _headers)
|
args=(self, entity, output_file, _headers, download_method)
|
||||||
)
|
)
|
||||||
download_thread.daemon = True
|
download_thread.daemon = True
|
||||||
download_thread.start()
|
download_thread.start()
|
||||||
@@ -443,14 +523,20 @@ class FfmpegQueue(object):
|
|||||||
|
|
||||||
def add_queue(self, entity):
|
def add_queue(self, entity):
|
||||||
try:
|
try:
|
||||||
# entity = QueueEntity.create(info)
|
|
||||||
# if entity is not None:
|
|
||||||
# LogicQueue.download_queue.put(entity)
|
|
||||||
# return True
|
|
||||||
entity.entity_id = self.static_index
|
entity.entity_id = self.static_index
|
||||||
self.static_index += 1
|
self.static_index += 1
|
||||||
self.entity_list.append(entity)
|
self.entity_list.append(entity)
|
||||||
self.download_queue.put(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
|
return True
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
self.P.logger.error("Exception:%s", exception)
|
self.P.logger.error("Exception:%s", exception)
|
||||||
@@ -528,7 +614,7 @@ class FfmpegQueue(object):
|
|||||||
|
|
||||||
def get_entity_list(self):
|
def get_entity_list(self):
|
||||||
ret = []
|
ret = []
|
||||||
P.logger.debug(self)
|
#P.logger.debug(self)
|
||||||
for x in self.entity_list:
|
for x in self.entity_list:
|
||||||
tmp = x.as_dict()
|
tmp = x.as_dict()
|
||||||
ret.append(tmp)
|
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
|
||||||
221
mod_linkkf.py
221
mod_linkkf.py
@@ -30,25 +30,8 @@ from lxml import html
|
|||||||
from plugin import PluginModuleBase
|
from plugin import PluginModuleBase
|
||||||
from requests_cache import CachedSession
|
from requests_cache import CachedSession
|
||||||
|
|
||||||
packages = ["beautifulsoup4", "requests-cache", "cloudscraper"]
|
# cloudscraper는 lazy import로 처리
|
||||||
|
import cloudscraper
|
||||||
for package in packages:
|
|
||||||
try:
|
|
||||||
import package
|
|
||||||
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
if package == "playwright":
|
|
||||||
pass
|
|
||||||
# os.system(f"pip3 install playwright")
|
|
||||||
# os.system(f"playwright install")
|
|
||||||
except ImportError:
|
|
||||||
# main(["install", package])
|
|
||||||
if package == "playwright":
|
|
||||||
pass
|
|
||||||
# os.system(f"pip3 install {package}")
|
|
||||||
# os.system(f"playwright install")
|
|
||||||
else:
|
|
||||||
os.system(f"pip3 install {package}")
|
|
||||||
|
|
||||||
from anime_downloader.lib.ffmpeg_queue_v1 import FfmpegQueue, FfmpegQueueEntity
|
from anime_downloader.lib.ffmpeg_queue_v1 import FfmpegQueue, FfmpegQueueEntity
|
||||||
from anime_downloader.lib.util import Util
|
from anime_downloader.lib.util import Util
|
||||||
@@ -75,6 +58,7 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
download_queue = None
|
download_queue = None
|
||||||
download_thread = None
|
download_thread = None
|
||||||
current_download_count = 0
|
current_download_count = 0
|
||||||
|
_scraper = None # cloudscraper 싱글톤
|
||||||
|
|
||||||
cache_path = os.path.dirname(__file__)
|
cache_path = os.path.dirname(__file__)
|
||||||
|
|
||||||
@@ -119,6 +103,7 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
"linkkf_image_url_prefix_series": "",
|
"linkkf_image_url_prefix_series": "",
|
||||||
"linkkf_image_url_prefix_episode": "",
|
"linkkf_image_url_prefix_episode": "",
|
||||||
"linkkf_discord_notify": "True",
|
"linkkf_discord_notify": "True",
|
||||||
|
"linkkf_download_method": "ffmpeg", # ffmpeg or ytdlp
|
||||||
}
|
}
|
||||||
# default_route_socketio(P, self)
|
# default_route_socketio(P, self)
|
||||||
default_route_socketio_module(self, attach="/setting")
|
default_route_socketio_module(self, attach="/setting")
|
||||||
@@ -230,7 +215,60 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
ret = {"ret": "error", "log": "Queue not initialized"}
|
ret = {"ret": "error", "log": "Queue not initialized"}
|
||||||
return jsonify(ret)
|
return jsonify(ret)
|
||||||
elif sub == "add_queue_checked_list":
|
elif sub == "add_queue_checked_list":
|
||||||
return jsonify({"ret": "not_implemented"})
|
# 선택된 에피소드 일괄 추가 (백그라운드 스레드로 처리)
|
||||||
|
import threading
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
logger.info("========= add_queue_checked_list START =========")
|
||||||
|
ret = {"ret": "success", "message": "백그라운드에서 추가 중..."}
|
||||||
|
try:
|
||||||
|
form_data = request.form.get("data")
|
||||||
|
if not form_data:
|
||||||
|
ret["ret"] = "error"
|
||||||
|
ret["log"] = "No data received"
|
||||||
|
return jsonify(ret)
|
||||||
|
|
||||||
|
episode_list = json.loads(form_data)
|
||||||
|
logger.info(f"Received {len(episode_list)} episodes to add in background")
|
||||||
|
|
||||||
|
# Flask app 참조 저장 (스레드에서 사용)
|
||||||
|
app = current_app._get_current_object()
|
||||||
|
|
||||||
|
# 백그라운드 스레드에서 추가 작업 수행
|
||||||
|
def add_episodes_background(flask_app, downloader_self, episodes):
|
||||||
|
added = 0
|
||||||
|
skipped = 0
|
||||||
|
with flask_app.app_context():
|
||||||
|
for episode_info in episodes:
|
||||||
|
try:
|
||||||
|
result = downloader_self.add(episode_info)
|
||||||
|
if result in ["enqueue_db_append", "enqueue_db_exist"]:
|
||||||
|
added += 1
|
||||||
|
logger.debug(f"Added episode {episode_info.get('_id')}")
|
||||||
|
else:
|
||||||
|
skipped += 1
|
||||||
|
logger.debug(f"Skipped episode {episode_info.get('_id')}: {result}")
|
||||||
|
except Exception as ep_err:
|
||||||
|
logger.error(f"Error adding episode: {ep_err}")
|
||||||
|
skipped += 1
|
||||||
|
|
||||||
|
logger.info(f"add_queue_checked_list completed: added={added}, skipped={skipped}")
|
||||||
|
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=add_episodes_background,
|
||||||
|
args=(app, self, episode_list)
|
||||||
|
)
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
ret["count"] = len(episode_list)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"add_queue_checked_list error: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
ret["ret"] = "error"
|
||||||
|
ret["log"] = str(e)
|
||||||
|
return jsonify(ret)
|
||||||
elif sub == "web_list":
|
elif sub == "web_list":
|
||||||
return jsonify({"ret": "not_implemented"})
|
return jsonify({"ret": "not_implemented"})
|
||||||
elif sub == "db_remove":
|
elif sub == "db_remove":
|
||||||
@@ -336,67 +374,50 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
logger.error(f"socketio_callback error: {e}")
|
logger.error(f"socketio_callback error: {e}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_html(url, cached=False):
|
def _extract_cat1_urls(html_content):
|
||||||
|
"""cat1 = [...] 패턴에서 URL 목록 추출 (중복 코드 제거용 헬퍼)"""
|
||||||
|
regex = r"cat1 = [^\[]*([^\]]*)"
|
||||||
|
cat_match = re.findall(regex, html_content)
|
||||||
|
if not cat_match:
|
||||||
|
return []
|
||||||
|
url_regex = r"\"([^\"]*)\""
|
||||||
|
return re.findall(url_regex, cat_match[0])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_html(url, cached=False, timeout=10):
|
||||||
try:
|
try:
|
||||||
if LogicLinkkf.referer is None:
|
if LogicLinkkf.referer is None:
|
||||||
LogicLinkkf.referer = f"{ModelSetting.get('linkkf_url')}"
|
LogicLinkkf.referer = f"{P.ModelSetting.get('linkkf_url')}"
|
||||||
|
|
||||||
# return LogicLinkkfYommi.get_html_requests(url)
|
return LogicLinkkf.get_html_cloudflare(url, timeout=timeout)
|
||||||
return LogicLinkkf.get_html_cloudflare(url)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Exception:%s", e)
|
logger.error("Exception:%s", e)
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_html_cloudflare(url, cached=False):
|
def get_html_cloudflare(url, cached=False, timeout=10):
|
||||||
logger.debug(f"cloudflare protection bypass {'=' * 30}")
|
"""Cloudflare 보호 우회를 위한 HTTP 요청 (싱글톤 패턴)"""
|
||||||
|
|
||||||
user_agents_list = [
|
user_agents_list = [
|
||||||
"Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
|
"Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36",
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36",
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36",
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36",
|
||||||
]
|
]
|
||||||
# ua = UserAgent(verify_ssl=False)
|
|
||||||
|
|
||||||
LogicLinkkf.headers["User-Agent"] = random.choice(user_agents_list)
|
LogicLinkkf.headers["User-Agent"] = random.choice(user_agents_list)
|
||||||
|
LogicLinkkf.headers["Referer"] = LogicLinkkf.referer or ""
|
||||||
|
|
||||||
LogicLinkkf.headers["Referer"] = LogicLinkkf.referer
|
# cloudscraper 싱글톤 패턴 - 매 요청마다 생성하지 않음
|
||||||
|
if LogicLinkkf._scraper is None:
|
||||||
|
LogicLinkkf._scraper = cloudscraper.create_scraper(
|
||||||
|
delay=10,
|
||||||
|
browser={"custom": "linkkf"},
|
||||||
|
)
|
||||||
|
|
||||||
# logger.debug(f"headers:: {LogicLinkkfYommi.headers}")
|
return LogicLinkkf._scraper.get(
|
||||||
|
|
||||||
if LogicLinkkf.session is None:
|
|
||||||
LogicLinkkf.session = requests.Session()
|
|
||||||
|
|
||||||
# LogicLinkkfYommi.session = requests.Session()
|
|
||||||
# re_sess = requests.Session()
|
|
||||||
# logger.debug(LogicLinkkfYommi.session)
|
|
||||||
|
|
||||||
# sess = cloudscraper.create_scraper(
|
|
||||||
# # browser={"browser": "firefox", "mobile": False},
|
|
||||||
# browser={"browser": "chrome", "mobile": False},
|
|
||||||
# debug=True,
|
|
||||||
# sess=LogicLinkkfYommi.session,
|
|
||||||
# delay=10,
|
|
||||||
# )
|
|
||||||
# scraper = cloudscraper.create_scraper(sess=re_sess)
|
|
||||||
scraper = cloudscraper.create_scraper(
|
|
||||||
# debug=True,
|
|
||||||
delay=10,
|
|
||||||
sess=LogicLinkkf.session,
|
|
||||||
browser={
|
|
||||||
"custom": "linkkf",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# print(scraper.get(url, headers=LogicLinkkfYommi.headers).content)
|
|
||||||
# print(scraper.get(url).content)
|
|
||||||
# return scraper.get(url, headers=LogicLinkkfYommi.headers).content
|
|
||||||
# logger.debug(LogicLinkkfYommi.headers)
|
|
||||||
return scraper.get(
|
|
||||||
url,
|
url,
|
||||||
headers=LogicLinkkf.headers,
|
headers=LogicLinkkf.headers,
|
||||||
timeout=10,
|
timeout=timeout,
|
||||||
).content.decode("utf8", errors="replace")
|
).content.decode("utf8", errors="replace")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -410,7 +431,7 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
else:
|
else:
|
||||||
code = str(args[0])
|
code = str(args[0])
|
||||||
|
|
||||||
print(code)
|
logger.debug(f"add_whitelist code: {code}")
|
||||||
|
|
||||||
whitelist_program = P.ModelSetting.get("linkkf_auto_code_list")
|
whitelist_program = P.ModelSetting.get("linkkf_auto_code_list")
|
||||||
# whitelist_programs = [
|
# whitelist_programs = [
|
||||||
@@ -462,15 +483,19 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def extract_video_url_from_playid(playid_url):
|
def extract_video_url_from_playid(playid_url):
|
||||||
"""
|
"""
|
||||||
linkkf.live의 playid URL에서 실제 비디오 URL(m3u8)을 추출합니다.
|
linkkf.live의 playid URL에서 실제 비디오 URL(m3u8)과 자막 URL(vtt)을 추출합니다.
|
||||||
|
|
||||||
예시:
|
예시:
|
||||||
- playid_url: https://linkkf.live/playid/403116/?server=12&slug=11
|
- playid_url: https://linkkf.live/playid/403116/?server=12&slug=11
|
||||||
- iframe: https://play.sub3.top/r2/play.php?id=n8&url=403116s11
|
- iframe: https://play.sub3.top/r2/play.php?id=n8&url=403116s11
|
||||||
- m3u8: https://n8.hlz3.top/403116s11/index.m3u8
|
- m3u8: https://n8.hlz3.top/403116s11/index.m3u8
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(video_url, referer_url, vtt_url)
|
||||||
"""
|
"""
|
||||||
video_url = None
|
video_url = None
|
||||||
referer_url = None
|
referer_url = None
|
||||||
|
vtt_url = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"Extracting video URL from: {playid_url}")
|
logger.info(f"Extracting video URL from: {playid_url}")
|
||||||
@@ -497,7 +522,7 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
iframe_src = iframe.get("src")
|
iframe_src = iframe.get("src")
|
||||||
logger.info(f"Found iframe: {iframe_src}")
|
logger.info(f"Found iframe: {iframe_src}")
|
||||||
|
|
||||||
# Step 2: iframe 페이지에서 m3u8 URL 추출
|
# Step 2: iframe 페이지에서 m3u8 URL과 vtt URL 추출
|
||||||
iframe_headers = {
|
iframe_headers = {
|
||||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
||||||
"Referer": playid_url
|
"Referer": playid_url
|
||||||
@@ -522,6 +547,21 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
video_url = source_match.group(1)
|
video_url = source_match.group(1)
|
||||||
logger.info(f"Found source URL: {video_url}")
|
logger.info(f"Found source URL: {video_url}")
|
||||||
|
|
||||||
|
# VTT 자막 URL 추출
|
||||||
|
# 예: <track src="https://...vtt" kind="subtitles">
|
||||||
|
vtt_pattern = re.compile(r"<track[^>]+src=['\"]([^'\"]*\.vtt)['\"]")
|
||||||
|
vtt_match = vtt_pattern.search(iframe_content)
|
||||||
|
if vtt_match:
|
||||||
|
vtt_url = vtt_match.group(1)
|
||||||
|
logger.info(f"Found VTT subtitle URL: {vtt_url}")
|
||||||
|
else:
|
||||||
|
# 대안 패턴: url: '...vtt'
|
||||||
|
vtt_pattern2 = re.compile(r"url:\s*['\"]([^'\"]*\.vtt)['\"]")
|
||||||
|
vtt_match2 = vtt_pattern2.search(iframe_content)
|
||||||
|
if vtt_match2:
|
||||||
|
vtt_url = vtt_match2.group(1)
|
||||||
|
logger.info(f"Found VTT subtitle URL (alt pattern): {vtt_url}")
|
||||||
|
|
||||||
referer_url = iframe_src
|
referer_url = iframe_src
|
||||||
else:
|
else:
|
||||||
logger.warning("No iframe found in playid page")
|
logger.warning("No iframe found in playid page")
|
||||||
@@ -530,7 +570,7 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
logger.error(f"Error extracting video URL: {e}")
|
logger.error(f"Error extracting video URL: {e}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
return video_url, referer_url
|
return video_url, referer_url, vtt_url
|
||||||
|
|
||||||
def get_video_url_from_url(url, url2):
|
def get_video_url_from_url(url, url2):
|
||||||
video_url = None
|
video_url = None
|
||||||
@@ -657,7 +697,7 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
referer_url = url2
|
referer_url = url2
|
||||||
|
|
||||||
elif "linkkf" in url2:
|
elif "linkkf" in url2:
|
||||||
logger.deubg("linkkf routine")
|
logger.debug("linkkf routine")
|
||||||
# linkkf 계열 처리 => URL 리스트를 받아오고, 하나 골라 방문 해서 m3u8을 받아온다.
|
# linkkf 계열 처리 => URL 리스트를 받아오고, 하나 골라 방문 해서 m3u8을 받아온다.
|
||||||
referer_url = url2
|
referer_url = url2
|
||||||
data2 = LogicLinkkf.get_html(url2)
|
data2 = LogicLinkkf.get_html(url2)
|
||||||
@@ -674,7 +714,7 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
return LogicLinkkf.get_video_url_from_url(url2, url3)
|
return LogicLinkkf.get_video_url_from_url(url2, url3)
|
||||||
elif url3.startswith("/"):
|
elif url3.startswith("/"):
|
||||||
url3 = urlparse.urljoin(url2, url3)
|
url3 = urlparse.urljoin(url2, url3)
|
||||||
print("url3 = ", url3)
|
logger.debug(f"url3 = {url3}")
|
||||||
LogicLinkkf.referer = url2
|
LogicLinkkf.referer = url2
|
||||||
data3 = LogicLinkkf.get_html(url3)
|
data3 = LogicLinkkf.get_html(url3)
|
||||||
# logger.info('data3: %s', data3)
|
# logger.info('data3: %s', data3)
|
||||||
@@ -706,7 +746,7 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
# logger.info("download url2 : %s , url3 : %s" % (url2, url3))
|
# logger.info("download url2 : %s , url3 : %s" % (url2, url3))
|
||||||
video_url = url3
|
video_url = url3
|
||||||
elif "#V" in url2: # V 패턴 추가
|
elif "#V" in url2: # V 패턴 추가
|
||||||
print("#v routine")
|
logger.debug("#v routine")
|
||||||
|
|
||||||
data2 = LogicLinkkf.get_html(url2)
|
data2 = LogicLinkkf.get_html(url2)
|
||||||
|
|
||||||
@@ -1223,38 +1263,6 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
logger.error("Exception:%s", e)
|
logger.error("Exception:%s", e)
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_html(
|
|
||||||
url: str,
|
|
||||||
referer: str = None,
|
|
||||||
cached: bool = False,
|
|
||||||
stream: bool = False,
|
|
||||||
timeout: int = 5,
|
|
||||||
):
|
|
||||||
data = ""
|
|
||||||
headers = {
|
|
||||||
"referer": "https://linkkf.live",
|
|
||||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
||||||
"Chrome/96.0.4664.110 Whale/3.12.129.46 Safari/537.36"
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel "
|
|
||||||
"Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 "
|
|
||||||
"Whale/3.12.129.46 Safari/537.36",
|
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
if LogicOhli24.session is None:
|
|
||||||
LogicOhli24.session = requests.session()
|
|
||||||
|
|
||||||
# logger.debug('get_html :%s', url)
|
|
||||||
headers["Referer"] = "" if referer is None else referer
|
|
||||||
page_content = LogicOhli24.session.get(
|
|
||||||
url, headers=headers, timeout=timeout
|
|
||||||
)
|
|
||||||
data = page_content.text
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Exception:%s", e)
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
return data
|
|
||||||
|
|
||||||
def get_html_requests(self, url, cached=False):
|
def get_html_requests(self, url, cached=False):
|
||||||
if LogicLinkkf.session is None:
|
if LogicLinkkf.session is None:
|
||||||
@@ -1486,9 +1494,9 @@ class LinkkfQueueEntity(FfmpegQueueEntity):
|
|||||||
self.filepath = os.path.join(self.savepath, self.filename) if self.filename else self.savepath
|
self.filepath = os.path.join(self.savepath, self.filename) if self.filename else self.savepath
|
||||||
logger.info(f"[DEBUG] filepath set to: '{self.filepath}'")
|
logger.info(f"[DEBUG] filepath set to: '{self.filepath}'")
|
||||||
|
|
||||||
# playid URL에서 실제 비디오 URL 추출
|
# playid URL에서 실제 비디오 URL과 자막 URL 추출
|
||||||
try:
|
try:
|
||||||
video_url, referer_url = LogicLinkkf.extract_video_url_from_playid(playid_url)
|
video_url, referer_url, vtt_url = LogicLinkkf.extract_video_url_from_playid(playid_url)
|
||||||
|
|
||||||
if video_url:
|
if video_url:
|
||||||
self.url = video_url
|
self.url = video_url
|
||||||
@@ -1498,6 +1506,11 @@ class LinkkfQueueEntity(FfmpegQueueEntity):
|
|||||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
|
||||||
}
|
}
|
||||||
logger.info(f"Video URL extracted: {self.url}")
|
logger.info(f"Video URL extracted: {self.url}")
|
||||||
|
|
||||||
|
# 자막 URL 저장
|
||||||
|
if vtt_url:
|
||||||
|
self.vtt = vtt_url
|
||||||
|
logger.info(f"Subtitle URL saved: {self.vtt}")
|
||||||
else:
|
else:
|
||||||
# 추출 실패 시 원본 URL 사용 (fallback)
|
# 추출 실패 시 원본 URL 사용 (fallback)
|
||||||
self.url = playid_url
|
self.url = playid_url
|
||||||
@@ -1564,7 +1577,7 @@ class LinkkfQueueEntity(FfmpegQueueEntity):
|
|||||||
|
|
||||||
if len(tree.xpath(xpath_select_query)) > 0:
|
if len(tree.xpath(xpath_select_query)) > 0:
|
||||||
# by k45734
|
# by k45734
|
||||||
print("ok")
|
logger.debug("make_episode_info: select found")
|
||||||
xpath_select_query = '//select[@class="switcher"]/option'
|
xpath_select_query = '//select[@class="switcher"]/option'
|
||||||
for tag in tree.xpath(xpath_select_query):
|
for tag in tree.xpath(xpath_select_query):
|
||||||
url2s2 = tag.attrib["value"]
|
url2s2 = tag.attrib["value"]
|
||||||
@@ -1575,7 +1588,7 @@ class LinkkfQueueEntity(FfmpegQueueEntity):
|
|||||||
else:
|
else:
|
||||||
url2s.append(url2s2)
|
url2s.append(url2s2)
|
||||||
else:
|
else:
|
||||||
print(":: else ::")
|
logger.debug("make_episode_info: else branch")
|
||||||
|
|
||||||
tt = re.search(r"var player_data=(.*?)<", data, re.S)
|
tt = re.search(r"var player_data=(.*?)<", data, re.S)
|
||||||
json_string = tt.group(1)
|
json_string = tt.group(1)
|
||||||
|
|||||||
614
mod_ohli24.py
614
mod_ohli24.py
@@ -28,16 +28,8 @@ from flask import request, render_template, jsonify
|
|||||||
from lxml import html
|
from lxml import html
|
||||||
from sqlalchemy import or_, desc
|
from sqlalchemy import or_, desc
|
||||||
|
|
||||||
pkgs = ["bs4", "jsbeautifier", "aiohttp", "lxml", "loguru"]
|
# third-party
|
||||||
for pkg in pkgs:
|
import requests
|
||||||
try:
|
|
||||||
importlib.import_module(pkg)
|
|
||||||
# except ImportError:
|
|
||||||
except ImportError:
|
|
||||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "pip"])
|
|
||||||
# main(["install", pkg])
|
|
||||||
subprocess.check_call([sys.executable, "-m", "pip", "install", pkg])
|
|
||||||
importlib.import_module(pkg)
|
|
||||||
|
|
||||||
# third party package
|
# third party package
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -101,13 +93,14 @@ class LogicOhli24(PluginModuleBase):
|
|||||||
|
|
||||||
self.db_default = {
|
self.db_default = {
|
||||||
"ohli24_db_version": "1",
|
"ohli24_db_version": "1",
|
||||||
"ohli24_url": "https://ohli24.org",
|
"ohli24_url": "https://ani.ohli24.com",
|
||||||
"ohli24_download_path": os.path.join(path_data, P.package_name, "ohli24"),
|
"ohli24_download_path": os.path.join(path_data, P.package_name, "ohli24"),
|
||||||
"ohli24_auto_make_folder": "True",
|
"ohli24_auto_make_folder": "True",
|
||||||
f"{self.name}_recent_code": "",
|
f"{self.name}_recent_code": "",
|
||||||
"ohli24_auto_make_season_folder": "True",
|
"ohli24_auto_make_season_folder": "True",
|
||||||
"ohli24_finished_insert": "[완결]",
|
"ohli24_finished_insert": "[완결]",
|
||||||
"ohli24_max_ffmpeg_process_count": "1",
|
"ohli24_max_ffmpeg_process_count": "1",
|
||||||
|
f"{self.name}_download_method": "ffmpeg", # ffmpeg or ytdlp
|
||||||
"ohli24_order_desc": "False",
|
"ohli24_order_desc": "False",
|
||||||
"ohli24_auto_start": "False",
|
"ohli24_auto_start": "False",
|
||||||
"ohli24_interval": "* 5 * * *",
|
"ohli24_interval": "* 5 * * *",
|
||||||
@@ -469,13 +462,6 @@ class LogicOhli24(PluginModuleBase):
|
|||||||
return self.current_data
|
return self.current_data
|
||||||
|
|
||||||
if code.startswith("http"):
|
if code.startswith("http"):
|
||||||
|
|
||||||
# if code.split('c/')[1] is not None:
|
|
||||||
# code = code.split('c/')[1]
|
|
||||||
# code_type = 'c'
|
|
||||||
# elif code.split('e/')[1] is not None:
|
|
||||||
# code_type = 'e'
|
|
||||||
# code = code.split('e/')[1]
|
|
||||||
if "/c/" in code:
|
if "/c/" in code:
|
||||||
code = code.split("c/")[1]
|
code = code.split("c/")[1]
|
||||||
code_type = "c"
|
code_type = "c"
|
||||||
@@ -485,43 +471,84 @@ class LogicOhli24(PluginModuleBase):
|
|||||||
|
|
||||||
logger.info(f"code:::: {code}")
|
logger.info(f"code:::: {code}")
|
||||||
|
|
||||||
|
base_url = P.ModelSetting.get("ohli24_url").rstrip("/") # 뒤에 슬래시 제거
|
||||||
|
|
||||||
if code_type == "c":
|
if code_type == "c":
|
||||||
url = P.ModelSetting.get("ohli24_url") + "/c/" + code
|
url = base_url + "/c/" + code
|
||||||
elif code_type == "e":
|
elif code_type == "e":
|
||||||
url = P.ModelSetting.get("ohli24_url") + "/e/" + code
|
url = base_url + "/e/" + code
|
||||||
else:
|
else:
|
||||||
url = P.ModelSetting.get("ohli24_url") + "/e/" + code
|
url = base_url + "/e/" + code
|
||||||
|
|
||||||
if wr_id is not None:
|
if wr_id is not None:
|
||||||
# print(len(wr_id))
|
|
||||||
if len(wr_id) > 0:
|
if len(wr_id) > 0:
|
||||||
url = P.ModelSetting.get("ohli24_url") + "/bbs/board.php?bo_table=" + bo_table + "&wr_id=" + wr_id
|
url = base_url + "/bbs/board.php?bo_table=" + bo_table + "&wr_id=" + wr_id
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
logger.debug("url:::> %s", url)
|
logger.debug("url:::> %s", url)
|
||||||
|
|
||||||
response_data = LogicOhli24.get_html(url, timeout=10)
|
response_data = LogicOhli24.get_html(url, timeout=10)
|
||||||
|
logger.debug(f"HTML length: {len(response_data)}")
|
||||||
|
# 디버깅: HTML 일부 출력
|
||||||
|
if len(response_data) < 1000:
|
||||||
|
logger.warning(f"Short HTML response: {response_data[:500]}")
|
||||||
|
else:
|
||||||
|
# item-subject 있는지 확인
|
||||||
|
if "item-subject" in response_data:
|
||||||
|
logger.info("Found item-subject in HTML")
|
||||||
|
else:
|
||||||
|
logger.warning("item-subject NOT found in HTML")
|
||||||
|
if "itemprop=\"image\"" in response_data:
|
||||||
|
logger.info("Found itemprop=image in HTML")
|
||||||
|
else:
|
||||||
|
logger.warning("itemprop=image NOT found in HTML")
|
||||||
|
|
||||||
tree = html.fromstring(response_data)
|
tree = html.fromstring(response_data)
|
||||||
title = tree.xpath('//div[@class="view-title"]/h1/text()')[0]
|
|
||||||
# image = tree.xpath('//div[@class="view-info"]/div[@class="image"]/div/img')[0]['src']
|
# 제목 추출 - h1[itemprop="headline"] 또는 기타 h1
|
||||||
image = tree.xpath('//div[@class="image"]/div/img/@src')[0]
|
title = ""
|
||||||
image = image.replace("..", P.ModelSetting.get("ohli24_url"))
|
title_xpaths = [
|
||||||
des_items = tree.xpath('//div[@class="list"]/p')
|
'//h1[@itemprop="headline"]/text()',
|
||||||
des = {}
|
'//h1[@itemprop="headline"]//text()',
|
||||||
des_key = [
|
'//div[@class="view-wrap"]//h1/text()',
|
||||||
"_otit",
|
'//h1/text()',
|
||||||
"_dir",
|
|
||||||
"_pub",
|
|
||||||
"_tag",
|
|
||||||
"_classifi",
|
|
||||||
"_country",
|
|
||||||
"_grade",
|
|
||||||
"_total_chapter",
|
|
||||||
"_show_time",
|
|
||||||
"_release_year",
|
|
||||||
"_drawing",
|
|
||||||
]
|
]
|
||||||
|
for xpath in title_xpaths:
|
||||||
|
result = tree.xpath(xpath)
|
||||||
|
if result:
|
||||||
|
title = "".join(result).strip()
|
||||||
|
if title and title != "OHLI24":
|
||||||
|
break
|
||||||
|
|
||||||
|
if not title or "OHLI24" in title:
|
||||||
|
title = urllib.parse.unquote(code)
|
||||||
|
|
||||||
|
logger.info(f"title:: {title}")
|
||||||
|
|
||||||
|
# 이미지 추출 - img[itemprop="image"] 또는 img.img-tag
|
||||||
|
image = ""
|
||||||
|
image_xpaths = [
|
||||||
|
'//img[@itemprop="image"]/@src',
|
||||||
|
'//img[@class="img-tag"]/@src',
|
||||||
|
'//div[@class="view-wrap"]//img/@src',
|
||||||
|
'//div[contains(@class, "view-img")]//img/@src',
|
||||||
|
]
|
||||||
|
for xpath in image_xpaths:
|
||||||
|
result = tree.xpath(xpath)
|
||||||
|
if result:
|
||||||
|
image = result[0]
|
||||||
|
if image and not "logo" in image.lower():
|
||||||
|
break
|
||||||
|
|
||||||
|
if image:
|
||||||
|
if image.startswith(".."):
|
||||||
|
image = image.replace("..", P.ModelSetting.get("ohli24_url"))
|
||||||
|
elif not image.startswith("http"):
|
||||||
|
image = P.ModelSetting.get("ohli24_url") + image
|
||||||
|
|
||||||
|
logger.info(f"image:: {image}")
|
||||||
|
|
||||||
|
# 설명 정보 추출
|
||||||
|
des = {}
|
||||||
description_dict = {
|
description_dict = {
|
||||||
"원제": "_otit",
|
"원제": "_otit",
|
||||||
"원작": "_org",
|
"원작": "_org",
|
||||||
@@ -544,69 +571,87 @@ class LogicOhli24(PluginModuleBase):
|
|||||||
"작화": "_drawing",
|
"작화": "_drawing",
|
||||||
}
|
}
|
||||||
|
|
||||||
list_body_li = tree.xpath('//ul[@class="list-body"]/li')
|
# view-fields에서 메타데이터 추출 시도
|
||||||
# logger.debug(f"list_body_li:: {list_body_li}")
|
des_items = tree.xpath('//div[@class="list"]/p')
|
||||||
|
if not des_items:
|
||||||
|
des_items = tree.xpath('//div[contains(@class, "view-field")]')
|
||||||
|
|
||||||
|
for item in des_items:
|
||||||
|
try:
|
||||||
|
span = item.xpath(".//span//text()")
|
||||||
|
if span and span[0] in description_dict:
|
||||||
|
key = description_dict[span[0]]
|
||||||
|
value = item.xpath(".//span/text()")
|
||||||
|
des[key] = value[1] if len(value) > 1 else ""
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 에피소드 목록 추출 - a.item-subject
|
||||||
episodes = []
|
episodes = []
|
||||||
vi = None
|
episode_links = tree.xpath('//a[@class="item-subject"]')
|
||||||
for li in list_body_li:
|
|
||||||
# logger.debug(li)
|
for a_elem in episode_links:
|
||||||
title = li.xpath(".//a/text()")[0].strip()
|
try:
|
||||||
thumbnail = image
|
ep_title = "".join(a_elem.xpath(".//text()")).strip()
|
||||||
# logger.info(li.xpath('//a[@class="item-subject"]/@href'))
|
href = a_elem.get("href", "")
|
||||||
link = P.ModelSetting.get("ohli24_url") + li.xpath('.//a[@class="item-subject"]/@href')[0]
|
|
||||||
# logger.debug(f"link:: {link}")
|
if not href.startswith("http"):
|
||||||
_date = li.xpath('.//div[@class="wr-date"]/text()')[0]
|
href = P.ModelSetting.get("ohli24_url").rstrip("/") + href
|
||||||
m = hashlib.md5(title.encode("utf-8"))
|
|
||||||
# _vi = hashlib.md5(title.encode('utf-8').hexdigest())
|
# 부모에서 날짜 찾기
|
||||||
# logger.info(m.hexdigest())
|
parent = a_elem.getparent()
|
||||||
_vi = m.hexdigest()
|
_date = ""
|
||||||
episodes.append(
|
if parent is not None:
|
||||||
{
|
grandparent = parent.getparent()
|
||||||
"title": title,
|
if grandparent is not None:
|
||||||
"link": link,
|
date_result = grandparent.xpath('.//div[@class="wr-date"]/text()')
|
||||||
|
if not date_result:
|
||||||
|
date_result = grandparent.xpath('.//*[contains(@class, "date")]/text()')
|
||||||
|
_date = date_result[0].strip() if date_result else ""
|
||||||
|
|
||||||
|
m = hashlib.md5(ep_title.encode("utf-8"))
|
||||||
|
_vi = m.hexdigest()
|
||||||
|
|
||||||
|
episodes.append({
|
||||||
|
"title": ep_title,
|
||||||
|
"link": href,
|
||||||
"thumbnail": image,
|
"thumbnail": image,
|
||||||
"date": _date,
|
"date": _date,
|
||||||
"day": _date,
|
"day": _date,
|
||||||
"_id": title,
|
"_id": ep_title,
|
||||||
"va": link,
|
"va": href,
|
||||||
"_vi": _vi,
|
"_vi": _vi,
|
||||||
"content_code": code,
|
"content_code": code,
|
||||||
}
|
})
|
||||||
)
|
except Exception as ep_err:
|
||||||
|
logger.warning(f"Episode parse error: {ep_err}")
|
||||||
|
continue
|
||||||
|
|
||||||
# logger.info("des_items length:: %s", len(des_items))
|
logger.info(f"Found {len(episodes)} episodes")
|
||||||
for idx, item in enumerate(des_items):
|
|
||||||
# key = des_key[idx]
|
|
||||||
span = item.xpath(".//span//text()")
|
|
||||||
# logger.info(span)
|
|
||||||
key = description_dict[span[0]]
|
|
||||||
try:
|
|
||||||
des[key] = item.xpath(".//span/text()")[1]
|
|
||||||
except IndexError:
|
|
||||||
des[key] = ""
|
|
||||||
|
|
||||||
# logger.info(f"des::>> {des}")
|
# 줄거리 추출
|
||||||
image = image.replace("..", P.ModelSetting.get("ohli24_url"))
|
ser_description_result = tree.xpath('//div[@class="view-stocon"]/div[@class="c"]/text()')
|
||||||
# logger.info("images:: %s", image)
|
if not ser_description_result:
|
||||||
logger.info("title:: %s", title)
|
ser_description_result = tree.xpath('//div[contains(@class, "view-story")]//text()')
|
||||||
|
ser_description = ser_description_result if ser_description_result else []
|
||||||
ser_description = tree.xpath('//div[@class="view-stocon"]/div[@class="c"]/text()')
|
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"title": title,
|
"title": title,
|
||||||
"image": image,
|
"image": image,
|
||||||
"date": "2022.01.11 00:30 (화)",
|
"date": "",
|
||||||
|
"day": "",
|
||||||
"ser_description": ser_description,
|
"ser_description": ser_description,
|
||||||
"des": des,
|
"des": des,
|
||||||
"episode": episodes,
|
"episode": episodes,
|
||||||
|
"code": code,
|
||||||
}
|
}
|
||||||
|
|
||||||
if not P.ModelSetting.get_bool("ohli24_order_desc"):
|
if not P.ModelSetting.get_bool("ohli24_order_desc"):
|
||||||
data["episode"] = list(reversed(data["episode"]))
|
data["episode"] = list(reversed(data["episode"]))
|
||||||
data["list_order"] = "desc"
|
data["list_order"] = "desc"
|
||||||
|
|
||||||
|
self.current_data = data
|
||||||
return data
|
return data
|
||||||
# logger.info(response_text)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
P.logger.error("Exception:%s", e)
|
P.logger.error("Exception:%s", e)
|
||||||
@@ -775,50 +820,88 @@ class LogicOhli24(PluginModuleBase):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_html(url, headers=None, referer=None, stream=False, timeout=10, stealth=False):
|
def get_html(url, headers=None, referer=None, stream=False, timeout=60, stealth=False, data=None, method='GET'):
|
||||||
data = ""
|
"""별도 스레드에서 cloudscraper 실행하여 gevent SSL 충돌 및 Cloudflare 우회"""
|
||||||
if headers is None:
|
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError
|
||||||
headers = {
|
import time
|
||||||
"referer": f"https://ohli24.org",
|
from urllib import parse
|
||||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
||||||
"Chrome/96.0.4664.110 Whale/3.12.129.46 Safari/537.36"
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel "
|
|
||||||
"Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 "
|
|
||||||
"Whale/3.12.129.46 Safari/537.36",
|
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
# URL 인코딩 (한글 주소 대응)
|
||||||
|
if '://' in url:
|
||||||
print("cloudflare protection bypass ==================P")
|
try:
|
||||||
response_date = ""
|
scheme, netloc, path, params, query, fragment = parse.urlparse(url)
|
||||||
if headers is not None:
|
# 이미 인코딩된 경우를 대비해 unquote 후 다시 quote
|
||||||
LogicOhli24.headers = headers
|
path = parse.quote(parse.unquote(path), safe='/')
|
||||||
if LogicOhli24.session is None:
|
query = parse.quote(parse.unquote(query), safe='=&%')
|
||||||
LogicOhli24.session = requests.session()
|
url = parse.urlunparse((scheme, netloc, path, params, query, fragment))
|
||||||
|
except:
|
||||||
LogicOhli24.session.verify = False
|
pass
|
||||||
# logger.debug('get_html :%s', url)
|
|
||||||
# LogicOhli24.headers["Referer"] = "" if referer is None else referer
|
|
||||||
# logger.debug(f"referer:: {referer}")
|
|
||||||
if referer:
|
|
||||||
LogicOhli24.headers["Referer"] = referer
|
|
||||||
|
|
||||||
# logger.info(headers)
|
|
||||||
# logger.debug(f"LogicOhli24.headers:: {LogicOhli24.headers}")
|
|
||||||
|
|
||||||
|
def fetch_url_with_cloudscraper(url, headers, timeout, data, method):
|
||||||
|
"""별도 스레드에서 cloudscraper로 실행"""
|
||||||
|
import cloudscraper
|
||||||
|
scraper = cloudscraper.create_scraper(
|
||||||
|
browser={'browser': 'chrome', 'platform': 'darwin', 'mobile': False},
|
||||||
|
delay=10
|
||||||
|
)
|
||||||
|
# 프록시 설정 (필요시 사용)
|
||||||
proxies = {
|
proxies = {
|
||||||
"http": "http://192.168.0.2:3138",
|
"http": "http://192.168.0.2:3138",
|
||||||
"https": "http://192.168.0.2:3138",
|
"https": "http://192.168.0.2:3138",
|
||||||
}
|
}
|
||||||
|
if method.upper() == 'POST':
|
||||||
|
response = scraper.post(url, headers=headers, data=data, timeout=timeout, proxies=proxies)
|
||||||
|
else:
|
||||||
|
response = scraper.get(url, headers=headers, timeout=timeout, proxies=proxies)
|
||||||
|
return response.text
|
||||||
|
|
||||||
|
response_data = ""
|
||||||
|
|
||||||
|
if headers is None:
|
||||||
|
headers = {
|
||||||
|
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
|
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||||
|
"accept-language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||||
|
}
|
||||||
|
|
||||||
|
if referer:
|
||||||
|
# Referer 인코딩
|
||||||
|
if '://' in referer:
|
||||||
|
try:
|
||||||
|
scheme, netloc, path, params, query, fragment = parse.urlparse(referer)
|
||||||
|
path = parse.quote(parse.unquote(path), safe='/')
|
||||||
|
query = parse.quote(parse.unquote(query), safe='=&%')
|
||||||
|
referer = parse.urlunparse((scheme, netloc, path, params, query, fragment))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
headers["referer"] = referer
|
||||||
|
elif "referer" not in headers:
|
||||||
|
headers["referer"] = "https://ani.ohli24.com"
|
||||||
|
|
||||||
|
max_retries = 3
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
logger.debug(f"get_html (cloudscraper in thread) {method} attempt {attempt + 1}: {url}")
|
||||||
|
|
||||||
|
# ThreadPoolExecutor로 별도 스레드에서 cloudscraper 실행
|
||||||
|
with ThreadPoolExecutor(max_workers=1) as executor:
|
||||||
|
future = executor.submit(fetch_url_with_cloudscraper, url, headers, timeout, data, method)
|
||||||
|
response_data = future.result(timeout=timeout + 10)
|
||||||
|
|
||||||
|
if response_data and (len(response_data) > 10 or method.upper() == 'POST'):
|
||||||
|
logger.debug(f"get_html success, length: {len(response_data)}")
|
||||||
|
return response_data
|
||||||
|
else:
|
||||||
|
logger.warning(f"Short response (len={len(response_data) if response_data else 0})")
|
||||||
|
|
||||||
|
except FuturesTimeoutError:
|
||||||
|
logger.warning(f"get_html attempt {attempt + 1} timed out")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"get_html attempt {attempt + 1} failed: {e}")
|
||||||
|
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
page_content = LogicOhli24.session.get(url, headers=LogicOhli24.headers, timeout=timeout, proxies=proxies)
|
|
||||||
response_data = page_content.text
|
|
||||||
# logger.debug(response_data)
|
|
||||||
return response_data
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Exception:%s", e)
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
return response_data
|
return response_data
|
||||||
|
|
||||||
#########################################################
|
#########################################################
|
||||||
@@ -1025,166 +1108,97 @@ class Ohli24QueueEntity(FfmpegQueueEntity):
|
|||||||
# Get episode info from OHLI24 site
|
# Get episode info from OHLI24 site
|
||||||
def make_episode_info(self):
|
def make_episode_info(self):
|
||||||
try:
|
try:
|
||||||
base_url = "https://a21.ohli24.com"
|
|
||||||
base_url = P.ModelSetting.get("ohli24_url")
|
base_url = P.ModelSetting.get("ohli24_url")
|
||||||
iframe_url = ""
|
|
||||||
|
|
||||||
# https://ohli24.org/e/%EB%85%B9%EC%9D%84%20%EB%A8%B9%EB%8A%94%20%EB%B9%84%EC%8A%A4%EC%BD%94%206%ED%99%94
|
# 에피소드 페이지 URL (예: https://ani.ohli24.com/e/원펀맨 3기 1화)
|
||||||
url = self.info["va"]
|
url = self.info["va"]
|
||||||
|
if "//e/" in url:
|
||||||
|
url = url.replace("//e/", "/e/")
|
||||||
|
|
||||||
ourls = parse.urlparse(url)
|
ourls = parse.urlparse(url)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Referer": f"{ourls.scheme}://{ourls.netloc}",
|
"Referer": f"{ourls.scheme}://{ourls.netloc}",
|
||||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) "
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
"Chrome/96.0.4664.110 Whale/3.12.129.46 Safari/537.36",
|
|
||||||
}
|
}
|
||||||
logger.debug(headers)
|
logger.debug(f"make_episode_info()::url==> {url}")
|
||||||
logger.debug("make_episode_info()::url==> %s", url)
|
|
||||||
logger.info(f"self.info:::> {self.info}")
|
logger.info(f"self.info:::> {self.info}")
|
||||||
|
|
||||||
# text = requests.get(url, headers=headers).text
|
# Step 1: 에피소드 페이지에서 cdndania.com iframe 찾기
|
||||||
text = LogicOhli24.get_html(url, headers=headers, referer=f"{ourls.scheme}://{ourls.netloc}")
|
text = LogicOhli24.get_html(url, headers=headers, referer=f"{ourls.scheme}://{ourls.netloc}")
|
||||||
# logger.debug(text)
|
|
||||||
soup1 = BeautifulSoup(text, "lxml")
|
|
||||||
pattern = re.compile(r"url : \"\.\.(.*)\"")
|
|
||||||
script = soup1.find("script", text=pattern)
|
|
||||||
|
|
||||||
if script:
|
# 디버깅: HTML에 cdndania 있는지 확인
|
||||||
match = pattern.search(script.text)
|
if "cdndania" in text:
|
||||||
if match:
|
logger.info("cdndania found in HTML")
|
||||||
iframe_url = match.group(1)
|
else:
|
||||||
logger.info("iframe_url::> %s", iframe_url)
|
logger.warning("cdndania NOT found in HTML - page may be dynamically loaded")
|
||||||
|
logger.debug(f"HTML snippet: {text[:1000]}")
|
||||||
|
|
||||||
# logger.debug(soup1.find("iframe"))
|
soup = BeautifulSoup(text, "lxml")
|
||||||
|
|
||||||
# iframe_url = soup1.find("iframe")["src"]
|
# mcpalyer 클래스 내의 iframe 찾기
|
||||||
# logger.info("iframe_url::> %s", iframe_url)
|
player_div = soup.find("div", class_="mcpalyer")
|
||||||
|
logger.debug(f"player_div (mcpalyer): {player_div is not None}")
|
||||||
|
|
||||||
print(base_url)
|
if not player_div:
|
||||||
print(iframe_url)
|
player_div = soup.find("div", class_="embed-responsive")
|
||||||
# exit()
|
logger.debug(f"player_div (embed-responsive): {player_div is not None}")
|
||||||
iframe_src = f'{P.ModelSetting.get("ohli24_url")}{iframe_url}'
|
|
||||||
|
|
||||||
iframe_html = LogicOhli24.get_html(iframe_src, headers=headers, timeout=600)
|
iframe = None
|
||||||
|
if player_div:
|
||||||
|
iframe = player_div.find("iframe")
|
||||||
|
logger.debug(f"iframe in player_div: {iframe is not None}")
|
||||||
|
if not iframe:
|
||||||
|
iframe = soup.find("iframe", src=re.compile(r"cdndania\.com"))
|
||||||
|
logger.debug(f"iframe with cdndania src: {iframe is not None}")
|
||||||
|
if not iframe:
|
||||||
|
# 모든 iframe 찾기
|
||||||
|
all_iframes = soup.find_all("iframe")
|
||||||
|
logger.debug(f"Total iframes found: {len(all_iframes)}")
|
||||||
|
for i, f in enumerate(all_iframes):
|
||||||
|
logger.debug(f"iframe {i}: src={f.get('src', 'no src')}")
|
||||||
|
if all_iframes:
|
||||||
|
iframe = all_iframes[0]
|
||||||
|
|
||||||
# print(iframe_html)
|
if not iframe or not iframe.get("src"):
|
||||||
pattern = r"<iframe src=\"(.*?)\" allowfullscreen>"
|
logger.error("No iframe found on episode page")
|
||||||
|
return
|
||||||
|
|
||||||
match = re.search(pattern, iframe_html)
|
iframe_src = iframe.get("src")
|
||||||
if match:
|
logger.info(f"Found cdndania iframe: {iframe_src}")
|
||||||
iframe_src = match.group(1)
|
|
||||||
print(iframe_src)
|
|
||||||
|
|
||||||
logger.debug(f"iframe_src:::> {iframe_src}")
|
# Step 2: cdndania.com 페이지에서 m3u8 URL 추출
|
||||||
|
video_url, vtt_url = self.extract_video_from_cdndania(iframe_src, url)
|
||||||
|
|
||||||
# resp1 = requests.get(iframe_src, headers=headers, timeout=600).text
|
if not video_url:
|
||||||
resp1 = LogicOhli24.get_html(iframe_src, headers=headers, timeout=600)
|
logger.error("Failed to extract video URL from cdndania")
|
||||||
# logger.info("resp1::>> %s", resp1)
|
return
|
||||||
soup3 = BeautifulSoup(resp1, "lxml")
|
|
||||||
# packed_pattern = re.compile(r'\\{*(eval.+)*\\}', re.MULTILINE | re.DOTALL)
|
|
||||||
s_pattern = re.compile(r"(eval.+)", re.MULTILINE | re.DOTALL)
|
|
||||||
packed_pattern = re.compile(r"if?.([^{}]+)\{.*(eval.+)\}.+else?.{.(eval.+)\}", re.DOTALL)
|
|
||||||
packed_script = soup3.find("script", text=s_pattern)
|
|
||||||
# packed_script = soup3.find('script')
|
|
||||||
# logger.info("packed_script>>> %s", packed_script.text)
|
|
||||||
unpack_script = None
|
|
||||||
if packed_script is not None:
|
|
||||||
# logger.debug('zzzzzzzzzzzz')
|
|
||||||
match = packed_pattern.search(packed_script.text)
|
|
||||||
# match = re.search(packed_pattern, packed_script.text)
|
|
||||||
# logger.debug("match::: %s", match.group())
|
|
||||||
# unpack_script = jsbeautifier.beautify(match.group(3))
|
|
||||||
|
|
||||||
logger.debug(type(packed_script))
|
self.url = video_url
|
||||||
unpack_script = jsbeautifier.beautify(str(packed_script))
|
self.srt_url = vtt_url
|
||||||
|
logger.info(f"Video URL: {self.url}")
|
||||||
|
if self.srt_url:
|
||||||
|
logger.info(f"Subtitle URL: {self.srt_url}")
|
||||||
|
|
||||||
p1 = re.compile(r"(\"tracks\".*\])\,\"captions\"", re.MULTILINE | re.DOTALL)
|
# 헤더 설정
|
||||||
m2 = re.search(
|
|
||||||
r"(\"tracks\".*\]).*\"captions\"",
|
|
||||||
unpack_script,
|
|
||||||
flags=re.MULTILINE | re.DOTALL,
|
|
||||||
)
|
|
||||||
# print(m2.group(1))
|
|
||||||
dict_string = "{" + m2.group(1) + "}"
|
|
||||||
|
|
||||||
logger.info(f"dict_string::> {dict_string}")
|
|
||||||
tracks = json.loads(dict_string)
|
|
||||||
self.srt_url = tracks["tracks"][0]["file"]
|
|
||||||
|
|
||||||
logger.debug(f'srt_url::: {tracks["tracks"][0]["file"]}')
|
|
||||||
|
|
||||||
video_hash = iframe_src.split("/")
|
|
||||||
video_hashcode = re.sub(r"index\.php\?data=", "", video_hash[-1])
|
|
||||||
self._vi = video_hashcode
|
|
||||||
|
|
||||||
logger.debug(f"video_hash::> {video_hash}")
|
|
||||||
video_info_url = f"{video_hash[0]}//{video_hash[2]}/player/index.php?data={video_hashcode}&do=getVideo"
|
|
||||||
# print('hash:::', video_hash)
|
|
||||||
logger.debug(f"video_info_url::: {video_info_url}")
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"referer": f"{iframe_src}",
|
|
||||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
||||||
"Chrome/96.0.4664.110 Whale/3.12.129.46 Safari/537.36"
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel "
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
||||||
"Chrome/116.0.0.0 Safari/537.36"
|
|
||||||
"Whale/3.12.129.46 Safari/537.36",
|
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
|
||||||
"Cookie": "PHPSESSID=b6hnl2crfvtg36sm6rjjkso4p0; 2a0d2363701f23f8a75028924a3af643=MTgwLjY2LjIyMi4xODk%3D; _ga=GA1.1.586565509.1695135593; __gads=ID=60e47defb3337e02-227f0fc9e3e3009a:T=1695135593:RT=1695135593:S=ALNI_MagY46XGCbx9E4Et2DRzfUHdTAKsg; __gpi=UID=00000c4bb3d077c8:T=1695135593:RT=1695135593:S=ALNI_MYvj_8OjdhtGPEGoXhPsQWq1qye8Q; _ga_MWWDFMDJR0=GS1.1.1695135593.1.1.1695135599.0.0.0",
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"hash": video_hash[-1],
|
|
||||||
}
|
|
||||||
resp2 = requests.post(video_info_url, headers=headers, data=payload, timeout=20).json()
|
|
||||||
|
|
||||||
logger.debug("resp2::> %s", resp2)
|
|
||||||
|
|
||||||
hls_url = resp2["videoSource"]
|
|
||||||
logger.debug(f"video_url::> {hls_url}")
|
|
||||||
|
|
||||||
resp3 = requests.get(hls_url, headers=headers).text
|
|
||||||
# logger.debug(resp3)
|
|
||||||
|
|
||||||
# stream_url = hls_url.split('\n')[-1].strip()
|
|
||||||
stream_info = resp3.split("\n")[-2:]
|
|
||||||
# logger.debug('stream_url:: %s', stream_url)
|
|
||||||
logger.debug(f"stream_info:: {stream_info}")
|
|
||||||
self.headers = {
|
self.headers = {
|
||||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
"Chrome/71.0.3554.0 Safari/537.36Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
"Referer": iframe_src,
|
||||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3554.0 Safari/537.36",
|
|
||||||
"Referer": "https://ndoodle.xyz/video/03a3655fff3e9bdea48de9f49e938e32",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.url = stream_info[1].strip()
|
# 파일명 생성
|
||||||
logger.info(self.url)
|
|
||||||
if "anibeast.com" in self.url:
|
|
||||||
self.headers["Referer"] = iframe_src
|
|
||||||
if "crazypatutu.com" in self.url:
|
|
||||||
self.headers["Referer"] = iframe_src
|
|
||||||
|
|
||||||
match = re.compile(r'NAME="(?P<quality>.*?)"').search(stream_info[0])
|
|
||||||
self.quality = "720P"
|
|
||||||
if match is not None:
|
|
||||||
self.quality = match.group("quality")
|
|
||||||
logger.info(self.quality)
|
|
||||||
|
|
||||||
match = re.compile(r"(?P<title>.*?)\s*((?P<season>\d+)%s)?\s*((?P<epi_no>\d+)%s)" % ("기", "화")).search(
|
match = re.compile(r"(?P<title>.*?)\s*((?P<season>\d+)%s)?\s*((?P<epi_no>\d+)%s)" % ("기", "화")).search(
|
||||||
self.info["title"]
|
self.info["title"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# epi_no 초기값
|
|
||||||
epi_no = 1
|
epi_no = 1
|
||||||
|
self.quality = "720P"
|
||||||
|
|
||||||
if match:
|
if match:
|
||||||
self.content_title = match.group("title").strip()
|
self.content_title = match.group("title").strip()
|
||||||
if "season" in match.groupdict() and match.group("season") is not None:
|
if "season" in match.groupdict() and match.group("season") is not None:
|
||||||
self.season = int(match.group("season"))
|
self.season = int(match.group("season"))
|
||||||
|
|
||||||
# epi_no = 1
|
|
||||||
epi_no = int(match.group("epi_no"))
|
epi_no = int(match.group("epi_no"))
|
||||||
ret = "%s.S%sE%s.%s-OHNI24.mp4" % (
|
ret = "%s.S%sE%s.%s-OHNI24.mp4" % (
|
||||||
self.content_title,
|
self.content_title,
|
||||||
@@ -1194,10 +1208,9 @@ class Ohli24QueueEntity(FfmpegQueueEntity):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.content_title = self.info["title"]
|
self.content_title = self.info["title"]
|
||||||
P.logger.debug("NOT MATCH")
|
logger.debug("NOT MATCH")
|
||||||
ret = "%s.720p-OHNI24.mp4" % self.info["title"]
|
ret = "%s.720p-OHNI24.mp4" % self.info["title"]
|
||||||
|
|
||||||
# logger.info('self.content_title:: %s', self.content_title)
|
|
||||||
self.epi_queue = epi_no
|
self.epi_queue = epi_no
|
||||||
self.filename = Util.change_text_for_use_filename(ret)
|
self.filename = Util.change_text_for_use_filename(ret)
|
||||||
logger.info(f"self.filename::> {self.filename}")
|
logger.info(f"self.filename::> {self.filename}")
|
||||||
@@ -1220,19 +1233,112 @@ class Ohli24QueueEntity(FfmpegQueueEntity):
|
|||||||
if not os.path.exists(self.savepath):
|
if not os.path.exists(self.savepath):
|
||||||
os.makedirs(self.savepath)
|
os.makedirs(self.savepath)
|
||||||
|
|
||||||
from framework.common.util import write_file, convert_vtt_to_srt
|
# 자막 다운로드
|
||||||
|
if self.srt_url and "thumbnails.vtt" not in self.srt_url:
|
||||||
srt_filepath = os.path.join(self.savepath, self.filename.replace(".mp4", ".ko.srt"))
|
try:
|
||||||
|
srt_filepath = os.path.join(self.savepath, self.filename.replace(".mp4", ".ko.srt"))
|
||||||
if self.srt_url is not None and not os.path.exists(srt_filepath) and not ("thumbnails.vtt" in self.srt_url):
|
if not os.path.exists(srt_filepath):
|
||||||
if requests.get(self.srt_url, headers=headers).status_code == 200:
|
srt_resp = requests.get(self.srt_url, headers=self.headers, timeout=30)
|
||||||
srt_data = requests.get(self.srt_url, headers=headers).text
|
if srt_resp.status_code == 200:
|
||||||
Util.write_file(srt_data, srt_filepath)
|
Util.write_file(srt_resp.text, srt_filepath)
|
||||||
|
logger.info(f"Subtitle saved: {srt_filepath}")
|
||||||
|
except Exception as srt_err:
|
||||||
|
logger.warning(f"Subtitle download failed: {srt_err}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
P.logger.error("Exception:%s", e)
|
P.logger.error("Exception:%s", e)
|
||||||
P.logger.error(traceback.format_exc())
|
P.logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
def extract_video_from_cdndania(self, iframe_src, referer_url):
|
||||||
|
"""cdndania.com 플레이어에서 API 호출을 통해 비디오(m3u8) 및 자막(vtt) URL 추출"""
|
||||||
|
video_url = None
|
||||||
|
vtt_url = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.debug(f"Extracting from cdndania: {iframe_src}")
|
||||||
|
|
||||||
|
# iframe URL에서 비디오 ID(hash) 추출
|
||||||
|
video_id = ""
|
||||||
|
if "/video/" in iframe_src:
|
||||||
|
video_id = iframe_src.split("/video/")[1].split("?")[0].split("&")[0]
|
||||||
|
elif "/v/" in iframe_src:
|
||||||
|
video_id = iframe_src.split("/v/")[1].split("?")[0].split("&")[0]
|
||||||
|
|
||||||
|
if not video_id:
|
||||||
|
logger.error(f"Could not find video ID in iframe URL: {iframe_src}")
|
||||||
|
return video_url, vtt_url
|
||||||
|
|
||||||
|
# getVideo API 호출
|
||||||
|
api_url = f"https://cdndania.com/player/index.php?data={video_id}&do=getVideo"
|
||||||
|
headers = {
|
||||||
|
"x-requested-with": "XMLHttpRequest",
|
||||||
|
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
|
"referer": iframe_src
|
||||||
|
}
|
||||||
|
# Referer는 메인 사이트 도메인만 보내는 것이 더 안정적일 수 있음
|
||||||
|
post_data = {
|
||||||
|
"hash": video_id,
|
||||||
|
"r": "https://ani.ohli24.com/"
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(f"Calling video API: {api_url}")
|
||||||
|
json_text = LogicOhli24.get_html(api_url, headers=headers, data=post_data, method='POST', timeout=30)
|
||||||
|
|
||||||
|
if json_text:
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
data = json.loads(json_text)
|
||||||
|
video_url = data.get("videoSource")
|
||||||
|
if not video_url:
|
||||||
|
video_url = data.get("securedLink")
|
||||||
|
|
||||||
|
if video_url:
|
||||||
|
logger.info(f"Found video URL via API: {video_url}")
|
||||||
|
|
||||||
|
# VTT 자막 확인 (있는 경우)
|
||||||
|
vtt_url = data.get("videoSubtitle")
|
||||||
|
if vtt_url:
|
||||||
|
logger.info(f"Found subtitle URL via API: {vtt_url}")
|
||||||
|
except Exception as json_err:
|
||||||
|
logger.warning(f"Failed to parse API JSON: {json_err}")
|
||||||
|
|
||||||
|
# API 실패 시 기존 방식(정규식)으로 폴백
|
||||||
|
if not video_url:
|
||||||
|
logger.info("API extraction failed, falling back to regex")
|
||||||
|
html_content = LogicOhli24.get_html(iframe_src, referer=referer_url, timeout=30)
|
||||||
|
if html_content:
|
||||||
|
# m3u8 URL 패턴 찾기
|
||||||
|
m3u8_patterns = [
|
||||||
|
re.compile(r"file:\s*['\"]([^'\"]*(?:\.m3u8|master\.txt)[^'\"]*)['\"]"),
|
||||||
|
re.compile(r"['\"]([^'\"]*(?:\.m3u8|master\.txt)[^'\"]*)['\"]"),
|
||||||
|
]
|
||||||
|
for pattern in m3u8_patterns:
|
||||||
|
match = pattern.search(html_content)
|
||||||
|
if match:
|
||||||
|
tmp_url = match.group(1)
|
||||||
|
if tmp_url.startswith("//"): tmp_url = "https:" + tmp_url
|
||||||
|
elif tmp_url.startswith("/"):
|
||||||
|
parsed = parse.urlparse(iframe_src)
|
||||||
|
tmp_url = f"{parsed.scheme}://{parsed.netloc}{tmp_url}"
|
||||||
|
video_url = tmp_url
|
||||||
|
logger.info(f"Found video URL via regex: {video_url}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not vtt_url:
|
||||||
|
vtt_match = re.search(r"['\"]([^'\"]*\.vtt[^'\"]*)['\"]", html_content)
|
||||||
|
if vtt_match:
|
||||||
|
vtt_url = vtt_match.group(1)
|
||||||
|
if vtt_url.startswith("//"): vtt_url = "https:" + vtt_url
|
||||||
|
elif vtt_url.startswith("/"):
|
||||||
|
parsed = parse.urlparse(iframe_src)
|
||||||
|
vtt_url = f"{parsed.scheme}://{parsed.netloc}{vtt_url}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in extract_video_from_cdndania: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
return video_url, vtt_url
|
||||||
|
|
||||||
# def callback_function(self, **args):
|
# def callback_function(self, **args):
|
||||||
# refresh_type = None
|
# refresh_type = None
|
||||||
# # entity = self.get_entity_by_entity_id(arg['plugin_id'])
|
# # entity = self.get_entity_by_entity_id(arg['plugin_id'])
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ $(document).ready(function(){
|
|||||||
|
|
||||||
// linkkf_status 이벤트로 다운로드 상태 업데이트 수신
|
// linkkf_status 이벤트로 다운로드 상태 업데이트 수신
|
||||||
frameworkSocket.on('linkkf_status', function(data) {
|
frameworkSocket.on('linkkf_status', function(data) {
|
||||||
console.log('linkkf_status received:', data.percent + '%');
|
|
||||||
status_html(data);
|
status_html(data);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -112,10 +111,75 @@ $(document).ready(function(){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var refreshIntervalId = null;
|
||||||
|
|
||||||
|
// 로딩 인디케이터 없이 조용히 목록 가져오기
|
||||||
|
function silentFetchList(callback) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/' + PACKAGE_NAME + '/ajax/' + MODULE_NAME + '/command',
|
||||||
|
type: 'POST',
|
||||||
|
cache: false,
|
||||||
|
global: false, // 로딩 인디케이터 표시 안함
|
||||||
|
data: {command: 'list'},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(data) {
|
||||||
|
if (callback) callback(data);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
// 에러 무시
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 자동 새로고침 (로딩 인디케이터 없음)
|
||||||
|
function autoRefreshList() {
|
||||||
|
silentFetchList(function(data) {
|
||||||
|
// 새 아이템이 추가되었는지 확인
|
||||||
|
if (!current_data || data.length !== current_data.length) {
|
||||||
|
current_data = data;
|
||||||
|
$("#list").html('');
|
||||||
|
console.log('Refreshed list:', data.length, 'items');
|
||||||
|
if (data.length == 0) {
|
||||||
|
str = "<tr><td colspan='10'><h4>작업이 없습니다.</h4><td><tr>";
|
||||||
|
} else {
|
||||||
|
str = ''
|
||||||
|
for(i in data) {
|
||||||
|
str += make_item(data[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$("#list").html(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 진행 중인 다운로드가 있는지 확인
|
||||||
|
var hasActiveDownload = false;
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
for (var j = 0; j < data.length; j++) {
|
||||||
|
if (data[j].status_str === 'DOWNLOADING' || data[j].status_str === 'WAITING') {
|
||||||
|
hasActiveDownload = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모든 다운로드 완료 시 자동 새로고침 중지
|
||||||
|
if (!hasActiveDownload && refreshIntervalId) {
|
||||||
|
console.log('All downloads completed, stopping auto-refresh');
|
||||||
|
clearInterval(refreshIntervalId);
|
||||||
|
refreshIntervalId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 활성 다운로드가 있고 새로고침이 중지된 경우 재시작
|
||||||
|
if (hasActiveDownload && !refreshIntervalId) {
|
||||||
|
console.log('Active downloads detected, starting auto-refresh');
|
||||||
|
refreshIntervalId = setInterval(autoRefreshList, 3000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 초기 로드 (로딩 인디케이터 표시)
|
||||||
globalSendCommand('list', null, null, null, function(data) {
|
globalSendCommand('list', null, null, null, function(data) {
|
||||||
current_data = data;
|
current_data = data;
|
||||||
$("#list").html('');
|
$("#list").html('');
|
||||||
console.log(data)
|
|
||||||
if (data.length == 0) {
|
if (data.length == 0) {
|
||||||
str = "<tr><td colspan='10'><h4>작업이 없습니다.</h4><td><tr>";
|
str = "<tr><td colspan='10'><h4>작업이 없습니다.</h4><td><tr>";
|
||||||
} else {
|
} else {
|
||||||
@@ -126,6 +190,9 @@ $(document).ready(function(){
|
|||||||
}
|
}
|
||||||
$("#list").html(str);
|
$("#list").html(str);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 3초마다 자동 새로고침 (로딩 인디케이터 없음)
|
||||||
|
refreshIntervalId = setInterval(autoRefreshList, 3000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -216,14 +283,27 @@ function button_html(data) {
|
|||||||
|
|
||||||
function status_html(data) {
|
function status_html(data) {
|
||||||
var progress = document.getElementById("progress_" + data.idx);
|
var progress = document.getElementById("progress_" + data.idx);
|
||||||
|
if (!progress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
progress.style.width = data.percent+ '%';
|
progress.style.width = data.percent+ '%';
|
||||||
progress.innerHTML = data.percent+ '%';
|
progress.innerHTML = data.percent+ '%';
|
||||||
progress.style.visibility = 'visible';
|
progress.style.visibility = 'visible';
|
||||||
document.getElementById("status_" + data.idx).innerHTML = data.status_kor;
|
|
||||||
document.getElementById("current_pf_count_" + data.idx).innerHTML = data.current_pf_count;
|
var statusEl = document.getElementById("status_" + data.idx);
|
||||||
document.getElementById("current_speed_" + data.idx).innerHTML = data.current_speed;
|
if (statusEl) statusEl.innerHTML = data.status_kor;
|
||||||
document.getElementById("download_time_" + data.idx).innerHTML = data.download_time;
|
|
||||||
document.getElementById("detail_" + data.idx).innerHTML = get_detail(data);
|
var pfEl = document.getElementById("current_pf_count_" + data.idx);
|
||||||
|
if (pfEl) pfEl.innerHTML = data.current_pf_count;
|
||||||
|
|
||||||
|
var speedEl = document.getElementById("current_speed_" + data.idx);
|
||||||
|
if (speedEl) speedEl.innerHTML = data.current_speed;
|
||||||
|
|
||||||
|
var timeEl = document.getElementById("download_time_" + data.idx);
|
||||||
|
if (timeEl) timeEl.innerHTML = data.download_time;
|
||||||
|
|
||||||
|
var detailEl = document.getElementById("detail_" + data.idx);
|
||||||
|
if (detailEl) detailEl.innerHTML = get_detail(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -165,9 +165,9 @@
|
|||||||
tmp += '<div class="form-inline">';
|
tmp += '<div class="form-inline">';
|
||||||
tmp +=
|
tmp +=
|
||||||
'<input id="checkbox_' +
|
'<input id="checkbox_' +
|
||||||
data.episode[i].code +
|
i +
|
||||||
'" name="checkbox_' +
|
'" name="checkbox_' +
|
||||||
data.episode[i].code +
|
i +
|
||||||
'" type="checkbox" checked data-toggle="toggle" data-on="선 택" data-off="-" data-onstyle="success" data-offstyle="danger" data-size="small"> ';
|
'" type="checkbox" checked data-toggle="toggle" data-on="선 택" data-off="-" data-onstyle="success" data-offstyle="danger" data-size="small"> ';
|
||||||
// tmp += m_button('add_queue_btn', '다운로드 추가', [{'key': 'code', 'value': data.episode[i].code}])
|
// tmp += m_button('add_queue_btn', '다운로드 추가', [{'key': 'code', 'value': data.episode[i].code}])
|
||||||
tmp += m_button("add_queue_btn", "다운로드 추가", [
|
tmp += m_button("add_queue_btn", "다운로드 추가", [
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
{{ macros.setting_input_text_and_buttons('linkkf_url', 'linkkf URL', [['go_btn', 'GO']], value=arg['linkkf_url']) }}
|
{{ macros.setting_input_text_and_buttons('linkkf_url', 'linkkf URL', [['go_btn', 'GO']], value=arg['linkkf_url']) }}
|
||||||
{{ macros.setting_input_text('linkkf_download_path', '저장 폴더', value=arg['linkkf_download_path'], desc='정상적으로 다운 완료 된 파일이 이동할 폴더 입니다. ') }}
|
{{ macros.setting_input_text('linkkf_download_path', '저장 폴더', value=arg['linkkf_download_path'], desc='정상적으로 다운 완료 된 파일이 이동할 폴더 입니다. ') }}
|
||||||
{{ macros.setting_input_int('linkkf_max_ffmpeg_process_count', '동시 다운로드 수', value=arg['linkkf_max_ffmpeg_process_count'], desc='동시에 다운로드 할 에피소드 갯수입니다.') }}
|
{{ macros.setting_input_int('linkkf_max_ffmpeg_process_count', '동시 다운로드 수', value=arg['linkkf_max_ffmpeg_process_count'], desc='동시에 다운로드 할 에피소드 갯수입니다.') }}
|
||||||
|
{{ macros.setting_select('linkkf_download_method', '다운로드 방법', [['ffmpeg', 'ffmpeg'], ['ytdlp', 'yt-dlp']], col='3', value=arg['linkkf_download_method'], desc='ffmpeg: HLS 다운로더 사용, yt-dlp: yt-dlp 사용') }}
|
||||||
{{ macros.setting_checkbox('linkkf_order_desc', '요청 화면 최신순 정렬', value=arg['linkkf_order_desc'], desc='On : 최신화부터, Off : 1화부터') }}
|
{{ macros.setting_checkbox('linkkf_order_desc', '요청 화면 최신순 정렬', value=arg['linkkf_order_desc'], desc='On : 최신화부터, Off : 1화부터') }}
|
||||||
{{ macros.setting_checkbox('linkkf_auto_make_folder', '제목 폴더 생성', value=arg['linkkf_auto_make_folder'], desc='제목으로 폴더를 생성하고 폴더 안에 다운로드합니다.') }}
|
{{ macros.setting_checkbox('linkkf_auto_make_folder', '제목 폴더 생성', value=arg['linkkf_auto_make_folder'], desc='제목으로 폴더를 생성하고 폴더 안에 다운로드합니다.') }}
|
||||||
<div id="linkkf_auto_make_folder_div" class="collapse">
|
<div id="linkkf_auto_make_folder_div" class="collapse">
|
||||||
|
|||||||
@@ -47,9 +47,53 @@
|
|||||||
on_status(data)
|
on_status(data)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 초기 목록 로드
|
||||||
|
on_start();
|
||||||
|
|
||||||
|
// 3초마다 자동 새로고침 시작
|
||||||
|
var refreshIntervalId = setInterval(silentRefresh, 3000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var current_list_length = 0;
|
||||||
|
var refreshIntervalId = null;
|
||||||
|
|
||||||
|
// 로딩 인디케이터 없이 조용히 목록 가져오기
|
||||||
|
function silentRefresh() {
|
||||||
|
$.ajax({
|
||||||
|
url: '/' + package_name + '/ajax/' + sub + '/entity_list',
|
||||||
|
type: "POST",
|
||||||
|
cache: false,
|
||||||
|
global: false, // 로딩 인디케이터 표시 안함
|
||||||
|
data: {},
|
||||||
|
dataType: "json",
|
||||||
|
success: function (data) {
|
||||||
|
// 목록이 변경된 경우에만 갱신
|
||||||
|
if (data.length !== current_list_length) {
|
||||||
|
current_list_length = data.length;
|
||||||
|
make_download_list(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 활성 다운로드 확인
|
||||||
|
var hasActive = false;
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
if (data[i].ffmpeg_status_kor === '다운로드중' || data[i].ffmpeg_status_kor === '대기') {
|
||||||
|
hasActive = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모든 다운로드 완료 시 새로고침 중지
|
||||||
|
if (!hasActive && refreshIntervalId) {
|
||||||
|
clearInterval(refreshIntervalId);
|
||||||
|
refreshIntervalId = null;
|
||||||
|
}
|
||||||
|
// 활성 다운로드 있고 새로고침 중지된 경우 재시작
|
||||||
|
if (hasActive && !refreshIntervalId) {
|
||||||
|
refreshIntervalId = setInterval(silentRefresh, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function on_start() {
|
function on_start() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -60,6 +104,7 @@
|
|||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
console.log("on_start():: ", data)
|
console.log("on_start():: ", data)
|
||||||
|
current_list_length = data.length;
|
||||||
make_download_list(data)
|
make_download_list(data)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -121,17 +121,18 @@
|
|||||||
tmp = '<img src="' + data.image + '" class="img-fluid" />';
|
tmp = '<img src="' + data.image + '" class="img-fluid" />';
|
||||||
str += m_col(3, tmp)
|
str += m_col(3, tmp)
|
||||||
tmp = ''
|
tmp = ''
|
||||||
tmp += m_row_start(2) + m_col(3, '제목', 'right') + m_col(9, data.title) + m_row_end();
|
var des = data.des || {};
|
||||||
tmp += m_row_start(2) + m_col(3, '원제', 'right') + m_col(9, data.des._otit) + m_row_end();
|
tmp += m_row_start(2) + m_col(3, '제목', 'right') + m_col(9, data.title || '') + m_row_end();
|
||||||
tmp += m_row_start(2) + m_col(3, '감독', 'right') + m_col(9, data.des._dir) + m_row_end();
|
tmp += m_row_start(2) + m_col(3, '원제', 'right') + m_col(9, des._otit || '') + m_row_end();
|
||||||
tmp += m_row_start(2) + m_col(3, '제작사', 'right') + m_col(9, data.des._pub) + m_row_end();
|
tmp += m_row_start(2) + m_col(3, '감독', 'right') + m_col(9, des._dir || '') + m_row_end();
|
||||||
tmp += m_row_start(2) + m_col(3, '장르', 'right') + m_col(9, data.des._tag) + m_row_end();
|
tmp += m_row_start(2) + m_col(3, '제작사', 'right') + m_col(9, des._pub || '') + m_row_end();
|
||||||
tmp += m_row_start(2) + m_col(3, '분류', 'right') + m_col(9, data.des._classifi) + m_row_end();
|
tmp += m_row_start(2) + m_col(3, '장르', 'right') + m_col(9, des._tag || '') + m_row_end();
|
||||||
tmp += m_row_start(2) + m_col(3, '방영일', 'right') + m_col(9, data.date + '(' + data.day + ')') + m_row_end();
|
tmp += m_row_start(2) + m_col(3, '분류', 'right') + m_col(9, des._classifi || '') + m_row_end();
|
||||||
tmp += m_row_start(2) + m_col(3, '등급', 'right') + m_col(9, data.des._grade) + m_row_end();
|
tmp += m_row_start(2) + m_col(3, '방영일', 'right') + m_col(9, (data.date || '') + '(' + (data.day || '') + ')') + m_row_end();
|
||||||
tmp += m_row_start(2) + m_col(3, '총화수', 'right') + m_col(9, data.des._total_chapter ? data.des._total_chapter : '') + m_row_end();
|
tmp += m_row_start(2) + m_col(3, '등급', 'right') + m_col(9, des._grade || '') + m_row_end();
|
||||||
tmp += m_row_start(2) + m_col(3, '상영시간', 'right') + m_col(9, data.des._show_time ? data.des._show_time : '') + m_row_end();
|
tmp += m_row_start(2) + m_col(3, '총화수', 'right') + m_col(9, des._total_chapter || '') + m_row_end();
|
||||||
tmp += m_row_start(2) + m_col(3, '줄거리', 'right') + m_col(9, data.ser_description) + m_row_end();
|
tmp += m_row_start(2) + m_col(3, '상영시간', 'right') + m_col(9, des._show_time || '') + m_row_end();
|
||||||
|
tmp += m_row_start(2) + m_col(3, '줄거리', 'right') + m_col(9, data.ser_description || '') + m_row_end();
|
||||||
str += m_col(9, tmp)
|
str += m_col(9, tmp)
|
||||||
str += m_row_end();
|
str += m_row_end();
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
{{ macros.setting_input_text_and_buttons('ohli24_url', 'ohli24 URL', [['go_btn', 'GO']], value=arg['ohli24_url']) }}
|
{{ macros.setting_input_text_and_buttons('ohli24_url', 'ohli24 URL', [['go_btn', 'GO']], value=arg['ohli24_url']) }}
|
||||||
{{ macros.setting_input_text('ohli24_download_path', '저장 폴더', value=arg['ohli24_download_path'], desc='정상적으로 다운 완료 된 파일이 이동할 폴더 입니다. ') }}
|
{{ macros.setting_input_text('ohli24_download_path', '저장 폴더', value=arg['ohli24_download_path'], desc='정상적으로 다운 완료 된 파일이 이동할 폴더 입니다. ') }}
|
||||||
{{ macros.setting_input_int('ohli24_max_ffmpeg_process_count', '동시 다운로드 수', value=arg['ohli24_max_ffmpeg_process_count'], desc='동시에 다운로드 할 에피소드 갯수입니다.') }}
|
{{ macros.setting_input_int('ohli24_max_ffmpeg_process_count', '동시 다운로드 수', value=arg['ohli24_max_ffmpeg_process_count'], desc='동시에 다운로드 할 에피소드 갯수입니다.') }}
|
||||||
|
{{ macros.setting_select('ohli24_download_method', '다운로드 방법', [['ffmpeg', 'ffmpeg (기본)'], ['ytdlp', 'yt-dlp']], value=arg.get('ohli24_download_method', 'ffmpeg'), desc='m3u8 다운로드에 사용할 도구를 선택합니다.') }}
|
||||||
{{ macros.setting_checkbox('ohli24_order_desc', '요청 화면 최신순 정렬', value=arg['ohli24_order_desc'], desc='On : 최신화부터, Off : 1화부터') }}
|
{{ macros.setting_checkbox('ohli24_order_desc', '요청 화면 최신순 정렬', value=arg['ohli24_order_desc'], desc='On : 최신화부터, Off : 1화부터') }}
|
||||||
{{ macros.setting_checkbox('ohli24_auto_make_folder', '제목 폴더 생성', value=arg['ohli24_auto_make_folder'], desc='제목으로 폴더를 생성하고 폴더 안에 다운로드합니다.') }}
|
{{ macros.setting_checkbox('ohli24_auto_make_folder', '제목 폴더 생성', value=arg['ohli24_auto_make_folder'], desc='제목으로 폴더를 생성하고 폴더 안에 다운로드합니다.') }}
|
||||||
<div id="ohli24_auto_make_folder_div" class="collapse">
|
<div id="ohli24_auto_make_folder_div" class="collapse">
|
||||||
|
|||||||
Reference in New Issue
Block a user