375 lines
16 KiB
Python
375 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
|
# @Author: 竹佀
|
|
# @Date: 2026-03-28
|
|
# @Description: 喜马拉雅音频爬虫
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
sys.path.append('../../')
|
|
from base.spider import Spider
|
|
|
|
class Spider(Spider):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.AUDIO_API_BASE = "https://api-v2.cenguigui.cn/api/music/ximalaya.php"
|
|
self.XIMALAYA_API = "https://www.ximalaya.com/revision"
|
|
self.MOBILE_API = "http://mobile.ximalaya.com/mobile"
|
|
self.headers = {
|
|
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148'
|
|
}
|
|
|
|
def getName(self):
|
|
return "喜马拉雅"
|
|
|
|
def init(self, extend=""):
|
|
pass
|
|
|
|
def isVideoFormat(self, url):
|
|
return False
|
|
|
|
def manualVideoCheck(self):
|
|
return False
|
|
|
|
def destroy(self):
|
|
pass
|
|
|
|
def homeContent(self, filter):
|
|
"""首页推荐分类"""
|
|
result = {}
|
|
classes = [
|
|
{"type_id": "3", "type_name": "有声书"},
|
|
{"type_id": "15", "type_name": "广播剧"},
|
|
{"type_id": "2", "type_name": "音乐"},
|
|
{"type_id": "5", "type_name": "儿童"},
|
|
{"type_id": "9", "type_name": "人文"},
|
|
{"type_id": "8", "type_name": "娱乐"}
|
|
]
|
|
result['class'] = classes
|
|
|
|
# 筛选器配置
|
|
if filter:
|
|
result['filters'] = {
|
|
"3": [{"key": "tag", "name": "标签", "value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "玄幻", "v": "玄幻"},
|
|
{"n": "穿越", "v": "穿越"},
|
|
{"n": "重生", "v": "重生"},
|
|
{"n": "系统流", "v": "系统流"},
|
|
{"n": "脑洞", "v": "脑洞"},
|
|
{"n": "穿书", "v": "穿书"},
|
|
{"n": "虐渣", "v": "虐渣"},
|
|
{"n": "异能", "v": "异能"},
|
|
{"n": "网游", "v": "网游"},
|
|
{"n": "下山", "v": "下山"},
|
|
{"n": "黑科技", "v": "黑科技"},
|
|
{"n": "囤物资", "v": "囤物资"},
|
|
{"n": "机甲", "v": "机甲"},
|
|
{"n": "美食", "v": "美食"},
|
|
{"n": "基建", "v": "基建"},
|
|
{"n": "群穿", "v": "群穿"},
|
|
{"n": "逃荒", "v": "逃荒"},
|
|
{"n": "废材流", "v": "废材流"},
|
|
{"n": "无女主", "v": "无女主"},
|
|
{"n": "多女主", "v": "多女主"},
|
|
{"n": "单女主", "v": "单女主"},
|
|
{"n": "仙侠", "v": "仙侠"},
|
|
{"n": "都市", "v": "都市"},
|
|
{"n": "历史", "v": "历史"},
|
|
{"n": "现代言情", "v": "现代言情"},
|
|
{"n": "古代言情", "v": "古代言情"}
|
|
]}],
|
|
"15": [{"key": "tag", "name": "标签", "value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "言情", "v": "言情"},
|
|
{"n": "纯爱", "v": "纯爱"},
|
|
{"n": "悬疑", "v": "悬疑"},
|
|
{"n": "搞笑", "v": "搞笑"}
|
|
]}],
|
|
"2": [{"key": "tag", "name": "标签", "value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "华语流行", "v": "华语流行"},
|
|
{"n": "欧美金曲", "v": "欧美金曲"}
|
|
]}],
|
|
"5": [{"key": "tag", "name": "标签", "value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "儿童故事", "v": "儿童故事"},
|
|
{"n": "儿歌", "v": "儿歌"},
|
|
{"n": "启蒙教育", "v": "启蒙教育"}
|
|
]}],
|
|
"9": [{"key": "tag", "name": "标签", "value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "历史人文", "v": "历史人文"},
|
|
{"n": "哲学", "v": "哲学"},
|
|
{"n": "国学", "v": "国学"}
|
|
]}],
|
|
"8": [{"key": "tag", "name": "标签", "value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "相声", "v": "相声"},
|
|
{"n": "脱口秀", "v": "脱口秀"},
|
|
{"n": "搞笑", "v": "搞笑"}
|
|
]}]
|
|
}
|
|
|
|
# 获取热门推荐
|
|
videos = []
|
|
try:
|
|
url = f"{self.XIMALAYA_API}/category/v2/albums"
|
|
params = {"pageNum": 1, "pageSize": 20, "sort": 2, "categoryId": 3}
|
|
response = self.fetch(url, params=params, headers=self.headers)
|
|
if response and response.text:
|
|
data = json.loads(response.text)
|
|
if data.get('data'):
|
|
albums = data['data'].get('albums', []) or data['data'].get('list', [])
|
|
for album in albums[:12]:
|
|
videos.append({
|
|
"vod_id": str(album.get('albumId') or album.get('id')),
|
|
"vod_name": album.get('albumTitle') or album.get('title', ''),
|
|
"vod_pic": self.fixImageUrl(album.get('coverPath') or album.get('cover_path') or album.get('albumCoverPath', '')),
|
|
"vod_remarks": f"{album.get('playCount', 0)}播放"
|
|
})
|
|
except Exception as e:
|
|
print(f"[喜马拉雅] 首页获取失败: {e}")
|
|
|
|
result['list'] = videos
|
|
return result
|
|
|
|
def homeVideoContent(self):
|
|
"""首页视频内容"""
|
|
result = {'list': []}
|
|
try:
|
|
url = f"{self.XIMALAYA_API}/category/v2/albums"
|
|
params = {"pageNum": 1, "pageSize": 20, "sort": 2, "categoryId": 3}
|
|
response = self.fetch(url, params=params, headers=self.headers)
|
|
if response and response.text:
|
|
data = json.loads(response.text)
|
|
if data.get('data'):
|
|
albums = data['data'].get('albums', []) or data['data'].get('list', [])
|
|
for album in albums[:12]:
|
|
result['list'].append({
|
|
"vod_id": str(album.get('albumId') or album.get('id')),
|
|
"vod_name": album.get('albumTitle') or album.get('title', ''),
|
|
"vod_pic": self.fixImageUrl(album.get('coverPath') or album.get('cover_path') or album.get('albumCoverPath', '')),
|
|
"vod_remarks": f"{album.get('playCount', 0)}播放"
|
|
})
|
|
except Exception as e:
|
|
print(f"[喜马拉雅] homeVideoContent失败: {e}")
|
|
return result
|
|
|
|
def categoryContent(self, tid, pg, filter, extend):
|
|
"""分类内容"""
|
|
result = {}
|
|
videos = []
|
|
try:
|
|
tag = extend.get('tag', '') if extend else ''
|
|
url = f"{self.XIMALAYA_API}/category/v2/albums"
|
|
params = {
|
|
"pageNum": int(pg),
|
|
"pageSize": 20,
|
|
"sort": 2,
|
|
"categoryId": int(tid)
|
|
}
|
|
if tag:
|
|
params['metadataValues'] = tag
|
|
|
|
response = self.fetch(url, params=params, headers=self.headers)
|
|
if response and response.text:
|
|
data = json.loads(response.text)
|
|
if data.get('data'):
|
|
albums = data['data'].get('albums', []) or data['data'].get('list', [])
|
|
total = data['data'].get('total', 0)
|
|
for album in albums:
|
|
videos.append({
|
|
"vod_id": str(album.get('albumId') or album.get('id')),
|
|
"vod_name": album.get('albumTitle') or album.get('title', ''),
|
|
"vod_pic": self.fixImageUrl(album.get('coverPath') or album.get('cover_path') or album.get('albumCoverPath', '')),
|
|
"vod_remarks": f"{album.get('trackCount', album.get('tracksCount', 0))}集"
|
|
})
|
|
result['pagecount'] = (total // 20) + (1 if total % 20 > 0 else 0) if total > 0 else 1
|
|
result['total'] = total
|
|
else:
|
|
result['pagecount'] = 1
|
|
result['total'] = len(videos)
|
|
else:
|
|
result['pagecount'] = 1
|
|
result['total'] = 0
|
|
except Exception as e:
|
|
print(f"[喜马拉雅] 分类获取失败: {e}")
|
|
result['pagecount'] = 1
|
|
result['total'] = 0
|
|
|
|
result['list'] = videos
|
|
result['page'] = pg
|
|
result['limit'] = 20
|
|
return result
|
|
|
|
def detailContent(self, ids):
|
|
"""详情页"""
|
|
result = {}
|
|
try:
|
|
album_id = ids[0]
|
|
# 获取专辑详情和曲目列表
|
|
url = f"{self.MOBILE_API}/others/ca/album/track/{album_id}/true/1/30"
|
|
response = self.fetch(url, headers=self.headers)
|
|
|
|
if response and response.text:
|
|
data = json.loads(response.text)
|
|
if data.get('tracks'):
|
|
tracks = data['tracks'].get('list', [])
|
|
album_info = data.get('albumInfo', {})
|
|
|
|
# 从第一个 track 中提取专辑信息(如果 albumInfo 为空)
|
|
first_track = tracks[0] if tracks else {}
|
|
|
|
# 专辑名称:优先使用 albumInfo,否则从 track 中提取
|
|
album_title = (album_info.get('title') or
|
|
album_info.get('albumTitle') or
|
|
first_track.get('albumTitle') or
|
|
first_track.get('album_title') or
|
|
'未知专辑')
|
|
|
|
# 主播名称:优先使用 albumInfo,否则从 track 中提取
|
|
anchor_name = (album_info.get('anchorName') or
|
|
album_info.get('nickname') or
|
|
first_track.get('anchorName') or
|
|
first_track.get('nickname') or
|
|
'未知主播')
|
|
|
|
# 封面图:优先使用 albumInfo,否则从 track 中提取
|
|
artwork = (album_info.get('coverPath') or
|
|
album_info.get('cover_path') or
|
|
album_info.get('coverLarge') or
|
|
first_track.get('albumCoverPath') or
|
|
first_track.get('coverPath') or
|
|
first_track.get('coverLarge') or
|
|
first_track.get('cover') or
|
|
'')
|
|
|
|
# 简介
|
|
intro = (album_info.get('intro') or
|
|
album_info.get('albumIntro') or
|
|
first_track.get('albumIntro') or
|
|
'暂无简介')
|
|
|
|
# 构建播放列表
|
|
play_urls = []
|
|
for track in tracks:
|
|
track_id = track.get('trackId') or track.get('id')
|
|
title = track.get('title', '')
|
|
if track_id and title:
|
|
play_urls.append(f"{title}${track_id}")
|
|
|
|
vod = {
|
|
"vod_id": album_id,
|
|
"vod_name": album_title,
|
|
"vod_pic": self.fixImageUrl(artwork),
|
|
"vod_actor": anchor_name,
|
|
"vod_content": self.cleanHtml(intro),
|
|
"vod_play_from": "喜马拉雅",
|
|
"vod_play_url": "#".join(play_urls) if play_urls else ""
|
|
}
|
|
result['list'] = [vod]
|
|
else:
|
|
result['list'] = []
|
|
else:
|
|
result['list'] = []
|
|
except Exception as e:
|
|
print(f"[喜马拉雅] 详情获取失败: {e}")
|
|
result['list'] = []
|
|
return result
|
|
|
|
def searchContent(self, key, quick, pg=1):
|
|
"""搜索内容"""
|
|
result = {}
|
|
videos = []
|
|
try:
|
|
url = f"{self.XIMALAYA_API}/search"
|
|
params = {
|
|
"kw": key,
|
|
"page": pg,
|
|
"spellchecker": "true",
|
|
"condition": "relation",
|
|
"rows": 20,
|
|
"device": "iPhone",
|
|
"core": "album",
|
|
"paidFilter": "false"
|
|
}
|
|
response = self.fetch(url, params=params, headers=self.headers)
|
|
if response and response.text:
|
|
data = json.loads(response.text)
|
|
if data.get('data') and data['data'].get('result') and data['data']['result'].get('response'):
|
|
albums = data['data']['result']['response'].get('docs', [])
|
|
for album in albums:
|
|
# 搜索API返回的集数字段是 tracks,不是 trackCount
|
|
track_count = album.get('tracks', 0)
|
|
videos.append({
|
|
"vod_id": str(album.get('albumId') or album.get('id')),
|
|
"vod_name": album.get('title') or album.get('albumTitle', ''),
|
|
"vod_pic": self.fixImageUrl(album.get('coverPath') or album.get('cover_path') or album.get('albumCoverPath', '')),
|
|
"vod_remarks": f"{track_count}集"
|
|
})
|
|
result['list'] = videos
|
|
result['page'] = pg
|
|
result['pagecount'] = 9999 if len(videos) >= 20 else pg
|
|
except Exception as e:
|
|
print(f"[喜马拉雅] 搜索失败: {e}")
|
|
result['list'] = []
|
|
result['page'] = pg
|
|
result['pagecount'] = pg
|
|
return result
|
|
|
|
def playerContent(self, flag, id, vipFlags):
|
|
"""获取播放地址"""
|
|
result = {}
|
|
try:
|
|
# 解析真实ID(处理 $ 分隔符)
|
|
real_id = id
|
|
if '$' in str(id):
|
|
real_id = str(id).split('$')[-1]
|
|
|
|
url = f"{self.AUDIO_API_BASE}?trackId={real_id}"
|
|
response = self.fetch(url, headers=self.headers)
|
|
|
|
if response and response.text:
|
|
data = json.loads(response.text)
|
|
if data.get('url'):
|
|
result = {
|
|
"parse": 0,
|
|
"url": data['url'],
|
|
"header": json.dumps(self.headers)
|
|
}
|
|
else:
|
|
result = {"parse": 0, "url": "", "header": "{}"}
|
|
else:
|
|
result = {"parse": 0, "url": "", "header": "{}"}
|
|
except Exception as e:
|
|
print(f"[喜马拉雅] 获取播放地址失败: {e}")
|
|
result = {"parse": 0, "url": "", "header": "{}"}
|
|
return result
|
|
|
|
def fixImageUrl(self, url):
|
|
"""修复图片URL"""
|
|
if not url:
|
|
return ''
|
|
if url.startswith('http://') or url.startswith('https://'):
|
|
if '!op_type=' in url:
|
|
url = url.split('!op_type=')[0]
|
|
return url
|
|
if url.startswith('//'):
|
|
return f"https:{url}"
|
|
if url.startswith('storages/'):
|
|
return f"https://imagev2.xmcdn.com/{url}"
|
|
if url.startswith('/'):
|
|
return f"https://imagev2.xmcdn.com{url}"
|
|
return f"https://imagev2.xmcdn.com/{url}"
|
|
|
|
def cleanHtml(self, text):
|
|
"""清理HTML标签"""
|
|
if not text:
|
|
return "暂无简介"
|
|
clean = re.sub('<[^<]+?>', '', text)
|
|
return clean.strip()[:200] + '...' if len(clean) > 200 else clean.strip()
|
|
|
|
def localProxy(self, param):
|
|
return {"action": ""}
|