# coding = utf-8 #!/usr/bin/python import re import sys import json import time import urllib.parse from base.spider import Spider sys.path.append('..') class Spider(Spider): def __init__(self): self.name = "糖豆广场舞" self.host = 'https://api-h5.tangdou.com' self.img_host = 'https://bimg.tangdou.com' # 图片域名前缀 self.header = { 'Accept': 'application/json, text/plain, */*', 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'zh,zh-CN;q=0.9', 'Connection': 'keep-alive', 'Host': 'api-h5.tangdou.com', 'Referer': 'https://www.tangdou.com/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } # 缓存机制 self.cache = {} self.cache_timeout = 300 # 5分钟缓存 # 生成UUID (时间戳_随机数格式) self.uuid = f"{int(time.time() * 1000)}_{int(time.time() % 100000)}" def getName(self): return self.name def init(self, extend=''): pass def homeContent(self, filter): result = {} # 糖豆广场舞分类 classes = [ {"type_name": "广场舞", "type_id": "1"}, {"type_name": "民族舞", "type_id": "2"}, {"type_name": "jazz/现代舞", "type_id": "3"}, {"type_name": "健身", "type_id": "4"}, {"type_name": "双人舞", "type_id": "5"}, {"type_name": "步法", "type_id": "6"}, {"type_name": "气球", "type_id": "7"}, {"type_name": "瑜伽", "type_id": "8"}, {"type_name": "二人转", "type_id": "9"} ] result['class'] = classes # 筛选条件 - 主要按年份和难度筛选 filters = {} for cate in classes: tid = cate['type_id'] filters[tid] = [ {"key": "year", "name": "年份", "value": [ {"n": "全部", "v": "0"}, {"n": "2026", "v": "2026"}, {"n": "2025", "v": "2025"}, {"n": "2024", "v": "2024"}, {"n": "2023", "v": "2023"}, {"n": "2022", "v": "2022"}, {"n": "2021", "v": "2021"}, {"n": "2020", "v": "2020"}, {"n": "2019", "v": "2019"}, {"n": "2018", "v": "2018"}, {"n": "2017及以前", "v": "2017"} ]}, {"key": "sort", "name": "排序", "value": [ {"n": "最新", "v": "new"}, {"n": "最热", "v": "hot"}, {"n": "推荐", "v": "rec"} ]} ] result['filters'] = filters return result def homeVideoContent(self): # 首页推荐 - 获取feed流 videos = [] try: cache_key = "home_feed" data = self.get_cached_data(cache_key, 1, 20) if data and 'data' in data: for item in data['data']: video = self._parse_video_item(item) if video: videos.append(video) except Exception as e: print(f"获取首页推荐失败: {e}") return {'list': videos} def categoryContent(self, tid, pg, filter, extend): videos = [] try: # 构建请求参数 page_size = 30 # 糖豆API: type 0=推荐, 1=广场舞, etc. api_url = f"{self.host}/mtangdou/home/feed?page={pg}&num={page_size}&uuid={self.uuid}" # 如果有分类ID且不是推荐,添加分类参数 if tid != "0": api_url += f"&type={tid}" # 排序参数 sort = extend.get('sort', 'new') if sort == 'hot': api_url += "&sort=hot" elif sort == 'rec': api_url += "&sort=rec" cache_key = f"category_{tid}_{pg}_{sort}" data = self.fetchData(api_url, cache_key) if data and 'data' in data: for item in data['data']: video = self._parse_video_item(item) if video: videos.append(video) except Exception as e: print(f"获取分类内容失败: {e}") return { 'list': videos, 'page': int(pg), 'pagecount': 9999, # 糖豆没有明确页数限制 'limit': 30, 'total': 999999 } def detailContent(self, ids): try: vid = ids[0].split('||')[0] if '||' in ids[0] else ids[0] # 获取视频详情和播放链接 play_url_api = f"{self.host}/mtangdou/video/play?vid={vid}&uuid={self.uuid}" share_api = f"{self.host}/sample/share/main?vid={vid}" # 尝试获取播放链接 play_data = self.fetchData(play_url_api, f"play_{vid}", use_cache=False) # 获取详情信息 share_data = self.fetchData(share_api, f"share_{vid}", use_cache=False) if not share_data or 'data' not in share_data: return {'list': []} data_info = share_data['data'] # 获取简介内容,优先使用API返回的desc或description,否则使用默认简介 content = data_info.get('desc', data_info.get('description', '')).strip() if not content: content = '醉卧东风祝您身体健康' # 修正:拼接完整缩略图URL cover_path = data_info.get('cover', data_info.get('img', '')) if cover_path and not cover_path.startswith('http'): cover_url = self.img_host + cover_path else: cover_url = cover_path # 构建视频详情对象 video_detail = { "vod_id": vid, "vod_name": data_info.get('title', '').strip(), "vod_pic": cover_url, "vod_year": str(data_info.get('year', '')), "vod_area": data_info.get('area', '大陆'), "vod_actor": data_info.get('teacher', data_info.get('author', '')), "vod_director": "", "vod_content": content, "vod_play_from": "糖豆播放", "vod_remarks": f"时长: {data_info.get('duration_str', '未知')}" if 'duration_str' in data_info else "" } # 构建播放链接 - 糖豆是单视频,没有多集 play_url = "" if play_data and 'data' in play_data: play_url = play_data['data'].get('play_url', '') # 尝试从share接口获取video_url作为备选 if not play_url and 'video_url' in data_info: play_url = data_info['video_url'] if play_url: # 糖豆视频直接播放,需要处理Referer video_detail["vod_play_url"] = f"{video_detail['vod_name']}${vid}||{play_url}" else: video_detail["vod_play_url"] = "" return {'list': [video_detail]} except Exception as e: print(f"获取详情失败: {e}") return {'list': []} def searchContent(self, key, quick, pg=1): videos = [] try: # 糖豆搜索API search_api = f"{self.host}/mtangdou/search?word={urllib.parse.quote(key)}&page={pg}&num=30&uuid={self.uuid}" # 搜索不使用缓存,确保实时性 data = self.fetchData(search_api, use_cache=False) if data and 'data' in data: for item in data['data']: video = self._parse_video_item(item) if video: videos.append(video) else: # 如果搜索API返回空,尝试从首页feed中过滤(仅第一页) if pg == 1: feed_data = self.get_cached_data("search_feed", 1, 100) if feed_data and 'data' in feed_data: key_lower = key.lower() for item in feed_data['data']: title = item.get('title', '').lower() if key_lower in title: video = self._parse_video_item(item) if video: videos.append(video) except Exception as e: print(f"搜索失败: {e}") return { 'list': videos, 'page': int(pg), 'pagecount': 9999, 'limit': 30, 'total': 999999 } def playerContent(self, flag, id, vipFlags): try: # 解析传入的id: vid||play_url if '||' in id: parts = id.split('||') vid = parts[0] play_url = parts[1] if len(parts) > 1 else "" else: vid = id play_url = "" # 如果没有play_url,实时获取 if not play_url: play_api = f"{self.host}/mtangdou/video/play?vid={vid}&uuid={self.uuid}" data = self.fetchData(play_api, use_cache=False) if data and 'data' in data: play_url = data['data'].get('play_url', '') if play_url: # 糖豆视频需要Referer才能正常播放 headers = { "Referer": "https://www.tangdou.com/", "User-Agent": self.header['User-Agent'] } return { "parse": 0, # 直接播放,不需要解析 "playUrl": "", "url": play_url, "header": json.dumps(headers) } else: return {"parse": 0, "playUrl": "", "url": ""} except Exception as e: print(f"播放解析失败: {e}") return {"parse": 0, "playUrl": "", "url": ""} def isVideoFormat(self, url): video_formats = ['.m3u8', '.mp4', '.avi', '.mkv', '.flv', '.ts', '.mov'] return any(url.lower().endswith(fmt) for fmt in video_formats) def manualVideoCheck(self): pass def localProxy(self, params): return None def _parse_video_item(self, item): """解析视频列表项为统一格式""" try: vid = str(item.get('vid', '')) if not vid: return None title = item.get('title', '').strip() # 修正:拼接完整缩略图URL,cover是相对路径,需要加域名前缀 cover_path = item.get('cover', item.get('img', item.get('video_img', ''))) if cover_path and not cover_path.startswith('http'): img = self.img_host + cover_path else: img = cover_path duration = item.get('duration_str', '') teacher = item.get('teacher', item.get('author', '')) # 是否有附属信息(如老师名字) remarks = duration if duration else teacher return { "vod_id": vid, "vod_name": title, "vod_pic": img, "vod_remarks": remarks } except Exception as e: print(f"解析视频项失败: {e}") return None def get_cached_data(self, cache_key, page=1, num=30): """获取首页feed缓存""" current_time = time.time() if cache_key in self.cache: cached_data, timestamp = self.cache[cache_key] if current_time - timestamp < self.cache_timeout: return cached_data # 缓存不存在或已过期,重新获取 api_url = f"{self.host}/mtangdou/home/feed?page={page}&num={num}&uuid={self.uuid}" result = self.fetchData(api_url, cache_key) if result: self.cache[cache_key] = (result, current_time) return result def fetchData(self, url, cache_key=None, use_cache=True): """封装的数据获取方法,支持缓存""" try: # 如果启用缓存且存在有效缓存 current_time = time.time() if use_cache and cache_key and cache_key in self.cache: cached_data, timestamp = self.cache[cache_key] if current_time - timestamp < self.cache_timeout: return cached_data start_time = time.time() response = self.fetch(url, headers=self.header) end_time = time.time() print(f"请求耗时: {end_time - start_time:.2f}秒, URL: {url[:60]}...") if response.status_code != 200: print(f"API请求失败: {response.status_code}") return None data = json.loads(response.text) # 缓存结果 if use_cache and cache_key: self.cache[cache_key] = (data, current_time) return data except Exception as e: print(f"获取数据失败: {e}, URL: {url}") return None def fetch(self, url, headers=None): """发送HTTP GET请求""" import requests try: if headers is None: headers = self.header response = requests.get(url, headers=headers, timeout=10) return response except Exception as e: print(f"请求异常: {e}") # 返回一个模拟的response对象 class FakeResponse: status_code = 0 text = "" return FakeResponse() if __name__ == '__main__': pass