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 os
|
||||
import subprocess
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
from framework import F
|
||||
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():
|
||||
missing = [package for package, module_name in REQUIRED_PACKAGES if importlib.util.find_spec(module_name) is None]
|
||||
if not missing:
|
||||
@@ -46,7 +76,9 @@ setting = {
|
||||
}
|
||||
|
||||
|
||||
ensure_sqlalchemy_bind()
|
||||
P = create_plugin_instance(setting)
|
||||
ensure_sqlalchemy_bind(P.package_name)
|
||||
_ensure_requirements()
|
||||
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user