Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-27 14:39:52 +00:00
parent 4e4e437247
commit f53091d694
46 changed files with 31149 additions and 23124 deletions
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import re
import requests
from urllib.parse import quote, urljoin
try:
from bs4 import BeautifulSoup
except Exception:
BeautifulSoup = None
try:
from base.spider import Spider as BaseSpider
except Exception:
BaseSpider = object
class Spider(BaseSpider):
BASE_URL = 'https://www.yasetube.com'
HEADERS = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36','Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8','Accept-Language':'zh-CN,zh;q=0.9,en;q=0.8','Referer':'https://www.yasetube.com/'}
CATS = {'nvce':'女厕偷拍','fc2-ppv':'FC2 PPV','me':'Mesubuta系列','milf':'MILF人妻无码','dalu':'自拍偷拍','madou':'品牌传媒'}
def __init__(self):
self.session = requests.Session()
self.session.headers.update(self.HEADERS)
def getName(self):
return '亚色影库'
def init(self, extend=''):
return None
def isVideoFormat(self, url):
return any(x in url.lower() for x in ['.m3u8','.mp4','.flv','.mkv','.avi','.ts'])
def manualVideoCheck(self):
return True
def destroy(self):
return None
def _get(self, url):
url = url if str(url).startswith('http') else urljoin(self.BASE_URL, url)
try:
r = self.session.get(url, timeout=12, verify=False, allow_redirects=True)
if not r.encoding or r.encoding.lower() == 'iso-8859-1':
r.encoding = 'utf-8'
return r.text
except requests.RequestException:
return ''
def _soup(self, html):
return BeautifulSoup(html, 'html.parser') if BeautifulSoup else None
def _txt(self, s):
return re.sub(r'\s+', ' ', s or '').strip()
def _abs(self, u):
if not u:
return ''
if u.startswith('//'):
return 'https:' + u
return urljoin(self.BASE_URL, u)
def _meta(self, html, name):
m = re.search(r'<meta[^>]+(?:property|name)=["\']%s["\'][^>]+content=["\']([^"\']+)' % re.escape(name), html, re.I)
return self._txt(m.group(1)) if m else ''
def _id(self, url):
url = self._abs(url).split('?')[0].rstrip('/')
return url.replace(self.BASE_URL + '/', '')
def _parse_list(self, html):
arr = []
if BeautifulSoup and html:
soup = self._soup(html)
for a in soup.select('article.loop-video.thumb-block a[href]'):
href = self._abs(a.get('href'))
if '/video/' not in href:
continue
img = a.select_one('img')
title = self._txt(a.get('title') or (img.get('alt') if img else '') or (a.select_one('header.entry-header span').get_text(' ', strip=True) if a.select_one('header.entry-header span') else ''))
pic = self._abs((img.get('data-src') or img.get('src') or img.get('data-original')) if img else '')
remark = self._txt(' '.join([x.get_text(' ', strip=True) for x in a.select('span.hd-video,span.views,span.duration')]))
if title and href:
arr.append({'vod_id':self._id(href),'vod_name':title,'vod_pic':pic,'vod_remarks':remark})
if not arr:
for it in re.findall(r'<article[^>]+loop-video[\s\S]*?</article>', html, re.I):
h = re.search(r'<a[^>]+href=["\']([^"\']+)["\'][^>]*title=["\']([^"\']+)', it, re.I)
p = re.search(r'<img[^>]+(?:data-src|src)=["\']([^"\']+)', it, re.I)
if h:
arr.append({'vod_id':self._id(h.group(1)),'vod_name':self._txt(h.group(2)),'vod_pic':self._abs(p.group(1)) if p else '','vod_remarks':'HD' if 'hd-video' in it else ''})
seen, out = set(), []
for v in arr:
if v['vod_id'] not in seen:
seen.add(v['vod_id'])
out.append(v)
return out
def _cats(self):
html = self._get('/categories')
classes = []
if BeautifulSoup and html:
soup = self._soup(html)
for a in soup.select('a[href*="/video/category/"]'):
href = a.get('href') or ''
slug = href.rstrip('/').split('/')[-1]
name = self._txt(a.get('title') or a.get_text(' ', strip=True))
if slug and name and slug not in [x['type_id'] for x in classes]:
classes.append({'type_id':slug,'type_name':name})
if not classes:
classes = [{'type_id':k,'type_name':v} for k,v in self.CATS.items()]
return classes[:30]
def homeContent(self, filter=False):
return {'class':self._cats(),'filters':{},'list':self.homeVideoContent()['list']}
def homeVideoContent(self):
return {'list':self._parse_list(self._get('/'))[:20]}
def categoryContent(self, tid, pg, filter, ext):
pg = str(pg or '1')
if tid in ['latest','home','']:
url = '/' if pg == '1' else '/page/%s/' % pg
else:
url = '/video/category/%s/' % tid if pg == '1' else '/video/category/%s/page/%s/' % (tid, pg)
return {'page':int(pg),'pagecount':999,'limit':20,'total':999,'list':self._parse_list(self._get(url))}
def detailContent(self, ids):
vid = ids[0] if isinstance(ids, list) else ids
url = vid if str(vid).startswith('http') else self.BASE_URL + '/' + str(vid).lstrip('/')
html = self._get(url)
name = self._meta(html, 'og:title')
pic = self._meta(html, 'og:image')
desc = self._meta(html, 'og:description')
if BeautifulSoup and html:
soup = self._soup(html)
h = soup.select_one('h1.entry-title')
if h:
name = self._txt(h.get_text(' ', strip=True)) or name
im = soup.select_one('meta[itemprop="thumbnailUrl"]')
if im and im.get('content'):
pic = im.get('content') or pic
ds = soup.select_one('meta[itemprop="description"]')
if ds and ds.get('content'):
desc = ds.get('content') or desc
play = self._find_play(html) or url
vod = {'vod_id':vid,'vod_name':name or str(vid),'vod_pic':self._abs(pic),'type_name':'','vod_year':'','vod_area':'','vod_actor':'','vod_director':'','vod_content':desc or name or '','vod_play_from':'嗅探','vod_play_url':'正片$%s' % play}
return {'list':[vod]}
def searchContent(self, key, quick, pg='1'):
pg = str(pg or '1')
kw = quote(key)
url = '/?s=%s' % kw if pg == '1' else '/page/%s/?s=%s' % (pg, kw)
return {'page':int(pg),'pagecount':999,'limit':20,'total':999,'list':self._parse_list(self._get(url))}
def playerContent(self, flag, id, vipFlags):
u = self._abs(id)
if self.isVideoFormat(u):
return {'parse':0,'playUrl':'','url':u,'header':self.HEADERS}
html = self._get(u)
play = self._find_play(html)
if play and self.isVideoFormat(play):
return {'parse':0,'playUrl':'','url':play,'header':self.HEADERS}
return {'parse':1,'playUrl':'','url':u,'header':self.HEADERS}
def _find_play(self, html):
if not html:
return ''
pats = [r'<source[^>]+src=["\']([^"\']+)',r'<video[^>]+src=["\']([^"\']+)',r'(https?:\\?/\\?/[^"\'<>]+?\.(?:m3u8|mp4)(?:\?[^"\'<>]*)?)',r'file["\']?\s*[:=]\s*["\']([^"\']+)',r'url["\']?\s*[:=]\s*["\']([^"\']+\.(?:m3u8|mp4)[^"\']*)']
for p in pats:
m = re.search(p, html, re.I)
if m:
return self._abs(m.group(1).replace('\\/','/'))
return ''
-557
View File
@@ -1,557 +0,0 @@
# -*- coding: utf-8 -*-
"""
TVBox 本地 Py/Js 爬虫聚合源(精简版)
========================================
仅保留扫描与配置生成功能。
包含:子文件夹后缀、增量合并、锁定站点、分页浏览。
"""
import os
import json
import base64
import hashlib
import time
from base.spider import Spider
class Spider(Spider):
# ==========================================================================
# 📂 【配置区】
# ==========================================================================
PY_DIR = "/storage/emulated/0/peekpro/py"
JS_DIR = "/storage/emulated/0/peekpro/js"
JAR_DIR = "/storage/emulated/0/peekpro/jar"
SAVE_PATH = "/storage/emulated/0/peekpro/智能接口.json"
LOGO_PATH = "/storage/emulated/0/peekpro/jar/头像.gif"
_LOCKED_SITES = [
]
_LOCKED_KEYS = {""}
GENERATED_KEY_PREFIX = "local_auto_"
PAGE_SIZE = 60
# ==========================================================================
def __init__(self):
super().__init__()
self.inited = False
self.cache = {
"categories": [],
"file_index": {},
"sources": [],
"source_index": {},
"type_counts": {},
}
self.status = {
"scan_time": "-",
"included": 0,
"manual_sites": 0,
"generated_sites": 0,
"added": 0,
"removed": 0,
"unchanged": 0,
"write_state": "尚未扫描",
"written": False,
}
def getName(self):
return "本地Py/Js聚合源(精简版)"
def init(self, extend):
if self.inited:
return
self._scan_all()
self._save_config_json()
self.inited = True
# ==========================================================================
# 🔍 【扫描核心】手动递归
# ==========================================================================
def _scan_dir(self, base_dir, ext_list):
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):
results.extend(self._scan_dir(full_path, ext_list))
elif os.path.isfile(full_path):
lower_name = entry.lower()
for ext in ext_list:
if lower_name.endswith(ext):
name_no_ext = entry[: -len(ext)]
results.append((full_path, name_no_ext, ext))
break
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):
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),
]
for dir_path, ext_list, type_tag, order in scan_specs:
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)
display_name = f"{type_tag}{name}{sub_sfx}"
source = {
"type_id": tid,
"type_name": display_name,
"identity": identity,
"_path": full_path,
"_ext": ext.lstrip("."),
"_dir": dir_path,
"_type_tag": type_tag,
"_sk": (order, name),
"_sub_sfx": sub_sfx,
}
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"])
self.cache["sources"] = sources
self.cache["source_index"] = {}
self.cache["type_counts"] = {}
for s in sources:
self.cache["source_index"][s["type_id"]] = s
tag = s["_type_tag"]
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)
# ==========================================================================
# 【增量合并配置生成】
# ==========================================================================
def _build_api(self, file_info):
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_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):
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
if not os.path.isfile(f_path):
continue
key = self.GENERATED_KEY_PREFIX + type_tag.lower() + "_" + hashlib.sha256(
(type_tag + "|" + f_path).encode("utf-8")
).hexdigest()[:14]
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):
self.status["scan_time"] = time.strftime("%Y-%m-%d %H:%M:%S")
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["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
return
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
except Exception as e:
self.status["write_state"] = "写入失败: {}".format(e)
self.status["written"] = False
# ==========================================================================
# 🔧 辅助方法
# ==========================================================================
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文件"
def _count_jar_str(self):
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):
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):
return {
"vod_id": source["type_id"],
"vod_name": source["type_name"],
"vod_pic": "",
"vod_remarks": source["_type_tag"],
}
# ==========================================================================
# 📺 【TVBox 标准接口】
# ==========================================================================
def homeContent(self, filter):
classes = [
{"type_id": "all", "type_name": f"全部 ({len(self.cache['sources'])})"}
]
for tag in ("PY", "JS"):
count = self.cache["type_counts"].get(tag, 0)
if count:
classes.append({"type_id": "type:" + tag, "type_name": f"{tag} ({count})"})
return {"class": classes, "list": []}
def homeVod(self):
info = self._count_str() + " | " + self._count_jar_str()
return {"list": [{
"vod_id": "__debug__",
"vod_name": info,
"vod_pic": "",
"vod_remarks": "统计",
}]}
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)
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")
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 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 == "__debug__":
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 ""
site_info = {
"key": f_base + "_" + ext_name,
"name": f"{f_base}{sub_sfx}",
"type": 3,
"searchable": 1,
"quickSearch": 1,
"filterable": 1,
"api": api_path,
}
info_text = json.dumps(site_info, ensure_ascii=False, indent=2)
return {"list": [{
"vod_name": "[" + ext_name.upper() + "] " + f_base + sub_sfx,
"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"]
content = (
"扫描时间: {scan_time}\n"
"有效源: {included}\n"
"分类统计: PY={py} JS={js}\n\n"
"保留手工站点: {manual}\n"
"自动注入站点: {generated}\n"
"变更预览: +{added} -{removed} ={unchanged}\n"
"写入状态: {state}\n\n"
"{py_info}\n"
"{jar_info}\n\n"
"配置文件: {save}\n\n"
"已扫描文件列表:\n"
"{file_list}"
).format(
scan_time=self.status["scan_time"],
included=self.status["included"],
py=c.get("PY", 0), js=c.get("JS", 0),
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"],
py_info=self._count_str(),
jar_info=self._count_jar_str(),
save=self.SAVE_PATH,
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": "__debug__",
"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}
def destroy(self):
return "destroy"
+26 -22
View File
@@ -14,33 +14,37 @@ class Spider(Spider):
# ==========================================================================
# 📂 【配置区】
# ==========================================================================
PY_DIR = "/storage/emulated/0/TV/小百合/py"
JS_DIR = "/storage/emulated/0/TV/小百合/js"
JAR_DIR = "/storage/emulated/0/TV/小百合/jar"
SAVE_PATH = "/storage/emulated/0/TV/小百合/自动接口.json"
LOGO_PATH = "/storage/emulated/0/TV/小百合/jar/头像.gif"
PY_DIR = "/storage/emulated/0/TV/海豚/py"
JS_DIR = "/storage/emulated/0/TV/海豚/js"
JAR_DIR = "/storage/emulated/0/TV/海豚/jar"
SAVE_PATH = "/storage/emulated/0/TV/海豚/自动接口.json"
LOGO_PATH = "/https://img.freepik.com/free-vector/cute-dolphin-swimming-cartoon-vector-icon-illustration-animal-nature-icon-isolated-flat-vector_138676-12582.jpg?semt=ais_hybrid&w=740&q=80"
# 🔒 锁定在 sites 第 0、1 位的配置,无论扫描结果如何始终存在
_LOCKED_SITES = [
{
"key": "FishConfig",
"name": "🍼┆设置┆中心[工具]",
"key": "自动加载",
"name": "自动加载",
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/自动加载.py"
},
{
"name": "弹幕",
"key": "弹幕豆瓣",
"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"
}
"api": "csp_SecureDanmu",
"searchable": 1,
"jar": "https://ghfast.top/https://raw.githubusercontent.com/goodcommunication/mydm/main/danmu-spider-native.jar",
"ext": {
"apiUrls": [
"https://danmu.iyo.us.ci/theft-dastardly-prognosis-hula-agenda2-dropkick|公益源",
"https://logo.saodu.work:8888/87654321|公益源1",
"https://dm.ljiaovm.com/luosen|公益源2"
],
"titleMappingsUrl": "https://ghfast.top/https://raw.githubusercontent.com/goodcommunication/mydm/main/yins.json",
"filter": "./lib/douban.json"
}
}
]
_LOCKED_KEYS = {"FishConfig", "Local"}
# ==========================================================================