1315 lines
50 KiB
Python
1315 lines
50 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
TVBox 本地 Py/Js/HTML/XBPQ 爬虫聚合源(增强版)
|
||
==================================================
|
||
在原有聚合功能基础上新增:
|
||
- 增量合并:保留手工站点,仅替换自动生成的站点
|
||
- 交互操作:扫描开关、重新扫描、清除自动站点、忽略/恢复源、撤销变更、分类开关
|
||
- 分页支持:分类列表和搜索结果支持分页浏览
|
||
- APP 重载:通过 WebHTV 本机管理接口自动重载并校验站点列表
|
||
- 子文件夹后缀:根据文件所在子文件夹名称自动添加后缀标识
|
||
"""
|
||
import os
|
||
import json
|
||
import base64
|
||
import hashlib
|
||
import shutil
|
||
import threading
|
||
import time
|
||
import urllib.parse
|
||
import urllib.request
|
||
from base.spider import Spider
|
||
|
||
|
||
class Spider(Spider):
|
||
# ==========================================================================
|
||
# 📂 【配置区】
|
||
# ==========================================================================
|
||
PY_DIR = "/storage/emulated/0/TV/小百合/py"
|
||
JS_DIR = "/storage/emulated/0/TV/小百合/js"
|
||
HTML_DIR = "/storage/emulated/0/TV/小百合/html"
|
||
XBPQ_DIR = "/storage/emulated/0/TV/小百合/XBPQ"
|
||
JAR_DIR = "/storage/emulated/0/TV/小百合/jar"
|
||
SAVE_PATH = "/storage/emulated/0/TV/小百合/自动接口.json"
|
||
LOGO_PATH = "/storage/emulated/0/TV/小百合/jar/头像.gif"
|
||
|
||
# 🆕 增强功能配置
|
||
SETTINGS_PATH = "/storage/emulated/0/TV/小百合/接口配置/扫描配置.json"
|
||
BACKUP_DIR = "/storage/emulated/0/TV/小百合/接口配置"
|
||
|
||
HTML_API = "csp_Nostr"
|
||
XBPQ_API = "csp_XBPQ"
|
||
|
||
# 🔒 锁定在 sites 第 0、1、2 位的配置,无论扫描结果如何始终存在
|
||
_LOCKED_SITES = [
|
||
{
|
||
"key": "FishConfig",
|
||
"name": "🍼┆设置┆中心[工具]",
|
||
"type": 3,
|
||
"api": "csp_FishConfig"
|
||
},
|
||
{
|
||
"key": "Local",
|
||
"name": "📁┆文件┆浏览[工具]",
|
||
"type": 3,
|
||
"api": "csp_Local",
|
||
"searchable": 0,
|
||
"changeable": 0,
|
||
"indexs": 0,
|
||
"style": {
|
||
"type": "list"
|
||
},
|
||
"ext": "https://6800.kstore.vip/share.json"
|
||
},
|
||
{
|
||
"key": "自动加载[工具]_py", # ✅ 修改:与 _LOCKED_KEYS 保持一致
|
||
"name": "自动加载[工具]",
|
||
"type": 3,
|
||
"searchable": 1,
|
||
"quickSearch": 1,
|
||
"filterable": 1,
|
||
"api": "./py/工具/自动加载.py"
|
||
}
|
||
]
|
||
|
||
_LOCKED_KEYS = {"FishConfig", "Local", "自动加载[工具]_py"}
|
||
|
||
# 🆕 增量合并前缀与分页大小
|
||
GENERATED_KEY_PREFIX = "local_auto_"
|
||
PAGE_SIZE = 60
|
||
|
||
# 🆕 APP 重载配置
|
||
AUTO_RELOAD_APP = True
|
||
APP_PORT_START = 9978
|
||
APP_PORT_END = 9998
|
||
APP_REQUEST_TIMEOUT = 0.35
|
||
|
||
# 🆕 交互操作常量
|
||
ACTION_RESCAN = "local_source_rescan"
|
||
ACTION_TOGGLE_SCAN = "local_source_toggle_scan"
|
||
ACTION_CLEAR_SITES = "local_source_clear_sites"
|
||
ACTION_TOGGLE_IGNORE_PFX = "local_source_toggle_ignore:"
|
||
ACTION_TOGGLE_TYPE_PFX = "local_source_toggle_type:"
|
||
ACTION_RESTORE_BACKUP = "local_source_restore_backup"
|
||
ACTION_DELETE_BACKUPS = "local_source_delete_backups"
|
||
ACTION_TOGGLE_APP_RELOAD = "local_source_toggle_app_reload"
|
||
|
||
STATUS_ID = "__local_source_status__"
|
||
RESCAN_ID = "__local_source_rescan__"
|
||
TOGGLE_SCAN_ID = "__local_source_toggle_scan__"
|
||
CLEAR_SITES_ID = "__local_source_clear_sites__"
|
||
RESTORE_BACKUP_ID = "__local_source_restore_backup__"
|
||
DELETE_BACKUPS_ID = "__local_source_delete_backups__"
|
||
# ==========================================================================
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.lock = threading.RLock()
|
||
self.inited = False
|
||
|
||
# 扫描开关与分类开关
|
||
self.scan_enabled = True
|
||
self.type_enabled = {"PY": True, "JS": True, "HTML": True, "XBPQ": True}
|
||
|
||
# 忽略源集合(存储 identity,如 "PY|/path/to/file")
|
||
self.ignored_sources = set()
|
||
|
||
# APP 重载相关
|
||
self.auto_reload_app = self.AUTO_RELOAD_APP
|
||
self.app_server_ports = list(range(self.APP_PORT_START, self.APP_PORT_END + 1))
|
||
self.last_app_port = 0
|
||
|
||
# 扫描结果缓存(扩展版)
|
||
self.cache = {
|
||
"categories": [],
|
||
"file_index": {},
|
||
"sources": [],
|
||
"ignored": [],
|
||
"source_index": {},
|
||
"type_counts": {},
|
||
"ignored_counts": {},
|
||
}
|
||
|
||
# 扫描状态
|
||
self.status = {
|
||
"scan_time": "-",
|
||
"included": 0,
|
||
"ignored": 0,
|
||
"manual_sites": 0,
|
||
"generated_sites": 0,
|
||
"added": 0,
|
||
"updated": 0,
|
||
"removed": 0,
|
||
"unchanged": 0,
|
||
"write_state": "尚未扫描",
|
||
"written": False,
|
||
"app_reload_state": "-",
|
||
}
|
||
|
||
def getName(self):
|
||
return "本地Py/Js/HTML/XBPQ聚合源(增强版)"
|
||
|
||
def init(self, extend):
|
||
with self.lock:
|
||
if self.inited:
|
||
return
|
||
self._load_settings()
|
||
if self.scan_enabled:
|
||
self._scan_all()
|
||
self._save_config_json()
|
||
self.inited = True
|
||
|
||
# ==========================================================================
|
||
# ⚙ 【设置持久化】
|
||
# ==========================================================================
|
||
def _load_settings(self):
|
||
path = self.SETTINGS_PATH
|
||
if not os.path.isfile(path):
|
||
return
|
||
try:
|
||
with open(path, "r", encoding="utf-8") as fp:
|
||
data = json.load(fp)
|
||
if not isinstance(data, dict):
|
||
return
|
||
self.scan_enabled = bool(data.get("scan_enabled", True))
|
||
te = data.get("type_enabled", {})
|
||
if isinstance(te, dict):
|
||
for t in self.type_enabled:
|
||
if t in te:
|
||
self.type_enabled[t] = bool(te[t])
|
||
ignored = data.get("ignored_sources", [])
|
||
if isinstance(ignored, list):
|
||
self.ignored_sources = {str(s).strip() for s in ignored if str(s).strip()}
|
||
self.auto_reload_app = bool(data.get("auto_reload_app", self.AUTO_RELOAD_APP))
|
||
try:
|
||
port = int(data.get("last_app_port", 0) or 0)
|
||
self.last_app_port = port if self.APP_PORT_START <= port <= 65535 else 0
|
||
except Exception:
|
||
self.last_app_port = 0
|
||
except Exception:
|
||
pass
|
||
|
||
def _save_settings(self):
|
||
data = {
|
||
"scan_enabled": bool(self.scan_enabled),
|
||
"type_enabled": {t: bool(v) for t, v in self.type_enabled.items()},
|
||
"ignored_sources": sorted(self.ignored_sources),
|
||
"auto_reload_app": bool(self.auto_reload_app),
|
||
"last_app_port": int(self.last_app_port or 0),
|
||
}
|
||
self._atomic_write_json_file(self.SETTINGS_PATH, data)
|
||
|
||
# ==========================================================================
|
||
# 📡 【APP 重载】通过 WebHTV 本机管理接口重载配置
|
||
# ==========================================================================
|
||
def _reload_app_vod_config(self):
|
||
"""
|
||
尝试通过 WebHTV 本机管理接口重载当前点播配置。
|
||
返回 (ok: bool, detail: str)
|
||
"""
|
||
if not self.auto_reload_app:
|
||
return False, "App重载已关闭"
|
||
|
||
last_error = "未发现WebHTV服务"
|
||
|
||
# 优先尝试上次成功的端口
|
||
ports = []
|
||
if self.last_app_port:
|
||
ports.append(self.last_app_port)
|
||
ports.extend(p for p in self.app_server_ports if p not in ports)
|
||
|
||
for port in ports:
|
||
base = "http://127.0.0.1:{}".format(port)
|
||
try:
|
||
# 第一步:获取当前活跃的点播配置
|
||
payload = self._request_json(
|
||
base + "/manage/configs", self.APP_REQUEST_TIMEOUT
|
||
)
|
||
items = payload.get("items", []) if isinstance(payload, dict) else []
|
||
current = next(
|
||
(
|
||
item for item in items
|
||
if isinstance(item, dict)
|
||
and int(item.get("type", -1)) == 0
|
||
and bool(item.get("active", False))
|
||
),
|
||
None,
|
||
)
|
||
if not current or not str(current.get("url", "")).strip():
|
||
last_error = "WebHTV未返回点播接口"
|
||
continue
|
||
|
||
# 第二步:请求重载当前点播配置
|
||
query = urllib.parse.urlencode(
|
||
{"type": 0, "url": str(current["url"]).strip()}
|
||
)
|
||
self._request_json(
|
||
base + "/manage/config/use?" + query,
|
||
max(1.5, self.APP_REQUEST_TIMEOUT * 4),
|
||
)
|
||
|
||
# 第三步:等待并验证站点列表已更新
|
||
verified = False
|
||
for _ in range(5):
|
||
try:
|
||
sites_payload = self._request_json(
|
||
base + "/manage/proxy/suggest/sites",
|
||
max(0.5, self.APP_REQUEST_TIMEOUT),
|
||
)
|
||
sites = sites_payload.get("sites", []) if isinstance(sites_payload, dict) else []
|
||
if sites:
|
||
verified = True
|
||
break
|
||
except Exception:
|
||
pass
|
||
time.sleep(0.12)
|
||
|
||
self._remember_app_port(port)
|
||
|
||
if verified:
|
||
return True, "重载成功"
|
||
else:
|
||
return True, "已发送重载请求"
|
||
|
||
except Exception as exc:
|
||
last_error = str(exc)
|
||
|
||
return False, "未连接({})".format(last_error[:20] if last_error else "无")
|
||
|
||
def _request_json(self, url, timeout):
|
||
"""发送 HTTP GET 请求并解析 JSON 响应"""
|
||
request = urllib.request.Request(
|
||
url,
|
||
headers={"Accept": "application/json", "Connection": "close"},
|
||
)
|
||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||
with opener.open(request, timeout=timeout) as response:
|
||
status = getattr(response, "status", response.getcode())
|
||
raw = response.read()
|
||
if int(status) < 200 or int(status) >= 300:
|
||
raise ValueError("HTTP {}".format(status))
|
||
data = json.loads(raw.decode("utf-8"))
|
||
if not isinstance(data, dict):
|
||
raise ValueError("WebHTV返回格式无效")
|
||
return data
|
||
|
||
def _remember_app_port(self, port):
|
||
"""缓存上次成功连接的 App 端口"""
|
||
if self.last_app_port == int(port):
|
||
return
|
||
self.last_app_port = int(port)
|
||
try:
|
||
self._save_settings()
|
||
except Exception:
|
||
pass
|
||
|
||
# ==========================================================================
|
||
# 💾 【备份管理】
|
||
# ==========================================================================
|
||
def _has_backup(self):
|
||
return os.path.isfile(self._backup_path())
|
||
|
||
def _backup_path(self):
|
||
return os.path.join(self.BACKUP_DIR, "回滚备份.json")
|
||
|
||
def _create_backup(self):
|
||
if not os.path.isfile(self.SAVE_PATH):
|
||
return
|
||
try:
|
||
os.makedirs(self.BACKUP_DIR, exist_ok=True)
|
||
bp = self._backup_path()
|
||
tmp = bp + ".tmp"
|
||
shutil.copy2(self.SAVE_PATH, tmp)
|
||
os.replace(tmp, bp)
|
||
except Exception:
|
||
pass
|
||
|
||
def _restore_backup(self):
|
||
bp = self._backup_path()
|
||
if not os.path.isfile(bp):
|
||
return False, "暂无可恢复的备份"
|
||
try:
|
||
self._create_backup()
|
||
tmp = self.SAVE_PATH + ".tmp"
|
||
shutil.copy2(bp, tmp)
|
||
os.replace(tmp, self.SAVE_PATH)
|
||
return True, "已恢复到上次备份"
|
||
except Exception as e:
|
||
return False, "恢复失败: {}".format(e)
|
||
|
||
def _delete_backups(self):
|
||
bp = self._backup_path()
|
||
if os.path.isfile(bp):
|
||
try:
|
||
os.remove(bp)
|
||
return True
|
||
except Exception:
|
||
pass
|
||
return False
|
||
|
||
# ==========================================================================
|
||
# 🔍 【扫描核心】手动递归,不依赖 os.walk
|
||
# ==========================================================================
|
||
def _scan_dir(self, base_dir, ext_list):
|
||
"""手动递归扫描目录,返回 [(full_path, file_name_no_ext, ext), ...]"""
|
||
results = []
|
||
if not base_dir:
|
||
return results
|
||
if not os.path.exists(base_dir):
|
||
try:
|
||
os.makedirs(base_dir, exist_ok=True)
|
||
except Exception:
|
||
return results
|
||
if not os.path.isdir(base_dir):
|
||
return results
|
||
|
||
try:
|
||
entries = os.listdir(base_dir)
|
||
except Exception:
|
||
return results
|
||
|
||
for entry in sorted(entries):
|
||
full_path = os.path.join(base_dir, entry)
|
||
if entry.startswith("."):
|
||
continue
|
||
|
||
if os.path.isdir(full_path):
|
||
sub_results = self._scan_dir(full_path, ext_list)
|
||
results.extend(sub_results)
|
||
elif os.path.isfile(full_path):
|
||
lower_name = entry.lower()
|
||
matched_ext = None
|
||
for ext in ext_list:
|
||
if lower_name.endswith(ext):
|
||
matched_ext = ext
|
||
break
|
||
if matched_ext:
|
||
name_no_ext = entry[: -len(matched_ext)]
|
||
results.append((full_path, name_no_ext, matched_ext))
|
||
|
||
return results
|
||
|
||
def _get_sub_sfx(self, full_path, base_dir):
|
||
"""计算子文件夹后缀:取相对路径的第一层子目录名"""
|
||
try:
|
||
rel = os.path.relpath(full_path, base_dir)
|
||
rel_parts = rel.split(os.sep)
|
||
subfolder = rel_parts[0] if len(rel_parts) > 1 else ""
|
||
except (ValueError, IndexError):
|
||
subfolder = ""
|
||
|
||
if not subfolder:
|
||
return ""
|
||
# 如果子文件夹名本身已带 [ ] 则原样返回,否则自动包裹
|
||
if subfolder.startswith("[") and subfolder.endswith("]"):
|
||
return subfolder
|
||
return f"[{subfolder}]"
|
||
|
||
def _scan_all(self):
|
||
"""扫描 py、js、html 和 XBPQ 四个目录,区分正常源与忽略源"""
|
||
sources = []
|
||
ignored_sources = []
|
||
self_path = os.path.abspath(__file__) if hasattr(__file__, '__file__') else ""
|
||
|
||
scan_specs = [
|
||
(self.PY_DIR, [".py"], "PY", 0),
|
||
(self.JS_DIR, [".js"], "JS", 1),
|
||
(self.HTML_DIR, [".html"], "HTML", 2),
|
||
(self.XBPQ_DIR, [".py", ".js", ".json"], "XBPQ", 3),
|
||
]
|
||
|
||
for dir_path, ext_list, type_tag, order in scan_specs:
|
||
if not self.type_enabled.get(type_tag, True):
|
||
continue
|
||
files = self._scan_dir(dir_path, ext_list)
|
||
for full_path, name, ext in files:
|
||
if self_path and os.path.abspath(full_path) == self_path:
|
||
continue
|
||
|
||
identity = type_tag + "|" + full_path
|
||
tid = base64.b64encode(identity.encode("utf-8")).decode("utf-8")
|
||
|
||
# 计算子文件夹后缀
|
||
sub_sfx = self._get_sub_sfx(full_path, dir_path)
|
||
|
||
if type_tag == "HTML":
|
||
display_name = f"{name}[网页]{sub_sfx}"
|
||
elif type_tag == "XBPQ":
|
||
display_name = f"⁽ˣᵇᵖ⁾{name}{sub_sfx}"
|
||
else:
|
||
display_name = f"【{type_tag}】{name}{sub_sfx}"
|
||
|
||
source = {
|
||
"type_id": tid,
|
||
"type_name": display_name,
|
||
"identity": identity,
|
||
"_path": full_path,
|
||
"_ext": ext.lstrip(".") if type_tag == "XBPQ" else ext,
|
||
"_dir": dir_path,
|
||
"_type_tag": type_tag,
|
||
"_sk": (order, name),
|
||
"ignored": identity in self.ignored_sources,
|
||
"_sub_sfx": sub_sfx,
|
||
}
|
||
|
||
if source["ignored"]:
|
||
ignored_sources.append(source)
|
||
else:
|
||
sources.append(source)
|
||
|
||
self.cache["file_index"][tid] = {
|
||
"path": full_path,
|
||
"ext": source["_ext"],
|
||
"dir": dir_path,
|
||
"type_tag": type_tag,
|
||
"sub_sfx": sub_sfx,
|
||
}
|
||
|
||
sources.sort(key=lambda x: x["_sk"])
|
||
ignored_sources.sort(key=lambda x: x["_sk"])
|
||
|
||
self.cache["sources"] = sources
|
||
self.cache["ignored"] = ignored_sources
|
||
self.cache["source_index"] = {}
|
||
self.cache["type_counts"] = {}
|
||
self.cache["ignored_counts"] = {}
|
||
for s in sources + ignored_sources:
|
||
self.cache["source_index"][s["type_id"]] = s
|
||
tag = s["_type_tag"]
|
||
if s["ignored"]:
|
||
self.cache["ignored_counts"][tag] = self.cache["ignored_counts"].get(tag, 0) + 1
|
||
else:
|
||
self.cache["type_counts"][tag] = self.cache["type_counts"].get(tag, 0) + 1
|
||
|
||
self.cache["categories"] = [
|
||
{"type_id": s["type_id"], "type_name": s["type_name"]}
|
||
for s in sources
|
||
]
|
||
|
||
self.status["included"] = len(sources)
|
||
self.status["ignored"] = len(ignored_sources)
|
||
|
||
all_identities = {s["identity"] for s in sources + ignored_sources}
|
||
stale = {i for i in self.ignored_sources if i not in all_identities}
|
||
if stale:
|
||
self.ignored_sources -= stale
|
||
try:
|
||
self._save_settings()
|
||
except Exception:
|
||
pass
|
||
|
||
# ==========================================================================
|
||
# 🆕 【增量合并配置生成】
|
||
# ==========================================================================
|
||
def _build_api(self, file_info):
|
||
"""拼接 api 相对路径"""
|
||
f_path = file_info["path"]
|
||
base_dir = file_info["dir"]
|
||
try:
|
||
rel = os.path.relpath(f_path, base_dir)
|
||
except ValueError:
|
||
rel = os.path.basename(f_path)
|
||
dir_name = os.path.basename(base_dir)
|
||
return "./" + dir_name + "/" + rel
|
||
|
||
def _build_spider_value(self):
|
||
"""扫描 jar 目录,返回用分号拼接的所有 jar 相对路径"""
|
||
jar_dir = self.JAR_DIR
|
||
if not jar_dir or not os.path.isdir(jar_dir):
|
||
return ""
|
||
jar_files = []
|
||
save_dir = os.path.dirname(self.SAVE_PATH)
|
||
try:
|
||
entries = sorted(os.listdir(jar_dir))
|
||
except Exception:
|
||
return ""
|
||
for entry in entries:
|
||
if entry.startswith("."):
|
||
continue
|
||
if entry.lower().endswith(".jar") and os.path.isfile(os.path.join(jar_dir, entry)):
|
||
abs_jar = os.path.join(jar_dir, entry)
|
||
try:
|
||
rel = os.path.relpath(abs_jar, save_dir)
|
||
except ValueError:
|
||
rel = "jar/" + entry
|
||
rel = "./" + rel.replace("\\", "/")
|
||
if not rel.startswith("./"):
|
||
rel = "./" + rel.lstrip("./")
|
||
jar_files.append(rel)
|
||
return ";".join(jar_files)
|
||
|
||
def _get_locked_api_set(self):
|
||
"""提取锁定站点中已占用的文件路径集合,用于自动站点去重"""
|
||
locked = set()
|
||
for site in self._LOCKED_SITES:
|
||
for field in ("api", "homePage", "ext"):
|
||
val = str(site.get(field, "")).strip()
|
||
if val.startswith("./"):
|
||
locked.add(val)
|
||
return locked
|
||
|
||
def _is_generated_key(self, key):
|
||
"""判断 key 是否为自动生成的站点 key"""
|
||
return str(key).startswith(self.GENERATED_KEY_PREFIX)
|
||
|
||
def _load_existing_config(self):
|
||
"""加载现有配置文件"""
|
||
if not os.path.isfile(self.SAVE_PATH):
|
||
return None
|
||
try:
|
||
with open(self.SAVE_PATH, "r", encoding="utf-8") as fp:
|
||
data = json.load(fp)
|
||
return data if isinstance(data, dict) else None
|
||
except Exception:
|
||
return None
|
||
|
||
def _generate_auto_sites(self):
|
||
"""从当前扫描结果生成自动站点配置列表,排除已被锁定站点占用的文件"""
|
||
locked_paths = self._get_locked_api_set()
|
||
sites = []
|
||
for source in self.cache["sources"]:
|
||
file_info = self.cache["file_index"].get(source["type_id"])
|
||
if not file_info:
|
||
continue
|
||
f_path = file_info["path"]
|
||
type_tag = file_info.get("type_tag", "PY")
|
||
f_base = os.path.basename(f_path)
|
||
if "." in f_base:
|
||
f_base = f_base.rsplit(".", 1)[0]
|
||
|
||
api_path = self._build_api(file_info)
|
||
sub_sfx = file_info.get("sub_sfx", "")
|
||
|
||
# 跳过已被锁定站点占用的文件,避免重复
|
||
if api_path in locked_paths:
|
||
continue
|
||
|
||
key = self.GENERATED_KEY_PREFIX + type_tag.lower() + "_" + hashlib.sha256(
|
||
(type_tag + "|" + f_path).encode("utf-8")
|
||
).hexdigest()[:14]
|
||
|
||
if type_tag == "HTML":
|
||
sites.append({
|
||
"key": key,
|
||
"name": f"{f_base}[网页]{sub_sfx}",
|
||
"type": 3,
|
||
"api": self.HTML_API,
|
||
"homePage": api_path,
|
||
})
|
||
elif type_tag == "XBPQ":
|
||
sites.append({
|
||
"key": key,
|
||
"name": f"⁽ˣᵇᵖ⁾{f_base}{sub_sfx}",
|
||
"type": 3,
|
||
"api": self.XBPQ_API,
|
||
"ext": api_path,
|
||
})
|
||
else:
|
||
sites.append({
|
||
"key": key,
|
||
"name": f"{f_base}{sub_sfx}",
|
||
"type": 3,
|
||
"searchable": 1,
|
||
"quickSearch": 1,
|
||
"filterable": 1,
|
||
"api": api_path,
|
||
})
|
||
return sites
|
||
|
||
def _save_config_json(self):
|
||
"""增量合并:保留手工站点,仅替换自动生成的站点,写入后尝试重载 App"""
|
||
new_auto_sites = self._generate_auto_sites()
|
||
|
||
existing = self._load_existing_config()
|
||
manual_sites = []
|
||
old_auto_keys = set()
|
||
if existing and isinstance(existing.get("sites"), list):
|
||
for site in existing["sites"]:
|
||
if not isinstance(site, dict):
|
||
continue
|
||
k = site.get("key", "")
|
||
if k in self._LOCKED_KEYS:
|
||
continue
|
||
if self._is_generated_key(k):
|
||
old_auto_keys.add(k)
|
||
else:
|
||
manual_sites.append(site)
|
||
|
||
new_auto_keys = {s.get("key") for s in new_auto_sites}
|
||
self.status["added"] = len(new_auto_keys - old_auto_keys)
|
||
self.status["removed"] = len(old_auto_keys - new_auto_keys)
|
||
self.status["unchanged"] = len(old_auto_keys & new_auto_keys)
|
||
self.status["updated"] = 0
|
||
self.status["manual_sites"] = len(manual_sites)
|
||
self.status["generated_sites"] = len(new_auto_sites)
|
||
|
||
config = {
|
||
"logo": self.LOGO_PATH,
|
||
"spider": self._build_spider_value(),
|
||
"sites": list(self._LOCKED_SITES) + manual_sites + new_auto_sites,
|
||
}
|
||
|
||
new_content = json.dumps(config, ensure_ascii=False, indent=2)
|
||
if existing:
|
||
old_content = json.dumps(existing, ensure_ascii=False, indent=2)
|
||
if new_content == old_content:
|
||
self.status["write_state"] = "配置未变化"
|
||
self.status["written"] = True
|
||
self.status["scan_time"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
||
self.status["app_reload_state"] = "配置未变化"
|
||
return
|
||
|
||
self._create_backup()
|
||
save_dir = os.path.dirname(self.SAVE_PATH)
|
||
if save_dir and not os.path.exists(save_dir):
|
||
try:
|
||
os.makedirs(save_dir, exist_ok=True)
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
tmp = self.SAVE_PATH + ".tmp"
|
||
with open(tmp, "w", encoding="utf-8") as fp:
|
||
fp.write(new_content)
|
||
fp.flush()
|
||
os.fsync(fp.fileno())
|
||
os.replace(tmp, self.SAVE_PATH)
|
||
self.status["write_state"] = "已写入配置"
|
||
self.status["written"] = True
|
||
self.status["scan_time"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
||
except Exception as e:
|
||
self.status["write_state"] = "写入失败: {}".format(e)
|
||
self.status["written"] = False
|
||
self.status["app_reload_state"] = "写入失败"
|
||
return
|
||
|
||
ok, detail = self._reload_app_vod_config()
|
||
self.status["app_reload_state"] = detail
|
||
|
||
def _clear_auto_sites(self):
|
||
"""从配置中移除自动站点,保留手工注入项和锁定项"""
|
||
existing = self._load_existing_config()
|
||
if not existing or not isinstance(existing.get("sites"), list):
|
||
return 0
|
||
old_count = len(existing["sites"])
|
||
kept = [s for s in existing["sites"]
|
||
if isinstance(s, dict) and not self._is_generated_key(s.get("key", ""))]
|
||
existing["sites"] = kept
|
||
removed = old_count - len(kept)
|
||
|
||
try:
|
||
self._create_backup()
|
||
content = json.dumps(existing, ensure_ascii=False, indent=2)
|
||
tmp = self.SAVE_PATH + ".tmp"
|
||
with open(tmp, "w", encoding="utf-8") as fp:
|
||
fp.write(content)
|
||
fp.flush()
|
||
os.fsync(fp.fileno())
|
||
os.replace(tmp, self.SAVE_PATH)
|
||
except Exception:
|
||
pass
|
||
return removed
|
||
|
||
# ==========================================================================
|
||
# 🔧 辅助方法
|
||
# ==========================================================================
|
||
def _get_file_info(self, tid):
|
||
return self.cache["file_index"].get(tid)
|
||
|
||
def _count_str(self):
|
||
c = self.cache["type_counts"]
|
||
return (
|
||
f"共扫描到 {c.get('PY', 0)} 个PY文件, {c.get('JS', 0)} 个JS文件, "
|
||
f"{c.get('HTML', 0)} 个HTML文件, {c.get('XBPQ', 0)} 个XBPQ文件"
|
||
)
|
||
|
||
def _count_jar_str(self):
|
||
"""统计 jar 文件数量"""
|
||
if not os.path.isdir(self.JAR_DIR):
|
||
return "jar 目录不存在"
|
||
count = sum(
|
||
1 for f in os.listdir(self.JAR_DIR)
|
||
if f.lower().endswith(".jar") and os.path.isfile(os.path.join(self.JAR_DIR, f))
|
||
)
|
||
return f"共扫描到 {count} 个JAR文件"
|
||
|
||
def _page_number(self, value):
|
||
try:
|
||
return max(1, int(value))
|
||
except Exception:
|
||
return 1
|
||
|
||
def _paged_result(self, items, page, make_vod):
|
||
"""通用分页结果生成,make_vod 为 item→vod 字典的转换函数"""
|
||
total = len(items)
|
||
page_size = max(1, self.PAGE_SIZE)
|
||
page_count = max(1, (total + page_size - 1) // page_size)
|
||
page = max(1, min(page, page_count))
|
||
start = (page - 1) * page_size
|
||
page_items = items[start: start + page_size]
|
||
return {
|
||
"page": page,
|
||
"pagecount": page_count,
|
||
"limit": page_size,
|
||
"total": total,
|
||
"list": [make_vod(item) for item in page_items],
|
||
}
|
||
|
||
def _source_to_vod(self, source):
|
||
"""将扫描源对象转换为 TVBox vod 条目"""
|
||
ignored = source.get("ignored", False)
|
||
return {
|
||
"vod_id": source["type_id"],
|
||
"vod_name": ("⛔ " if ignored else "") + source["type_name"],
|
||
"vod_pic": "",
|
||
"vod_remarks": (
|
||
f"{source['_type_tag']} · "
|
||
+ ("已忽略·点击恢复" if ignored else "点击忽略")
|
||
),
|
||
"action": self.ACTION_TOGGLE_IGNORE_PFX + source["type_id"],
|
||
}
|
||
|
||
def _atomic_write_json_file(self, path, data):
|
||
"""原子写入普通 JSON 文件"""
|
||
directory = os.path.dirname(path)
|
||
if directory and not os.path.isdir(directory):
|
||
os.makedirs(directory, exist_ok=True)
|
||
tmp = path + ".tmp"
|
||
try:
|
||
with open(tmp, "w", encoding="utf-8") as fp:
|
||
json.dump(data, fp, ensure_ascii=False, indent=2)
|
||
fp.flush()
|
||
os.fsync(fp.fileno())
|
||
os.replace(tmp, path)
|
||
except Exception:
|
||
try:
|
||
if os.path.exists(tmp):
|
||
os.remove(tmp)
|
||
except Exception:
|
||
pass
|
||
|
||
# ==========================================================================
|
||
# 📺 【TVBox 标准接口】
|
||
# ==========================================================================
|
||
def homeContent(self, filter):
|
||
classes = [
|
||
{"type_id": "all", "type_name": f"全部 ({len(self.cache['sources'])})"}
|
||
]
|
||
for tag in ("PY", "JS", "XBPQ", "HTML"):
|
||
count = self.cache["type_counts"].get(tag, 0)
|
||
if count:
|
||
classes.append({"type_id": "type:" + tag, "type_name": f"{tag} ({count})"})
|
||
if self.cache["ignored"]:
|
||
classes.append({"type_id": "ignored", "type_name": f"忽略 ({len(self.cache['ignored'])})"})
|
||
classes.append({"type_id": "scan_settings", "type_name": "扫描配置"})
|
||
if self._has_backup():
|
||
classes.append({"type_id": "backups", "type_name": "历史备份"})
|
||
return {"class": classes, "list": self._home_items()}
|
||
|
||
def homeVod(self):
|
||
info = self._count_str() + " | " + self._count_jar_str()
|
||
items = self._home_items()
|
||
items.insert(0, {
|
||
"vod_id": "__debug__",
|
||
"vod_name": info,
|
||
"vod_pic": "",
|
||
"vod_remarks": "统计",
|
||
})
|
||
return {"list": items}
|
||
|
||
def _home_items(self):
|
||
"""构建首页操作项"""
|
||
if not self.scan_enabled:
|
||
status_name = "自动扫描已关闭"
|
||
elif self.status["written"]:
|
||
status_name = "站点已合并"
|
||
else:
|
||
status_name = "站点未合并"
|
||
|
||
remarks = f"{self.status['included']}个源·{self.status['write_state']}"
|
||
app_state = self.status.get("app_reload_state", "")
|
||
if app_state and app_state != "-":
|
||
remarks += f"·App:{app_state}"
|
||
|
||
items = [
|
||
{
|
||
"vod_id": self.STATUS_ID,
|
||
"vod_name": status_name,
|
||
"vod_pic": "",
|
||
"vod_remarks": remarks,
|
||
},
|
||
{
|
||
"vod_id": self.TOGGLE_SCAN_ID,
|
||
"vod_name": "扫描开关:{}".format("开" if self.scan_enabled else "关"),
|
||
"vod_pic": "",
|
||
"vod_remarks": "点击切换",
|
||
"action": self.ACTION_TOGGLE_SCAN,
|
||
},
|
||
{
|
||
"vod_id": self.RESCAN_ID,
|
||
"vod_name": "一键扫描加载",
|
||
"vod_pic": "",
|
||
"vod_remarks": "扫描·写入·重载App",
|
||
"action": self.ACTION_RESCAN,
|
||
},
|
||
{
|
||
"vod_id": self.CLEAR_SITES_ID,
|
||
"vod_name": "清除自动站点",
|
||
"vod_pic": "",
|
||
"vod_remarks": "保留手工项",
|
||
"action": self.ACTION_CLEAR_SITES,
|
||
},
|
||
]
|
||
if self._has_backup():
|
||
items.append({
|
||
"vod_id": self.RESTORE_BACKUP_ID,
|
||
"vod_name": "撤销上次变更",
|
||
"vod_pic": "",
|
||
"vod_remarks": "恢复备份",
|
||
"action": self.ACTION_RESTORE_BACKUP,
|
||
})
|
||
items.append({
|
||
"vod_id": self.DELETE_BACKUPS_ID,
|
||
"vod_name": "删除历史备份",
|
||
"vod_pic": "",
|
||
"vod_remarks": "删除备份",
|
||
"action": self.ACTION_DELETE_BACKUPS,
|
||
})
|
||
return items
|
||
|
||
def categoryContent(self, tid, pg, filter, ext):
|
||
page = self._page_number(pg)
|
||
|
||
# ---- 全部源(分页)----
|
||
if tid == "all":
|
||
return self._paged_result(self.cache["sources"], page, self._source_to_vod)
|
||
|
||
# ---- 按类型筛选(分页)----
|
||
if str(tid).startswith("type:"):
|
||
source_type = str(tid).split(":", 1)[1].upper()
|
||
items = [s for s in self.cache["sources"] if s["_type_tag"] == source_type]
|
||
return self._paged_result(items, page, self._source_to_vod)
|
||
|
||
# ---- 忽略源(分页)----
|
||
if tid == "ignored":
|
||
return self._paged_result(self.cache["ignored"], page, self._source_to_vod)
|
||
|
||
# ---- 扫描配置 ----
|
||
if tid == "scan_settings":
|
||
items = self._scan_setting_items()
|
||
return self._paged_result(items, page, lambda s: s)
|
||
|
||
# ---- 历史备份 ----
|
||
if tid == "backups":
|
||
items = self._backup_items()
|
||
return self._paged_result(items, page, lambda s: s)
|
||
|
||
# ---- 原始逐文件行为(向后兼容)----
|
||
return self._category_content_single(tid)
|
||
|
||
def _category_content_single(self, tid):
|
||
"""原始的逐文件分类内容(单个条目)"""
|
||
file_info = self._get_file_info(tid)
|
||
if not file_info:
|
||
return {"list": []}
|
||
f_path = file_info["path"]
|
||
if not os.path.exists(f_path):
|
||
return {"list": []}
|
||
|
||
f_base = os.path.basename(f_path)
|
||
if "." in f_base:
|
||
f_base = f_base.rsplit(".", 1)[0]
|
||
ext_name = file_info["ext"]
|
||
type_tag = file_info.get("type_tag", "PY")
|
||
sub_sfx = file_info.get("sub_sfx", "")
|
||
|
||
v_id = base64.b64encode(
|
||
(type_tag + "|" + f_path).encode("utf-8")
|
||
).decode("utf-8")
|
||
|
||
if type_tag == "HTML":
|
||
vod_name = f"{f_base}[网页]{sub_sfx}"
|
||
vod_remarks = "[网页]"
|
||
elif type_tag == "XBPQ":
|
||
vod_name = f"⁽ˣᵇᵖ⁾{f_base}{sub_sfx}"
|
||
vod_remarks = "[XBPQ]"
|
||
else:
|
||
vod_name = f"{f_base}{sub_sfx}"
|
||
vod_remarks = "[" + ext_name.upper() + "]"
|
||
|
||
return {
|
||
"page": 1, "pagecount": 1, "limit": 1, "total": 1,
|
||
"list": [{
|
||
"vod_id": v_id,
|
||
"vod_name": vod_name,
|
||
"vod_pic": "",
|
||
"vod_remarks": vod_remarks,
|
||
}]
|
||
}
|
||
|
||
def _scan_setting_items(self):
|
||
"""扫描配置条目列表(分类开关 + App 重载开关)"""
|
||
items = []
|
||
type_labels = {"PY": "PY源", "JS": "JS源", "XBPQ": "XBPQ源", "HTML": "HTML源"}
|
||
for tag in ("PY", "JS", "XBPQ", "HTML"):
|
||
enabled = self.type_enabled.get(tag, True)
|
||
items.append({
|
||
"vod_id": f"setting_type_{tag.lower()}",
|
||
"vod_name": f"{'✅' if enabled else '⬜'} {type_labels[tag]}",
|
||
"vod_pic": "",
|
||
"vod_remarks": "开" if enabled else "关",
|
||
"action": self.ACTION_TOGGLE_TYPE_PFX + tag,
|
||
})
|
||
# App 自动重载开关
|
||
reload_on = self.auto_reload_app
|
||
port_info = f"{self.APP_PORT_START}-{self.APP_PORT_END}"
|
||
last_port = str(self.last_app_port) if self.last_app_port else ""
|
||
remarks = ("开" if reload_on else "关") + f"·{port_info}"
|
||
if last_port:
|
||
remarks += f"·上次{last_port}"
|
||
items.append({
|
||
"vod_id": "setting_app_reload",
|
||
"vod_name": f"{'✅' if reload_on else '⬜'} 重载App",
|
||
"vod_pic": "",
|
||
"vod_remarks": remarks,
|
||
"action": self.ACTION_TOGGLE_APP_RELOAD,
|
||
})
|
||
return items
|
||
|
||
def _backup_items(self):
|
||
"""历史备份条目列表"""
|
||
items = []
|
||
bp = self._backup_path()
|
||
if os.path.isfile(bp):
|
||
try:
|
||
modified = time.strftime(
|
||
"%Y-%m-%d %H:%M:%S",
|
||
time.localtime(os.path.getmtime(bp))
|
||
)
|
||
items.append({
|
||
"vod_id": "backup_latest",
|
||
"vod_name": f"恢复 {modified}",
|
||
"vod_pic": "",
|
||
"vod_remarks": "点击恢复此备份",
|
||
"action": self.ACTION_RESTORE_BACKUP,
|
||
})
|
||
except Exception:
|
||
pass
|
||
items.append({
|
||
"vod_id": "delete_backups",
|
||
"vod_name": "删除历史备份",
|
||
"vod_pic": "",
|
||
"vod_remarks": "删除所有备份",
|
||
"action": self.ACTION_DELETE_BACKUPS,
|
||
})
|
||
return items
|
||
|
||
def detailContent(self, array):
|
||
try:
|
||
v_id_raw = str(array[0]) if isinstance(array, (list, tuple)) and array else str(array or "")
|
||
|
||
# ---- 调试 / 状态信息 ----
|
||
if v_id_raw in ("__debug__", self.STATUS_ID):
|
||
return {"list": [self._status_detail()]}
|
||
|
||
# ---- 源文件详情(原始行为)----
|
||
v_id_padded = v_id_raw + "=" * ((4 - len(v_id_raw) % 4) % 4)
|
||
raw = base64.b64decode(v_id_padded).decode("utf-8", errors="ignore")
|
||
|
||
if "|" in raw:
|
||
type_tag, f_path = raw.split("|", 1)
|
||
else:
|
||
type_tag, f_path = "PY", raw
|
||
|
||
if not os.path.exists(f_path):
|
||
return {"list": [{"vod_name": "文件不存在", "vod_content": "路径: " + f_path}]}
|
||
|
||
f_base = os.path.basename(f_path)
|
||
if "." in f_base:
|
||
f_base = f_base.rsplit(".", 1)[0]
|
||
ext_name = f_path.rsplit(".", 1)[-1] if "." in f_path else "unknown"
|
||
|
||
file_info = self.cache["file_index"].get(v_id_raw)
|
||
api_path = self._build_api(file_info) if file_info else f_path
|
||
sub_sfx = file_info.get("sub_sfx", "") if file_info else ""
|
||
|
||
if type_tag == "HTML":
|
||
site_info = {
|
||
"key": f_base, "name": f"{f_base}[网页]{sub_sfx}", "type": 3,
|
||
"api": self.HTML_API, "homePage": api_path,
|
||
}
|
||
display_name = f"{f_base}[网页]{sub_sfx}"
|
||
elif type_tag == "XBPQ":
|
||
site_info = {
|
||
"key": f_base, "name": f"⁽ˣᵇᵖ⁾{f_base}{sub_sfx}", "type": 3,
|
||
"api": self.XBPQ_API, "ext": api_path,
|
||
}
|
||
display_name = f"⁽ˣᵇᵖ⁾{f_base}{sub_sfx}"
|
||
else:
|
||
site_info = {
|
||
"key": f_base + "_" + ext_name, "name": f"{f_base}{sub_sfx}", "type": 3,
|
||
"searchable": 1, "quickSearch": 1, "filterable": 1, "api": api_path,
|
||
}
|
||
display_name = "[" + ext_name.upper() + "] " + f_base + sub_sfx
|
||
|
||
info_text = json.dumps(site_info, ensure_ascii=False, indent=2)
|
||
|
||
return {"list": [{
|
||
"vod_name": display_name,
|
||
"vod_pic": "",
|
||
"vod_play_from": "配置信息",
|
||
"vod_play_url": "查看配置$" + f_path,
|
||
"vod_content": (
|
||
"配置文件: " + self.SAVE_PATH + "\n\n"
|
||
"站点类型: " + type_tag + " | 后缀: ." + ext_name + "\n\n"
|
||
"站点配置:\n" + info_text + "\n\n"
|
||
"文件路径: " + f_path
|
||
),
|
||
}]}
|
||
except Exception as e:
|
||
return {"list": [{"vod_name": "解析错误", "vod_content": str(e)}]}
|
||
|
||
def _status_detail(self):
|
||
"""生成详细扫描状态信息"""
|
||
c = self.cache["type_counts"]
|
||
ic = self.cache["ignored_counts"]
|
||
types_info = " ".join(
|
||
f"{t}:{c.get(t, 0)}/{ic.get(t, 0)}"
|
||
for t in ("PY", "JS", "XBPQ", "HTML")
|
||
)
|
||
|
||
content = (
|
||
"自动扫描: {scan_enabled}\n"
|
||
"扫描时间: {scan_time}\n"
|
||
"分类开关: {types}\n\n"
|
||
"有效源: {included}\n"
|
||
"忽略源: {ignored}\n\n"
|
||
"保留手工站点: {manual}\n"
|
||
"自动注入站点: {generated}\n"
|
||
"变更预览: +{added} -{removed} ={unchanged}\n"
|
||
"写入状态: {state}\n\n"
|
||
"App重载: {app_reload}\n"
|
||
"App自动重载: {auto_reload_enabled}\n"
|
||
"端口范围: {port_start}-{port_end}\n"
|
||
"上次连接: {last_port}\n\n"
|
||
"{py_info}\n"
|
||
"{jar_info}\n\n"
|
||
"配置文件: {save}\n"
|
||
"设置文件: {settings}\n"
|
||
"备份目录: {backup}\n\n"
|
||
"已扫描文件列表:\n"
|
||
"{file_list}"
|
||
).format(
|
||
scan_enabled="开启" if self.scan_enabled else "关闭",
|
||
scan_time=self.status["scan_time"],
|
||
types=types_info,
|
||
included=self.status["included"],
|
||
ignored=self.status["ignored"],
|
||
manual=self.status["manual_sites"],
|
||
generated=self.status["generated_sites"],
|
||
added=self.status["added"],
|
||
removed=self.status["removed"],
|
||
unchanged=self.status["unchanged"],
|
||
state=self.status["write_state"],
|
||
app_reload=self.status.get("app_reload_state", "-"),
|
||
auto_reload_enabled="开启" if self.auto_reload_app else "关闭",
|
||
port_start=self.APP_PORT_START,
|
||
port_end=self.APP_PORT_END,
|
||
last_port=self.last_app_port or "无",
|
||
py_info=self._count_str(),
|
||
jar_info=self._count_jar_str(),
|
||
save=self.SAVE_PATH,
|
||
settings=self.SETTINGS_PATH,
|
||
backup=self.BACKUP_DIR,
|
||
file_list="\n".join(
|
||
f" [{fin.get('type_tag', fin['ext'].upper())}] {fin['path']}"
|
||
for fin in self.cache["file_index"].values()
|
||
) or " 无",
|
||
)
|
||
return {
|
||
"vod_id": self.STATUS_ID,
|
||
"vod_name": "扫描状态详情",
|
||
"vod_pic": "",
|
||
"vod_remarks": self.status["write_state"],
|
||
"vod_content": content,
|
||
}
|
||
|
||
def searchContent(self, key, quick, pg="1"):
|
||
"""搜索已扫描的源(支持分页)"""
|
||
page = self._page_number(pg)
|
||
keyword = str(key or "").strip().lower()
|
||
if not keyword:
|
||
return {"list": []}
|
||
|
||
items = [
|
||
s for s in self.cache["sources"]
|
||
if keyword in s["type_name"].lower()
|
||
or keyword in s["_type_tag"].lower()
|
||
or keyword in os.path.basename(s["_path"]).lower()
|
||
]
|
||
return self._paged_result(items, page, self._source_to_vod)
|
||
|
||
def playerContent(self, flag, id, vipFlags):
|
||
url = id.split("$")[-1] if "$" in id else id
|
||
return {"url": url, "header": {}, "parse": 0}
|
||
|
||
# ==========================================================================
|
||
# 🎮 【交互操作】action 方法
|
||
# ==========================================================================
|
||
def action(self, action):
|
||
action = str(action)
|
||
|
||
# ---- 忽略 / 恢复单个源 ----
|
||
if action.startswith(self.ACTION_TOGGLE_IGNORE_PFX):
|
||
tid = action[len(self.ACTION_TOGGLE_IGNORE_PFX):]
|
||
source = self.cache["source_index"].get(tid)
|
||
if not source:
|
||
return {"code": 0, "msg": "源不存在,请重新扫描"}
|
||
with self.lock:
|
||
identity = source["identity"]
|
||
now_ignored = identity not in self.ignored_sources
|
||
if now_ignored:
|
||
self.ignored_sources.add(identity)
|
||
else:
|
||
self.ignored_sources.discard(identity)
|
||
try:
|
||
self._save_settings()
|
||
if self.scan_enabled:
|
||
self._scan_all()
|
||
self._save_config_json()
|
||
return {
|
||
"code": 0,
|
||
"msg": f"已忽略:{source['type_name']}" if now_ignored
|
||
else f"已恢复:{source['type_name']}",
|
||
}
|
||
except Exception as exc:
|
||
if now_ignored:
|
||
self.ignored_sources.discard(identity)
|
||
else:
|
||
self.ignored_sources.add(identity)
|
||
return {"code": 0, "msg": "操作失败:{}".format(exc)}
|
||
|
||
# ---- 切换分类扫描开关 ----
|
||
if action.startswith(self.ACTION_TOGGLE_TYPE_PFX):
|
||
tag = action[len(self.ACTION_TOGGLE_TYPE_PFX):].upper()
|
||
if tag not in self.type_enabled:
|
||
return {"code": 0, "msg": "未知类型:{}".format(tag)}
|
||
with self.lock:
|
||
self.type_enabled[tag] = not self.type_enabled[tag]
|
||
try:
|
||
self._save_settings()
|
||
return {
|
||
"code": 0,
|
||
"msg": "{}扫描已{},重扫后生效".format(
|
||
tag, "开" if self.type_enabled[tag] else "关"
|
||
),
|
||
"list": self._scan_setting_items(),
|
||
}
|
||
except Exception as exc:
|
||
self.type_enabled[tag] = not self.type_enabled[tag]
|
||
return {"code": 0, "msg": "保存失败:{}".format(exc)}
|
||
|
||
# ---- 切换 App 自动重载开关 ----
|
||
if action == self.ACTION_TOGGLE_APP_RELOAD:
|
||
with self.lock:
|
||
self.auto_reload_app = not self.auto_reload_app
|
||
try:
|
||
self._save_settings()
|
||
return {
|
||
"code": 0,
|
||
"msg": "App自动重载已{}".format(
|
||
"开启" if self.auto_reload_app else "关闭"
|
||
),
|
||
"list": self._scan_setting_items(),
|
||
}
|
||
except Exception as exc:
|
||
self.auto_reload_app = not self.auto_reload_app
|
||
return {"code": 0, "msg": "保存失败:{}".format(exc)}
|
||
|
||
# ---- 切换总扫描开关 ----
|
||
if action == self.ACTION_TOGGLE_SCAN:
|
||
with self.lock:
|
||
prev = self.scan_enabled
|
||
self.scan_enabled = not self.scan_enabled
|
||
try:
|
||
self._save_settings()
|
||
if self.scan_enabled:
|
||
self._scan_all()
|
||
self._save_config_json()
|
||
msg = "扫描已开:{}个源,{}".format(
|
||
self.status["included"], self.status["write_state"]
|
||
)
|
||
else:
|
||
self.cache["categories"] = []
|
||
self.cache["sources"] = []
|
||
self.status["write_state"] = "自动扫描已关闭"
|
||
msg = "扫描已关,现有配置已保留"
|
||
return {"code": 0, "msg": msg}
|
||
except Exception as exc:
|
||
self.scan_enabled = prev
|
||
return {"code": 0, "msg": "操作失败:{}".format(exc)}
|
||
|
||
# ---- 一键扫描并加载(含 App 重载)----
|
||
if action == self.ACTION_RESCAN:
|
||
with self.lock:
|
||
if self.scan_enabled:
|
||
self._scan_all()
|
||
self._save_config_json()
|
||
app_state = self.status.get("app_reload_state", "")
|
||
msg = "已重扫:{}个源,{}".format(
|
||
self.status["included"], self.status["write_state"]
|
||
)
|
||
if app_state and app_state != "-":
|
||
msg += ",App:{}".format(app_state)
|
||
else:
|
||
msg = "扫描已关,请先开启"
|
||
return {"code": 0, "msg": msg}
|
||
|
||
# ---- 一键清除自动站点(含 App 重载)----
|
||
if action == self.ACTION_CLEAR_SITES:
|
||
with self.lock:
|
||
removed = self._clear_auto_sites()
|
||
self.scan_enabled = False
|
||
self._save_settings()
|
||
self.cache = {
|
||
"categories": [], "file_index": {}, "sources": [], "ignored": [],
|
||
"source_index": {}, "type_counts": {}, "ignored_counts": {},
|
||
}
|
||
self.status["write_state"] = "已清除{}个自动站点".format(removed)
|
||
|
||
_, detail = self._reload_app_vod_config()
|
||
self.status["app_reload_state"] = detail
|
||
|
||
msg = "已清除{}个自动站点,手工项保留,扫描已关".format(removed)
|
||
if detail and "未连接" not in detail:
|
||
msg += ",App:{}".format(detail)
|
||
return {"code": 0, "msg": msg}
|
||
|
||
# ---- 撤销上次变更(恢复备份 + App 重载)----
|
||
if action == self.ACTION_RESTORE_BACKUP:
|
||
with self.lock:
|
||
ok, msg = self._restore_backup()
|
||
if ok and self.scan_enabled:
|
||
self._scan_all()
|
||
self._save_config_json()
|
||
app_state = self.status.get("app_reload_state", "")
|
||
if app_state and app_state != "-":
|
||
msg += ",App:{}".format(app_state)
|
||
elif ok:
|
||
_, detail = self._reload_app_vod_config()
|
||
if detail and "未连接" not in detail:
|
||
msg += ",App:{}".format(detail)
|
||
return {"code": 0, "msg": msg}
|
||
|
||
# ---- 删除历史备份 ----
|
||
if action == self.ACTION_DELETE_BACKUPS:
|
||
with self.lock:
|
||
if self._delete_backups():
|
||
return {"code": 0, "msg": "已删除历史备份"}
|
||
return {"code": 0, "msg": "暂无可删除的备份"}
|
||
|
||
return {"code": 0, "msg": "未知操作:{}".format(action)}
|
||
|
||
# ==========================================================================
|
||
def destroy(self):
|
||
return "destroy" |