delete some trash
This commit is contained in:
@@ -1,15 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
*.ipynb
|
|
||||||
test.ipynb
|
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
*.pyo
|
|
||||||
*.pyc
|
|
||||||
/linkkf_cache.sqlite
|
|
||||||
bin/Darwin/mp4decrypt
|
|
||||||
bin/Darwin/mp4dump
|
|
||||||
bin/Darwin/mp4dump_
|
|
||||||
bin/Darwin/mp4info
|
|
||||||
bin/Linux/mp4decrypt
|
|
||||||
bin/Linux/mp4dump
|
|
||||||
bin/Linux/mp4info
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# linkkf
|
|
||||||
|
|
||||||
FlaskFarm용 `linkkf.tv` 플러그인입니다.
|
|
||||||
|
|
||||||
설치 전 의존 패키지:
|
|
||||||
`pip install -r requirements.txt`
|
|
||||||
|
|
||||||
플러그인 최초 로딩 시 누락된 패키지는 자동 설치를 시도합니다.
|
|
||||||
|
|
||||||
현재 기준 주요 기능:
|
|
||||||
- 카테고리 목록 조회
|
|
||||||
- 작품 분석 및 회차 목록 조회
|
|
||||||
- 재생 URL 추출 및 브라우저 플레이어 프록시
|
|
||||||
- ffmpeg 다운로드 대기열 연동
|
|
||||||
- 다운로드 이력 목록 표시
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from .setup import P
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.3.2.0",
|
|
||||||
"name": "linkkf",
|
|
||||||
"category_name": "vod",
|
|
||||||
"icon": "",
|
|
||||||
"developer": "projectdx && persuade",
|
|
||||||
"description": "linkkf 사이트에서 애니 다운로드",
|
|
||||||
"home": "https://linkkf.tv",
|
|
||||||
"more": "",
|
|
||||||
"category": "vod"
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
title: "linkkf"
|
|
||||||
version: "0.3.2.0"
|
|
||||||
package_name: "linkkf"
|
|
||||||
developer: "projectdx && persuade"
|
|
||||||
description: "linkkf 사이트에서 애니 다운로드"
|
|
||||||
home: "https://linkkf.tv"
|
|
||||||
more: ""
|
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import time
|
|
||||||
import logging
|
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
logger = logging.getLogger("linkkf")
|
|
||||||
|
|
||||||
def linkkf_async_timeit(func):
|
|
||||||
@wraps(func)
|
|
||||||
async def wrapper(*args, **kwargs):
|
|
||||||
start_time = time.perf_counter()
|
|
||||||
try:
|
|
||||||
return await func(*args, **kwargs)
|
|
||||||
finally:
|
|
||||||
total_time = time.perf_counter() - start_time
|
|
||||||
logger.debug("%s%r %r took %.4fs", func.__name__, args, kwargs, total_time)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
-137
@@ -1,137 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from framework import F, Job, db, get_logger, path_data, scheduler
|
|
||||||
from support import SupportFile
|
|
||||||
|
|
||||||
from .logic_linkkf import LogicLinkkf
|
|
||||||
from .logic_queue import LogicQueue
|
|
||||||
from .model import ModelLinkkf, ModelSetting
|
|
||||||
|
|
||||||
|
|
||||||
package_name = __name__.split(".")[0]
|
|
||||||
logger = get_logger(package_name)
|
|
||||||
|
|
||||||
|
|
||||||
class Logic(object):
|
|
||||||
db_default = {
|
|
||||||
"linkkf_url": "https://linkkf.tv",
|
|
||||||
"download_path": os.path.join(path_data, "linkkf"),
|
|
||||||
"linkkf_auto_make_folder": "True",
|
|
||||||
"linkkf_auto_make_season_folder": "True",
|
|
||||||
"linkkf_finished_insert": "[완결]",
|
|
||||||
"include_date": "False",
|
|
||||||
"date_option": "0",
|
|
||||||
"auto_make_folder": "True",
|
|
||||||
"max_ffmpeg_process_count": "4",
|
|
||||||
"auto_interval": "* 20 * * *",
|
|
||||||
"auto_start": "False",
|
|
||||||
"whitelist_program": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def db_init():
|
|
||||||
try:
|
|
||||||
with F.app.app_context():
|
|
||||||
logger.debug(Logic.db_default.items())
|
|
||||||
for key, value in Logic.db_default.items():
|
|
||||||
logger.debug(f"{key}: {value}")
|
|
||||||
if db.session.query(ModelSetting).filter_by(key=key).count() == 0:
|
|
||||||
db.session.add(ModelSetting(key, value))
|
|
||||||
db.session.commit()
|
|
||||||
Logic.db_migration()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Exception:%s", e)
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def plugin_load():
|
|
||||||
try:
|
|
||||||
logger.debug("%s plugin_load", package_name)
|
|
||||||
Logic.db_init()
|
|
||||||
|
|
||||||
if ModelSetting.get("auto_start") == "True":
|
|
||||||
Logic.scheduler_start()
|
|
||||||
|
|
||||||
from .plugin import plugin_info
|
|
||||||
|
|
||||||
SupportFile.write_json(
|
|
||||||
os.path.join(os.path.dirname(__file__), "info.json"),
|
|
||||||
plugin_info,
|
|
||||||
)
|
|
||||||
LogicQueue.queue_start()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Exception:%s", e)
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def plugin_unload():
|
|
||||||
try:
|
|
||||||
logger.debug("%s plugin_unload", package_name)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Exception:%s", e)
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def db_migration():
|
|
||||||
logger.debug("db_migration::=======================")
|
|
||||||
try:
|
|
||||||
migrated = ModelLinkkf.migrate_existing_rows()
|
|
||||||
logger.debug("ModelLinkkf migrate_existing_rows: %s", migrated)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Exception:%s", e)
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def scheduler_start():
|
|
||||||
try:
|
|
||||||
interval = ModelSetting.get("auto_interval")
|
|
||||||
job = Job(
|
|
||||||
package_name,
|
|
||||||
package_name,
|
|
||||||
interval,
|
|
||||||
Logic.scheduler_function,
|
|
||||||
"linkkf 다운로드",
|
|
||||||
True,
|
|
||||||
)
|
|
||||||
scheduler.add_job_instance(job)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Exception:%s", e)
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def scheduler_stop():
|
|
||||||
try:
|
|
||||||
scheduler.remove_job(package_name)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Exception:%s", e)
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def setting_save(req):
|
|
||||||
try:
|
|
||||||
for key, value in req.form.items():
|
|
||||||
logger.debug("Key:%s Value:%s", key, value)
|
|
||||||
entity = (
|
|
||||||
db.session.query(ModelSetting)
|
|
||||||
.filter_by(key=key)
|
|
||||||
.with_for_update()
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
entity.value = value
|
|
||||||
db.session.commit()
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Exception:%s", e)
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
logger.error("key:%s value:%s", key, value)
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def scheduler_function():
|
|
||||||
try:
|
|
||||||
LogicLinkkf.scheduler_function()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Exception:%s", e)
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import threading
|
|
||||||
import traceback
|
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from flask import Response, jsonify, render_template, stream_with_context
|
|
||||||
from plugin import PluginModuleBase, default_route_socketio_module
|
|
||||||
from framework import F
|
|
||||||
|
|
||||||
from .logic import Logic
|
|
||||||
from .logic_linkkf import LogicLinkkf
|
|
||||||
from .logic_queue import LogicQueue, QueueEntity
|
|
||||||
from .model import ModelLinkkf
|
|
||||||
from .setup import P
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleBasic(PluginModuleBase):
|
|
||||||
def __init__(self, P):
|
|
||||||
super(ModuleBasic, self).__init__(P, name="main")
|
|
||||||
default_route_socketio_module(self)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _make_proxy_url(target, referer):
|
|
||||||
return (
|
|
||||||
f"/{P.package_name}/normal/proxy"
|
|
||||||
f"?target={urllib.parse.quote(str(target), safe='')}"
|
|
||||||
f"&referer={urllib.parse.quote(str(referer or ''), safe='')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_proxy_headers(referer):
|
|
||||||
return {
|
|
||||||
"User-Agent": (
|
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
||||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
||||||
"Chrome/122.0.0.0 Safari/537.36"
|
|
||||||
),
|
|
||||||
"Referer": referer or P.ModelSetting.get("linkkf_url"),
|
|
||||||
"Origin": urllib.parse.urlsplit(referer or P.ModelSetting.get("linkkf_url")).scheme
|
|
||||||
+ "://"
|
|
||||||
+ urllib.parse.urlsplit(referer or P.ModelSetting.get("linkkf_url")).netloc,
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _rewrite_m3u8(content, target_url, referer):
|
|
||||||
def replace_uri_attr(line):
|
|
||||||
def repl(match):
|
|
||||||
absolute = urllib.parse.urljoin(target_url, match.group(1))
|
|
||||||
proxied = ModuleBasic._make_proxy_url(absolute, referer)
|
|
||||||
return f'URI="{proxied}"'
|
|
||||||
|
|
||||||
return re.sub(r'URI="([^"]+)"', repl, line)
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
for raw_line in content.splitlines():
|
|
||||||
line = raw_line.strip()
|
|
||||||
if line == "":
|
|
||||||
lines.append(raw_line)
|
|
||||||
continue
|
|
||||||
if line.startswith("#"):
|
|
||||||
lines.append(replace_uri_attr(raw_line))
|
|
||||||
continue
|
|
||||||
absolute = urllib.parse.urljoin(target_url, line)
|
|
||||||
lines.append(ModuleBasic._make_proxy_url(absolute, referer))
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
def process_menu(self, sub, req):
|
|
||||||
if sub == "log":
|
|
||||||
return render_template("log.html", package=self.P.package_name)
|
|
||||||
|
|
||||||
arg = self.P.ModelSetting.to_dict() if self.P.ModelSetting is not None else {}
|
|
||||||
arg["package_name"] = self.P.package_name
|
|
||||||
arg["sub"] = sub
|
|
||||||
arg["template_name"] = f"{self.P.package_name}_{sub}"
|
|
||||||
|
|
||||||
if sub == "setting":
|
|
||||||
arg["scheduler"] = str(F.scheduler.is_include(self.P.package_name))
|
|
||||||
arg["is_running"] = str(F.scheduler.is_running(self.P.package_name))
|
|
||||||
elif sub in ["request", "queue", "list"]:
|
|
||||||
arg["current_code"] = (
|
|
||||||
LogicLinkkf.current_data["code"]
|
|
||||||
if LogicLinkkf.current_data is not None
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
|
|
||||||
return render_template(f"{self.P.package_name}_{sub}.html", arg=arg)
|
|
||||||
|
|
||||||
def process_ajax(self, sub, req):
|
|
||||||
try:
|
|
||||||
if sub == "scheduler_toggle":
|
|
||||||
go = req.form["scheduler"]
|
|
||||||
if go == "true":
|
|
||||||
Logic.scheduler_start()
|
|
||||||
else:
|
|
||||||
Logic.scheduler_stop()
|
|
||||||
return jsonify(go)
|
|
||||||
if sub == "execute_once":
|
|
||||||
threading.Thread(target=Logic.scheduler_function, daemon=True).start()
|
|
||||||
return jsonify({"ret": "success"})
|
|
||||||
if sub == "analysis":
|
|
||||||
code = req.form["code"]
|
|
||||||
data = LogicLinkkf.get_title_info(code)
|
|
||||||
if data["ret"] == "error":
|
|
||||||
return jsonify(data)
|
|
||||||
return jsonify({"ret": "success", "data": data})
|
|
||||||
if sub == "play":
|
|
||||||
episode_url = req.form["url"]
|
|
||||||
play_title = req.form.get("title", "LinkKF")
|
|
||||||
return jsonify(
|
|
||||||
{
|
|
||||||
"ret": "success",
|
|
||||||
"data": {
|
|
||||||
"play_url": (
|
|
||||||
f"/{self.P.package_name}/normal/play"
|
|
||||||
f"?url={urllib.parse.quote(str(episode_url), safe='')}"
|
|
||||||
f"&title={urllib.parse.quote(str(play_title), safe='')}"
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if sub == "play_latest":
|
|
||||||
code = req.form["code"]
|
|
||||||
data = LogicLinkkf.get_title_info(code)
|
|
||||||
if data["ret"] == "error":
|
|
||||||
return jsonify(data)
|
|
||||||
if "episode" not in data or len(data["episode"]) == 0:
|
|
||||||
return jsonify({"ret": "error", "log": "최신 화 정보를 찾지 못했습니다."})
|
|
||||||
latest_episode = data["episode"][0]
|
|
||||||
latest_title = f"{data['title']} - {latest_episode['title']}"
|
|
||||||
return jsonify(
|
|
||||||
{
|
|
||||||
"ret": "success",
|
|
||||||
"data": {
|
|
||||||
"play_url": (
|
|
||||||
f"/{self.P.package_name}/normal/play"
|
|
||||||
f"?url={urllib.parse.quote(str(latest_episode['url']), safe='')}"
|
|
||||||
f"&title={urllib.parse.quote(str(latest_title), safe='')}"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if sub == "search":
|
|
||||||
query = req.form["query"]
|
|
||||||
return jsonify(LogicLinkkf.get_search_result(str(query)))
|
|
||||||
if sub == "anime_list":
|
|
||||||
page = req.form["page"]
|
|
||||||
cate = req.form["type"]
|
|
||||||
return jsonify(LogicLinkkf.get_anime_list_info(cate, page))
|
|
||||||
if sub == "airing_list":
|
|
||||||
return jsonify(LogicLinkkf.get_airing_info())
|
|
||||||
if sub == "get_airing_code":
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
data = loop.run_until_complete(LogicLinkkf.get_airing_code())
|
|
||||||
return jsonify({"ret": "success", "data": data})
|
|
||||||
if sub == "screen_movie_list":
|
|
||||||
page = req.form["page"]
|
|
||||||
return jsonify(LogicLinkkf.get_screen_movie_info(page))
|
|
||||||
if sub == "complete_anilist":
|
|
||||||
page = req.form["page"]
|
|
||||||
return jsonify(LogicLinkkf.get_complete_anilist_info(page))
|
|
||||||
if sub == "apply_new_title":
|
|
||||||
return jsonify(LogicLinkkf.apply_new_title(req.form["new_title"]))
|
|
||||||
if sub == "apply_new_season":
|
|
||||||
return jsonify(LogicLinkkf.apply_new_season(req.form["new_season"]))
|
|
||||||
if sub == "add_whitelist":
|
|
||||||
payload = req.get_json()
|
|
||||||
ret = LogicLinkkf.add_whitelist(payload if payload is not None else None)
|
|
||||||
return jsonify(ret)
|
|
||||||
if sub == "add_queue":
|
|
||||||
code = req.form["code"]
|
|
||||||
info = LogicLinkkf.get_info_by_code(code)
|
|
||||||
if info is not None:
|
|
||||||
return jsonify({"ret": LogicQueue.add_queue(info)})
|
|
||||||
return jsonify({"ret": "no_data"})
|
|
||||||
if sub == "add_queue_checked_list":
|
|
||||||
code_list = req.form["code"].split(",")
|
|
||||||
count = 0
|
|
||||||
for code in code_list:
|
|
||||||
info = LogicLinkkf.get_info_by_code(code)
|
|
||||||
if info is not None:
|
|
||||||
LogicQueue.add_queue(info)
|
|
||||||
count += 1
|
|
||||||
return jsonify({"ret": "success", "log": str(count)})
|
|
||||||
if sub == "down_subtitle_list":
|
|
||||||
code_list = req.form["code"].split(",")
|
|
||||||
count = 0
|
|
||||||
for code in code_list:
|
|
||||||
info = LogicLinkkf.get_info_by_code(code)
|
|
||||||
if info is not None:
|
|
||||||
LogicLinkkf.download_subtitle(info)
|
|
||||||
count += 1
|
|
||||||
return jsonify({"ret": "success", "log": str(count)})
|
|
||||||
if sub == "program_auto_command":
|
|
||||||
return jsonify(LogicQueue.program_auto_command(req))
|
|
||||||
if sub == "web_list":
|
|
||||||
LogicQueue.sync_entities_to_db()
|
|
||||||
return jsonify(ModelLinkkf.web_list(req))
|
|
||||||
if sub == "db_remove":
|
|
||||||
return jsonify(ModelLinkkf.delete_by_id(req.form["id"]))
|
|
||||||
if sub == "reset_db":
|
|
||||||
res = LogicLinkkf.reset_db()
|
|
||||||
return jsonify({"ret": "success" if res else "error"})
|
|
||||||
except Exception as e:
|
|
||||||
self.P.logger.error(f"Exception:{str(e)}")
|
|
||||||
self.P.logger.error(traceback.format_exc())
|
|
||||||
return jsonify({"ret": "error", "log": str(e)})
|
|
||||||
return jsonify({"ret": "error", "log": f"unsupported ajax: {sub}"})
|
|
||||||
|
|
||||||
def process_normal(self, sub, req):
|
|
||||||
try:
|
|
||||||
if sub == "play":
|
|
||||||
episode_url = req.args.get("url", "").strip()
|
|
||||||
play_title = req.args.get("title", "LinkKF").strip()
|
|
||||||
video_info = LogicLinkkf.get_video_url(episode_url)
|
|
||||||
if video_info is None or video_info[0] in [None, ""]:
|
|
||||||
return f"재생 URL을 가져오지 못했습니다: {episode_url}", 500
|
|
||||||
|
|
||||||
referer = video_info[1] or self.P.ModelSetting.get("linkkf_url")
|
|
||||||
data = {
|
|
||||||
"play_title": play_title or "LinkKF",
|
|
||||||
"play_source_src": self._make_proxy_url(video_info[0], referer),
|
|
||||||
"play_source_type": "application/x-mpegURL" if ".m3u8" in str(video_info[0]).lower() else "video/mp4",
|
|
||||||
"play_subtitle_src": "",
|
|
||||||
}
|
|
||||||
if len(video_info) > 2 and video_info[2] not in [None, ""]:
|
|
||||||
data["play_subtitle_src"] = self._make_proxy_url(video_info[2], referer)
|
|
||||||
return render_template("videojs.html", data=data)
|
|
||||||
|
|
||||||
if sub == "proxy":
|
|
||||||
target = req.args.get("target", "").strip()
|
|
||||||
referer = req.args.get("referer", self.P.ModelSetting.get("linkkf_url")).strip()
|
|
||||||
if target == "":
|
|
||||||
return "missing target", 400
|
|
||||||
|
|
||||||
headers = self._get_proxy_headers(referer)
|
|
||||||
if req.headers.get("Range") is not None:
|
|
||||||
headers["Range"] = req.headers.get("Range")
|
|
||||||
upstream = requests.get(target, headers=headers, stream=True, timeout=30)
|
|
||||||
content_type = upstream.headers.get("content-type", "")
|
|
||||||
|
|
||||||
if upstream.status_code >= 400:
|
|
||||||
return Response(
|
|
||||||
upstream.content,
|
|
||||||
status=upstream.status_code,
|
|
||||||
content_type=content_type or "text/plain",
|
|
||||||
)
|
|
||||||
|
|
||||||
if ".m3u8" in target.lower() or "mpegurl" in content_type.lower():
|
|
||||||
text = upstream.text
|
|
||||||
rewritten = self._rewrite_m3u8(text, target, referer)
|
|
||||||
return Response(
|
|
||||||
rewritten,
|
|
||||||
content_type=content_type or "application/vnd.apple.mpegurl",
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate():
|
|
||||||
try:
|
|
||||||
for chunk in upstream.iter_content(chunk_size=64 * 1024):
|
|
||||||
if chunk:
|
|
||||||
yield chunk
|
|
||||||
finally:
|
|
||||||
upstream.close()
|
|
||||||
|
|
||||||
response = Response(
|
|
||||||
stream_with_context(generate()),
|
|
||||||
status=upstream.status_code,
|
|
||||||
content_type=content_type or "application/octet-stream",
|
|
||||||
)
|
|
||||||
if upstream.headers.get("Accept-Ranges") is not None:
|
|
||||||
response.headers["Accept-Ranges"] = upstream.headers.get("Accept-Ranges")
|
|
||||||
if upstream.headers.get("Content-Length") is not None:
|
|
||||||
response.headers["Content-Length"] = upstream.headers.get("Content-Length")
|
|
||||||
if upstream.headers.get("Content-Range") is not None:
|
|
||||||
response.headers["Content-Range"] = upstream.headers.get("Content-Range")
|
|
||||||
return response
|
|
||||||
except Exception as e:
|
|
||||||
self.P.logger.error(f"Exception:{str(e)}")
|
|
||||||
self.P.logger.error(traceback.format_exc())
|
|
||||||
return f"playback proxy error: {e}", 500
|
|
||||||
return "unsupported normal route", 404
|
|
||||||
|
|
||||||
def plugin_load(self):
|
|
||||||
Logic.plugin_load()
|
|
||||||
|
|
||||||
def plugin_unload(self):
|
|
||||||
Logic.plugin_unload()
|
|
||||||
|
|
||||||
def setting_save_after(self, change_list):
|
|
||||||
if "linkkf_url" in change_list:
|
|
||||||
LogicLinkkf.referer = None
|
|
||||||
LogicLinkkf.headers["Referer"] = self.P.ModelSetting.get("linkkf_url")
|
|
||||||
|
|
||||||
def socketio_connect(self):
|
|
||||||
data = json.loads(json.dumps([item.__dict__ for item in QueueEntity.entity_list], default=str))
|
|
||||||
self.socketio_callback("on_connect", data, encoding=False)
|
|
||||||
|
|
||||||
def socketio_list_refresh(self):
|
|
||||||
data = json.loads(json.dumps([item.__dict__ for item in QueueEntity.entity_list], default=str))
|
|
||||||
self.socketio_callback("list_refresh", data, encoding=False)
|
|
||||||
-273
@@ -1,273 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from sqlalchemy import desc, or_
|
|
||||||
|
|
||||||
from .setup import *
|
|
||||||
|
|
||||||
|
|
||||||
ModelSetting = P.ModelSetting
|
|
||||||
|
|
||||||
|
|
||||||
class ModelLinkkfProgram(ModelBase):
|
|
||||||
P = P
|
|
||||||
__tablename__ = "linkkf_program"
|
|
||||||
__bind_key__ = P.package_name
|
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
contents_json = db.Column(db.JSON)
|
|
||||||
created_time = db.Column(db.DateTime)
|
|
||||||
programcode = db.Column(db.String)
|
|
||||||
save_folder = db.Column(db.String)
|
|
||||||
season = db.Column(db.Integer)
|
|
||||||
|
|
||||||
def __init__(self, data):
|
|
||||||
self.created_time = datetime.now()
|
|
||||||
self.programcode = data["code"]
|
|
||||||
self.save_folder = data["title"]
|
|
||||||
self.season = data["season"]
|
|
||||||
|
|
||||||
def set_info(self, data):
|
|
||||||
self.contents_json = data
|
|
||||||
self.programcode = data["code"]
|
|
||||||
self.save_folder = data["save_folder"]
|
|
||||||
self.season = data["season"]
|
|
||||||
|
|
||||||
|
|
||||||
class ModelLinkkf(ModelBase):
|
|
||||||
P = P
|
|
||||||
__tablename__ = "linkkf_auto_episode"
|
|
||||||
__bind_key__ = P.package_name
|
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
contents_json = db.Column(db.JSON)
|
|
||||||
created_time = db.Column(db.DateTime)
|
|
||||||
completed_time = db.Column(db.DateTime)
|
|
||||||
|
|
||||||
programcode = db.Column(db.String)
|
|
||||||
episodecode = db.Column(db.String)
|
|
||||||
filename = db.Column(db.String)
|
|
||||||
duration = db.Column(db.Integer)
|
|
||||||
start_time = db.Column(db.DateTime)
|
|
||||||
end_time = db.Column(db.DateTime)
|
|
||||||
download_time = db.Column(db.Integer)
|
|
||||||
completed = db.Column(db.Boolean)
|
|
||||||
user_abort = db.Column(db.Boolean)
|
|
||||||
pf_abort = db.Column(db.Boolean)
|
|
||||||
etc_abort = db.Column(db.Integer)
|
|
||||||
ffmpeg_status = db.Column(db.Integer)
|
|
||||||
temp_path = db.Column(db.String)
|
|
||||||
save_path = db.Column(db.String)
|
|
||||||
pf = db.Column(db.Integer)
|
|
||||||
retry = db.Column(db.Integer)
|
|
||||||
filesize = db.Column(db.Integer)
|
|
||||||
filesize_str = db.Column(db.String)
|
|
||||||
download_speed = db.Column(db.String)
|
|
||||||
call = db.Column(db.String)
|
|
||||||
status = db.Column(db.String)
|
|
||||||
linkkf_info = db.Column(db.JSON)
|
|
||||||
|
|
||||||
def __init__(self, call, info):
|
|
||||||
self.created_time = datetime.now()
|
|
||||||
self.completed = False
|
|
||||||
self.start_time = datetime.now()
|
|
||||||
self.user_abort = False
|
|
||||||
self.pf_abort = False
|
|
||||||
self.etc_abort = 0
|
|
||||||
self.ffmpeg_status = -1
|
|
||||||
self.pf = 0
|
|
||||||
self.retry = 0
|
|
||||||
self.call = call
|
|
||||||
self.set_info(info)
|
|
||||||
|
|
||||||
def as_dict(self):
|
|
||||||
ret = super().as_dict()
|
|
||||||
if ret.get("status") in [None, ""]:
|
|
||||||
if self.completed is True:
|
|
||||||
ret["status"] = "completed"
|
|
||||||
elif self.user_abort is True:
|
|
||||||
ret["status"] = "canceled"
|
|
||||||
elif self.pf_abort is True or (self.etc_abort is not None and int(self.etc_abort) > 0):
|
|
||||||
ret["status"] = "error"
|
|
||||||
elif self.ffmpeg_status in [0, 5]:
|
|
||||||
ret["status"] = "downloading"
|
|
||||||
ret["created_time"] = self.created_time.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
ret["completed_time"] = (
|
|
||||||
self.completed_time.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
if self.completed_time is not None
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def set_info(self, data):
|
|
||||||
self.contents_json = data
|
|
||||||
self.programcode = data["program_code"]
|
|
||||||
self.episodecode = data["code"]
|
|
||||||
self.filename = data.get("filename", self.filename)
|
|
||||||
self.linkkf_info = data
|
|
||||||
if self.status in [None, ""]:
|
|
||||||
self.status = "waiting"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _normalize_json_data(data):
|
|
||||||
if isinstance(data, dict):
|
|
||||||
return data
|
|
||||||
if isinstance(data, str) and data.strip() != "":
|
|
||||||
try:
|
|
||||||
return json.loads(data)
|
|
||||||
except Exception:
|
|
||||||
return {}
|
|
||||||
return {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def sync_completed_from_filesystem(cls):
|
|
||||||
with F.app.app_context():
|
|
||||||
changed = 0
|
|
||||||
rows = F.db.session.query(cls).filter(
|
|
||||||
or_(cls.status != "completed", cls.status.is_(None), cls.completed.is_(False))
|
|
||||||
).all()
|
|
||||||
for row in rows:
|
|
||||||
info = cls._normalize_json_data(row.linkkf_info) or cls._normalize_json_data(row.contents_json)
|
|
||||||
save_path = row.save_path or info.get("save_path")
|
|
||||||
filename = row.filename or info.get("filename")
|
|
||||||
if not save_path or not filename:
|
|
||||||
continue
|
|
||||||
fullpath = os.path.join(save_path, filename)
|
|
||||||
if os.path.exists(fullpath) is False:
|
|
||||||
continue
|
|
||||||
row.completed = True
|
|
||||||
row.user_abort = False
|
|
||||||
row.pf_abort = False
|
|
||||||
row.etc_abort = 0
|
|
||||||
row.ffmpeg_status = 7 if row.ffmpeg_status in [None, -1, 0, 5] else row.ffmpeg_status
|
|
||||||
row.status = "completed"
|
|
||||||
file_time = datetime.fromtimestamp(os.path.getmtime(fullpath))
|
|
||||||
if row.end_time is None:
|
|
||||||
row.end_time = file_time
|
|
||||||
if row.completed_time is None:
|
|
||||||
row.completed_time = file_time
|
|
||||||
changed += 1
|
|
||||||
if changed > 0:
|
|
||||||
F.db.session.commit()
|
|
||||||
return changed
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def migrate_existing_rows(cls):
|
|
||||||
with F.app.app_context():
|
|
||||||
changed = 0
|
|
||||||
rows = F.db.session.query(cls).all()
|
|
||||||
for row in rows:
|
|
||||||
info = cls._normalize_json_data(row.contents_json) or cls._normalize_json_data(row.linkkf_info)
|
|
||||||
updated = False
|
|
||||||
if row.programcode in [None, ""] and info.get("program_code"):
|
|
||||||
row.programcode = info.get("program_code")
|
|
||||||
updated = True
|
|
||||||
if row.episodecode in [None, ""] and info.get("code"):
|
|
||||||
row.episodecode = info.get("code")
|
|
||||||
updated = True
|
|
||||||
if row.filename in [None, ""] and info.get("filename"):
|
|
||||||
row.filename = info.get("filename")
|
|
||||||
updated = True
|
|
||||||
if row.linkkf_info in [None, {}] and info:
|
|
||||||
row.linkkf_info = info
|
|
||||||
updated = True
|
|
||||||
if row.status in [None, ""]:
|
|
||||||
if row.completed is True:
|
|
||||||
row.status = "completed"
|
|
||||||
elif row.user_abort is True:
|
|
||||||
row.status = "canceled"
|
|
||||||
elif row.pf_abort is True or (row.etc_abort is not None and int(row.etc_abort) > 0):
|
|
||||||
row.status = "error"
|
|
||||||
elif row.ffmpeg_status in [0, 5]:
|
|
||||||
row.status = "downloading"
|
|
||||||
else:
|
|
||||||
row.status = "waiting"
|
|
||||||
updated = True
|
|
||||||
if updated:
|
|
||||||
changed += 1
|
|
||||||
if changed > 0:
|
|
||||||
F.db.session.commit()
|
|
||||||
changed += cls.sync_completed_from_filesystem()
|
|
||||||
return changed
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def web_list(cls, req):
|
|
||||||
with F.app.app_context():
|
|
||||||
ret = {}
|
|
||||||
cls.sync_completed_from_filesystem()
|
|
||||||
page = int(req.form["page"]) if "page" in req.form else 1
|
|
||||||
page_size = 30
|
|
||||||
search = req.form["search_word"] if "search_word" in req.form else req.form.get("keyword", "")
|
|
||||||
option = req.form["option"] if "option" in req.form else req.form.get("option1", "finished")
|
|
||||||
order = req.form["order"] if "order" in req.form else "desc"
|
|
||||||
|
|
||||||
query = cls.make_query(search=search, order=order, option=option)
|
|
||||||
count = query.count()
|
|
||||||
query = query.limit(page_size).offset((page - 1) * page_size)
|
|
||||||
lists = query.all()
|
|
||||||
ret["list"] = [item.as_dict() for item in lists]
|
|
||||||
ret["paging"] = cls.get_paging_info(count, page, page_size)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_by_linkkf_id(cls, linkkf_id):
|
|
||||||
with F.app.app_context():
|
|
||||||
return F.db.session.query(cls).filter_by(episodecode=linkkf_id).first()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def make_query(cls, search="", order="desc", option="all"):
|
|
||||||
query = F.db.session.query(cls)
|
|
||||||
if search is not None and search != "":
|
|
||||||
if "|" in search:
|
|
||||||
conditions = []
|
|
||||||
for token in [x.strip() for x in search.split("|") if x.strip()]:
|
|
||||||
conditions.append(cls.filename.like(f"%{token}%"))
|
|
||||||
conditions.append(cls.programcode.like(f"%{token}%"))
|
|
||||||
if conditions:
|
|
||||||
query = query.filter(or_(*conditions))
|
|
||||||
elif "," in search:
|
|
||||||
for token in [x.strip() for x in search.split(",") if x.strip()]:
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
cls.filename.like(f"%{token}%"),
|
|
||||||
cls.programcode.like(f"%{token}%"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
cls.filename.like(f"%{search}%"),
|
|
||||||
cls.programcode.like(f"%{search}%"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if option == "completed":
|
|
||||||
query = query.filter(or_(cls.status == "completed", cls.completed.is_(True)))
|
|
||||||
elif option == "canceled":
|
|
||||||
query = query.filter(or_(cls.status == "canceled", cls.user_abort.is_(True)))
|
|
||||||
elif option == "error":
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
cls.status == "error",
|
|
||||||
cls.pf_abort.is_(True),
|
|
||||||
cls.etc_abort > 0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif option == "finished":
|
|
||||||
query = query.filter(
|
|
||||||
or_(
|
|
||||||
cls.status.in_(["completed", "error", "canceled"]),
|
|
||||||
cls.completed.is_(True),
|
|
||||||
cls.user_abort.is_(True),
|
|
||||||
cls.pf_abort.is_(True),
|
|
||||||
cls.etc_abort > 0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif option == "downloading":
|
|
||||||
query = query.filter(cls.status == "downloading")
|
|
||||||
if order == "desc":
|
|
||||||
query = query.order_by(desc(cls.id))
|
|
||||||
else:
|
|
||||||
query = query.order_by(cls.id)
|
|
||||||
return query
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
from .setup import P
|
|
||||||
|
|
||||||
|
|
||||||
package_name = P.package_name
|
|
||||||
logger = P.logger
|
|
||||||
plugin_info = P.plugin_info
|
|
||||||
|
|
||||||
|
|
||||||
def _module():
|
|
||||||
if P is None or P.module_list is None or len(P.module_list) == 0:
|
|
||||||
return None
|
|
||||||
return P.module_list[0]
|
|
||||||
|
|
||||||
|
|
||||||
def plugin_load():
|
|
||||||
module = _module()
|
|
||||||
if module is not None:
|
|
||||||
module.plugin_load()
|
|
||||||
|
|
||||||
|
|
||||||
def plugin_unload():
|
|
||||||
module = _module()
|
|
||||||
if module is not None:
|
|
||||||
module.plugin_unload()
|
|
||||||
|
|
||||||
|
|
||||||
def socketio_callback(cmd, data):
|
|
||||||
module = _module()
|
|
||||||
if module is not None and hasattr(module, "socketio_callback"):
|
|
||||||
module.socketio_callback(cmd, data)
|
|
||||||
|
|
||||||
|
|
||||||
def socketio_list_refresh():
|
|
||||||
module = _module()
|
|
||||||
if module is not None and hasattr(module, "socketio_list_refresh"):
|
|
||||||
module.socketio_list_refresh()
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
cloudscraper
|
|
||||||
beautifulsoup4
|
|
||||||
requests-cache
|
|
||||||
lxml
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import importlib.util
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from framework import F
|
|
||||||
from plugin import *
|
|
||||||
|
|
||||||
|
|
||||||
REQUIRED_PACKAGES = [
|
|
||||||
("cloudscraper", "cloudscraper"),
|
|
||||||
("beautifulsoup4", "bs4"),
|
|
||||||
("requests-cache", "requests_cache"),
|
|
||||||
("lxml", "lxml"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _get_declared_package_name():
|
|
||||||
info_path = os.path.join(os.path.dirname(__file__), "info.yaml")
|
|
||||||
try:
|
|
||||||
with open(info_path, encoding="utf-8") as file:
|
|
||||||
info = yaml.safe_load(file) or {}
|
|
||||||
package_name = str(info.get("package_name", "")).strip()
|
|
||||||
if package_name != "":
|
|
||||||
return package_name
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return os.path.basename(os.path.dirname(__file__))
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_sqlalchemy_bind(package_name=None):
|
|
||||||
package_name = package_name or _get_declared_package_name()
|
|
||||||
try:
|
|
||||||
if getattr(F, "app", None) is None:
|
|
||||||
return package_name
|
|
||||||
binds = F.app.config.setdefault("SQLALCHEMY_BINDS", {})
|
|
||||||
if package_name not in binds:
|
|
||||||
db_path = os.path.join(F.config["path_data"], "db", f"{package_name}.db")
|
|
||||||
binds[package_name] = f"sqlite:///{db_path}?check_same_thread=False"
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return package_name
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_requirements():
|
|
||||||
missing = [package for package, module_name in REQUIRED_PACKAGES if importlib.util.find_spec(module_name) is None]
|
|
||||||
if not missing:
|
|
||||||
return
|
|
||||||
|
|
||||||
if getattr(P, "logger", None) is not None:
|
|
||||||
P.logger.info("Installing missing packages: %s", ", ".join(missing))
|
|
||||||
|
|
||||||
subprocess.check_call([sys.executable, "-m", "pip", "install", *missing])
|
|
||||||
|
|
||||||
|
|
||||||
setting = {
|
|
||||||
"filepath": __file__,
|
|
||||||
"use_db": True,
|
|
||||||
"use_default_setting": True,
|
|
||||||
"home_module": "category",
|
|
||||||
"menu": {
|
|
||||||
"uri": __package__,
|
|
||||||
"name": "linkkf",
|
|
||||||
"list": [
|
|
||||||
{"uri": "setting", "name": "설정"},
|
|
||||||
{"uri": "request", "name": "요청"},
|
|
||||||
{"uri": "category", "name": "카테고리"},
|
|
||||||
{"uri": "queue", "name": "대기열"},
|
|
||||||
{"uri": "list", "name": "목록"},
|
|
||||||
{"uri": "log", "name": "로그"},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"setting_menu": None,
|
|
||||||
"default_route": "single",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ensure_sqlalchemy_bind()
|
|
||||||
P = create_plugin_instance(setting)
|
|
||||||
ensure_sqlalchemy_bind(P.package_name)
|
|
||||||
_ensure_requirements()
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .mod_basic import ModuleBasic
|
|
||||||
|
|
||||||
P.set_module_list([ModuleBasic])
|
|
||||||
except Exception as e:
|
|
||||||
P.logger.error(f"Exception:{str(e)}")
|
|
||||||
P.logger.error(traceback.format_exc())
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
from support import SupportFile
|
|
||||||
|
|
||||||
|
|
||||||
def write_file(data, filepath):
|
|
||||||
SupportFile.write_file(filepath, data)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_vtt_to_srt(vtt_data):
|
|
||||||
lines = []
|
|
||||||
counter = 1
|
|
||||||
|
|
||||||
for block in re.split(r"\r?\n\r?\n", vtt_data.strip()):
|
|
||||||
block = block.strip()
|
|
||||||
if block == "" or block == "WEBVTT":
|
|
||||||
continue
|
|
||||||
|
|
||||||
block_lines = [line.strip("\ufeff") for line in block.splitlines() if line.strip() != ""]
|
|
||||||
if not block_lines:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if block_lines[0].startswith("WEBVTT"):
|
|
||||||
block_lines = block_lines[1:]
|
|
||||||
if not block_lines:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "-->" not in block_lines[0] and len(block_lines) > 1 and "-->" in block_lines[1]:
|
|
||||||
block_lines = block_lines[1:]
|
|
||||||
|
|
||||||
if "-->" not in block_lines[0]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
timing = block_lines[0].replace(".", ",")
|
|
||||||
payload = block_lines[1:]
|
|
||||||
|
|
||||||
lines.append(str(counter))
|
|
||||||
lines.append(timing)
|
|
||||||
lines.extend(payload)
|
|
||||||
lines.append("")
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
return "\n".join(lines).strip() + "\n"
|
|
||||||
|
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import importlib.util
|
import importlib.util
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from framework import F
|
||||||
from plugin import *
|
from plugin import *
|
||||||
|
|
||||||
|
|
||||||
@@ -13,6 +16,33 @@ REQUIRED_PACKAGES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_declared_package_name():
|
||||||
|
info_path = os.path.join(os.path.dirname(__file__), "info.yaml")
|
||||||
|
try:
|
||||||
|
with open(info_path, encoding="utf-8") as file:
|
||||||
|
info = yaml.safe_load(file) or {}
|
||||||
|
package_name = str(info.get("package_name", "")).strip()
|
||||||
|
if package_name != "":
|
||||||
|
return package_name
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return os.path.basename(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_sqlalchemy_bind(package_name=None):
|
||||||
|
package_name = package_name or _get_declared_package_name()
|
||||||
|
try:
|
||||||
|
if getattr(F, "app", None) is None:
|
||||||
|
return package_name
|
||||||
|
binds = F.app.config.setdefault("SQLALCHEMY_BINDS", {})
|
||||||
|
if package_name not in binds:
|
||||||
|
db_path = os.path.join(F.config["path_data"], "db", f"{package_name}.db")
|
||||||
|
binds[package_name] = f"sqlite:///{db_path}?check_same_thread=False"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return package_name
|
||||||
|
|
||||||
|
|
||||||
def _ensure_requirements():
|
def _ensure_requirements():
|
||||||
missing = [package for package, module_name in REQUIRED_PACKAGES if importlib.util.find_spec(module_name) is None]
|
missing = [package for package, module_name in REQUIRED_PACKAGES if importlib.util.find_spec(module_name) is None]
|
||||||
if not missing:
|
if not missing:
|
||||||
@@ -46,7 +76,9 @@ setting = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ensure_sqlalchemy_bind()
|
||||||
P = create_plugin_instance(setting)
|
P = create_plugin_instance(setting)
|
||||||
|
ensure_sqlalchemy_bind(P.package_name)
|
||||||
_ensure_requirements()
|
_ensure_requirements()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user