更新源 202607010650

This commit is contained in:
github-actions[bot]
2026-07-01 06:50:39 +00:00
parent 1fd39cbfae
commit 8d78992c6c
7 changed files with 1010 additions and 489 deletions
+2 -3
View File
@@ -1,5 +1,5 @@
{ {
"spider": "./jar/spider.jar;md5;5a5c64f87f0b0066919fa9b499912e4e", "spider": "./jar/spider.jar;md5;feb3dd35be7c31aa2a39bea423961279",
"lives": [ "lives": [
{"name": "live","boot": false,"type": 0,"url": "./tv.txt","playerType": 2,"ua": "okhttp/3.8.1","timeout": 20,"epg": "https://epg.cdn.loc.cc/?ch={name}&date={date}","logo": "https://logo.wyfc.qzz.io/{name}.png"} {"name": "live","boot": false,"type": 0,"url": "./tv.txt","playerType": 2,"ua": "okhttp/3.8.1","timeout": 20,"epg": "https://epg.cdn.loc.cc/?ch={name}&date={date}","logo": "https://logo.wyfc.qzz.io/{name}.png"}
], ],
@@ -97,9 +97,8 @@
{"key": "百度网盘","name": "百度 • 网盘","type": 3,"api": "csp_PanBaidu","searchable": 0,"filterable": 0,"changeable": 0,"style": {"type": "list","ratio": 1.433}}, {"key": "百度网盘","name": "百度 • 网盘","type": 3,"api": "csp_PanBaidu","searchable": 0,"filterable": 0,"changeable": 0,"style": {"type": "list","ratio": 1.433}},
{"key": "盘搜","name": "盘搜 • 搜索","type": 3,"api": "csp_PanSou","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "盘搜","name": "盘搜 • 搜索","type": 3,"api": "csp_PanSou","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "古哥","name": "古哥 • 搜索","type": 3,"api": "csp_GugeSo","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "古哥","name": "古哥 • 搜索","type": 3,"api": "csp_GugeSo","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "兄弟","name": "兄弟 • 搜索","type": 3,"api": "csp_XiongdiPan","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "百酷","name": "百酷 • 搜索","type": 3,"api": "csp_Baiku","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "百酷","name": "百酷 • 搜索","type": 3,"api": "csp_Baiku","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "米搜","name": "米搜 • 搜索","type": 3,"api": "csp_MiSou","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "米搜","name": "米搜 • 搜索","type": 3,"api": "csp_MiSou","searchable": 1,"quickSearch": 0,"filterable": 0,"ext": {"site": ["https://misoso.shop","http://www.misoso.shop"]},"timeout": 60},
{"key": "鬼鬼","name": "鬼鬼 • 搜索","type": 3,"api": "csp_GuiGui","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "鬼鬼","name": "鬼鬼 • 搜索","type": 3,"api": "csp_GuiGui","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "混合","name": "混合 • 搜索","type": 3,"api": "csp_HunHePan","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "混合","name": "混合 • 搜索","type": 3,"api": "csp_HunHePan","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "天翼","name": "天翼 • 搜索","type": 3,"api": "csp_TianYiSou","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "天翼","name": "天翼 • 搜索","type": 3,"api": "csp_TianYiSou","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
BIN
View File
Binary file not shown.
+2 -3
View File
@@ -1,5 +1,5 @@
{ {
"spider": "./jar/spider.jar;md5;5a5c64f87f0b0066919fa9b499912e4e", "spider": "./jar/spider.jar;md5;feb3dd35be7c31aa2a39bea423961279",
"lives": [ "lives": [
{"name": "migu","type": 0,"url": "https://develop202.github.io/migu_video/interface.txt","playerType": 1,"ua": "okhttp/3.8.1","timeout": 20,"epg": "https://epg.cdn.loc.cc/?ch={name}&date={date}","logo": "https://logo.wyfc.qzz.io/{name}.png"}, {"name": "migu","type": 0,"url": "https://develop202.github.io/migu_video/interface.txt","playerType": 1,"ua": "okhttp/3.8.1","timeout": 20,"epg": "https://epg.cdn.loc.cc/?ch={name}&date={date}","logo": "https://logo.wyfc.qzz.io/{name}.png"},
{"name": "live","type": 0,"url": "https://epg.pw/test_channels.m3u","playerType": 1,"ua": "okhttp/3.8.1","timeout": 20,"epg": "https://epg.cdn.loc.cc/?ch={name}&date={date}","logo": "https://logo.wyfc.qzz.io/{name}.png"}, {"name": "live","type": 0,"url": "https://epg.pw/test_channels.m3u","playerType": 1,"ua": "okhttp/3.8.1","timeout": 20,"epg": "https://epg.cdn.loc.cc/?ch={name}&date={date}","logo": "https://logo.wyfc.qzz.io/{name}.png"},
@@ -98,9 +98,8 @@
{"key": "百度网盘","name": "百度 • 网盘","type": 3,"api": "csp_PanBaidu","searchable": 0,"filterable": 0,"changeable": 0,"style": {"type": "list","ratio": 1.433}}, {"key": "百度网盘","name": "百度 • 网盘","type": 3,"api": "csp_PanBaidu","searchable": 0,"filterable": 0,"changeable": 0,"style": {"type": "list","ratio": 1.433}},
{"key": "盘搜","name": "盘搜 • 搜索","type": 3,"api": "csp_PanSou","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "盘搜","name": "盘搜 • 搜索","type": 3,"api": "csp_PanSou","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "古哥","name": "古哥 • 搜索","type": 3,"api": "csp_GugeSo","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "古哥","name": "古哥 • 搜索","type": 3,"api": "csp_GugeSo","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "兄弟","name": "兄弟 • 搜索","type": 3,"api": "csp_XiongdiPan","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "百酷","name": "百酷 • 搜索","type": 3,"api": "csp_Baiku","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "百酷","name": "百酷 • 搜索","type": 3,"api": "csp_Baiku","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "米搜","name": "米搜 • 搜索","type": 3,"api": "csp_MiSou","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "米搜","name": "米搜 • 搜索","type": 3,"api": "csp_MiSou","searchable": 1,"quickSearch": 0,"filterable": 0,"ext": {"site": ["https://misoso.shop","http://www.misoso.shop"]},"timeout": 60},
{"key": "鬼鬼","name": "鬼鬼 • 搜索","type": 3,"api": "csp_GuiGui","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "鬼鬼","name": "鬼鬼 • 搜索","type": 3,"api": "csp_GuiGui","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "混合","name": "混合 • 搜索","type": 3,"api": "csp_HunHePan","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "混合","name": "混合 • 搜索","type": 3,"api": "csp_HunHePan","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "天翼","name": "天翼 • 搜索","type": 3,"api": "csp_TianYiSou","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "天翼","name": "天翼 • 搜索","type": 3,"api": "csp_TianYiSou","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
+328 -66
View File
@@ -1,81 +1,254 @@
# -*- coding: utf-8 -*-
# 修复:歌手不显示歌手图片
# by:垃圾星河
# 代码指导:嗷呜呜呜呜
# 增加:自动人机验证绕过
import re import re
import sys import sys
import time
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from urllib.parse import quote, unquote from urllib.parse import quote, unquote
from pyquery import PyQuery as pq from pyquery import PyQuery as pq
from requests import Session, adapters from requests import Session, adapters
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
sys.path.append('..') sys.path.append('..')
from base.spider import Spider from base.spider import Spider
class Spider(Spider): class Spider(Spider):
def init(self, extend=""): def init(self, extend=""):
self.host = "https://www.22a5.com" self.host = "https://www.22a5.com"
self.session = Session() self.session = Session()
adapter = adapters.HTTPAdapter(max_retries=Retry(total=3, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504]), pool_connections=20, pool_maxsize=50) adapter = adapters.HTTPAdapter(
max_retries=Retry(total=3, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504]),
pool_connections=20,
pool_maxsize=50
)
self.session.mount("http://", adapter) self.session.mount("http://", adapter)
self.session.mount("https://", adapter) self.session.mount("https://", adapter)
self.headers = {"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"} self.headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"
}
self.session.headers.update(self.headers) self.session.headers.update(self.headers)
def getName(self): return "爱听音乐" def getName(self):
def isVideoFormat(self, url): return bool(re.search(r'\.(m3u8|mp4|mp3|m4a|flv)(\?|$)', url or "", re.I)) return "爱听音乐"
def manualVideoCheck(self): return False
def destroy(self): self.session.close() def isVideoFormat(self, url):
return bool(re.search(r'\.(m3u8|mp4|mp3|m4a|flv)(\?|$)', url or "", re.I))
def manualVideoCheck(self):
return False
def destroy(self):
self.session.close()
# ==================== 新增人机验证绕过 ====================
def _bypass_verification(self, url, response_text):
"""若当前页面是人机验证,自动提交勾选"""
if '安全人机验证' not in response_text and 'human_check' not in response_text:
return None
# 提取 csrf_token(支持多种格式)
token_match = re.search(r'name="csrf_token"\s+value="([^"]+)"', response_text)
if not token_match:
print("[爱听音乐] 未找到 csrf_token,跳过绕过")
return None
token = token_match.group(1)
# 构造提交数据
data = {
'csrf_token': token,
'human_check': 'on'
}
try:
# 发送 POST 请求,自动跟随重定向
post_resp = self.session.post(url, data=data, allow_redirects=True, timeout=10)
print("[爱听音乐] 人机验证已绕过")
return post_resp
except Exception as e:
print(f"[爱听音乐] 人机验证提交失败: {e}")
return None
def getpq(self, url):
"""获取页面并自动处理人机验证(重试3次)"""
full_url = self._abs(url)
for attempt in range(3):
try:
resp = self.session.get(full_url, timeout=5)
# 若遇到验证页,尝试绕过
if '安全人机验证' in resp.text or 'human_check' in resp.text:
print("[爱听音乐] 检测到人机验证,尝试自动绕过...")
bypass_resp = self._bypass_verification(full_url, resp.text)
if bypass_resp:
# 验证成功后,返回最终页面(可能是重定向后的目标)
return pq(bypass_resp.text)
else:
# 绕过失败,等待后重试
time.sleep(1)
continue
else:
# 正常页面直接返回
return pq(resp.text)
except Exception as e:
print(f"[爱听音乐] 请求失败 (尝试 {attempt+1}/3): {e}")
time.sleep(0.5 * (attempt + 1))
# 全部失败,返回空文档
return pq("<html></html>")
# ==================== 原有功能(保持不变) ====================
def homeContent(self, filter): def homeContent(self, filter):
classes = [{"type_name": n, "type_id": i} for n, i in [("歌手","/singerlist/index/index/index/index.html"), ("TOP榜单","/list/top.html"), ("新歌榜","/list/new.html"), ("电台","/radiolist/index.html"), ("高清MV","/mvlist/oumei.html"), ("专辑","/albumlist/index.html"), ("歌单","/playtype/index.html")]] classes = [
filters = {p: d for p in [c["type_id"] for c in classes if "singer" not in c["type_id"]] if (d := self._fetch_filters(p))} {"type_name": n, "type_id": i}
for n, i in [
("歌手", "/singerlist/index/index/index/index.html"),
("TOP榜单", "/list/top.html"),
("新歌榜", "/list/new.html"),
("电台", "/radiolist/index.html"),
("高清MV", "/mvlist/oumei.html"),
("专辑", "/albumlist/index.html"),
("歌单", "/playtype/index.html")
]
]
filters = {}
for p in [c["type_id"] for c in classes if "singer" not in c["type_id"]]:
d = self._fetch_filters(p)
if d:
filters[p] = d
if "/radiolist/index.html" not in filters: if "/radiolist/index.html" not in filters:
filters["/radiolist/index.html"] = [{"key": "id", "name": "分类", "value": [{"n": n, "v": v} for n,v in zip(["最新","最热","有声小说","相声","音乐","情感","国漫","影视","脱口秀","历史","儿童","教育","八卦","推理","头条"], ["index","hot","novel","xiangyi","music","emotion","game","yingshi","talkshow","history","children","education","gossip","tuili","headline"])]}] filters["/radiolist/index.html"] = [{
"key": "id",
"name": "分类",
"value": [
{"n": n, "v": v}
for n, v in zip(
["最新", "最热", "有声小说", "相声", "音乐", "情感", "国漫", "影视", "脱口秀", "历史", "儿童", "教育", "八卦", "推理", "头条"],
["index", "hot", "novel", "xiangyi", "music", "emotion", "game", "yingshi", "talkshow", "history",
"children", "education", "gossip", "tuili", "headline"]
)
]
}]
filters["/singerlist/index/index/index/index.html"] = [ filters["/singerlist/index/index/index/index.html"] = [
{"key": "area", "name": "地区", "value": [{"n": n, "v": v} for n,v in [("全部","index"),("华语","huayu"),("欧美","oumei"),("韩国","hanguo"),("日本","ribrn")]]}, {
{"key": "sex", "name": "性别", "value": [{"n": n, "v": v} for n,v in [("全部","index"),("","male"),("","girl"),("组合","band")]]}, "key": "area",
{"key": "genre", "name": "流派", "value": [{"n": n, "v": v} for n,v in [("全部","index"),("流行","liuxing"),("电子","dianzi"),("摇滚","yaogun"),("嘻哈","xiha"),("R&B","rb"),("民谣","minyao"),("爵士","jueshi"),("古典","gudian")]]}, "name": "地区",
{"key": "char", "name": "字母", "value": [{"n": n, "v": v} for n,v in [("全部","index")] + [{"n": chr(i), "v": chr(i).lower()} for i in range(65, 91)]]} "value": [
{"n": "全部", "v": "index"},
{"n": "华语", "v": "huayu"},
{"n": "欧美", "v": "oumei"},
{"n": "韩国", "v": "hanguo"},
{"n": "日本", "v": "ribrn"}
]
},
{
"key": "sex",
"name": "性别",
"value": [
{"n": "全部", "v": "index"},
{"n": "", "v": "male"},
{"n": "", "v": "girl"},
{"n": "组合", "v": "band"}
]
},
{
"key": "genre",
"name": "流派",
"value": [
{"n": "全部", "v": "index"},
{"n": "流行", "v": "liuxing"},
{"n": "电子", "v": "dianzi"},
{"n": "摇滚", "v": "yaogun"},
{"n": "嘻哈", "v": "xiha"},
{"n": "R&B", "v": "rb"},
{"n": "民谣", "v": "minyao"},
{"n": "爵士", "v": "jueshi"},
{"n": "古典", "v": "gudian"}
]
}
] ]
return {"class": classes, "filters": filters, "list": []} return {"class": classes, "filters": filters, "list": []}
def homeVideoContent(self): return {"list": []} def homeVideoContent(self):
return {"list": []}
def categoryContent(self, tid, pg, filter, extend): def categoryContent(self, tid, pg, filter, extend):
pg = int(pg or 1) pg = int(pg or 1)
url = tid url = tid
if "/singerlist/" in tid: if "/singerlist/" in tid:
p = tid.split('/') parts = tid.split('/')
if len(p) >= 6: if len(parts) >= 6:
url = "/".join(p[:2] + [extend.get(k, p[i]) for i, k in enumerate(["area", "sex", "genre"], 2)] + [f"{extend.get('char', 'index')}.html"]) url = "/".join(parts[:2] + [extend.get(k, parts[i]) for i, k in enumerate(["area", "sex", "genre"], 2)] +
[f"{extend.get('char', 'index')}.html"])
elif "id" in extend and extend["id"] not in ["index", "top"]: elif "id" in extend and extend["id"] not in ["index", "top"]:
url = tid.replace("index.html", f"{extend['id']}.html").replace("top.html", f"{extend['id']}.html") url = tid.replace("index.html", f"{extend['id']}.html").replace("top.html", f"{extend['id']}.html")
if url == tid: url = f"{tid.rsplit('/', 1)[0]}/{extend['id']}.html" if url == tid:
url = f"{tid.rsplit('/', 1)[0]}/{extend['id']}.html"
if pg > 1: if pg > 1:
sep = "/" if any(x in url for x in ["/singerlist/", "/radiolist/", "/mvlist/", "/playtype/", "/list/"]) else "_" sep = "/" if any(x in url for x in ["/singerlist/", "/radiolist/", "/mvlist/", "/playtype/", "/list/"]) else "_"
url = re.sub(r'(_\d+|/\d+)?\.html$', f'{sep}{pg}.html', url) url = re.sub(r'(_\d+|/\d+)?\.html$', f'{sep}{pg}.html', url)
doc = self.getpq(url) doc = self.getpq(url)
return {"list": self._parse_list(doc(".play_list li, .video_list li, .pic_list li, .singer_list li, .ali li, .layui-row li, .base_l li"), tid), "page": pg, "pagecount": 9999, "limit": 90, "total": 999999} items = doc(".play_list li, .video_list li, .pic_list li, .singer_list li, .ali li, .layui-row li, .base_l li")
return {
"list": self._parse_list(items, tid),
"page": pg,
"pagecount": 9999,
"limit": 90,
"total": 999999
}
def searchContent(self, key, quick, pg="1"): def searchContent(self, key, quick, pg="1"):
return {"list": self._parse_list(self.getpq(f"/so/{quote(key)}/{pg}.html")(".base_l li, .play_list li"), "search"), "page": int(pg)} doc = self.getpq(f"/so/{quote(key)}/{pg}.html")
items = doc(".base_l li, .play_list li")
return {
"list": self._parse_list(items, "search"),
"page": int(pg)
}
def detailContent(self, ids): def detailContent(self, ids):
url = self._abs(ids[0]) url = self._abs(ids[0])
doc = self.getpq(url) doc = self.getpq(url)
vod = {"vod_id": url, "vod_name": self._clean(doc("h1").text() or doc("title").text()), "vod_pic": self._abs(doc(".djpg img, .pic img, .djpic img").attr("src")), "vod_play_from": "爱听音乐", "vod_content": ""} vod = {
"vod_id": url,
"vod_name": self._clean(doc("h1").text() or doc("title").text()),
"vod_pic": self._abs(doc(".djpg img, .pic img, .djpic img").attr("src")),
"vod_play_from": "爱听音乐",
"vod_content": ""
}
if any(x in url for x in ["/playlist/", "/album/", "/list/", "/singer/", "/special/", "/radio/", "/radiolist/"]): if any(x in url for x in ["/playlist/", "/album/", "/list/", "/singer/", "/special/", "/radio/", "/radiolist/"]):
eps = self._get_eps(doc) eps = self._get_eps(doc)
page_urls = {self._abs(a.attr("href")) for a in doc(".page a, .dede_pages a, .pagelist a").items() if a.attr("href") and "javascript" not in a.attr("href")} - {url} page_urls = {
self._abs(a.attr("href"))
for a in doc(".page a, .dede_pages a, .pagelist a").items()
if a.attr("href") and "javascript" not in a.attr("href")
} - {url}
if page_urls: if page_urls:
with ThreadPoolExecutor(max_workers=5) as ex: with ThreadPoolExecutor(max_workers=5) as ex:
for r in as_completed([ex.submit(lambda u: self._get_eps(self.getpq(u)), u) for u in sorted(page_urls, key=lambda x: int(re.search(r'[_\/](\d+)\.html', x).group(1)) if re.search(r'[_\/](\d+)\.html', x) else 0)]): futures = []
eps.extend(r.result() or []) for u in sorted(page_urls, key=lambda x: int(re.search(r'[_/](\d+)\.html', x).group(1)) if re.search(
r'[_/](\d+)\.html', x) else 0):
futures.append(ex.submit(lambda uu: self._get_eps(self.getpq(uu)), u))
for f in as_completed(futures):
eps.extend(f.result() or [])
if eps: if eps:
vod.update({"vod_play_from": "播放列表", "vod_play_url": "#".join(eps)}) vod.update({
"vod_play_from": "播放列表",
"vod_play_url": "#".join(eps)
})
return {"list": [vod]} return {"list": [vod]}
play_list = [] play_list = []
@@ -85,9 +258,12 @@ class Spider(Spider):
elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url): elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url):
with ThreadPoolExecutor(max_workers=3) as ex: with ThreadPoolExecutor(max_workers=3) as ex:
fs = {ex.submit(self._api, "/plug/down.php", {"ac": "vplay", "id": vid.group(2), "q": q}): n for n, q in [("蓝光", 1080), ("超清", 720), ("高清", 480)]} fs = {
ex.submit(self._api, "/plug/down.php", {"ac": "vplay", "id": vid.group(2), "q": q}): n
for n, q in [("蓝光", 1080), ("超清", 720), ("高清", 480)]
}
play_list = [f"{fs[f]}${self.e64('0@@@@'+u)}" for f in as_completed(fs) if (u := f.result())] play_list = [f"{fs[f]}${self.e64('0@@@@'+u)}" for f in as_completed(fs) if (u := f.result())]
play_list.sort(key=lambda x: {"":0, "":1, "":2}.get(x[0], 3)) play_list.sort(key=lambda x: {"": 0, "": 1, "": 2}.get(x[0], 3))
vod["vod_play_url"] = "#".join(play_list) if play_list else f"解析失败${self.e64('1@@@@'+url)}" vod["vod_play_url"] = "#".join(play_list) if play_list else f"解析失败${self.e64('1@@@@'+url)}"
return {"list": [vod]} return {"list": [vod]}
@@ -99,16 +275,22 @@ class Spider(Spider):
if ".html" in url and not self.isVideoFormat(url): if ".html" in url and not self.isVideoFormat(url):
if mid := re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', url): if mid := re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', url):
if r_url := self._api("/js/play.php", method="POST", data={"id": mid.group(2), "type": "music"}, headers={"Referer": url.replace("http://","https://"), "X-Requested-With": "XMLHttpRequest"}): if r_url := self._api("/js/play.php", method="POST", data={"id": mid.group(2), "type": "music"},
headers={"Referer": url.replace("http://", "https://"),
"X-Requested-With": "XMLHttpRequest"}):
url = r_url if ".php" not in r_url else url url = r_url if ".php" not in r_url else url
elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url): elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url):
with ThreadPoolExecutor(max_workers=3) as ex: with ThreadPoolExecutor(max_workers=3) as ex:
for f in as_completed([ex.submit(self._api, "/plug/down.php", {"ac": "vplay", "id": vid.group(2), "q": q}) for q in [1080, 720, 480]]): for f in as_completed(
[ex.submit(self._api, "/plug/down.php", {"ac": "vplay", "id": vid.group(2), "q": q}) for q in
[1080, 720, 480]]):
if v_url := f.result(): if v_url := f.result():
url = v_url; break url = v_url
break
result = {"parse": 0, "url": url, "header": {"User-Agent": self.headers["User-Agent"]}} result = {"parse": 0, "url": url, "header": {"User-Agent": self.headers["User-Agent"]}}
if "22a5.com" in url: result["header"]["Referer"] = self.host + "/" if "22a5.com" in url:
result["header"]["Referer"] = self.host + "/"
# OK影视3.6.5+支持LRC格式滚动歌词 # OK影视3.6.5+支持LRC格式滚动歌词
if subt: if subt:
@@ -116,7 +298,6 @@ class Spider(Spider):
r = self.session.get(subt, headers={"Referer": self.host + "/"}, timeout=5) r = self.session.get(subt, headers={"Referer": self.host + "/"}, timeout=5)
lrc_content = r.text lrc_content = r.text
if lrc_content: if lrc_content:
# 过滤广告内容
lrc_content = self._filter_lrc_ads(lrc_content) lrc_content = self._filter_lrc_ads(lrc_content)
result["lrc"] = lrc_content result["lrc"] = lrc_content
except: except:
@@ -146,19 +327,15 @@ class Spider(Spider):
] ]
for line in lines: for line in lines:
# 保留时间标签行,但过滤掉广告文本
if re.match(r'\[\d{2}:\d{2}', line): if re.match(r'\[\d{2}:\d{2}', line):
# 检查是否包含广告
is_ad = False is_ad = False
for pattern in ad_patterns: for pattern in ad_patterns:
if re.search(pattern, line, re.IGNORECASE): if re.search(pattern, line, re.IGNORECASE):
is_ad = True is_ad = True
break break
if not is_ad: if not is_ad:
filtered_lines.append(line) filtered_lines.append(line)
else: else:
# 非时间标签行(可能是元数据),保留
filtered_lines.append(line) filtered_lines.append(line)
return '\n'.join(filtered_lines) return '\n'.join(filtered_lines)
@@ -168,12 +345,22 @@ class Spider(Spider):
type_ = param.get("type") type_ = param.get("type")
if type_ == "img": if type_ == "img":
return [200, "image/jpeg", self.session.get(url, headers={"Referer": self.host + "/"}, timeout=5).content, {}] try:
headers = {
"Referer": "https://www.baidu.com/",
"User-Agent": self.headers["User-Agent"],
"Accept": "image/webp,image/apng,image/*,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9"
}
resp = self.session.get(url, headers=headers, timeout=10)
return [200, "image/jpeg", resp.content, {}]
except Exception as e:
print(f"图片代理失败: {e}")
return [404, "text/plain", b"", {}]
elif type_ == "lrc": elif type_ == "lrc":
try: try:
r = self.session.get(url, headers={"Referer": self.host + "/"}, timeout=5) r = self.session.get(url, headers={"Referer": self.host + "/"}, timeout=5)
# 同时过滤代理中的广告
lrc_content = r.text lrc_content = r.text
lrc_content = self._filter_lrc_ads(lrc_content) lrc_content = self._filter_lrc_ads(lrc_content)
return [200, "application/octet-stream", lrc_content.encode('utf-8'), {}] return [200, "application/octet-stream", lrc_content.encode('utf-8'), {}]
@@ -182,21 +369,63 @@ class Spider(Spider):
return None return None
# ==================== 辅助方法 ====================
def _parse_list(self, items, tid=""): def _parse_list(self, items, tid=""):
"""解析列表项,修复歌手头像 - 直接返回原始图片URL"""
res = [] res = []
for li in items.items(): for li in items.items():
a = li("a").eq(0) a = li("a").eq(0)
if not (href := a.attr("href")) or href == "/" or any(x in href for x in ["/user/", "/login/", "javascript"]): continue if not (href := a.attr("href")) or href == "/" or any(x in href for x in ["/user/", "/login/", "javascript"]):
if not (name := self._clean(li(".name").text() or a.attr("title") or a.text())): continue continue
pic = self._abs((li("img").attr("src") or "").replace('120', '500')) if not (name := self._clean(li(".name").text() or a.attr("title") or a.text())):
res.append({"vod_id": self._abs(href), "vod_name": name, "vod_pic": f"{self.getProxyUrl()}&url={pic}&type=img" if pic else "", "style": {"type": "oval" if "/singer/" in href else ("list" if any(x in tid for x in ["/list/", "/playtype/", "/albumlist/"]) else "rect"), "ratio": 1 if "/singer/" in href else 1.33}}) continue
is_singer = "/singer/" in href or "/singerlist" in tid
pic = ""
src = ""
if is_singer:
img = li(".pic img").eq(0)
src = img.attr("src") or ""
if not src:
img = li("img").eq(0)
src = img.attr("src") or ""
else:
img = li("img").eq(0)
src = img.attr("src") or ""
if not src:
img = li(".pic img").eq(0)
src = img.attr("src") or ""
if src:
if src.startswith('//'):
src = 'https:' + src
elif src.startswith('/'):
src = self.host + src
pic = src
res.append({
"vod_id": self._abs(href),
"vod_name": name,
"vod_pic": pic,
"style": {
"type": "oval" if is_singer else ("list" if any(
x in tid for x in ["/list/", "/playtype/", "/albumlist/"]) else "rect"),
"ratio": 1 if is_singer else 1.33
}
})
return res return res
def _get_eps(self, doc): def _get_eps(self, doc):
eps = [] eps = []
for li in doc(".play_list li, .song_list li, .music_list li").items(): for li in doc(".play_list li, .song_list li, .music_list li").items():
if not (a := li("a").eq(0)).attr("href") or not re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', a.attr("href")): continue a = li("a").eq(0)
full_url = self._abs(a.attr("href")) href = a.attr("href")
if not href or not re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', href):
continue
full_url = self._abs(href)
lrc_part = "" lrc_part = ""
mid = re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', full_url) mid = re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', full_url)
@@ -207,34 +436,67 @@ class Spider(Spider):
eps.append(f"{self._clean(a.text() or li('.name').text())}${self.e64('0@@@@' + full_url + lrc_part)}") eps.append(f"{self._clean(a.text() or li('.name').text())}${self.e64('0@@@@' + full_url + lrc_part)}")
return eps return eps
def _clean(self, text): return re.sub(r'(爱玩音乐网|视频下载说明|视频下载地址|www\.2t58\.com|MP3免费下载|LRC歌词下载|全部歌曲|\[第\d+页\]|刷新|每日推荐|最新|热门|推荐|MV|高清|无损)', '', text or "", flags=re.I).strip() def _clean(self, text):
return re.sub(
r'(爱玩音乐网|视频下载说明|视频下载地址|www\.2t58\.com|MP3免费下载|LRC歌词下载|全部歌曲|\[第\d+页\]|刷新|每日推荐|最新|热门|推荐|MV|高清|无损)',
'',
text or '',
flags=re.I
).strip()
def _fetch_filters(self, url): def _fetch_filters(self, url):
doc, filters = self.getpq(url), [] doc = self.getpq(url)
for i, group in enumerate([doc(s) for s in [".ilingku_fl", ".class_list", ".screen_list", ".box_list", ".nav_list"] if doc(s)]): filters = []
opts, seen = [{"n": "全部", "v": "top" if "top" in url else "index"}], set() for i, group in enumerate([
for a in group("a").items(): doc(".ilingku_fl"),
if (v := (a.attr("href") or "").split("?")[0].rstrip('/').split('/')[-1].replace('.html','')) and v not in seen: doc(".class_list"),
opts.append({"n": a.text().strip(), "v": v}); seen.add(v) doc(".screen_list"),
if len(opts) > 1: filters.append({"key": f"id{i}" if i else "id", "name": "分类", "value": opts}) doc(".box_list"),
doc(".nav_list")
]):
if group:
opts = [{"n": "全部", "v": "top" if "top" in url else "index"}]
seen = set()
for a in group("a").items():
v = (a.attr("href") or "").split("?")[0].rstrip('/').split('/')[-1].replace('.html', '')
if v and v not in seen:
opts.append({"n": a.text().strip(), "v": v})
seen.add(v)
if len(opts) > 1:
filters.append({"key": f"id{i}" if i else "id", "name": "分类", "value": opts})
return filters return filters
def _api(self, path, params=None, method="GET", headers=None, data=None): def _api(self, path, params=None, method="GET", headers=None, data=None):
try: try:
h = self.headers.copy() h = self.headers.copy()
if headers: h.update(headers) if headers:
r = (self.session.post if method == "POST" else self.session.get)(f"{self.host}{path}", params=params, data=data, headers=h, timeout=10, allow_redirects=False) h.update(headers)
if loc := r.headers.get("Location"): return self._abs(loc.strip()) r = (self.session.post if method == "POST" else self.session.get)(
return self._abs(r.json().get("url", "").replace(r"\/", "/")) or (r.text.strip() if r.text.strip().startswith("http") else "") f"{self.host}{path}",
except: return "" params=params,
data=data,
headers=h,
timeout=10,
allow_redirects=False
)
if loc := r.headers.get("Location"):
return self._abs(loc.strip())
return self._abs(r.json().get("url", "").replace(r"\/", "/")) or (r.text.strip() if r.text.strip().startswith(
"http") else "")
except:
return ""
def getpq(self, url): def _abs(self, url):
import time if not url:
for _ in range(2): return ""
try: return pq(self.session.get(self._abs(url), timeout=5).text) if url.startswith("http"):
except: time.sleep(0.1) return url
return pq("<html></html>") if url.startswith("//"):
return "https:" + url
return f"{self.host}{'/' if not url.startswith('/') else ''}{url}"
def _abs(self, url): return url if url.startswith("http") else (f"{self.host}{'/' if not url.startswith('/') else ''}{url}" if url else "") def e64(self, text):
def e64(self, text): return b64encode(text.encode("utf-8")).decode("utf-8") return b64encode(text.encode("utf-8")).decode("utf-8")
def d64(self, text): return b64decode(text.encode("utf-8")).decode("utf-8")
def d64(self, text):
return b64decode(text.encode("utf-8")).decode("utf-8")
+2 -3
View File
@@ -1,5 +1,5 @@
{ {
"spider": "./spider.jar;md5;5a5c64f87f0b0066919fa9b499912e4e", "spider": "./spider.jar;md5;feb3dd35be7c31aa2a39bea423961279",
"wallpaper": "http://127.0.0.1:9978/proxy?do=wallpaper", "wallpaper": "http://127.0.0.1:9978/proxy?do=wallpaper",
"sites": [ "sites": [
{"key": "豆瓣","name": "豆瓣 • 首页","type": 3,"api": "csp_Douban","searchable": 0}, {"key": "豆瓣","name": "豆瓣 • 首页","type": 3,"api": "csp_Douban","searchable": 0},
@@ -88,9 +88,8 @@
{"key": "百度网盘","name": "百度 • 网盘","type": 3,"api": "csp_PanBaidu","searchable": 0,"filterable": 0,"changeable": 0,"style": {"type": "list","ratio": 1.433}}, {"key": "百度网盘","name": "百度 • 网盘","type": 3,"api": "csp_PanBaidu","searchable": 0,"filterable": 0,"changeable": 0,"style": {"type": "list","ratio": 1.433}},
{"key": "盘搜","name": "盘搜 • 搜索","type": 3,"api": "csp_PanSou","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "盘搜","name": "盘搜 • 搜索","type": 3,"api": "csp_PanSou","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "古哥","name": "古哥 • 搜索","type": 3,"api": "csp_GugeSo","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "古哥","name": "古哥 • 搜索","type": 3,"api": "csp_GugeSo","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "兄弟","name": "兄弟 • 搜索","type": 3,"api": "csp_XiongdiPan","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "百酷","name": "百酷 • 搜索","type": 3,"api": "csp_Baiku","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "百酷","name": "百酷 • 搜索","type": 3,"api": "csp_Baiku","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "米搜","name": "米搜 • 搜索","type": 3,"api": "csp_MiSou","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "米搜","name": "米搜 • 搜索","type": 3,"api": "csp_MiSou","searchable": 1,"quickSearch": 0,"filterable": 0,"ext": {"site": ["https://misoso.shop","http://www.misoso.shop"]},"timeout": 60},
{"key": "鬼鬼","name": "鬼鬼 • 搜索","type": 3,"api": "csp_GuiGui","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "鬼鬼","name": "鬼鬼 • 搜索","type": 3,"api": "csp_GuiGui","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "混合","name": "混合 • 搜索","type": 3,"api": "csp_HunHePan","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "混合","name": "混合 • 搜索","type": 3,"api": "csp_HunHePan","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
{"key": "天翼","name": "天翼 • 搜索","type": 3,"api": "csp_TianYiSou","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60}, {"key": "天翼","name": "天翼 • 搜索","type": 3,"api": "csp_TianYiSou","searchable": 1,"quickSearch": 0,"filterable": 0,"timeout": 60},
+328 -66
View File
@@ -1,81 +1,254 @@
# -*- coding: utf-8 -*-
# 修复:歌手不显示歌手图片
# by:垃圾星河
# 代码指导:嗷呜呜呜呜
# 增加:自动人机验证绕过
import re import re
import sys import sys
import time
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from urllib.parse import quote, unquote from urllib.parse import quote, unquote
from pyquery import PyQuery as pq from pyquery import PyQuery as pq
from requests import Session, adapters from requests import Session, adapters
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
sys.path.append('..') sys.path.append('..')
from base.spider import Spider from base.spider import Spider
class Spider(Spider): class Spider(Spider):
def init(self, extend=""): def init(self, extend=""):
self.host = "https://www.22a5.com" self.host = "https://www.22a5.com"
self.session = Session() self.session = Session()
adapter = adapters.HTTPAdapter(max_retries=Retry(total=3, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504]), pool_connections=20, pool_maxsize=50) adapter = adapters.HTTPAdapter(
max_retries=Retry(total=3, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504]),
pool_connections=20,
pool_maxsize=50
)
self.session.mount("http://", adapter) self.session.mount("http://", adapter)
self.session.mount("https://", adapter) self.session.mount("https://", adapter)
self.headers = {"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"} self.headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"
}
self.session.headers.update(self.headers) self.session.headers.update(self.headers)
def getName(self): return "爱听音乐" def getName(self):
def isVideoFormat(self, url): return bool(re.search(r'\.(m3u8|mp4|mp3|m4a|flv)(\?|$)', url or "", re.I)) return "爱听音乐"
def manualVideoCheck(self): return False
def destroy(self): self.session.close() def isVideoFormat(self, url):
return bool(re.search(r'\.(m3u8|mp4|mp3|m4a|flv)(\?|$)', url or "", re.I))
def manualVideoCheck(self):
return False
def destroy(self):
self.session.close()
# ==================== 新增人机验证绕过 ====================
def _bypass_verification(self, url, response_text):
"""若当前页面是人机验证,自动提交勾选"""
if '安全人机验证' not in response_text and 'human_check' not in response_text:
return None
# 提取 csrf_token(支持多种格式)
token_match = re.search(r'name="csrf_token"\s+value="([^"]+)"', response_text)
if not token_match:
print("[爱听音乐] 未找到 csrf_token,跳过绕过")
return None
token = token_match.group(1)
# 构造提交数据
data = {
'csrf_token': token,
'human_check': 'on'
}
try:
# 发送 POST 请求,自动跟随重定向
post_resp = self.session.post(url, data=data, allow_redirects=True, timeout=10)
print("[爱听音乐] 人机验证已绕过")
return post_resp
except Exception as e:
print(f"[爱听音乐] 人机验证提交失败: {e}")
return None
def getpq(self, url):
"""获取页面并自动处理人机验证(重试3次)"""
full_url = self._abs(url)
for attempt in range(3):
try:
resp = self.session.get(full_url, timeout=5)
# 若遇到验证页,尝试绕过
if '安全人机验证' in resp.text or 'human_check' in resp.text:
print("[爱听音乐] 检测到人机验证,尝试自动绕过...")
bypass_resp = self._bypass_verification(full_url, resp.text)
if bypass_resp:
# 验证成功后,返回最终页面(可能是重定向后的目标)
return pq(bypass_resp.text)
else:
# 绕过失败,等待后重试
time.sleep(1)
continue
else:
# 正常页面直接返回
return pq(resp.text)
except Exception as e:
print(f"[爱听音乐] 请求失败 (尝试 {attempt+1}/3): {e}")
time.sleep(0.5 * (attempt + 1))
# 全部失败,返回空文档
return pq("<html></html>")
# ==================== 原有功能(保持不变) ====================
def homeContent(self, filter): def homeContent(self, filter):
classes = [{"type_name": n, "type_id": i} for n, i in [("歌手","/singerlist/index/index/index/index.html"), ("TOP榜单","/list/top.html"), ("新歌榜","/list/new.html"), ("电台","/radiolist/index.html"), ("高清MV","/mvlist/oumei.html"), ("专辑","/albumlist/index.html"), ("歌单","/playtype/index.html")]] classes = [
filters = {p: d for p in [c["type_id"] for c in classes if "singer" not in c["type_id"]] if (d := self._fetch_filters(p))} {"type_name": n, "type_id": i}
for n, i in [
("歌手", "/singerlist/index/index/index/index.html"),
("TOP榜单", "/list/top.html"),
("新歌榜", "/list/new.html"),
("电台", "/radiolist/index.html"),
("高清MV", "/mvlist/oumei.html"),
("专辑", "/albumlist/index.html"),
("歌单", "/playtype/index.html")
]
]
filters = {}
for p in [c["type_id"] for c in classes if "singer" not in c["type_id"]]:
d = self._fetch_filters(p)
if d:
filters[p] = d
if "/radiolist/index.html" not in filters: if "/radiolist/index.html" not in filters:
filters["/radiolist/index.html"] = [{"key": "id", "name": "分类", "value": [{"n": n, "v": v} for n,v in zip(["最新","最热","有声小说","相声","音乐","情感","国漫","影视","脱口秀","历史","儿童","教育","八卦","推理","头条"], ["index","hot","novel","xiangyi","music","emotion","game","yingshi","talkshow","history","children","education","gossip","tuili","headline"])]}] filters["/radiolist/index.html"] = [{
"key": "id",
"name": "分类",
"value": [
{"n": n, "v": v}
for n, v in zip(
["最新", "最热", "有声小说", "相声", "音乐", "情感", "国漫", "影视", "脱口秀", "历史", "儿童", "教育", "八卦", "推理", "头条"],
["index", "hot", "novel", "xiangyi", "music", "emotion", "game", "yingshi", "talkshow", "history",
"children", "education", "gossip", "tuili", "headline"]
)
]
}]
filters["/singerlist/index/index/index/index.html"] = [ filters["/singerlist/index/index/index/index.html"] = [
{"key": "area", "name": "地区", "value": [{"n": n, "v": v} for n,v in [("全部","index"),("华语","huayu"),("欧美","oumei"),("韩国","hanguo"),("日本","ribrn")]]}, {
{"key": "sex", "name": "性别", "value": [{"n": n, "v": v} for n,v in [("全部","index"),("","male"),("","girl"),("组合","band")]]}, "key": "area",
{"key": "genre", "name": "流派", "value": [{"n": n, "v": v} for n,v in [("全部","index"),("流行","liuxing"),("电子","dianzi"),("摇滚","yaogun"),("嘻哈","xiha"),("R&B","rb"),("民谣","minyao"),("爵士","jueshi"),("古典","gudian")]]}, "name": "地区",
{"key": "char", "name": "字母", "value": [{"n": n, "v": v} for n,v in [("全部","index")] + [{"n": chr(i), "v": chr(i).lower()} for i in range(65, 91)]]} "value": [
{"n": "全部", "v": "index"},
{"n": "华语", "v": "huayu"},
{"n": "欧美", "v": "oumei"},
{"n": "韩国", "v": "hanguo"},
{"n": "日本", "v": "ribrn"}
]
},
{
"key": "sex",
"name": "性别",
"value": [
{"n": "全部", "v": "index"},
{"n": "", "v": "male"},
{"n": "", "v": "girl"},
{"n": "组合", "v": "band"}
]
},
{
"key": "genre",
"name": "流派",
"value": [
{"n": "全部", "v": "index"},
{"n": "流行", "v": "liuxing"},
{"n": "电子", "v": "dianzi"},
{"n": "摇滚", "v": "yaogun"},
{"n": "嘻哈", "v": "xiha"},
{"n": "R&B", "v": "rb"},
{"n": "民谣", "v": "minyao"},
{"n": "爵士", "v": "jueshi"},
{"n": "古典", "v": "gudian"}
]
}
] ]
return {"class": classes, "filters": filters, "list": []} return {"class": classes, "filters": filters, "list": []}
def homeVideoContent(self): return {"list": []} def homeVideoContent(self):
return {"list": []}
def categoryContent(self, tid, pg, filter, extend): def categoryContent(self, tid, pg, filter, extend):
pg = int(pg or 1) pg = int(pg or 1)
url = tid url = tid
if "/singerlist/" in tid: if "/singerlist/" in tid:
p = tid.split('/') parts = tid.split('/')
if len(p) >= 6: if len(parts) >= 6:
url = "/".join(p[:2] + [extend.get(k, p[i]) for i, k in enumerate(["area", "sex", "genre"], 2)] + [f"{extend.get('char', 'index')}.html"]) url = "/".join(parts[:2] + [extend.get(k, parts[i]) for i, k in enumerate(["area", "sex", "genre"], 2)] +
[f"{extend.get('char', 'index')}.html"])
elif "id" in extend and extend["id"] not in ["index", "top"]: elif "id" in extend and extend["id"] not in ["index", "top"]:
url = tid.replace("index.html", f"{extend['id']}.html").replace("top.html", f"{extend['id']}.html") url = tid.replace("index.html", f"{extend['id']}.html").replace("top.html", f"{extend['id']}.html")
if url == tid: url = f"{tid.rsplit('/', 1)[0]}/{extend['id']}.html" if url == tid:
url = f"{tid.rsplit('/', 1)[0]}/{extend['id']}.html"
if pg > 1: if pg > 1:
sep = "/" if any(x in url for x in ["/singerlist/", "/radiolist/", "/mvlist/", "/playtype/", "/list/"]) else "_" sep = "/" if any(x in url for x in ["/singerlist/", "/radiolist/", "/mvlist/", "/playtype/", "/list/"]) else "_"
url = re.sub(r'(_\d+|/\d+)?\.html$', f'{sep}{pg}.html', url) url = re.sub(r'(_\d+|/\d+)?\.html$', f'{sep}{pg}.html', url)
doc = self.getpq(url) doc = self.getpq(url)
return {"list": self._parse_list(doc(".play_list li, .video_list li, .pic_list li, .singer_list li, .ali li, .layui-row li, .base_l li"), tid), "page": pg, "pagecount": 9999, "limit": 90, "total": 999999} items = doc(".play_list li, .video_list li, .pic_list li, .singer_list li, .ali li, .layui-row li, .base_l li")
return {
"list": self._parse_list(items, tid),
"page": pg,
"pagecount": 9999,
"limit": 90,
"total": 999999
}
def searchContent(self, key, quick, pg="1"): def searchContent(self, key, quick, pg="1"):
return {"list": self._parse_list(self.getpq(f"/so/{quote(key)}/{pg}.html")(".base_l li, .play_list li"), "search"), "page": int(pg)} doc = self.getpq(f"/so/{quote(key)}/{pg}.html")
items = doc(".base_l li, .play_list li")
return {
"list": self._parse_list(items, "search"),
"page": int(pg)
}
def detailContent(self, ids): def detailContent(self, ids):
url = self._abs(ids[0]) url = self._abs(ids[0])
doc = self.getpq(url) doc = self.getpq(url)
vod = {"vod_id": url, "vod_name": self._clean(doc("h1").text() or doc("title").text()), "vod_pic": self._abs(doc(".djpg img, .pic img, .djpic img").attr("src")), "vod_play_from": "爱听音乐", "vod_content": ""} vod = {
"vod_id": url,
"vod_name": self._clean(doc("h1").text() or doc("title").text()),
"vod_pic": self._abs(doc(".djpg img, .pic img, .djpic img").attr("src")),
"vod_play_from": "爱听音乐",
"vod_content": ""
}
if any(x in url for x in ["/playlist/", "/album/", "/list/", "/singer/", "/special/", "/radio/", "/radiolist/"]): if any(x in url for x in ["/playlist/", "/album/", "/list/", "/singer/", "/special/", "/radio/", "/radiolist/"]):
eps = self._get_eps(doc) eps = self._get_eps(doc)
page_urls = {self._abs(a.attr("href")) for a in doc(".page a, .dede_pages a, .pagelist a").items() if a.attr("href") and "javascript" not in a.attr("href")} - {url} page_urls = {
self._abs(a.attr("href"))
for a in doc(".page a, .dede_pages a, .pagelist a").items()
if a.attr("href") and "javascript" not in a.attr("href")
} - {url}
if page_urls: if page_urls:
with ThreadPoolExecutor(max_workers=5) as ex: with ThreadPoolExecutor(max_workers=5) as ex:
for r in as_completed([ex.submit(lambda u: self._get_eps(self.getpq(u)), u) for u in sorted(page_urls, key=lambda x: int(re.search(r'[_\/](\d+)\.html', x).group(1)) if re.search(r'[_\/](\d+)\.html', x) else 0)]): futures = []
eps.extend(r.result() or []) for u in sorted(page_urls, key=lambda x: int(re.search(r'[_/](\d+)\.html', x).group(1)) if re.search(
r'[_/](\d+)\.html', x) else 0):
futures.append(ex.submit(lambda uu: self._get_eps(self.getpq(uu)), u))
for f in as_completed(futures):
eps.extend(f.result() or [])
if eps: if eps:
vod.update({"vod_play_from": "播放列表", "vod_play_url": "#".join(eps)}) vod.update({
"vod_play_from": "播放列表",
"vod_play_url": "#".join(eps)
})
return {"list": [vod]} return {"list": [vod]}
play_list = [] play_list = []
@@ -85,9 +258,12 @@ class Spider(Spider):
elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url): elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url):
with ThreadPoolExecutor(max_workers=3) as ex: with ThreadPoolExecutor(max_workers=3) as ex:
fs = {ex.submit(self._api, "/plug/down.php", {"ac": "vplay", "id": vid.group(2), "q": q}): n for n, q in [("蓝光", 1080), ("超清", 720), ("高清", 480)]} fs = {
ex.submit(self._api, "/plug/down.php", {"ac": "vplay", "id": vid.group(2), "q": q}): n
for n, q in [("蓝光", 1080), ("超清", 720), ("高清", 480)]
}
play_list = [f"{fs[f]}${self.e64('0@@@@'+u)}" for f in as_completed(fs) if (u := f.result())] play_list = [f"{fs[f]}${self.e64('0@@@@'+u)}" for f in as_completed(fs) if (u := f.result())]
play_list.sort(key=lambda x: {"":0, "":1, "":2}.get(x[0], 3)) play_list.sort(key=lambda x: {"": 0, "": 1, "": 2}.get(x[0], 3))
vod["vod_play_url"] = "#".join(play_list) if play_list else f"解析失败${self.e64('1@@@@'+url)}" vod["vod_play_url"] = "#".join(play_list) if play_list else f"解析失败${self.e64('1@@@@'+url)}"
return {"list": [vod]} return {"list": [vod]}
@@ -99,16 +275,22 @@ class Spider(Spider):
if ".html" in url and not self.isVideoFormat(url): if ".html" in url and not self.isVideoFormat(url):
if mid := re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', url): if mid := re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', url):
if r_url := self._api("/js/play.php", method="POST", data={"id": mid.group(2), "type": "music"}, headers={"Referer": url.replace("http://","https://"), "X-Requested-With": "XMLHttpRequest"}): if r_url := self._api("/js/play.php", method="POST", data={"id": mid.group(2), "type": "music"},
headers={"Referer": url.replace("http://", "https://"),
"X-Requested-With": "XMLHttpRequest"}):
url = r_url if ".php" not in r_url else url url = r_url if ".php" not in r_url else url
elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url): elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url):
with ThreadPoolExecutor(max_workers=3) as ex: with ThreadPoolExecutor(max_workers=3) as ex:
for f in as_completed([ex.submit(self._api, "/plug/down.php", {"ac": "vplay", "id": vid.group(2), "q": q}) for q in [1080, 720, 480]]): for f in as_completed(
[ex.submit(self._api, "/plug/down.php", {"ac": "vplay", "id": vid.group(2), "q": q}) for q in
[1080, 720, 480]]):
if v_url := f.result(): if v_url := f.result():
url = v_url; break url = v_url
break
result = {"parse": 0, "url": url, "header": {"User-Agent": self.headers["User-Agent"]}} result = {"parse": 0, "url": url, "header": {"User-Agent": self.headers["User-Agent"]}}
if "22a5.com" in url: result["header"]["Referer"] = self.host + "/" if "22a5.com" in url:
result["header"]["Referer"] = self.host + "/"
# OK影视3.6.5+支持LRC格式滚动歌词 # OK影视3.6.5+支持LRC格式滚动歌词
if subt: if subt:
@@ -116,7 +298,6 @@ class Spider(Spider):
r = self.session.get(subt, headers={"Referer": self.host + "/"}, timeout=5) r = self.session.get(subt, headers={"Referer": self.host + "/"}, timeout=5)
lrc_content = r.text lrc_content = r.text
if lrc_content: if lrc_content:
# 过滤广告内容
lrc_content = self._filter_lrc_ads(lrc_content) lrc_content = self._filter_lrc_ads(lrc_content)
result["lrc"] = lrc_content result["lrc"] = lrc_content
except: except:
@@ -146,19 +327,15 @@ class Spider(Spider):
] ]
for line in lines: for line in lines:
# 保留时间标签行,但过滤掉广告文本
if re.match(r'\[\d{2}:\d{2}', line): if re.match(r'\[\d{2}:\d{2}', line):
# 检查是否包含广告
is_ad = False is_ad = False
for pattern in ad_patterns: for pattern in ad_patterns:
if re.search(pattern, line, re.IGNORECASE): if re.search(pattern, line, re.IGNORECASE):
is_ad = True is_ad = True
break break
if not is_ad: if not is_ad:
filtered_lines.append(line) filtered_lines.append(line)
else: else:
# 非时间标签行(可能是元数据),保留
filtered_lines.append(line) filtered_lines.append(line)
return '\n'.join(filtered_lines) return '\n'.join(filtered_lines)
@@ -168,12 +345,22 @@ class Spider(Spider):
type_ = param.get("type") type_ = param.get("type")
if type_ == "img": if type_ == "img":
return [200, "image/jpeg", self.session.get(url, headers={"Referer": self.host + "/"}, timeout=5).content, {}] try:
headers = {
"Referer": "https://www.baidu.com/",
"User-Agent": self.headers["User-Agent"],
"Accept": "image/webp,image/apng,image/*,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9"
}
resp = self.session.get(url, headers=headers, timeout=10)
return [200, "image/jpeg", resp.content, {}]
except Exception as e:
print(f"图片代理失败: {e}")
return [404, "text/plain", b"", {}]
elif type_ == "lrc": elif type_ == "lrc":
try: try:
r = self.session.get(url, headers={"Referer": self.host + "/"}, timeout=5) r = self.session.get(url, headers={"Referer": self.host + "/"}, timeout=5)
# 同时过滤代理中的广告
lrc_content = r.text lrc_content = r.text
lrc_content = self._filter_lrc_ads(lrc_content) lrc_content = self._filter_lrc_ads(lrc_content)
return [200, "application/octet-stream", lrc_content.encode('utf-8'), {}] return [200, "application/octet-stream", lrc_content.encode('utf-8'), {}]
@@ -182,21 +369,63 @@ class Spider(Spider):
return None return None
# ==================== 辅助方法 ====================
def _parse_list(self, items, tid=""): def _parse_list(self, items, tid=""):
"""解析列表项,修复歌手头像 - 直接返回原始图片URL"""
res = [] res = []
for li in items.items(): for li in items.items():
a = li("a").eq(0) a = li("a").eq(0)
if not (href := a.attr("href")) or href == "/" or any(x in href for x in ["/user/", "/login/", "javascript"]): continue if not (href := a.attr("href")) or href == "/" or any(x in href for x in ["/user/", "/login/", "javascript"]):
if not (name := self._clean(li(".name").text() or a.attr("title") or a.text())): continue continue
pic = self._abs((li("img").attr("src") or "").replace('120', '500')) if not (name := self._clean(li(".name").text() or a.attr("title") or a.text())):
res.append({"vod_id": self._abs(href), "vod_name": name, "vod_pic": f"{self.getProxyUrl()}&url={pic}&type=img" if pic else "", "style": {"type": "oval" if "/singer/" in href else ("list" if any(x in tid for x in ["/list/", "/playtype/", "/albumlist/"]) else "rect"), "ratio": 1 if "/singer/" in href else 1.33}}) continue
is_singer = "/singer/" in href or "/singerlist" in tid
pic = ""
src = ""
if is_singer:
img = li(".pic img").eq(0)
src = img.attr("src") or ""
if not src:
img = li("img").eq(0)
src = img.attr("src") or ""
else:
img = li("img").eq(0)
src = img.attr("src") or ""
if not src:
img = li(".pic img").eq(0)
src = img.attr("src") or ""
if src:
if src.startswith('//'):
src = 'https:' + src
elif src.startswith('/'):
src = self.host + src
pic = src
res.append({
"vod_id": self._abs(href),
"vod_name": name,
"vod_pic": pic,
"style": {
"type": "oval" if is_singer else ("list" if any(
x in tid for x in ["/list/", "/playtype/", "/albumlist/"]) else "rect"),
"ratio": 1 if is_singer else 1.33
}
})
return res return res
def _get_eps(self, doc): def _get_eps(self, doc):
eps = [] eps = []
for li in doc(".play_list li, .song_list li, .music_list li").items(): for li in doc(".play_list li, .song_list li, .music_list li").items():
if not (a := li("a").eq(0)).attr("href") or not re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', a.attr("href")): continue a = li("a").eq(0)
full_url = self._abs(a.attr("href")) href = a.attr("href")
if not href or not re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', href):
continue
full_url = self._abs(href)
lrc_part = "" lrc_part = ""
mid = re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', full_url) mid = re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', full_url)
@@ -207,34 +436,67 @@ class Spider(Spider):
eps.append(f"{self._clean(a.text() or li('.name').text())}${self.e64('0@@@@' + full_url + lrc_part)}") eps.append(f"{self._clean(a.text() or li('.name').text())}${self.e64('0@@@@' + full_url + lrc_part)}")
return eps return eps
def _clean(self, text): return re.sub(r'(爱玩音乐网|视频下载说明|视频下载地址|www\.2t58\.com|MP3免费下载|LRC歌词下载|全部歌曲|\[第\d+页\]|刷新|每日推荐|最新|热门|推荐|MV|高清|无损)', '', text or "", flags=re.I).strip() def _clean(self, text):
return re.sub(
r'(爱玩音乐网|视频下载说明|视频下载地址|www\.2t58\.com|MP3免费下载|LRC歌词下载|全部歌曲|\[第\d+页\]|刷新|每日推荐|最新|热门|推荐|MV|高清|无损)',
'',
text or '',
flags=re.I
).strip()
def _fetch_filters(self, url): def _fetch_filters(self, url):
doc, filters = self.getpq(url), [] doc = self.getpq(url)
for i, group in enumerate([doc(s) for s in [".ilingku_fl", ".class_list", ".screen_list", ".box_list", ".nav_list"] if doc(s)]): filters = []
opts, seen = [{"n": "全部", "v": "top" if "top" in url else "index"}], set() for i, group in enumerate([
for a in group("a").items(): doc(".ilingku_fl"),
if (v := (a.attr("href") or "").split("?")[0].rstrip('/').split('/')[-1].replace('.html','')) and v not in seen: doc(".class_list"),
opts.append({"n": a.text().strip(), "v": v}); seen.add(v) doc(".screen_list"),
if len(opts) > 1: filters.append({"key": f"id{i}" if i else "id", "name": "分类", "value": opts}) doc(".box_list"),
doc(".nav_list")
]):
if group:
opts = [{"n": "全部", "v": "top" if "top" in url else "index"}]
seen = set()
for a in group("a").items():
v = (a.attr("href") or "").split("?")[0].rstrip('/').split('/')[-1].replace('.html', '')
if v and v not in seen:
opts.append({"n": a.text().strip(), "v": v})
seen.add(v)
if len(opts) > 1:
filters.append({"key": f"id{i}" if i else "id", "name": "分类", "value": opts})
return filters return filters
def _api(self, path, params=None, method="GET", headers=None, data=None): def _api(self, path, params=None, method="GET", headers=None, data=None):
try: try:
h = self.headers.copy() h = self.headers.copy()
if headers: h.update(headers) if headers:
r = (self.session.post if method == "POST" else self.session.get)(f"{self.host}{path}", params=params, data=data, headers=h, timeout=10, allow_redirects=False) h.update(headers)
if loc := r.headers.get("Location"): return self._abs(loc.strip()) r = (self.session.post if method == "POST" else self.session.get)(
return self._abs(r.json().get("url", "").replace(r"\/", "/")) or (r.text.strip() if r.text.strip().startswith("http") else "") f"{self.host}{path}",
except: return "" params=params,
data=data,
headers=h,
timeout=10,
allow_redirects=False
)
if loc := r.headers.get("Location"):
return self._abs(loc.strip())
return self._abs(r.json().get("url", "").replace(r"\/", "/")) or (r.text.strip() if r.text.strip().startswith(
"http") else "")
except:
return ""
def getpq(self, url): def _abs(self, url):
import time if not url:
for _ in range(2): return ""
try: return pq(self.session.get(self._abs(url), timeout=5).text) if url.startswith("http"):
except: time.sleep(0.1) return url
return pq("<html></html>") if url.startswith("//"):
return "https:" + url
return f"{self.host}{'/' if not url.startswith('/') else ''}{url}"
def _abs(self, url): return url if url.startswith("http") else (f"{self.host}{'/' if not url.startswith('/') else ''}{url}" if url else "") def e64(self, text):
def e64(self, text): return b64encode(text.encode("utf-8")).decode("utf-8") return b64encode(text.encode("utf-8")).decode("utf-8")
def d64(self, text): return b64decode(text.encode("utf-8")).decode("utf-8")
def d64(self, text):
return b64decode(text.encode("utf-8")).decode("utf-8")
BIN
View File
Binary file not shown.