수정사항
This commit is contained in:
4
.flake8
Normal file
4
.flake8
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[flake8]
|
||||||
|
max-line-length = 120
|
||||||
|
exclude = .git,__pycache__,node_modules,nest_api,yommi_api
|
||||||
|
ignore = E501, W503, E203
|
||||||
439
mod_linkkf.py
439
mod_linkkf.py
@@ -7,17 +7,15 @@
|
|||||||
# @Software: PyCharm
|
# @Software: PyCharm
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
|
||||||
from datetime import datetime
|
|
||||||
import random
|
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
import urllib
|
import urllib
|
||||||
|
from datetime import datetime
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import PIL.Image
|
|
||||||
|
|
||||||
# third-party
|
# third-party
|
||||||
import requests
|
import requests
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
@@ -52,7 +50,7 @@ for package in packages:
|
|||||||
else:
|
else:
|
||||||
os.system(f"pip3 install {package}")
|
os.system(f"pip3 install {package}")
|
||||||
|
|
||||||
from anime_downloader.lib.ffmpeg_queue_v1 import FfmpegQueueEntity, FfmpegQueue
|
from anime_downloader.lib.ffmpeg_queue_v1 import FfmpegQueue, FfmpegQueueEntity
|
||||||
from anime_downloader.lib.util import Util
|
from anime_downloader.lib.util import Util
|
||||||
|
|
||||||
# 패키지
|
# 패키지
|
||||||
@@ -94,12 +92,14 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, P):
|
def __init__(self, P):
|
||||||
super(LogicLinkkf, self).__init__(P, "setting", scheduler_desc="linkkf 자동 다운로드")
|
super(LogicLinkkf, self).__init__(
|
||||||
|
P, "setting", scheduler_desc="linkkf 자동 다운로드"
|
||||||
|
)
|
||||||
self.queue = None
|
self.queue = None
|
||||||
self.name = name
|
self.name = name
|
||||||
self.db_default = {
|
self.db_default = {
|
||||||
"linkkf_db_version": "1",
|
"linkkf_db_version": "1",
|
||||||
"linkkf_url": "https://linkkf.app",
|
"linkkf_url": "https://linkkf.live",
|
||||||
f"{self.name}_recent_code": "",
|
f"{self.name}_recent_code": "",
|
||||||
"linkkf_download_path": os.path.join(path_data, P.package_name, "linkkf"),
|
"linkkf_download_path": os.path.join(path_data, P.package_name, "linkkf"),
|
||||||
"linkkf_save_path": os.path.join(path_data, P.package_name, "linkkf"),
|
"linkkf_save_path": os.path.join(path_data, P.package_name, "linkkf"),
|
||||||
@@ -128,8 +128,8 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
arg = P.ModelSetting.to_dict()
|
arg = P.ModelSetting.to_dict()
|
||||||
arg["sub"] = self.name
|
arg["sub"] = self.name
|
||||||
if sub in ["setting", "queue", "category", "list", "request", "search"]:
|
if sub in ["setting", "queue", "category", "list", "request", "search"]:
|
||||||
if sub == "request" and req.args.get("content_code") is not None:
|
if sub == "request" and req.args.get("code") is not None:
|
||||||
arg["linkkf_current_code"] = req.args.get("content_code")
|
arg["linkkf_current_code"] = req.args.get("code")
|
||||||
if sub == "setting":
|
if sub == "setting":
|
||||||
job_id = "%s_%s" % (self.P.package_name, self.name)
|
job_id = "%s_%s" % (self.P.package_name, self.name)
|
||||||
arg["scheduler"] = str(scheduler.is_include(job_id))
|
arg["scheduler"] = str(scheduler.is_include(job_id))
|
||||||
@@ -164,7 +164,9 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
|
|
||||||
data = self.get_anime_info(cate, page)
|
data = self.get_anime_info(cate, page)
|
||||||
# self.current_data = data
|
# self.current_data = data
|
||||||
return jsonify({"ret": "success", "cate": cate, "page": page, "data": data})
|
return jsonify(
|
||||||
|
{"ret": "success", "cate": cate, "page": page, "data": data}
|
||||||
|
)
|
||||||
elif sub == "screen_movie_list":
|
elif sub == "screen_movie_list":
|
||||||
try:
|
try:
|
||||||
logger.debug("request:::> %s", request.form["page"])
|
logger.debug("request:::> %s", request.form["page"])
|
||||||
@@ -197,7 +199,7 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif sub == "add_queue":
|
elif sub == "add_queue":
|
||||||
logger.debug(f"linkkf add_queue routine ===============")
|
logger.debug("linkkf add_queue routine ===============")
|
||||||
ret = {}
|
ret = {}
|
||||||
info = json.loads(request.form["data"])
|
info = json.loads(request.form["data"])
|
||||||
logger.info(f"info:: {info}")
|
logger.info(f"info:: {info}")
|
||||||
@@ -222,7 +224,6 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_html(url, cached=False):
|
def get_html(url, cached=False):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if LogicLinkkf.referer is None:
|
if LogicLinkkf.referer is None:
|
||||||
LogicLinkkf.referer = f"{ModelSetting.get('linkkf_url')}"
|
LogicLinkkf.referer = f"{ModelSetting.get('linkkf_url')}"
|
||||||
@@ -291,7 +292,6 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
|
|
||||||
logger.debug(f"args: {args}")
|
logger.debug(f"args: {args}")
|
||||||
try:
|
try:
|
||||||
|
|
||||||
if len(args) == 0:
|
if len(args) == 0:
|
||||||
code = str(LogicLinkkf.current_data["code"])
|
code = str(LogicLinkkf.current_data["code"])
|
||||||
else:
|
else:
|
||||||
@@ -304,14 +304,21 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
# str(x.strip().replace(" ", ""))
|
# str(x.strip().replace(" ", ""))
|
||||||
# for x in whitelist_program.replace("\n", "|").split("|")
|
# for x in whitelist_program.replace("\n", "|").split("|")
|
||||||
# ]
|
# ]
|
||||||
whitelist_programs = [str(x.strip()) for x in whitelist_program.replace("\n", "|").split("|")]
|
whitelist_programs = [
|
||||||
|
str(x.strip()) for x in whitelist_program.replace("\n", "|").split("|")
|
||||||
|
]
|
||||||
|
|
||||||
if code not in whitelist_programs:
|
if code not in whitelist_programs:
|
||||||
whitelist_programs.append(code)
|
whitelist_programs.append(code)
|
||||||
whitelist_programs = filter(lambda x: x != "", whitelist_programs) # remove blank code
|
whitelist_programs = filter(
|
||||||
|
lambda x: x != "", whitelist_programs
|
||||||
|
) # remove blank code
|
||||||
whitelist_program = "|".join(whitelist_programs)
|
whitelist_program = "|".join(whitelist_programs)
|
||||||
entity = (
|
entity = (
|
||||||
db.session.query(P.ModelSetting).filter_by(key="linkkf_auto_code_list").with_for_update().first()
|
db.session.query(P.ModelSetting)
|
||||||
|
.filter_by(key="linkkf_auto_code_list")
|
||||||
|
.with_for_update()
|
||||||
|
.first()
|
||||||
)
|
)
|
||||||
entity.value = whitelist_program
|
entity.value = whitelist_program
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@@ -332,8 +339,12 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def setting_save_after(self):
|
def setting_save_after(self):
|
||||||
if self.queue.get_max_ffmpeg_count() != P.ModelSetting.get_int("linkkf_max_ffmpeg_process_count"):
|
if self.queue.get_max_ffmpeg_count() != P.ModelSetting.get_int(
|
||||||
self.queue.set_max_ffmpeg_count(P.ModelSetting.get_int("linkkf_max_ffmpeg_process_count"))
|
"linkkf_max_ffmpeg_process_count"
|
||||||
|
):
|
||||||
|
self.queue.set_max_ffmpeg_count(
|
||||||
|
P.ModelSetting.get_int("linkkf_max_ffmpeg_process_count")
|
||||||
|
)
|
||||||
|
|
||||||
def get_video_url_from_url(url, url2):
|
def get_video_url_from_url(url, url2):
|
||||||
video_url = None
|
video_url = None
|
||||||
@@ -378,7 +389,9 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
|
|
||||||
# print(vtt_elem)
|
# print(vtt_elem)
|
||||||
|
|
||||||
match = re.compile(r"<track.+src=\"(?P<vtt_url>.*?.vtt)\"", re.MULTILINE).search(data)
|
match = re.compile(
|
||||||
|
r"<track.+src=\"(?P<vtt_url>.*?.vtt)\"", re.MULTILINE
|
||||||
|
).search(data)
|
||||||
|
|
||||||
vtt_url = match.group("vtt_url")
|
vtt_url = match.group("vtt_url")
|
||||||
|
|
||||||
@@ -407,10 +420,14 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
# @k45734
|
# @k45734
|
||||||
vtt_url = None
|
vtt_url = None
|
||||||
try:
|
try:
|
||||||
_match1 = re.compile(r"<track.+src=\"(?P<vtt_url>.*?.vtt)", re.MULTILINE).search(data)
|
_match1 = re.compile(
|
||||||
|
r"<track.+src=\"(?P<vtt_url>.*?.vtt)", re.MULTILINE
|
||||||
|
).search(data)
|
||||||
vtt_url = _match1.group("vtt_url")
|
vtt_url = _match1.group("vtt_url")
|
||||||
except:
|
except:
|
||||||
_match2 = re.compile(r"url: \'(?P<vtt_url>.*?.vtt)", re.MULTILINE).search(data)
|
_match2 = re.compile(
|
||||||
|
r"url: \'(?P<vtt_url>.*?.vtt)", re.MULTILINE
|
||||||
|
).search(data)
|
||||||
vtt_url = _match2.group("vtt_url")
|
vtt_url = _match2.group("vtt_url")
|
||||||
|
|
||||||
logger.info("vtt_url: %s", vtt_url)
|
logger.info("vtt_url: %s", vtt_url)
|
||||||
@@ -486,13 +503,19 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
elif "kakao" in url2:
|
elif "kakao" in url2:
|
||||||
# kakao 계열 처리, 외부 API 이용
|
# kakao 계열 처리, 외부 API 이용
|
||||||
payload = {"inputUrl": url2}
|
payload = {"inputUrl": url2}
|
||||||
kakao_url = "http://webtool.cusis.net/wp-pages/download-kakaotv-video/video.php"
|
kakao_url = (
|
||||||
|
"http://webtool.cusis.net/wp-pages/download-kakaotv-video/video.php"
|
||||||
|
)
|
||||||
data2 = requests.post(
|
data2 = requests.post(
|
||||||
kakao_url,
|
kakao_url,
|
||||||
json=payload,
|
json=payload,
|
||||||
headers={"referer": "http://webtool.cusis.net/download-kakaotv-video/"},
|
headers={
|
||||||
|
"referer": "http://webtool.cusis.net/download-kakaotv-video/"
|
||||||
|
},
|
||||||
).content
|
).content
|
||||||
time.sleep(3) # 서버 부하 방지를 위해 단시간에 너무 많은 URL전송을 하면 IP를 차단합니다.
|
time.sleep(
|
||||||
|
3
|
||||||
|
) # 서버 부하 방지를 위해 단시간에 너무 많은 URL전송을 하면 IP를 차단합니다.
|
||||||
url3 = json.loads(data2)
|
url3 = json.loads(data2)
|
||||||
# logger.info("download url2 : %s , url3 : %s" % (url2, url3))
|
# logger.info("download url2 : %s , url3 : %s" % (url2, url3))
|
||||||
video_url = url3
|
video_url = url3
|
||||||
@@ -619,12 +642,13 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
index = 0
|
index = 0
|
||||||
|
|
||||||
for js_script in js_scripts:
|
for js_script in js_scripts:
|
||||||
|
|
||||||
# print(f"{index}.. {js_script.text_content()}")
|
# print(f"{index}.. {js_script.text_content()}")
|
||||||
if pattern.match(js_script.text_content()):
|
if pattern.match(js_script.text_content()):
|
||||||
# logger.debug("match::::")
|
# logger.debug("match::::")
|
||||||
match_data = pattern.match(js_script.text_content())
|
match_data = pattern.match(js_script.text_content())
|
||||||
iframe_info = json.loads(match_data.groups()[0].replace("path:", '"path":'))
|
iframe_info = json.loads(
|
||||||
|
match_data.groups()[0].replace("path:", '"path":')
|
||||||
|
)
|
||||||
# logger.debug(f"iframe_info:: {iframe_info}")
|
# logger.debug(f"iframe_info:: {iframe_info}")
|
||||||
|
|
||||||
index += 1
|
index += 1
|
||||||
@@ -633,7 +657,7 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
# iframe url:: https://s2.ani1c12.top/player/index.php?data='+player_data.url+'
|
# iframe url:: https://s2.ani1c12.top/player/index.php?data='+player_data.url+'
|
||||||
####################################################
|
####################################################
|
||||||
|
|
||||||
url = f'https://s2.ani1c12.top/player/index.php?data={iframe_info["url"]}'
|
url = f"https://s2.ani1c12.top/player/index.php?data={iframe_info['url']}"
|
||||||
html_data = LogicLinkkf.get_html(url)
|
html_data = LogicLinkkf.get_html(url)
|
||||||
|
|
||||||
return html_data
|
return html_data
|
||||||
@@ -708,7 +732,9 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
entity["title"] = item.xpath(title_xpath)[0].strip()
|
entity["title"] = item.xpath(title_xpath)[0].strip()
|
||||||
entity["image_link"] = item.xpath("./a/@data-original")[0]
|
entity["image_link"] = item.xpath("./a/@data-original")[0]
|
||||||
entity["chapter"] = (
|
entity["chapter"] = (
|
||||||
item.xpath("./a/span//text()")[0].strip() if len(item.xpath("./a/span//text()")) > 0 else ""
|
item.xpath("./a/span//text()")[0].strip()
|
||||||
|
if len(item.xpath("./a/span//text()")) > 0
|
||||||
|
else ""
|
||||||
)
|
)
|
||||||
# logger.info('entity:::', entity['title'])
|
# logger.info('entity:::', entity['title'])
|
||||||
data["episode"].append(entity)
|
data["episode"].append(entity)
|
||||||
@@ -746,10 +772,14 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
entity = {}
|
entity = {}
|
||||||
entity["link"] = item.xpath(".//a/@href")[0]
|
entity["link"] = item.xpath(".//a/@href")[0]
|
||||||
entity["code"] = re.search(r"[0-9]+", entity["link"]).group()
|
entity["code"] = re.search(r"[0-9]+", entity["link"]).group()
|
||||||
entity["title"] = item.xpath('.//a[@class="text-fff"]//text()')[0].strip()
|
entity["title"] = item.xpath('.//a[@class="text-fff"]//text()')[
|
||||||
|
0
|
||||||
|
].strip()
|
||||||
entity["image_link"] = item.xpath("./a/@data-original")[0]
|
entity["image_link"] = item.xpath("./a/@data-original")[0]
|
||||||
entity["chapter"] = (
|
entity["chapter"] = (
|
||||||
item.xpath("./a/span//text()")[0].strip() if len(item.xpath("./a/span//text()")) > 0 else ""
|
item.xpath("./a/span//text()")[0].strip()
|
||||||
|
if len(item.xpath("./a/span//text()")) > 0
|
||||||
|
else ""
|
||||||
)
|
)
|
||||||
data["episode"].append(entity)
|
data["episode"].append(entity)
|
||||||
|
|
||||||
@@ -769,159 +799,170 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
and LogicLinkkf.current_data["ret"]
|
and LogicLinkkf.current_data["ret"]
|
||||||
):
|
):
|
||||||
return LogicLinkkf.current_data
|
return LogicLinkkf.current_data
|
||||||
url = "%s/%s" % (P.ModelSetting.get("linkkf_url"), code)
|
|
||||||
logger.info(url)
|
url = "%s/%s/" % (P.ModelSetting.get("linkkf_url"), code)
|
||||||
|
|
||||||
|
logger.info(f"get_series_info URL: {url}")
|
||||||
|
|
||||||
logger.debug(LogicLinkkf.headers)
|
|
||||||
html_content = LogicLinkkf.get_html(url, cached=False)
|
html_content = LogicLinkkf.get_html(url, cached=False)
|
||||||
# html_content = LogicLinkkf.get_html_playwright(url)
|
|
||||||
# html_content = LogicLinkkf.get_html_cloudflare(url, cached=False)
|
if not html_content:
|
||||||
|
data["log"] = "Failed to fetch page content"
|
||||||
sys.setrecursionlimit(10**7)
|
data["ret"] = "error"
|
||||||
# logger.info(html_content)
|
return data
|
||||||
tree = html.fromstring(html_content)
|
|
||||||
# tree = etree.fromstring(
|
|
||||||
# html_content, parser=etree.XMLParser(huge_tree=True)
|
|
||||||
# )
|
|
||||||
# tree1 = BeautifulSoup(html_content, "lxml")
|
|
||||||
|
|
||||||
soup = BeautifulSoup(html_content, "html.parser")
|
soup = BeautifulSoup(html_content, "html.parser")
|
||||||
# tree = etree.HTML(str(soup))
|
|
||||||
# logger.info(tree)
|
# === 제목 추출 ===
|
||||||
|
# 방법 1: #anime-details > h3 (가장 정확)
|
||||||
tmp2 = soup.select("ul > a")
|
title_elem = soup.select_one("#anime-details > h3")
|
||||||
if len(tmp2) == 0:
|
if not title_elem:
|
||||||
tmp = soup.select("u > a")
|
# 방법 2: .anime-tab-content > h3
|
||||||
|
title_elem = soup.select_one(".anime-tab-content > h3")
|
||||||
|
|
||||||
|
title_text = ""
|
||||||
|
if title_elem:
|
||||||
|
title_text = title_elem.get_text(strip=True)
|
||||||
|
# "11/12 - 너와 넘어 사랑이 된다" 형식에서 제목만 추출
|
||||||
|
if " - " in title_text:
|
||||||
|
data["title"] = title_text.split(" - ", 1)[1]
|
||||||
|
else:
|
||||||
|
data["title"] = title_text
|
||||||
else:
|
else:
|
||||||
tmp = soup.select("ul > a")
|
# 방법 3: gemini-dark-card__link의 title 속성
|
||||||
|
card_link = soup.select_one("a.gemini-dark-card__link")
|
||||||
# logger.debug(f"tmp1 size:=> {str(len(tmp))}")
|
if card_link and card_link.get("title"):
|
||||||
|
data["title"] = card_link.get("title")
|
||||||
try:
|
else:
|
||||||
tmp = tree.xpath('//div[@class="hrecipe"]/article/center/strong')[0].text_content().strip()
|
# 방법 4: 포스터 이미지의 alt 속성
|
||||||
except IndexError:
|
poster_img = soup.select_one("img.gemini-dark-card__image")
|
||||||
tmp = tree.xpath("//article/center/strong")[0].text_content().strip()
|
if poster_img and poster_img.get("alt"):
|
||||||
|
data["title"] = poster_img.get("alt")
|
||||||
# logger.info(tmp)
|
else:
|
||||||
match = re.compile(r"(?P<season>\d+)기").search(tmp)
|
# 방법 5: 페이지 title에서 추출
|
||||||
|
page_title = soup.select_one("title")
|
||||||
|
if page_title:
|
||||||
|
title_text = page_title.get_text(strip=True)
|
||||||
|
# "제목 자막 / 더빙 / Linkkf" 형식 처리
|
||||||
|
data["title"] = title_text.split(" 자막")[0].split(" /")[0].strip()
|
||||||
|
else:
|
||||||
|
data["title"] = f"Unknown-{code}"
|
||||||
|
|
||||||
|
# 제목 정리
|
||||||
|
data["title"] = Util.change_text_for_use_filename(data["title"]).strip()
|
||||||
|
data["_id"] = str(code)
|
||||||
|
|
||||||
|
# === 시즌 추출 ===
|
||||||
|
match = re.compile(r"(?P<season>\d+)기").search(data.get("title", ""))
|
||||||
if match:
|
if match:
|
||||||
data["season"] = match.group("season")
|
data["season"] = match.group("season")
|
||||||
|
data["title"] = data["title"].replace(data["season"] + "기", "").strip()
|
||||||
else:
|
else:
|
||||||
data["season"] = "1"
|
data["season"] = "1"
|
||||||
|
|
||||||
data["_id"] = str(code)
|
# === 포스터 이미지 ===
|
||||||
data["title"] = tmp.replace(data["season"] + "기", "").strip()
|
poster_elem = soup.select_one("img.gemini-dark-card__image")
|
||||||
data["title"] = data["title"].replace("()", "").strip()
|
if poster_elem:
|
||||||
data["title"] = Util.change_text_for_use_filename(data["title"]).replace("OVA", "").strip()
|
# lazy loading 대응: data-lazy-src (사이트에서 사용하는 속성), data-src, src 순서로 확인
|
||||||
|
data["poster_url"] = (
|
||||||
try:
|
poster_elem.get("data-lazy-src") or
|
||||||
data["poster_url"] = tree.xpath('//div[@class="myui-content__thumb"]/a/@data-original')
|
poster_elem.get("data-src") or
|
||||||
# print(tree.xpath('//div[@class="myui-content__detail"]/text()'))
|
poster_elem.get("src") or ""
|
||||||
if len(tree.xpath('//div[@class="myui-content__detail"]/text()')) > 3:
|
)
|
||||||
data["detail"] = [{"info": str(tree.xpath("//div[@class='myui-content__detail']/text()")[3])}]
|
# placeholder SVG 제외
|
||||||
|
if data["poster_url"].startswith("data:image/svg"):
|
||||||
|
data["poster_url"] = poster_elem.get("data-lazy-src") or poster_elem.get("data-src") or ""
|
||||||
|
else:
|
||||||
|
# 대안 선택자
|
||||||
|
poster_alt = soup.select_one("a.gemini-dark-card__link img")
|
||||||
|
if poster_alt:
|
||||||
|
data["poster_url"] = (
|
||||||
|
poster_alt.get("data-lazy-src") or
|
||||||
|
poster_alt.get("data-src") or
|
||||||
|
poster_alt.get("src") or ""
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
data["detail"] = [{"정보없음": ""}]
|
data["poster_url"] = None
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
# === 상세 정보 ===
|
||||||
|
data["detail"] = []
|
||||||
|
info_items = soup.select("li")
|
||||||
|
for item in info_items:
|
||||||
|
text = item.get_text(strip=True)
|
||||||
|
if any(keyword in text for keyword in ["방영일", "제작사", "장르", "분류", "년"]):
|
||||||
|
data["detail"].append({"info": text})
|
||||||
|
|
||||||
|
if not data["detail"]:
|
||||||
data["detail"] = [{"정보없음": ""}]
|
data["detail"] = [{"정보없음": ""}]
|
||||||
data["poster_url"] = None
|
|
||||||
|
# === 에피소드 목록 - API에서 가져오기 ===
|
||||||
data["rate"] = tree.xpath('span[@class="tag-score"]')
|
|
||||||
|
|
||||||
tag_score = tree.xpath('//span[@class="taq-score"]')[0].text_content()
|
|
||||||
# logger.debug(tag_score)
|
|
||||||
tag_count = tree.xpath('//span[contains(@class, "taq-count")]')[0].text_content().strip()
|
|
||||||
data_rate = tree.xpath('//div[@class="rating"]/div/@data-rate')
|
|
||||||
|
|
||||||
tmp2 = soup.select("ul > a")
|
|
||||||
if len(tmp) == 0:
|
|
||||||
tmp = soup.select("u > a")
|
|
||||||
else:
|
|
||||||
tmp = soup.select("ul > a")
|
|
||||||
|
|
||||||
if tmp is not None:
|
|
||||||
data["episode_count"] = str(len(tmp))
|
|
||||||
else:
|
|
||||||
data["episode_count"] = "0"
|
|
||||||
|
|
||||||
data["episode"] = []
|
data["episode"] = []
|
||||||
# tags = tree.xpath(
|
|
||||||
# '//*[@id="syno-nsc-ext-gen3"]/article/div[1]/article/a')
|
|
||||||
# tags = tree.xpath("//ul/a")
|
|
||||||
tags = soup.select("ul > u > a")
|
|
||||||
if len(tags) > 0:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
tags = soup.select("ul > a")
|
|
||||||
|
|
||||||
logger.debug(len(tags))
|
|
||||||
|
|
||||||
# logger.info("tags", tags)
|
|
||||||
# re1 = re.compile(r'\/(?P<code>\d+)')
|
|
||||||
re1 = re.compile(r"\-([^-])+\.")
|
|
||||||
|
|
||||||
data["save_folder"] = data["title"]
|
data["save_folder"] = data["title"]
|
||||||
# logger.debug(f"save_folder::> {data['save_folder']}")
|
|
||||||
|
# 에피소드 API 호출
|
||||||
# program = (
|
episode_api_url = f"https://linkkfep.5imgdarr.top/api2.php?epid={code}"
|
||||||
# db.session.query(ModelLinkkfProgram).filter_by(programcode=code).first()
|
try:
|
||||||
# )
|
episode_response = requests.get(episode_api_url, timeout=10)
|
||||||
|
episode_data = episode_response.json()
|
||||||
idx = 1
|
|
||||||
for t in tags:
|
logger.debug(f"Episode API response: {len(episode_data)} servers found")
|
||||||
entity = {
|
|
||||||
"_id": data["code"],
|
# 첫 번째 서버 (보통 자막-S)의 에피소드 목록 사용
|
||||||
"program_code": data["code"],
|
if episode_data and len(episode_data) > 0:
|
||||||
"program_title": data["title"],
|
server_data = episode_data[0].get("server_data", [])
|
||||||
"save_folder": Util.change_text_for_use_filename(data["save_folder"]),
|
# 역순 정렬 (최신 에피소드가 위로)
|
||||||
"title": t.text.strip(),
|
server_data = list(reversed(server_data))
|
||||||
# "title": t.text_content().strip(),
|
|
||||||
}
|
for idx, ep_info in enumerate(server_data):
|
||||||
# entity['code'] = re1.search(t.attrib['href']).group('code')
|
ep_name = ep_info.get("name", str(idx + 1))
|
||||||
|
ep_slug = ep_info.get("slug", str(idx + 1))
|
||||||
# logger.debug(f"title ::>{entity['title']}")
|
ep_link = ep_info.get("link", "")
|
||||||
|
|
||||||
# 고유id임을 알수 없는 말도 안됨..
|
# 화면 표시용 title은 "01화" 형태
|
||||||
# 에피소드 코드가 고유해야 상태값 갱신이 제대로 된 값에 넣어짐
|
ep_title = f"{ep_name}화"
|
||||||
p = re.compile(r"([0-9]+)화?")
|
|
||||||
m_obj = p.match(entity["title"])
|
entity = {
|
||||||
# logger.info(m_obj.group())
|
"_id": data["code"],
|
||||||
# entity['code'] = data['code'] + '_' +str(idx)
|
"program_code": data["code"],
|
||||||
|
"program_title": data["title"],
|
||||||
episode_code = None
|
"save_folder": Util.change_text_for_use_filename(data["save_folder"]),
|
||||||
# logger.debug(f"m_obj::> {m_obj}")
|
"title": ep_title,
|
||||||
if m_obj is not None:
|
"season": data["season"],
|
||||||
episode_code = m_obj.group(1)
|
}
|
||||||
entity["code"] = data["code"] + episode_code.zfill(4)
|
|
||||||
else:
|
# 에피소드 코드 생성
|
||||||
entity["code"] = data["code"]
|
entity["code"] = data["code"] + ep_name.zfill(4)
|
||||||
|
|
||||||
aa = t["href"]
|
# URL 생성: playid/{code}/?server=12&slug={slug} 형태
|
||||||
if "/player" in aa:
|
entity["url"] = f"https://linkkf.live/playid/{code}/?server=12&slug={ep_slug}"
|
||||||
entity["url"] = "https://linkkf.app" + t["href"]
|
|
||||||
else:
|
# 저장 경로 설정
|
||||||
entity["url"] = t["href"]
|
tmp_save_path = P.ModelSetting.get("linkkf_download_path")
|
||||||
entity["season"] = data["season"]
|
if P.ModelSetting.get("linkkf_auto_make_folder") == "True":
|
||||||
|
program_path = os.path.join(tmp_save_path, entity["save_folder"])
|
||||||
# 저장 경로 저장
|
entity["save_path"] = program_path
|
||||||
# Todo: db
|
if P.ModelSetting.get("linkkf_auto_make_season_folder"):
|
||||||
tmp_save_path = P.ModelSetting.get(f"linkkf_download_path")
|
entity["save_path"] = os.path.join(
|
||||||
if P.ModelSetting.get("linkkf_auto_make_folder") == "True":
|
entity["save_path"], "Season %s" % int(entity["season"])
|
||||||
program_path = os.path.join(tmp_save_path, entity["save_folder"])
|
)
|
||||||
entity["save_path"] = program_path
|
|
||||||
if P.ModelSetting.get("linkkf_auto_make_season_folder"):
|
entity["image"] = data["poster_url"]
|
||||||
entity["save_path"] = os.path.join(entity["save_path"], "Season %s" % int(entity["season"]))
|
# filename 생성 시 숫자만 전달 ("01화" 아님)
|
||||||
|
entity["filename"] = LogicLinkkf.get_filename(
|
||||||
entity["image"] = data["poster_url"]
|
data["save_folder"], data["season"], ep_name
|
||||||
|
)
|
||||||
entity["filename"] = LogicLinkkf.get_filename(data["save_folder"], data["season"], entity["title"])
|
|
||||||
data["episode"].append(entity)
|
data["episode"].append(entity)
|
||||||
idx = idx + 1
|
|
||||||
|
except Exception as ep_error:
|
||||||
|
logger.error(f"Episode API error: {ep_error}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
data["episode_count"] = str(len(data["episode"]))
|
||||||
data["ret"] = True
|
data["ret"] = True
|
||||||
# logger.info('data', data)
|
|
||||||
self.current_data = data
|
self.current_data = data
|
||||||
|
|
||||||
|
logger.info(f"Parsed series: {data['title']}, Episodes: {data['episode_count']}")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -930,12 +971,6 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
data["log"] = str(e)
|
data["log"] = str(e)
|
||||||
data["ret"] = "error"
|
data["ret"] = "error"
|
||||||
return data
|
return data
|
||||||
except IndexError as e:
|
|
||||||
logger.error("Exception:%s", e)
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
data["log"] = str(e)
|
|
||||||
data["ret"] = "error"
|
|
||||||
return data
|
|
||||||
|
|
||||||
def get_screen_movie_info(self, page):
|
def get_screen_movie_info(self, page):
|
||||||
try:
|
try:
|
||||||
@@ -973,7 +1008,11 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
else:
|
else:
|
||||||
entity["image_link"] = ""
|
entity["image_link"] = ""
|
||||||
# entity["image_link"] = item.xpath("./a/@data-original")[0]
|
# entity["image_link"] = item.xpath("./a/@data-original")[0]
|
||||||
entity["chapter"] = item.xpath("./a/span//text()")[0] if len(item.xpath("./a/span//text()")) > 0 else ""
|
entity["chapter"] = (
|
||||||
|
item.xpath("./a/span//text()")[0]
|
||||||
|
if len(item.xpath("./a/span//text()")) > 0
|
||||||
|
else ""
|
||||||
|
)
|
||||||
# logger.info('entity:::', entity['title'])
|
# logger.info('entity:::', entity['title'])
|
||||||
data["episode"].append(entity)
|
data["episode"].append(entity)
|
||||||
|
|
||||||
@@ -999,7 +1038,7 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
):
|
):
|
||||||
data = ""
|
data = ""
|
||||||
headers = {
|
headers = {
|
||||||
"referer": f"https://linkkf.app",
|
"referer": "https://linkkf.live",
|
||||||
"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/96.0.4664.110 Whale/3.12.129.46 Safari/537.36"
|
"Chrome/96.0.4664.110 Whale/3.12.129.46 Safari/537.36"
|
||||||
"Mozilla/5.0 (Macintosh; Intel "
|
"Mozilla/5.0 (Macintosh; Intel "
|
||||||
@@ -1008,13 +1047,14 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
"X-Requested-With": "XMLHttpRequest",
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
|
|
||||||
if LogicOhli24.session is None:
|
if LogicOhli24.session is None:
|
||||||
LogicOhli24.session = requests.session()
|
LogicOhli24.session = requests.session()
|
||||||
|
|
||||||
# logger.debug('get_html :%s', url)
|
# logger.debug('get_html :%s', url)
|
||||||
headers["Referer"] = "" if referer is None else referer
|
headers["Referer"] = "" if referer is None else referer
|
||||||
page_content = LogicOhli24.session.get(url, headers=headers, timeout=timeout)
|
page_content = LogicOhli24.session.get(
|
||||||
|
url, headers=headers, timeout=timeout
|
||||||
|
)
|
||||||
data = page_content.text
|
data = page_content.text
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Exception:%s", e)
|
logger.error("Exception:%s", e)
|
||||||
@@ -1037,7 +1077,7 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
else:
|
else:
|
||||||
LogicLinkkf.session = requests.Session()
|
LogicLinkkf.session = requests.Session()
|
||||||
|
|
||||||
LogicLinkkf.referer = "https://linkkf.app"
|
LogicLinkkf.referer = "https://linkkf.live"
|
||||||
|
|
||||||
LogicLinkkf.headers["Referer"] = LogicLinkkf.referer
|
LogicLinkkf.headers["Referer"] = LogicLinkkf.referer
|
||||||
|
|
||||||
@@ -1055,7 +1095,9 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
# logger.debug("get_filename()===")
|
# logger.debug("get_filename()===")
|
||||||
# logger.info("title:: %s", title)
|
# logger.info("title:: %s", title)
|
||||||
# logger.info("maintitle:: %s", maintitle)
|
# logger.info("maintitle:: %s", maintitle)
|
||||||
match = re.compile(r"(?P<title>.*?)\s?((?P<season>\d+)기)?\s?((?P<epi_no>\d+)화?)").search(title)
|
match = re.compile(
|
||||||
|
r"(?P<title>.*?)\s?((?P<season>\d+)기)?\s?((?P<epi_no>\d+)화?)"
|
||||||
|
).search(title)
|
||||||
if match:
|
if match:
|
||||||
epi_no = int(match.group("epi_no"))
|
epi_no = int(match.group("epi_no"))
|
||||||
if epi_no < 10:
|
if epi_no < 10:
|
||||||
@@ -1084,7 +1126,6 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
if self.is_exist(episode_info):
|
if self.is_exist(episode_info):
|
||||||
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.debug("db_entity:::> %s", db_entity)
|
logger.debug("db_entity:::> %s", db_entity)
|
||||||
@@ -1151,7 +1192,9 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
try:
|
try:
|
||||||
logger.debug("%s plugin_load", P.package_name)
|
logger.debug("%s plugin_load", P.package_name)
|
||||||
# old version
|
# old version
|
||||||
self.queue = FfmpegQueue(P, P.ModelSetting.get_int("linkkf_max_ffmpeg_process_count"))
|
self.queue = FfmpegQueue(
|
||||||
|
P, P.ModelSetting.get_int("linkkf_max_ffmpeg_process_count")
|
||||||
|
)
|
||||||
self.current_data = None
|
self.current_data = None
|
||||||
self.queue.queue_start()
|
self.queue.queue_start()
|
||||||
|
|
||||||
@@ -1176,7 +1219,9 @@ class LogicLinkkf(PluginModuleBase):
|
|||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
logger.debug(self.current_download_count)
|
logger.debug(self.current_download_count)
|
||||||
if self.current_download_count < P.ModelSetting.get_int(f"{self.name}_max_download_count"):
|
if self.current_download_count < P.ModelSetting.get_int(
|
||||||
|
f"{self.name}_max_download_count"
|
||||||
|
):
|
||||||
break
|
break
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
@@ -1197,19 +1242,23 @@ class LinkkfQueueEntity(FfmpegQueueEntity):
|
|||||||
def __init__(self, P, module_logic, info):
|
def __init__(self, P, module_logic, info):
|
||||||
super(LinkkfQueueEntity, self).__init__(P, module_logic, info)
|
super(LinkkfQueueEntity, self).__init__(P, module_logic, info)
|
||||||
self._vi = None
|
self._vi = None
|
||||||
self.url = None
|
|
||||||
self.epi_queue = None
|
self.epi_queue = None
|
||||||
self.filepath = None
|
|
||||||
self.savepath = None
|
|
||||||
self.quality = None
|
|
||||||
self.filename = None
|
|
||||||
self.vtt = None
|
self.vtt = None
|
||||||
self.season = 1
|
|
||||||
self.content_title = None
|
|
||||||
self.srt_url = None
|
self.srt_url = None
|
||||||
self.headers = None
|
self.headers = None
|
||||||
# Todo::: 임시 주석 처리
|
|
||||||
self.make_episode_info()
|
# info에서 필요한 정보 설정
|
||||||
|
self.url = info.get("url", "")
|
||||||
|
self.filename = info.get("filename", "")
|
||||||
|
self.filepath = info.get("filename", "")
|
||||||
|
self.savepath = info.get("save_path", "")
|
||||||
|
self.quality = info.get("quality", "720p")
|
||||||
|
self.season = info.get("season", "1")
|
||||||
|
self.content_title = info.get("program_title", "")
|
||||||
|
|
||||||
|
# make_episode_info는 비디오 URL 추출이 필요할 때만 호출
|
||||||
|
# 현재는 바로 다운로드 큐에 추가하므로 주석 처리
|
||||||
|
# self.make_episode_info()
|
||||||
|
|
||||||
def refresh_status(self):
|
def refresh_status(self):
|
||||||
self.module_logic.socketio_callback("status", self.as_dict())
|
self.module_logic.socketio_callback("status", self.as_dict())
|
||||||
@@ -1338,7 +1387,9 @@ class ModelLinkkfItem(db.Model):
|
|||||||
ret = {x.name: getattr(self, x.name) for x in self.__table__.columns}
|
ret = {x.name: getattr(self, x.name) for x in self.__table__.columns}
|
||||||
ret["created_time"] = self.created_time.strftime("%Y-%m-%d %H:%M:%S")
|
ret["created_time"] = self.created_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
ret["completed_time"] = (
|
ret["completed_time"] = (
|
||||||
self.completed_time.strftime("%Y-%m-%d %H:%M:%S") if self.completed_time is not None else None
|
self.completed_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if self.completed_time is not None
|
||||||
|
else None
|
||||||
)
|
)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|||||||
124
outline.md
Normal file
124
outline.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# anime_downloader 플러그인 구조 분석
|
||||||
|
|
||||||
|
## 📌 개요
|
||||||
|
|
||||||
|
FlaskFarm용 **애니메이션 다운로드 플러그인**으로, 여러 애니메이션 스트리밍 사이트에서 콘텐츠를 검색하고 다운로드할 수 있는 기능을 제공합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ 전체 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
anime_downloader/
|
||||||
|
├── __init__.py # 플러그인 초기화 (현재 비활성화됨)
|
||||||
|
├── setup.py # 플러그인 설정 및 메뉴 구조, 모듈 로드
|
||||||
|
├── info.yaml # 플러그인 메타데이터 (이름, 버전, 개발자)
|
||||||
|
├── mod_ohli24.py # 애니24 사이트 모듈 (1,542줄)
|
||||||
|
├── mod_anilife.py # 애니라이프 사이트 모듈 (1,322줄)
|
||||||
|
├── mod_linkkf.py # 링크애니 사이트 모듈 (1,449줄)
|
||||||
|
├── lib/ # 공용 라이브러리
|
||||||
|
│ ├── crawler.py # 웹 크롤링 엔진 (Playwright, Selenium, Cloudscraper)
|
||||||
|
│ ├── ffmpeg_queue_v1.py# FFmpeg 다운로드 큐 관리
|
||||||
|
│ ├── util.py # 유틸리티 함수 (파일명 정리, 타이밍 등)
|
||||||
|
│ └── misc.py # 비동기 실행 헬퍼 함수
|
||||||
|
├── templates/ # HTML 템플릿 (18개 파일)
|
||||||
|
├── static/ # CSS, JS, 이미지 리소스
|
||||||
|
├── bin/ # 플랫폼별 바이너리 (Darwin, Linux)
|
||||||
|
├── nest_api/ # 애니 API 관련 (서브디렉토리)
|
||||||
|
└── yommi_api/ # 커스텀 API 관련
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 핵심 컴포넌트
|
||||||
|
|
||||||
|
### 1. setup.py - 플러그인 엔트리포인트
|
||||||
|
|
||||||
|
| 항목 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| `__menu` | 3개 사이트별 서브메뉴 (설정, 요청, 큐, 검색, 목록) |
|
||||||
|
| `setting` | DB 사용, 기본 설정, 홈 모듈(`ohli24`) 지정 |
|
||||||
|
| `P` | FlaskFarm 플러그인 인스턴스 생성 |
|
||||||
|
| 모듈 로드 | `LogicOhli24`, `LogicAniLife`, `LogicLinkkf` |
|
||||||
|
|
||||||
|
### 2. 사이트 모듈 (mod_*.py)
|
||||||
|
|
||||||
|
각 모듈은 동일한 구조를 따릅니다:
|
||||||
|
|
||||||
|
| 클래스 | 역할 |
|
||||||
|
|--------|------|
|
||||||
|
| `LogicXxx` | 사이트별 비즈니스 로직 (검색, 시리즈 정보, 다운로드 추가) |
|
||||||
|
| `XxxQueueEntity` | 다운로드 큐 항목 (에피소드 정보, 상태 관리) |
|
||||||
|
| `ModelXxxItem` | SQLAlchemy DB 모델 (다운로드 기록 저장) |
|
||||||
|
|
||||||
|
**LogicXxx 주요 메서드:**
|
||||||
|
|
||||||
|
- `process_menu()` / `process_ajax()` - 웹 요청 처리
|
||||||
|
- `get_series_info()` - 시리즈/에피소드 정보 파싱
|
||||||
|
- `get_anime_info()` / `get_search_result()` - 목록/검색
|
||||||
|
- `add()` - 다운로드 큐에 추가
|
||||||
|
- `scheduler_function()` - 자동 다운로드 스케줄러
|
||||||
|
- `plugin_load()` / `plugin_unload()` - 생명주기 관리
|
||||||
|
|
||||||
|
### 3. lib/crawler.py - 웹 크롤링 엔진
|
||||||
|
|
||||||
|
| 메서드 | 기술 |
|
||||||
|
|--------|------|
|
||||||
|
| `get_html_requests()` | 기본 requests 요청 |
|
||||||
|
| `get_html_playwright()` | Playwright 비동기 (헤드리스 브라우저) |
|
||||||
|
| `get_html_playwright_sync()` | Playwright 동기 |
|
||||||
|
| `get_html_selenium()` | Selenium WebDriver |
|
||||||
|
| `get_html_cloudflare()` | Cloudscraper (CF 우회) |
|
||||||
|
|
||||||
|
### 4. lib/ffmpeg_queue_v1.py - 다운로드 큐
|
||||||
|
|
||||||
|
| 클래스 | 역할 |
|
||||||
|
|--------|------|
|
||||||
|
| `FfmpegQueueEntity` | 개별 다운로드 항목 (URL, 파일경로, 상태) |
|
||||||
|
| `FfmpegQueue` | 큐 관리자 (스레드 기반 다운로드, 동시 다운로드 수 제어) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖥️ 지원 사이트 (3개)
|
||||||
|
|
||||||
|
| 모듈 | 사이트 | URI |
|
||||||
|
|------|--------|-----|
|
||||||
|
| `mod_ohli24.py` | 애니24 (ohli24) | `/ohli24` |
|
||||||
|
| `mod_anilife.py` | 애니라이프 | `/anilife` |
|
||||||
|
| `mod_linkkf.py` | 링크애니 | `/linkkf` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 템플릿 구조
|
||||||
|
|
||||||
|
각 사이트별로 6개 템플릿 제공:
|
||||||
|
|
||||||
|
- `*_setting.html` - 사이트 설정
|
||||||
|
- `*_request.html` - 다운로드 요청 페이지
|
||||||
|
- `*_queue.html` - 다운로드 큐 현황
|
||||||
|
- `*_search.html` - 검색 인터페이스
|
||||||
|
- `*_list.html` - 다운로드 목록
|
||||||
|
- `*_category.html` - 카테고리 탐색
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 동작 흐름
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[사용자] --> B[검색/탐색]
|
||||||
|
B --> C[시리즈 선택]
|
||||||
|
C --> D[에피소드 선택]
|
||||||
|
D --> E[다운로드 큐 추가]
|
||||||
|
E --> F[FfmpegQueue]
|
||||||
|
F --> G[FFmpeg 다운로드]
|
||||||
|
G --> H[DB 기록 저장]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 주의사항
|
||||||
|
|
||||||
|
1. **개발 모드**: `setup.py`에서 `DEFINE_DEV = True`로 설정되어 직접 모듈 import
|
||||||
|
2. **`__init__.py` 비활성화**: 현재 주석 처리되어 `setup.py`가 실제 엔트리포인트 역할
|
||||||
|
3. **크롤링 기술 혼용**: Cloudflare 우회를 위해 Playwright, Selenium, cloudscraper 등 다양한 기술 사용
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
str += m_row_start();
|
str += m_row_start();
|
||||||
// tmp = '<img src="' + data.episode[i].image + '" class="img-fluid">'
|
// tmp = '<img src="' + data.episode[i].image + '" class="img-fluid">'
|
||||||
// str += m_col(3, tmp)
|
// str += m_col(3, tmp)
|
||||||
tmp = "<strong>" + data.episode[i].title + "</strong><span>화. </span>";
|
tmp = "<strong>" + data.episode[i].title + "</strong><span>. </span>";
|
||||||
{#tmp += "<br>";#}
|
{#tmp += "<br>";#}
|
||||||
tmp += data.episode[i].filename + "<br><p></p>";
|
tmp += data.episode[i].filename + "<br><p></p>";
|
||||||
|
|
||||||
@@ -202,13 +202,13 @@
|
|||||||
// {#document.getElementById("analysis_btn").click();#}
|
// {#document.getElementById("analysis_btn").click();#}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("{{arg['ohli24_current_code']}}" !== "") {
|
if ("{{arg['linkkf_current_code']}}" !== "") {
|
||||||
if (params.code === null) {
|
if (params.code === null) {
|
||||||
console.log('params.code === null')
|
console.log('params.code === null')
|
||||||
document.getElementById("code").value = "{{arg['ohli24_current_code']}}";
|
document.getElementById("code").value = "{{arg['linkkf_current_code']}}";
|
||||||
|
|
||||||
} else if (params.code === '') {
|
} else if (params.code === '') {
|
||||||
document.getElementById("code").value = "{{arg['ohli24_current_code']}}";
|
document.getElementById("code").value = "{{arg['linkkf_current_code']}}";
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
console.log('params code exist')
|
console.log('params code exist')
|
||||||
|
|||||||
Reference in New Issue
Block a user