更新源 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},
+502 -240
View File
@@ -1,240 +1,502 @@
import re # -*- coding: utf-8 -*-
import sys # 修复:歌手不显示歌手图片
from base64 import b64encode, b64decode # by:垃圾星河
from urllib.parse import quote, unquote # 代码指导:嗷呜呜呜呜
from pyquery import PyQuery as pq # 增加:自动人机验证绕过
from requests import Session, adapters
from urllib3.util.retry import Retry import re
from concurrent.futures import ThreadPoolExecutor, as_completed import sys
sys.path.append('..') import time
from base.spider import Spider from base64 import b64encode, b64decode
from urllib.parse import quote, unquote
class Spider(Spider): from pyquery import PyQuery as pq
def init(self, extend=""): from requests import Session, adapters
self.host = "https://www.22a5.com" from urllib3.util.retry import Retry
self.session = Session() from concurrent.futures import ThreadPoolExecutor, as_completed
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) sys.path.append('..')
self.session.mount("https://", adapter) from base.spider import Spider
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)
class Spider(Spider):
def getName(self): return "爱听音乐" def init(self, extend=""):
def isVideoFormat(self, url): return bool(re.search(r'\.(m3u8|mp4|mp3|m4a|flv)(\?|$)', url or "", re.I)) self.host = "https://www.22a5.com"
def manualVideoCheck(self): return False self.session = Session()
def destroy(self): self.session.close() adapter = adapters.HTTPAdapter(
max_retries=Retry(total=3, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504]),
def homeContent(self, filter): pool_connections=20,
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")]] pool_maxsize=50
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))} )
self.session.mount("http://", adapter)
if "/radiolist/index.html" not in filters: self.session.mount("https://", adapter)
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"])]}] 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"
filters["/singerlist/index/index/index/index.html"] = [ }
{"key": "area", "name": "地区", "value": [{"n": n, "v": v} for n,v in [("全部","index"),("华语","huayu"),("欧美","oumei"),("韩国","hanguo"),("日本","ribrn")]]}, self.session.headers.update(self.headers)
{"key": "sex", "name": "性别", "value": [{"n": n, "v": v} for n,v in [("全部","index"),("","male"),("","girl"),("组合","band")]]},
{"key": "genre", "name": "流派", "value": [{"n": n, "v": v} for n,v in [("全部","index"),("流行","liuxing"),("电子","dianzi"),("摇滚","yaogun"),("嘻哈","xiha"),("R&B","rb"),("民谣","minyao"),("爵士","jueshi"),("古典","gudian")]]}, def getName(self):
{"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)]]} return "爱听音乐"
]
return {"class": classes, "filters": filters, "list": []} def isVideoFormat(self, url):
return bool(re.search(r'\.(m3u8|mp4|mp3|m4a|flv)(\?|$)', url or "", re.I))
def homeVideoContent(self): return {"list": []}
def manualVideoCheck(self):
def categoryContent(self, tid, pg, filter, extend): return False
pg = int(pg or 1)
url = tid def destroy(self):
if "/singerlist/" in tid: self.session.close()
p = tid.split('/')
if len(p) >= 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"])
elif "id" in extend and extend["id"] not in ["index", "top"]: def _bypass_verification(self, url, response_text):
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 '安全人机验证' not in response_text and 'human_check' not in response_text:
return None
if pg > 1:
sep = "/" if any(x in url for x in ["/singerlist/", "/radiolist/", "/mvlist/", "/playtype/", "/list/"]) else "_" # 提取 csrf_token(支持多种格式)
url = re.sub(r'(_\d+|/\d+)?\.html$', f'{sep}{pg}.html', url) token_match = re.search(r'name="csrf_token"\s+value="([^"]+)"', response_text)
if not token_match:
doc = self.getpq(url) print("[爱听音乐] 未找到 csrf_token,跳过绕过")
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} return None
token = token_match.group(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)} # 构造提交数据
data = {
def detailContent(self, ids): 'csrf_token': token,
url = self._abs(ids[0]) 'human_check': 'on'
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": ""}
try:
if any(x in url for x in ["/playlist/", "/album/", "/list/", "/singer/", "/special/", "/radio/", "/radiolist/"]): # 发送 POST 请求,自动跟随重定向
eps = self._get_eps(doc) post_resp = self.session.post(url, data=data, allow_redirects=True, timeout=10)
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} print("[爱听音乐] 人机验证已绕过")
if page_urls: return post_resp
with ThreadPoolExecutor(max_workers=5) as ex: except Exception as e:
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)]): print(f"[爱听音乐] 人机验证提交失败: {e}")
eps.extend(r.result() or []) return None
if eps:
vod.update({"vod_play_from": "播放列表", "vod_play_url": "#".join(eps)}) def getpq(self, url):
return {"list": [vod]} """获取页面并自动处理人机验证(重试3次)"""
full_url = self._abs(url)
play_list = []
if mid := re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', url): for attempt in range(3):
lrc_url = f"{self.host}/plug/down.php?ac=music&lk=lrc&id={mid.group(2)}" try:
play_list = [f"播放${self.e64('0@@@@' + url + '|||' + lrc_url)}"] resp = self.session.get(full_url, timeout=5)
elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url): # 若遇到验证页,尝试绕过
with ThreadPoolExecutor(max_workers=3) as ex: if '安全人机验证' in resp.text or 'human_check' in resp.text:
fs = {ex.submit(self._api, "/plug/down.php", {"ac": "vplay", "id": vid.group(2), "q": q}): n for n, q in [("蓝光", 1080), ("超清", 720), ("高清", 480)]} print("[爱听音乐] 检测到人机验证,尝试自动绕过...")
play_list = [f"{fs[f]}${self.e64('0@@@@'+u)}" for f in as_completed(fs) if (u := f.result())] bypass_resp = self._bypass_verification(full_url, resp.text)
play_list.sort(key=lambda x: {"":0, "":1, "":2}.get(x[0], 3)) if bypass_resp:
# 验证成功后,返回最终页面(可能是重定向后的目标)
vod["vod_play_url"] = "#".join(play_list) if play_list else f"解析失败${self.e64('1@@@@'+url)}" return pq(bypass_resp.text)
return {"list": [vod]} else:
# 绕过失败,等待后重试
def playerContent(self, flag, id, vipFlags): time.sleep(1)
raw = self.d64(id).split("@@@@")[-1] continue
url, subt = raw.split("|||") if "|||" in raw else (raw, "") else:
url = url.replace(r"\/", "/") # 正常页面直接返回
return pq(resp.text)
if ".html" in url and not self.isVideoFormat(url):
if mid := re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', url): except Exception as e:
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"}): print(f"[爱听音乐] 请求失败 (尝试 {attempt+1}/3): {e}")
url = r_url if ".php" not in r_url else url time.sleep(0.5 * (attempt + 1))
elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url):
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]]): return pq("<html></html>")
if v_url := f.result():
url = v_url; break # ==================== 原有功能(保持不变) ====================
result = {"parse": 0, "url": url, "header": {"User-Agent": self.headers["User-Agent"]}} def homeContent(self, filter):
if "22a5.com" in url: result["header"]["Referer"] = self.host + "/" classes = [
{"type_name": n, "type_id": i}
# OK影视3.6.5+支持LRC格式滚动歌词 for n, i in [
if subt: ("歌手", "/singerlist/index/index/index/index.html"),
try: ("TOP榜单", "/list/top.html"),
r = self.session.get(subt, headers={"Referer": self.host + "/"}, timeout=5) ("新歌榜", "/list/new.html"),
lrc_content = r.text ("电台", "/radiolist/index.html"),
if lrc_content: ("高清MV", "/mvlist/oumei.html"),
# 过滤广告内容 ("专辑", "/albumlist/index.html"),
lrc_content = self._filter_lrc_ads(lrc_content) ("歌单", "/playtype/index.html")
result["lrc"] = lrc_content ]
except: ]
pass filters = {}
for p in [c["type_id"] for c in classes if "singer" not in c["type_id"]]:
return result d = self._fetch_filters(p)
if d:
def _filter_lrc_ads(self, lrc_text): filters[p] = d
"""过滤LRC歌词中的广告内容"""
lines = lrc_text.splitlines() if "/radiolist/index.html" not in filters:
filtered_lines = [] filters["/radiolist/index.html"] = [{
"key": "id",
# 广告关键词模式 "name": "分类",
ad_patterns = [ "value": [
r'欢迎来访.*', {"n": n, "v": v}
r'本站.*', for n, v in zip(
r'.*广告.*', ["最新", "最热", "有声小说", "相声", "音乐", "情感", "国漫", "影视", "脱口秀", "历史", "儿童", "教育", "八卦", "推理", "头条"],
r'QQ群.*', ["index", "hot", "novel", "xiangyi", "music", "emotion", "game", "yingshi", "talkshow", "history",
r'.*www\..*', "children", "education", "gossip", "tuili", "headline"]
r'.*http.*', )
r'.*\.com.*', ]
r'.*\.cn.*', }]
r'.*\.net.*',
r'.*音乐网.*', filters["/singerlist/index/index/index/index.html"] = [
r'.*提供.*', {
r'.*下载.*', "key": "area",
] "name": "地区",
"value": [
for line in lines: {"n": "全部", "v": "index"},
# 保留时间标签行,但过滤掉广告文本 {"n": "华语", "v": "huayu"},
if re.match(r'\[\d{2}:\d{2}', line): {"n": "欧美", "v": "oumei"},
# 检查是否包含广告 {"n": "韩国", "v": "hanguo"},
is_ad = False {"n": "日本", "v": "ribrn"}
for pattern in ad_patterns: ]
if re.search(pattern, line, re.IGNORECASE): },
is_ad = True {
break "key": "sex",
"name": "性别",
if not is_ad: "value": [
filtered_lines.append(line) {"n": "全部", "v": "index"},
else: {"n": "", "v": "male"},
# 非时间标签行(可能是元数据),保留 {"n": "", "v": "girl"},
filtered_lines.append(line) {"n": "组合", "v": "band"}
]
return '\n'.join(filtered_lines) },
{
def localProxy(self, param): "key": "genre",
url = unquote(param.get("url", "")) "name": "流派",
type_ = param.get("type") "value": [
{"n": "全部", "v": "index"},
if type_ == "img": {"n": "流行", "v": "liuxing"},
return [200, "image/jpeg", self.session.get(url, headers={"Referer": self.host + "/"}, timeout=5).content, {}] {"n": "电子", "v": "dianzi"},
{"n": "摇滚", "v": "yaogun"},
elif type_ == "lrc": {"n": "嘻哈", "v": "xiha"},
try: {"n": "R&B", "v": "rb"},
r = self.session.get(url, headers={"Referer": self.host + "/"}, timeout=5) {"n": "民谣", "v": "minyao"},
# 同时过滤代理中的广告 {"n": "爵士", "v": "jueshi"},
lrc_content = r.text {"n": "古典", "v": "gudian"}
lrc_content = self._filter_lrc_ads(lrc_content) ]
return [200, "application/octet-stream", lrc_content.encode('utf-8'), {}] }
except: ]
return [404, "text/plain", "Error", {}] return {"class": classes, "filters": filters, "list": []}
return None def homeVideoContent(self):
return {"list": []}
def _parse_list(self, items, tid=""):
res = [] def categoryContent(self, tid, pg, filter, extend):
for li in items.items(): pg = int(pg or 1)
a = li("a").eq(0) url = tid
if not (href := a.attr("href")) or href == "/" or any(x in href for x in ["/user/", "/login/", "javascript"]): continue if "/singerlist/" in tid:
if not (name := self._clean(li(".name").text() or a.attr("title") or a.text())): continue parts = tid.split('/')
pic = self._abs((li("img").attr("src") or "").replace('120', '500')) if len(parts) >= 6:
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}}) url = "/".join(parts[:2] + [extend.get(k, parts[i]) for i, k in enumerate(["area", "sex", "genre"], 2)] +
return res [f"{extend.get('char', 'index')}.html"])
elif "id" in extend and extend["id"] not in ["index", "top"]:
def _get_eps(self, doc): url = tid.replace("index.html", f"{extend['id']}.html").replace("top.html", f"{extend['id']}.html")
eps = [] if url == tid:
for li in doc(".play_list li, .song_list li, .music_list li").items(): url = f"{tid.rsplit('/', 1)[0]}/{extend['id']}.html"
if not (a := li("a").eq(0)).attr("href") or not re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', a.attr("href")): continue
full_url = self._abs(a.attr("href")) if pg > 1:
sep = "/" if any(x in url for x in ["/singerlist/", "/radiolist/", "/mvlist/", "/playtype/", "/list/"]) else "_"
lrc_part = "" url = re.sub(r'(_\d+|/\d+)?\.html$', f'{sep}{pg}.html', url)
mid = re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', full_url)
if mid: doc = self.getpq(url)
lrc_url = f"{self.host}/plug/down.php?ac=music&lk=lrc&id={mid.group(2)}" items = doc(".play_list li, .video_list li, .pic_list li, .singer_list li, .ali li, .layui-row li, .base_l li")
lrc_part = f"|||{lrc_url}" return {
"list": self._parse_list(items, tid),
eps.append(f"{self._clean(a.text() or li('.name').text())}${self.e64('0@@@@' + full_url + lrc_part)}") "page": pg,
return eps "pagecount": 9999,
"limit": 90,
def _clean(self, text): return re.sub(r'(爱玩音乐网|视频下载说明|视频下载地址|www\.2t58\.com|MP3免费下载|LRC歌词下载|全部歌曲|\[第\d+页\]|刷新|每日推荐|最新|热门|推荐|MV|高清|无损)', '', text or "", flags=re.I).strip() "total": 999999
}
def _fetch_filters(self, url):
doc, filters = self.getpq(url), [] def searchContent(self, key, quick, pg="1"):
for i, group in enumerate([doc(s) for s in [".ilingku_fl", ".class_list", ".screen_list", ".box_list", ".nav_list"] if doc(s)]): doc = self.getpq(f"/so/{quote(key)}/{pg}.html")
opts, seen = [{"n": "全部", "v": "top" if "top" in url else "index"}], set() items = doc(".base_l li, .play_list li")
for a in group("a").items(): return {
if (v := (a.attr("href") or "").split("?")[0].rstrip('/').split('/')[-1].replace('.html','')) and v not in seen: "list": self._parse_list(items, "search"),
opts.append({"n": a.text().strip(), "v": v}); seen.add(v) "page": int(pg)
if len(opts) > 1: filters.append({"key": f"id{i}" if i else "id", "name": "分类", "value": opts}) }
return filters
def detailContent(self, ids):
def _api(self, path, params=None, method="GET", headers=None, data=None): url = self._abs(ids[0])
try: doc = self.getpq(url)
h = self.headers.copy() vod = {
if headers: h.update(headers) "vod_id": url,
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) "vod_name": self._clean(doc("h1").text() or doc("title").text()),
if loc := r.headers.get("Location"): return self._abs(loc.strip()) "vod_pic": self._abs(doc(".djpg img, .pic img, .djpic img").attr("src")),
return self._abs(r.json().get("url", "").replace(r"\/", "/")) or (r.text.strip() if r.text.strip().startswith("http") else "") "vod_play_from": "爱听音乐",
except: return "" "vod_content": ""
}
def getpq(self, url):
import time if any(x in url for x in ["/playlist/", "/album/", "/list/", "/singer/", "/special/", "/radio/", "/radiolist/"]):
for _ in range(2): eps = self._get_eps(doc)
try: return pq(self.session.get(self._abs(url), timeout=5).text) page_urls = {
except: time.sleep(0.1) self._abs(a.attr("href"))
return pq("<html></html>") for a in doc(".page a, .dede_pages a, .pagelist a").items()
if a.attr("href") and "javascript" not in a.attr("href")
def _abs(self, url): return url if url.startswith("http") else (f"{self.host}{'/' if not url.startswith('/') else ''}{url}" if url else "") } - {url}
def e64(self, text): return b64encode(text.encode("utf-8")).decode("utf-8") if page_urls:
def d64(self, text): return b64decode(text.encode("utf-8")).decode("utf-8") with ThreadPoolExecutor(max_workers=5) as ex:
futures = []
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:
vod.update({
"vod_play_from": "播放列表",
"vod_play_url": "#".join(eps)
})
return {"list": [vod]}
play_list = []
if mid := re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', url):
lrc_url = f"{self.host}/plug/down.php?ac=music&lk=lrc&id={mid.group(2)}"
play_list = [f"播放${self.e64('0@@@@' + url + '|||' + lrc_url)}"]
elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url):
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)]
}
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))
vod["vod_play_url"] = "#".join(play_list) if play_list else f"解析失败${self.e64('1@@@@'+url)}"
return {"list": [vod]}
def playerContent(self, flag, id, vipFlags):
raw = self.d64(id).split("@@@@")[-1]
url, subt = raw.split("|||") if "|||" in raw else (raw, "")
url = url.replace(r"\/", "/")
if ".html" in url and not self.isVideoFormat(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"}):
url = r_url if ".php" not in r_url else url
elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url):
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]]):
if v_url := f.result():
url = v_url
break
result = {"parse": 0, "url": url, "header": {"User-Agent": self.headers["User-Agent"]}}
if "22a5.com" in url:
result["header"]["Referer"] = self.host + "/"
# OK影视3.6.5+支持LRC格式滚动歌词
if subt:
try:
r = self.session.get(subt, headers={"Referer": self.host + "/"}, timeout=5)
lrc_content = r.text
if lrc_content:
lrc_content = self._filter_lrc_ads(lrc_content)
result["lrc"] = lrc_content
except:
pass
return result
def _filter_lrc_ads(self, lrc_text):
"""过滤LRC歌词中的广告内容"""
lines = lrc_text.splitlines()
filtered_lines = []
# 广告关键词模式
ad_patterns = [
r'欢迎来访.*',
r'本站.*',
r'.*广告.*',
r'QQ群.*',
r'.*www\..*',
r'.*http.*',
r'.*\.com.*',
r'.*\.cn.*',
r'.*\.net.*',
r'.*音乐网.*',
r'.*提供.*',
r'.*下载.*',
]
for line in lines:
if re.match(r'\[\d{2}:\d{2}', line):
is_ad = False
for pattern in ad_patterns:
if re.search(pattern, line, re.IGNORECASE):
is_ad = True
break
if not is_ad:
filtered_lines.append(line)
else:
filtered_lines.append(line)
return '\n'.join(filtered_lines)
def localProxy(self, param):
url = unquote(param.get("url", ""))
type_ = param.get("type")
if type_ == "img":
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":
try:
r = self.session.get(url, headers={"Referer": self.host + "/"}, timeout=5)
lrc_content = r.text
lrc_content = self._filter_lrc_ads(lrc_content)
return [200, "application/octet-stream", lrc_content.encode('utf-8'), {}]
except:
return [404, "text/plain", "Error", {}]
return None
# ==================== 辅助方法 ====================
def _parse_list(self, items, tid=""):
"""解析列表项,修复歌手头像 - 直接返回原始图片URL"""
res = []
for li in items.items():
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 (name := self._clean(li(".name").text() or a.attr("title") or a.text())):
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
def _get_eps(self, doc):
eps = []
for li in doc(".play_list li, .song_list li, .music_list li").items():
a = li("a").eq(0)
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 = ""
mid = re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', full_url)
if mid:
lrc_url = f"{self.host}/plug/down.php?ac=music&lk=lrc&id={mid.group(2)}"
lrc_part = f"|||{lrc_url}"
eps.append(f"{self._clean(a.text() or li('.name').text())}${self.e64('0@@@@' + full_url + lrc_part)}")
return eps
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):
doc = self.getpq(url)
filters = []
for i, group in enumerate([
doc(".ilingku_fl"),
doc(".class_list"),
doc(".screen_list"),
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
def _api(self, path, params=None, method="GET", headers=None, data=None):
try:
h = self.headers.copy()
if headers:
h.update(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
)
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 _abs(self, url):
if not url:
return ""
if url.startswith("http"):
return url
if url.startswith("//"):
return "https:" + url
return f"{self.host}{'/' if not url.startswith('/') else ''}{url}"
def e64(self, text):
return b64encode(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},
+502 -240
View File
@@ -1,240 +1,502 @@
import re # -*- coding: utf-8 -*-
import sys # 修复:歌手不显示歌手图片
from base64 import b64encode, b64decode # by:垃圾星河
from urllib.parse import quote, unquote # 代码指导:嗷呜呜呜呜
from pyquery import PyQuery as pq # 增加:自动人机验证绕过
from requests import Session, adapters
from urllib3.util.retry import Retry import re
from concurrent.futures import ThreadPoolExecutor, as_completed import sys
sys.path.append('..') import time
from base.spider import Spider from base64 import b64encode, b64decode
from urllib.parse import quote, unquote
class Spider(Spider): from pyquery import PyQuery as pq
def init(self, extend=""): from requests import Session, adapters
self.host = "https://www.22a5.com" from urllib3.util.retry import Retry
self.session = Session() from concurrent.futures import ThreadPoolExecutor, as_completed
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) sys.path.append('..')
self.session.mount("https://", adapter) from base.spider import Spider
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)
class Spider(Spider):
def getName(self): return "爱听音乐" def init(self, extend=""):
def isVideoFormat(self, url): return bool(re.search(r'\.(m3u8|mp4|mp3|m4a|flv)(\?|$)', url or "", re.I)) self.host = "https://www.22a5.com"
def manualVideoCheck(self): return False self.session = Session()
def destroy(self): self.session.close() adapter = adapters.HTTPAdapter(
max_retries=Retry(total=3, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504]),
def homeContent(self, filter): pool_connections=20,
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")]] pool_maxsize=50
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))} )
self.session.mount("http://", adapter)
if "/radiolist/index.html" not in filters: self.session.mount("https://", adapter)
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"])]}] 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"
filters["/singerlist/index/index/index/index.html"] = [ }
{"key": "area", "name": "地区", "value": [{"n": n, "v": v} for n,v in [("全部","index"),("华语","huayu"),("欧美","oumei"),("韩国","hanguo"),("日本","ribrn")]]}, self.session.headers.update(self.headers)
{"key": "sex", "name": "性别", "value": [{"n": n, "v": v} for n,v in [("全部","index"),("","male"),("","girl"),("组合","band")]]},
{"key": "genre", "name": "流派", "value": [{"n": n, "v": v} for n,v in [("全部","index"),("流行","liuxing"),("电子","dianzi"),("摇滚","yaogun"),("嘻哈","xiha"),("R&B","rb"),("民谣","minyao"),("爵士","jueshi"),("古典","gudian")]]}, def getName(self):
{"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)]]} return "爱听音乐"
]
return {"class": classes, "filters": filters, "list": []} def isVideoFormat(self, url):
return bool(re.search(r'\.(m3u8|mp4|mp3|m4a|flv)(\?|$)', url or "", re.I))
def homeVideoContent(self): return {"list": []}
def manualVideoCheck(self):
def categoryContent(self, tid, pg, filter, extend): return False
pg = int(pg or 1)
url = tid def destroy(self):
if "/singerlist/" in tid: self.session.close()
p = tid.split('/')
if len(p) >= 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"])
elif "id" in extend and extend["id"] not in ["index", "top"]: def _bypass_verification(self, url, response_text):
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 '安全人机验证' not in response_text and 'human_check' not in response_text:
return None
if pg > 1:
sep = "/" if any(x in url for x in ["/singerlist/", "/radiolist/", "/mvlist/", "/playtype/", "/list/"]) else "_" # 提取 csrf_token(支持多种格式)
url = re.sub(r'(_\d+|/\d+)?\.html$', f'{sep}{pg}.html', url) token_match = re.search(r'name="csrf_token"\s+value="([^"]+)"', response_text)
if not token_match:
doc = self.getpq(url) print("[爱听音乐] 未找到 csrf_token,跳过绕过")
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} return None
token = token_match.group(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)} # 构造提交数据
data = {
def detailContent(self, ids): 'csrf_token': token,
url = self._abs(ids[0]) 'human_check': 'on'
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": ""}
try:
if any(x in url for x in ["/playlist/", "/album/", "/list/", "/singer/", "/special/", "/radio/", "/radiolist/"]): # 发送 POST 请求,自动跟随重定向
eps = self._get_eps(doc) post_resp = self.session.post(url, data=data, allow_redirects=True, timeout=10)
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} print("[爱听音乐] 人机验证已绕过")
if page_urls: return post_resp
with ThreadPoolExecutor(max_workers=5) as ex: except Exception as e:
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)]): print(f"[爱听音乐] 人机验证提交失败: {e}")
eps.extend(r.result() or []) return None
if eps:
vod.update({"vod_play_from": "播放列表", "vod_play_url": "#".join(eps)}) def getpq(self, url):
return {"list": [vod]} """获取页面并自动处理人机验证(重试3次)"""
full_url = self._abs(url)
play_list = []
if mid := re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', url): for attempt in range(3):
lrc_url = f"{self.host}/plug/down.php?ac=music&lk=lrc&id={mid.group(2)}" try:
play_list = [f"播放${self.e64('0@@@@' + url + '|||' + lrc_url)}"] resp = self.session.get(full_url, timeout=5)
elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url): # 若遇到验证页,尝试绕过
with ThreadPoolExecutor(max_workers=3) as ex: if '安全人机验证' in resp.text or 'human_check' in resp.text:
fs = {ex.submit(self._api, "/plug/down.php", {"ac": "vplay", "id": vid.group(2), "q": q}): n for n, q in [("蓝光", 1080), ("超清", 720), ("高清", 480)]} print("[爱听音乐] 检测到人机验证,尝试自动绕过...")
play_list = [f"{fs[f]}${self.e64('0@@@@'+u)}" for f in as_completed(fs) if (u := f.result())] bypass_resp = self._bypass_verification(full_url, resp.text)
play_list.sort(key=lambda x: {"":0, "":1, "":2}.get(x[0], 3)) if bypass_resp:
# 验证成功后,返回最终页面(可能是重定向后的目标)
vod["vod_play_url"] = "#".join(play_list) if play_list else f"解析失败${self.e64('1@@@@'+url)}" return pq(bypass_resp.text)
return {"list": [vod]} else:
# 绕过失败,等待后重试
def playerContent(self, flag, id, vipFlags): time.sleep(1)
raw = self.d64(id).split("@@@@")[-1] continue
url, subt = raw.split("|||") if "|||" in raw else (raw, "") else:
url = url.replace(r"\/", "/") # 正常页面直接返回
return pq(resp.text)
if ".html" in url and not self.isVideoFormat(url):
if mid := re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', url): except Exception as e:
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"}): print(f"[爱听音乐] 请求失败 (尝试 {attempt+1}/3): {e}")
url = r_url if ".php" not in r_url else url time.sleep(0.5 * (attempt + 1))
elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url):
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]]): return pq("<html></html>")
if v_url := f.result():
url = v_url; break # ==================== 原有功能(保持不变) ====================
result = {"parse": 0, "url": url, "header": {"User-Agent": self.headers["User-Agent"]}} def homeContent(self, filter):
if "22a5.com" in url: result["header"]["Referer"] = self.host + "/" classes = [
{"type_name": n, "type_id": i}
# OK影视3.6.5+支持LRC格式滚动歌词 for n, i in [
if subt: ("歌手", "/singerlist/index/index/index/index.html"),
try: ("TOP榜单", "/list/top.html"),
r = self.session.get(subt, headers={"Referer": self.host + "/"}, timeout=5) ("新歌榜", "/list/new.html"),
lrc_content = r.text ("电台", "/radiolist/index.html"),
if lrc_content: ("高清MV", "/mvlist/oumei.html"),
# 过滤广告内容 ("专辑", "/albumlist/index.html"),
lrc_content = self._filter_lrc_ads(lrc_content) ("歌单", "/playtype/index.html")
result["lrc"] = lrc_content ]
except: ]
pass filters = {}
for p in [c["type_id"] for c in classes if "singer" not in c["type_id"]]:
return result d = self._fetch_filters(p)
if d:
def _filter_lrc_ads(self, lrc_text): filters[p] = d
"""过滤LRC歌词中的广告内容"""
lines = lrc_text.splitlines() if "/radiolist/index.html" not in filters:
filtered_lines = [] filters["/radiolist/index.html"] = [{
"key": "id",
# 广告关键词模式 "name": "分类",
ad_patterns = [ "value": [
r'欢迎来访.*', {"n": n, "v": v}
r'本站.*', for n, v in zip(
r'.*广告.*', ["最新", "最热", "有声小说", "相声", "音乐", "情感", "国漫", "影视", "脱口秀", "历史", "儿童", "教育", "八卦", "推理", "头条"],
r'QQ群.*', ["index", "hot", "novel", "xiangyi", "music", "emotion", "game", "yingshi", "talkshow", "history",
r'.*www\..*', "children", "education", "gossip", "tuili", "headline"]
r'.*http.*', )
r'.*\.com.*', ]
r'.*\.cn.*', }]
r'.*\.net.*',
r'.*音乐网.*', filters["/singerlist/index/index/index/index.html"] = [
r'.*提供.*', {
r'.*下载.*', "key": "area",
] "name": "地区",
"value": [
for line in lines: {"n": "全部", "v": "index"},
# 保留时间标签行,但过滤掉广告文本 {"n": "华语", "v": "huayu"},
if re.match(r'\[\d{2}:\d{2}', line): {"n": "欧美", "v": "oumei"},
# 检查是否包含广告 {"n": "韩国", "v": "hanguo"},
is_ad = False {"n": "日本", "v": "ribrn"}
for pattern in ad_patterns: ]
if re.search(pattern, line, re.IGNORECASE): },
is_ad = True {
break "key": "sex",
"name": "性别",
if not is_ad: "value": [
filtered_lines.append(line) {"n": "全部", "v": "index"},
else: {"n": "", "v": "male"},
# 非时间标签行(可能是元数据),保留 {"n": "", "v": "girl"},
filtered_lines.append(line) {"n": "组合", "v": "band"}
]
return '\n'.join(filtered_lines) },
{
def localProxy(self, param): "key": "genre",
url = unquote(param.get("url", "")) "name": "流派",
type_ = param.get("type") "value": [
{"n": "全部", "v": "index"},
if type_ == "img": {"n": "流行", "v": "liuxing"},
return [200, "image/jpeg", self.session.get(url, headers={"Referer": self.host + "/"}, timeout=5).content, {}] {"n": "电子", "v": "dianzi"},
{"n": "摇滚", "v": "yaogun"},
elif type_ == "lrc": {"n": "嘻哈", "v": "xiha"},
try: {"n": "R&B", "v": "rb"},
r = self.session.get(url, headers={"Referer": self.host + "/"}, timeout=5) {"n": "民谣", "v": "minyao"},
# 同时过滤代理中的广告 {"n": "爵士", "v": "jueshi"},
lrc_content = r.text {"n": "古典", "v": "gudian"}
lrc_content = self._filter_lrc_ads(lrc_content) ]
return [200, "application/octet-stream", lrc_content.encode('utf-8'), {}] }
except: ]
return [404, "text/plain", "Error", {}] return {"class": classes, "filters": filters, "list": []}
return None def homeVideoContent(self):
return {"list": []}
def _parse_list(self, items, tid=""):
res = [] def categoryContent(self, tid, pg, filter, extend):
for li in items.items(): pg = int(pg or 1)
a = li("a").eq(0) url = tid
if not (href := a.attr("href")) or href == "/" or any(x in href for x in ["/user/", "/login/", "javascript"]): continue if "/singerlist/" in tid:
if not (name := self._clean(li(".name").text() or a.attr("title") or a.text())): continue parts = tid.split('/')
pic = self._abs((li("img").attr("src") or "").replace('120', '500')) if len(parts) >= 6:
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}}) url = "/".join(parts[:2] + [extend.get(k, parts[i]) for i, k in enumerate(["area", "sex", "genre"], 2)] +
return res [f"{extend.get('char', 'index')}.html"])
elif "id" in extend and extend["id"] not in ["index", "top"]:
def _get_eps(self, doc): url = tid.replace("index.html", f"{extend['id']}.html").replace("top.html", f"{extend['id']}.html")
eps = [] if url == tid:
for li in doc(".play_list li, .song_list li, .music_list li").items(): url = f"{tid.rsplit('/', 1)[0]}/{extend['id']}.html"
if not (a := li("a").eq(0)).attr("href") or not re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', a.attr("href")): continue
full_url = self._abs(a.attr("href")) if pg > 1:
sep = "/" if any(x in url for x in ["/singerlist/", "/radiolist/", "/mvlist/", "/playtype/", "/list/"]) else "_"
lrc_part = "" url = re.sub(r'(_\d+|/\d+)?\.html$', f'{sep}{pg}.html', url)
mid = re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', full_url)
if mid: doc = self.getpq(url)
lrc_url = f"{self.host}/plug/down.php?ac=music&lk=lrc&id={mid.group(2)}" items = doc(".play_list li, .video_list li, .pic_list li, .singer_list li, .ali li, .layui-row li, .base_l li")
lrc_part = f"|||{lrc_url}" return {
"list": self._parse_list(items, tid),
eps.append(f"{self._clean(a.text() or li('.name').text())}${self.e64('0@@@@' + full_url + lrc_part)}") "page": pg,
return eps "pagecount": 9999,
"limit": 90,
def _clean(self, text): return re.sub(r'(爱玩音乐网|视频下载说明|视频下载地址|www\.2t58\.com|MP3免费下载|LRC歌词下载|全部歌曲|\[第\d+页\]|刷新|每日推荐|最新|热门|推荐|MV|高清|无损)', '', text or "", flags=re.I).strip() "total": 999999
}
def _fetch_filters(self, url):
doc, filters = self.getpq(url), [] def searchContent(self, key, quick, pg="1"):
for i, group in enumerate([doc(s) for s in [".ilingku_fl", ".class_list", ".screen_list", ".box_list", ".nav_list"] if doc(s)]): doc = self.getpq(f"/so/{quote(key)}/{pg}.html")
opts, seen = [{"n": "全部", "v": "top" if "top" in url else "index"}], set() items = doc(".base_l li, .play_list li")
for a in group("a").items(): return {
if (v := (a.attr("href") or "").split("?")[0].rstrip('/').split('/')[-1].replace('.html','')) and v not in seen: "list": self._parse_list(items, "search"),
opts.append({"n": a.text().strip(), "v": v}); seen.add(v) "page": int(pg)
if len(opts) > 1: filters.append({"key": f"id{i}" if i else "id", "name": "分类", "value": opts}) }
return filters
def detailContent(self, ids):
def _api(self, path, params=None, method="GET", headers=None, data=None): url = self._abs(ids[0])
try: doc = self.getpq(url)
h = self.headers.copy() vod = {
if headers: h.update(headers) "vod_id": url,
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) "vod_name": self._clean(doc("h1").text() or doc("title").text()),
if loc := r.headers.get("Location"): return self._abs(loc.strip()) "vod_pic": self._abs(doc(".djpg img, .pic img, .djpic img").attr("src")),
return self._abs(r.json().get("url", "").replace(r"\/", "/")) or (r.text.strip() if r.text.strip().startswith("http") else "") "vod_play_from": "爱听音乐",
except: return "" "vod_content": ""
}
def getpq(self, url):
import time if any(x in url for x in ["/playlist/", "/album/", "/list/", "/singer/", "/special/", "/radio/", "/radiolist/"]):
for _ in range(2): eps = self._get_eps(doc)
try: return pq(self.session.get(self._abs(url), timeout=5).text) page_urls = {
except: time.sleep(0.1) self._abs(a.attr("href"))
return pq("<html></html>") for a in doc(".page a, .dede_pages a, .pagelist a").items()
if a.attr("href") and "javascript" not in a.attr("href")
def _abs(self, url): return url if url.startswith("http") else (f"{self.host}{'/' if not url.startswith('/') else ''}{url}" if url else "") } - {url}
def e64(self, text): return b64encode(text.encode("utf-8")).decode("utf-8") if page_urls:
def d64(self, text): return b64decode(text.encode("utf-8")).decode("utf-8") with ThreadPoolExecutor(max_workers=5) as ex:
futures = []
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:
vod.update({
"vod_play_from": "播放列表",
"vod_play_url": "#".join(eps)
})
return {"list": [vod]}
play_list = []
if mid := re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', url):
lrc_url = f"{self.host}/plug/down.php?ac=music&lk=lrc&id={mid.group(2)}"
play_list = [f"播放${self.e64('0@@@@' + url + '|||' + lrc_url)}"]
elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url):
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)]
}
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))
vod["vod_play_url"] = "#".join(play_list) if play_list else f"解析失败${self.e64('1@@@@'+url)}"
return {"list": [vod]}
def playerContent(self, flag, id, vipFlags):
raw = self.d64(id).split("@@@@")[-1]
url, subt = raw.split("|||") if "|||" in raw else (raw, "")
url = url.replace(r"\/", "/")
if ".html" in url and not self.isVideoFormat(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"}):
url = r_url if ".php" not in r_url else url
elif vid := re.search(r'/(video|mp4)/([^/]+)\.html', url):
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]]):
if v_url := f.result():
url = v_url
break
result = {"parse": 0, "url": url, "header": {"User-Agent": self.headers["User-Agent"]}}
if "22a5.com" in url:
result["header"]["Referer"] = self.host + "/"
# OK影视3.6.5+支持LRC格式滚动歌词
if subt:
try:
r = self.session.get(subt, headers={"Referer": self.host + "/"}, timeout=5)
lrc_content = r.text
if lrc_content:
lrc_content = self._filter_lrc_ads(lrc_content)
result["lrc"] = lrc_content
except:
pass
return result
def _filter_lrc_ads(self, lrc_text):
"""过滤LRC歌词中的广告内容"""
lines = lrc_text.splitlines()
filtered_lines = []
# 广告关键词模式
ad_patterns = [
r'欢迎来访.*',
r'本站.*',
r'.*广告.*',
r'QQ群.*',
r'.*www\..*',
r'.*http.*',
r'.*\.com.*',
r'.*\.cn.*',
r'.*\.net.*',
r'.*音乐网.*',
r'.*提供.*',
r'.*下载.*',
]
for line in lines:
if re.match(r'\[\d{2}:\d{2}', line):
is_ad = False
for pattern in ad_patterns:
if re.search(pattern, line, re.IGNORECASE):
is_ad = True
break
if not is_ad:
filtered_lines.append(line)
else:
filtered_lines.append(line)
return '\n'.join(filtered_lines)
def localProxy(self, param):
url = unquote(param.get("url", ""))
type_ = param.get("type")
if type_ == "img":
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":
try:
r = self.session.get(url, headers={"Referer": self.host + "/"}, timeout=5)
lrc_content = r.text
lrc_content = self._filter_lrc_ads(lrc_content)
return [200, "application/octet-stream", lrc_content.encode('utf-8'), {}]
except:
return [404, "text/plain", "Error", {}]
return None
# ==================== 辅助方法 ====================
def _parse_list(self, items, tid=""):
"""解析列表项,修复歌手头像 - 直接返回原始图片URL"""
res = []
for li in items.items():
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 (name := self._clean(li(".name").text() or a.attr("title") or a.text())):
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
def _get_eps(self, doc):
eps = []
for li in doc(".play_list li, .song_list li, .music_list li").items():
a = li("a").eq(0)
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 = ""
mid = re.search(r'/(song|mp3|radio|radiolist|radioplay)/([^/]+)\.html', full_url)
if mid:
lrc_url = f"{self.host}/plug/down.php?ac=music&lk=lrc&id={mid.group(2)}"
lrc_part = f"|||{lrc_url}"
eps.append(f"{self._clean(a.text() or li('.name').text())}${self.e64('0@@@@' + full_url + lrc_part)}")
return eps
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):
doc = self.getpq(url)
filters = []
for i, group in enumerate([
doc(".ilingku_fl"),
doc(".class_list"),
doc(".screen_list"),
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
def _api(self, path, params=None, method="GET", headers=None, data=None):
try:
h = self.headers.copy()
if headers:
h.update(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
)
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 _abs(self, url):
if not url:
return ""
if url.startswith("http"):
return url
if url.startswith("//"):
return "https:" + url
return f"{self.host}{'/' if not url.startswith('/') else ''}{url}"
def e64(self, text):
return b64encode(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.