# -*- coding: utf-8 -*- # @Author : 老王叔叔 for 泥視頻.CC with Multi-Source Support # @Time : 2025/04/06 import sys import requests from lxml import etree import json import re from urllib.parse import urlencode sys.path.append('..') from base.spider import Spider class Spider(Spider): def __init__(self): self.home_url = 'https://www.nivod.cc' self.headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", "Referer": "https://www.nivod.cc/", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "keep-alive", } self.placeholder_pic = 'https://image.tmdb.org/t/p/w600_and_h900_bestv2/placeholder.jpg' def init(self, extend=""): pass def getName(self): return "泥視頻.CC" def getDependence(self): return [] def isVideoFormat(self, url): return url.endswith('.m3u8') or url.endswith('.mp4') def manualVideoCheck(self): return False def homeContent(self, filter): categories = "電影$movie#電視劇$tv#綜藝$show#動漫$anime" class_list = [{'type_id': v.split('$')[1], 'type_name': v.split('$')[0]} for v in categories.split('#')] filters = { 'movie': [ {'key': 'class', 'name': '剧情', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in "冒险$mao-xian#剧情$ju-qing#动作$dong-zuo#同性$tong-xing#喜剧$xi-ju#奇幻$qi-huan#恐怖$kong-bu#悬疑$xuan-yi#惊悚$jing-song#战争$zhan-zheng#歌舞$ge-wu#灾难$zai-nan#爱情$ai-qing#犯罪$fan-zui#科幻$ke-huan".split('#')]}, {'key': 'area', 'name': '地区', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in "大陆$cn#香港$hk#台湾$tw#欧美$west#泰国$th#新马$sg-my#其他$other".split('#')]}, {'key': 'year', 'name': '年份', 'value': [{'n': v, 'v': v} for v in ["2025", "2024", "2023", "2022", "2021", "2020", "2019-2010", "2009-2000", "90年代", "80年代", "更早"]]} ], 'tv': [ {'key': 'class', 'name': '剧情', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in "剧情$ju-qing#动作$dong-zuo#历史$li-shi#历险$mao-xian#古装$gu-zhuang#同性$tong-xing#喜剧$xi-ju#奇幻$qi-huan#家庭$jia-ting#悬疑$xuan-yi#惊悚$jing-song#战争$zhan-zheng#武侠$wu-xia#爱情$ai-qing#科幻$ke-huan#罪案$zui-an".split('#')]}, {'key': 'area', 'name': '地区', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in "大陆$cn#香港$hk#台湾$tw#日本$jp#韩国$kr#欧美$west#泰国$th#新马$sg-my".split('#')]}, {'key': 'year', 'name': '年份', 'value': [{'n': v, 'v': v} for v in ["2025", "2024", "2023", "2022", "2021", "2020", "2019-2015", "2014-2010", "2009-2000", "90年代", "80年代", "更早"]]} ], 'show': [ {'key': 'class', 'name': '剧情', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in "搞笑$gao-xiao#音乐$yin-yue#真人秀$zhen-ren-xiu#脱口秀$tuo-kou-xiu".split('#')]}, {'key': 'area', 'name': '地区', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in "大陆$cn#韩国$kr#欧美$west#其它$other".split('#')]}, {'key': 'year', 'name': '年份', 'value': [{'n': v, 'v': v} for v in ["2025", "2024", "2023", "2022", "2021", "2020", "2019-2015", "2014-2010", "2009-2000", "90年代", "80年代", "更早"]]} ], 'anime': [ {'key': 'class', 'name': '剧情', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in "冒险$mao-xian#动画电影$movie#推理$tui-li#校园$xiao-yuan#治愈$zhi-yu#泡面$pao-mian#热血$re-xue#科幻$ke-huan#魔幻$mo-huan".split('#')]}, {'key': 'area', 'name': '地区', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in "大陆$cn#日本$jp#欧美$west".split('#')]}, {'key': 'year', 'name': '年份', 'value': [{'n': v, 'v': v} for v in ["2025", "2024", "2023", "2022", "2021", "2020", "2019-2015", "2014-2010", "2009-2000", "90年代", "80年代", "更早"]]} ] } return {'class': class_list, 'filters': filters if filter else {}} def homeVideoContent(self): result = {'list': []} try: res = requests.get(self.home_url, headers=self.headers) res.encoding = 'utf-8' root = etree.HTML(res.text) data_list = root.xpath('//div[contains(@class, "qy-mod-link-wrap")]/a') for i in data_list: name_nodes = i.xpath('.//picture[@class="video-item-preview-img"]/img/@alt') vod_name = name_nodes[0].strip() if name_nodes else None if not vod_name: vod_name = i.xpath('./@title') vod_name = vod_name[0].strip() if vod_name else "未知" vod_id = i.get('href', '') pic_nodes = i.xpath('.//picture[@class="video-item-preview-img"]/img/@src') vod_pic = pic_nodes[0] if pic_nodes else self.placeholder_pic if vod_pic.startswith('/'): vod_pic = self.home_url + vod_pic remark_nodes = i.xpath('.//span[contains(@class, "qy-mod-label")]/text()') vod_remarks = remark_nodes[0].strip() if remark_nodes else '' result['list'].append({ 'vod_id': vod_id, 'vod_name': vod_name, 'vod_pic': vod_pic, 'vod_remarks': vod_remarks }) result['list'] = result['list'][:10] except Exception as e: print(f"Error in homeVideoContent: {e}") return result def categoryContent(self, tid, pg, filter, ext): result = {'list': []} _year = ext.get('year', '') _class = ext.get('class', '') _area = ext.get('area', '') params = { 'channel': tid, 'region': _area, 'class': _class, 'year': _year, 'page': pg } url = f"{self.home_url}/filter.html?{urlencode(params)}" # 如果頁碼超過 5,直接返回空結果 if int(pg) > 5: result['page'] = int(pg) result['pagecount'] = 5 result['limit'] = 0 result['total'] = 240 return result try: res = requests.get(url, headers=self.headers) res.encoding = 'utf-8' print(f"categoryContent URL: {url}") print(f"categoryContent Response Status: {res.status_code}") print(f"categoryContent HTML length: {len(res.text)}") root = etree.HTML(res.text) data_list = root.xpath('//li[contains(@class, "qy-mod-li")]') for i in data_list: vod_id = i.xpath('.//a/@href')[0] if i.xpath('.//a/@href') else '' name_nodes = i.xpath('.//a/@title') vod_name = name_nodes[0].strip() if name_nodes else "未知" pic_nodes = i.xpath('.//div[@class="qy-mod-cover"]/@style') vod_pic = None if pic_nodes: style = pic_nodes[0] match = re.search(r'url\((.*?)\)', style) vod_pic = match.group(1).strip() if match else None if not vod_pic: vod_pic = self.placeholder_pic if vod_pic.startswith('/'): vod_pic = self.home_url + vod_pic remark_nodes = i.xpath('.//span[@class="qy-mod-label"]/text()') vod_remarks = remark_nodes[0].strip() if remark_nodes else '' result['list'].append({ 'vod_id': vod_id, 'vod_name': vod_name, 'vod_pic': vod_pic, 'vod_remarks': vod_remarks }) # 假設每頁最多 48 個項目,網站分頁上限為 5 頁 current_items = len(data_list) total_pages = 5 # 網站分頁上限為 5 if current_items < 48: # 如果當前頁項目少於 48,假設是最後一頁 total_pages = int(pg) total_items = (int(pg) - 1) * 48 + current_items if total_pages == int(pg) else total_pages * 48 result['page'] = int(pg) result['pagecount'] = total_pages result['limit'] = current_items result['total'] = total_items except Exception as e: print(f"Error in categoryContent: {e}") return result def detailContent(self, array): result = {'list': []} ids = array[0] detail_url = f"{self.home_url}{ids}" try: res = requests.get(detail_url, headers=self.headers) res.encoding = 'utf-8' root = etree.HTML(res.text) vod_name = root.xpath('//div[@class="right-title"]/text()')[0].strip() if root.xpath('//div[@class="right-title"]') else "未知" vod_year = root.xpath('//div[@id="postYear"]/text()')[0].strip() if root.xpath('//div[@id="postYear"]') else "" vod_area = root.xpath('//div[@id="region"]/text()')[0].strip() if root.xpath('//div[@id="region"]') else "" vod_content = root.xpath('//div[@id="show-desc"]/text()')[0].strip() if root.xpath('//div[@id="show-desc"]') else "" vod_remarks = root.xpath('//div[@id="updateTxt"]/text()')[0].strip() if root.xpath('//div[@id="updateTxt"]') else "" vod_actor = root.xpath('//div[@id="actors"]/text()')[0].strip() if root.xpath('//div[@id="actors"]') else "" vod_director = root.xpath('//div[@id="director"]/text()')[0].strip() if root.xpath('//div[@id="director"]') else "" vod_pic = root.xpath('//img[@class="left-img"]/@src')[0] if root.xpath('//img[@class="left-img"]') else self.placeholder_pic if vod_pic.startswith('/'): vod_pic = self.home_url + vod_pic episodes = root.xpath('//div[@id="list-jj"]/a') if not episodes: vod = { 'vod_id': ids, 'vod_name': vod_name, 'vod_pic': vod_pic, 'type_name': '', 'vod_year': vod_year, 'vod_area': vod_area, 'vod_remarks': vod_remarks, 'vod_actor': vod_actor, 'vod_director': vod_director, 'vod_content': vod_content, 'vod_play_from': '泥視頻', 'vod_play_url': '第1集$https://www.nivod.cc/vodplay/202552243/ep1' } else: play_from = set() play_urls = {} for ep in episodes[::-1]: ep_name = ep.xpath('.//div[@class="item"]/text()')[0].strip() if ep.xpath('.//div[@class="item"]') else "未知" ep_url = self.home_url + ep.get('href', '') vod_id = ids.split('/')[2] ep_id = ep_url.split('/')[-1] xhr_url = f"{self.home_url}/xhr_playinfo/{vod_id}-{ep_id}" res = requests.get(xhr_url, headers=self.headers) res.encoding = 'utf-8' data = res.json() if 'pdatas' in data and data['pdatas']: for source in data['pdatas']: source_name = source['from'] play_from.add(source_name) if source_name not in play_urls: play_urls[source_name] = [] play_urls[source_name].append(f"{ep_name}${source['playurl']}") vod_play_from = '$$$'.join(play_from) vod_play_url = '$$$'.join(['#'.join(play_urls[source]) for source in play_from]) vod = { 'vod_id': ids, 'vod_name': vod_name, 'vod_pic': vod_pic, 'type_name': '', 'vod_year': vod_year, 'vod_area': vod_area, 'vod_remarks': vod_remarks, 'vod_actor': vod_actor, 'vod_director': vod_director, 'vod_content': vod_content, 'vod_play_from': vod_play_from, 'vod_play_url': vod_play_url } result['list'].append(vod) except Exception as e: print(f"Error in detailContent: {e}") result['list'].append({ 'vod_id': ids, 'vod_name': '未知', 'vod_pic': self.placeholder_pic, 'vod_play_from': '泥視頻', 'vod_play_url': '' }) return result def searchContent(self, key, quick, pg='1'): result = {'list': []} try: search_url = f"{self.home_url}/search_x.html?keyword={key}&page={pg}" res = requests.get(search_url, headers=self.headers) res.encoding = 'utf-8' root = etree.HTML(res.text) data_list = root.xpath('//a[contains(@class, "qy-mod-link")]') for item in data_list: name_nodes = item.xpath('.//picture[@class="video-item-preview-img"]/img/@alt') vod_name = name_nodes[0].strip() if name_nodes else None if not vod_name: vod_name = item.xpath('./@title') vod_name = vod_name[0].strip() if vod_name else "未知" vod_id = item.get('href', '') pic_nodes = item.xpath('.//picture[@class="video-item-preview-img"]/img/@src') vod_pic = pic_nodes[0] if pic_nodes else self.placeholder_pic if vod_pic.startswith('/'): vod_pic = self.home_url + vod_pic vod_remarks = '' result['list'].append({ 'vod_id': vod_id, 'vod_name': vod_name, 'vod_pic': vod_pic, 'vod_remarks': vod_remarks }) except Exception as e: print(f"Error in searchContent: {e}") return result def playerContent(self, flag, id, vipFlags): result = {} try: play_url = id.split('$')[1] if '$' in id else id result = { 'url': play_url, 'header': json.dumps(self.headers), 'parse': 0, 'playUrl': '' } except Exception as e: print(f"Error in playerContent: {e}") result = {'url': '', 'parse': 0} return result def localProxy(self, param): return [200, "video/MP2T", {}, b""] def destroy(self): pass