# -*- coding: utf-8 -*- import re, urllib.parse import json from bs4 import BeautifulSoup import requests from base.spider import Spider as BaseSpider class Spider(BaseSpider): def init(self, extend=""): self.host = "https://maihaolian.com" self.headers = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", } def getName(self): return '枫叶影院' def homeContent(self, filter): return {"class": [ {'type_id': "/label/qq", 'type_name': "腾讯VIP精选"}, {'type_id': "/label/bli", 'type_name': "B站VIP精选"}, {'type_id': "/label/youku", 'type_name': "优酷VIP精选"}, {"type_id": "5", "type_name": "红果短剧"}, {"type_id": "2", "type_name": "电视剧"}, {"type_id": "1", "type_name": "电影"}, {"type_id": "4", "type_name": "动漫"}, {"type_id": "3", "type_name": "综艺"}, ], "filters": self._build_filters()} def _build_filters(self): area = [{"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": "其它"}] year = [{"n": "全部", "v": ""}, {"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"}, {"n": "2016", "v": "2016"}, {"n": "2015", "v": "2015"}, {"n": "2014", "v": "2014"}, {"n": "2013", "v": "2013"}, {"n": "2012", "v": "2012"}, {"n": "2011", "v": "2011"}, {"n": "2010", "v": "2010"}, {"n": "2009", "v": "2009"}, {"n": "2008", "v": "2008"}, {"n": "2007", "v": "2007"}, {"n": "2006", "v": "2006"}, {"n": "2005", "v": "2005"}, {"n": "2004", "v": "2004"}] lang = [{"n": "全部", "v": ""}, {"n": "国语", "v": "国语"}, {"n": "英语", "v": "英语"}, {"n": "粤语", "v": "粤语"}, {"n": "闽南语", "v": "闽南语"}, {"n": "韩语", "v": "韩语"}, {"n": "日语", "v": "日语"}, {"n": "法语", "v": "法语"}, {"n": "德语", "v": "德语"}, {"n": "其它", "v": "其它"}] sort = [{"n": "时间", "v": "time"}, {"n": "人气", "v": "hits"}, {"n": "评分", "v": "score"}] letter = [{"n": "全部", "v": ""}, {"n": "A", "v": "A"}, {"n": "B", "v": "B"}, {"n": "C", "v": "C"}, {"n": "D", "v": "D"}, {"n": "E", "v": "E"}, {"n": "F", "v": "F"}, {"n": "G", "v": "G"}, {"n": "H", "v": "H"}, {"n": "I", "v": "I"}, {"n": "J", "v": "J"}, {"n": "K", "v": "K"}, {"n": "L", "v": "L"}, {"n": "M", "v": "M"}, {"n": "N", "v": "N"}, {"n": "O", "v": "O"}, {"n": "P", "v": "P"}, {"n": "Q", "v": "Q"}, {"n": "R", "v": "R"}, {"n": "S", "v": "S"}, {"n": "T", "v": "T"}, {"n": "U", "v": "U"}, {"n": "V", "v": "V"}, {"n": "W", "v": "W"}, {"n": "X", "v": "X"}, {"n": "Y", "v": "Y"}, {"n": "Z", "v": "Z"}, {"n": "0-9", "v": "0-9"}] return { "2": [ {"key": "class", "name": "类型", "value": [{"n": "全部", "v": "2"}, {"n": "国产剧", "v": "13"}, {"n": "日韩剧", "v": "15"}, {"n": "海外剧", "v": "16"}]}, {"key": "area", "name": "地区", "value": area}, {"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in [("全部", ""), ("古装", "古装"), ("战争", "战争"), ("青春偶像", "青春偶像"), ("喜剧", "喜剧"), ("家庭", "家庭"), ("犯罪", "犯罪"), ("动作", "动作"), ("奇幻", "奇幻"), ("剧情", "剧情"), ("历史", "历史"), ("经典", "经典"), ("乡村", "乡村"), ("情景", "情景"), ("商战", "商战"), ("网剧", "网剧"), ("其他", "其他")]]}, {"key": "year", "name": "年份", "value": year}, {"key": "lang", "name": "语言", "value": lang}, {"key": "letter", "name": "字母", "value": letter}, {"key": "sort", "name": "排序", "value": sort}, ], "1": [ {"key": "class", "name": "类型", "value": [{"n": "全部", "v": "1"}, {"n": "动作片", "v": "6"}, {"n": "喜剧片", "v": "7"}, {"n": "恐怖片", "v": "8"}, {"n": "科幻片", "v": "9"}, {"n": "爱情片", "v": "10"}, {"n": "剧情片", "v": "11"}, {"n": "战争片", "v": "12"}, {"n": "纪录片", "v": "20"}]}, {"key": "area", "name": "地区", "value": area}, {"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in [("全部", ""), ("喜剧", "喜剧"), ("爱情", "爱情"), ("恐怖", "恐怖"), ("动作", "动作"), ("科幻", "科幻"), ("剧情", "剧情"), ("战争", "战争"), ("警匪", "警匪"), ("犯罪", "犯罪"), ("动画", "动画"), ("奇幻", "奇幻"), ("武侠", "武侠"), ("冒险", "冒险"), ("枪战", "枪战"), ("悬疑", "悬疑"), ("惊悚", "惊悚"), ("经典", "经典"), ("青春", "青春"), ("文艺", "文艺"), ("微电影", "微电影"), ("古装", "古装"), ("历史", "历史"), ("运动", "运动"), ("农村", "农村"), ("儿童", "儿童"), ("网络电影", "网络电影")]]}, {"key": "year", "name": "年份", "value": year}, {"key": "lang", "name": "语言", "value": lang}, {"key": "letter", "name": "字母", "value": letter}, {"key": "sort", "name": "排序", "value": sort}, ], "4": [ {"key": "class", "name": "类型", "value": [{"n": "全部", "v": "4"}, {"n": "国产动漫", "v": "25"}, {"n": "日韩动漫", "v": "26"}]}, {"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in [("全部", ""), ("情感", "情感"), ("科幻", "科幻"), ("热血", "热血"), ("推理", "推理"), ("搞笑", "搞笑"), ("冒险", "冒险"), ("奇幻", "奇幻"), ("战斗", "战斗"), ("校园", "校园"), ("萝莉", "萝莉"), ("治愈", "治愈"), ("原创", "原创"), ("亲子", "亲子"), ("益智", "益智"), ("励志", "励志"), ("其他", "其他")]]}, {"key": "area", "name": "地区", "value": [{"n": "全部", "v": ""}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"}, {"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"}, {"n": "日本", "v": "日本"}, {"n": "法国", "v": "法国"}, {"n": "英国", "v": "英国"}, {"n": "其它", "v": "其它"}]}, {"key": "year", "name": "年份", "value": year}, {"key": "lang", "name": "语言", "value": lang}, {"key": "letter", "name": "字母", "value": letter}, {"key": "sort", "name": "排序", "value": sort}, ], "3": [ {"key": "class", "name": "类型", "value": [{"n": "全部", "v": "3"}, {"n": "大陆综艺", "v": "21"}, {"n": "日韩综艺", "v": "22"}]}, {"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in [("全部", ""), ("选秀", "选秀"), ("情感", "情感"), ("访谈", "访谈"), ("播报", "播报"), ("音乐", "音乐"), ("美食", "美食"), ("旅游", "旅游"), ("搞笑", "搞笑"), ("游戏", "游戏"), ("亲子", "亲子"), ("其它", "其它")]]}, {"key": "area", "name": "地区", "value": [{"n": "全部", "v": ""}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"}, {"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"}, {"n": "日本", "v": "日本"}, {"n": "英国", "v": "英国"}, {"n": "其它", "v": "其它"}]}, {"key": "year", "name": "年份", "value": year}, {"key": "lang", "name": "语言", "value": lang}, {"key": "letter", "name": "字母", "value": letter}, {"key": "sort", "name": "排序", "value": sort}, ], } def homeVideoContent(self): html = self._fetch('/') return {"list": self._parse_video_list(html)} def categoryContent(self, tid, pg, filter, extend): # 构建筛选参数:参照歪比巴卜,直接取extend里的值,fallback到filter if tid.startswith('/label'): url = f'{tid}/page/{pg}.html' html = self._fetch(url) items = self._parse_video_list(html) page = int(pg) page_count = page if len(items) < 24 else page + 2 return {"list": items, "page": page, "pagecount": page_count, "limit": 24, "total": page_count * 24} args = {} if extend and isinstance(extend, dict): for k, v in extend.items(): if v: args[k] = str(v) if isinstance(filter, dict): for k, v in filter.items(): if v and k not in args: args[k] = str(v) route_tid = args.get('class', args.get('tid', str(tid))) area = args.get('area', '') genre = args.get('genre', '') year = args.get('year', '') lang = args.get('lang', '') letter = args.get('letter', '') sort = args.get('sort', '') # 无筛选走正常分页 if not area and not genre and not year and not lang and not letter and not sort: url = f'/cupfox-list/{route_tid}--------{pg}---.html' html = self._fetch(url) items = self._parse_video_list(html) page = int(pg) soup = BeautifulSoup(html, 'html.parser') pagecount = page for a in soup.select('a.page-link'): if a.text == '尾页': m = re.search(r'---(\d+)---', a.get('href', '')) if m: pagecount = int(m.group(1)) break if not items: pagecount = 0 return {"list": items, "page": page, "pagecount": pagecount, "limit": 36, "total": 9999} # 有筛选:{tid}-{area}-{sort}-{genre}-{lang}-{letter}------{year}.html segs = [route_tid, area, sort, genre, lang, letter, '', '', year] url = '/cupfox-list/' + '-'.join(segs) + '.html' html = self._fetch(url) items = self._parse_video_list(html) return {"list": items, "page": 1, "pagecount": 1, "limit": 36, "total": 9999} def detailContent(self, ids): result = {"list": []} vid = ids[0].split(',')[0].strip() try: html = self._fetch(f'/detail/{vid}.html') if not html: return result soup = BeautifulSoup(html, 'html.parser') vod_name = soup.select_one('h3.slide-info-title') vod_name = vod_name.text.strip() if vod_name else '' vod_pic = soup.select_one('img.lazy') vod_pic = self._fix_pic(vod_pic.get('data-src', '')) if vod_pic else '' vod_director = '' vod_actor = '' for el in soup.select('.slide-info'): text = el.get_text(' ').strip() if text.startswith('导演:'): vod_director = text.replace('导演:', '').strip() elif text.startswith('演员:'): vod_actor = text.replace('演员:', '').strip() vod_content = soup.select_one('#height_limit') vod_content = vod_content.get_text(' ', strip=True) if vod_content else '' play_from, play_url = [], [] for tab in soup.select('.anthology-tab a.swiper-slide'): src_name = re.sub(r'<[^>]+>', '', str(tab)).strip() or tab.get_text(' ', strip=True).strip() if src_name: play_from.append(src_name) tab_blocks = soup.select('.anthology-list-box') for i, block in enumerate(tab_blocks): ep_list = [] for a in block.select('li a'): href = a.get('href', '') m = re.search(r'/play/(.*?)\.html', href) if m: ep_list.append(f'{a.text.strip()}${vid}-{m.group(1)}') ep_list.reverse() if ep_list and i < len(play_from): play_url.append('#'.join(ep_list)) valid_from = [pf for i, pf in enumerate(play_from) if i < len(play_url)] result["list"].append({ "vod_id": vid, "vod_name": vod_name, "vod_pic": vod_pic, "vod_director": vod_director, "vod_actor": vod_actor, "vod_content": vod_content, "vod_play_from": "$$$".join(valid_from), "vod_play_url": "$$$".join(play_url), }) except: pass return result def searchContent(self, key, quick, pg="1"): try: decoded = urllib.parse.unquote(key) except: decoded = key html = self._fetch(f'/cupfox-search/{urllib.parse.quote(decoded)}----------{pg}---.html') items = self._parse_search_list(html) return {"list": items, "page": int(pg), "pagecount": 1, "limit": 36, "total": len(items)} def playerContent(self, flag, id, vipFlags): url = '' try: url = id if id.startswith('http') else f'{self.host}/play/{id}.html' html = self._fetch(url) if html: m = re.search(r'player_aaaa=(.*?)', html, re.S) if m: try: pd = json.loads(m.group(1)) except Exception as e: print(e) pd = {} # print('pd:', pd) play_url = pd.get('url') play_id = pd.get('from') api_map = { 'YYNB': 'https://zzrs.mfdyvip.com/player/mplayer.php', 'JD4K': 'https://fgsrg.hzqingshan.com/player/mplayer.php', } if not play_url: return {"parse": 0, "url": 'https://php.doube.eu.org/error.m3u8', "header": {'User-Agent': 'Mozilla/5.0'}} if play_url.startswith('http') and (play_url.endswith('.m3u8') or play_url.endswith('.mp4')): return {"parse": 0, "url": play_url, "header": {'User-Agent': 'Mozilla/5.0'}} else: headers = { 'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", 'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 'accept-language': "zh-CN,zh;q=0.9", 'cache-control': "no-cache", 'pragma': "no-cache", 'priority': "u=0, i", 'referer': "https://www.ht10010.com/", 'Content-Type': 'application/x-www-form-urlencoded', } response = requests.get(f"https://fgsrg.hzqingshan.com/player/?url={play_url}", headers=headers) token = re.search(r'data-te="(.*?)"', response.text) if token: token = token.group(1) payload = { 'url': play_url, 'token': token } # print('payload', payload) try: response = self.post(api_map[play_id], data=payload, headers=headers) response.raise_for_status() result = response.json() # print('result:', result) if result['code'] == 200 and 'url' in result: play_url = result['url'] return {"parse": 0, "url": play_url, "header": { 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1'}} except Exception as e: print(e) except Exception as e: print(e) return {"parse": 1, "url": url} def localProxy(self, param=''): return {} def isVideoFormat(self, url): return False def manualVideoCheck(self): return False def _fetch(self, url): try: if not url.startswith('http'): url = self.host + url rsp = self.fetch(url, headers=self.headers) return rsp.text if rsp else '' except: return '' def _fix_pic(self, u): if not u: return '' if u.startswith('//'): return 'https:' + u return u.replace('&', '&') def _parse_video_list(self, html): videos, seen = [], set() soup = BeautifulSoup(html, 'html.parser') cards = soup.select('a.public-list-exp') for a in cards: href = a.get('href', '') m = re.search(r'/detail/(\d+)\.html', href) if not m: continue vod_id = m.group(1) if vod_id in seen: continue seen.add(vod_id) span = ','.join([span.text for span in a.select('span.public-prt')]) # print('span', span) vod_name = a.get('title', '') or (a.select_one('img') and a.select_one('img').get('alt', '')) or '' pic_el = a.select_one('img') vod_pic = self._fix_pic(pic_el.get('data-src', '')) if pic_el else '' remark_el = a.select_one('.ft2') or a.select_one('.public-list-prb') vod_remarks = remark_el.text.strip() if remark_el else '' videos.append( {"vod_id": vod_id, "vod_name": vod_name.strip(), "vod_pic": vod_pic, "vod_remarks": vod_remarks, "vod_year": span}) return videos def _parse_search_list(self, html): videos, seen = [], set() soup = BeautifulSoup(html, 'html.parser') cards = soup.select('a.public-list-exp') for a in cards: href = a.get('href', '') m = re.search(r'/detail/(\d+)\.html', href) if not m: continue vod_id = m.group(1) if vod_id in seen: continue seen.add(vod_id) pic_el = a.select_one('img') vod_pic = self._fix_pic(pic_el.get('data-src', '')) if pic_el else '' title_el = soup.select_one(f'a.thumb-txt[href="/detail/{vod_id}.html"]') if title_el: vod_name = title_el.text.strip() else: vod_name = a.select_one('img') and a.select_one('img').get('alt', '') or '' remark_el = a.select_one('.public-list-prb') or a.select_one('.ft2') vod_remarks = remark_el.text.strip() if remark_el else '' videos.append( {"vod_id": vod_id, "vod_name": vod_name.strip(), "vod_pic": vod_pic, "vod_remarks": vod_remarks}) return videos if __name__ == '__main__': sp = Spider() sp.init() # 20067-5-189 print(sp.categoryContent('/label/qq','1',True, {})) # print(sp.playerContent('', '20067-6-189', [])) # print(sp.playerContent('', '20067-5-189', [])) pass