# 脚本名称：片库影院
# 作者：小柱子
# 创建时间：2026‑09‑11
# 文件编码utf‑8，防止中文乱码【固定，不要改】
# -*- coding: utf-8 -*-
import sys  # 系统模块【固定】
import re   # 正则，解析html【固定】
import json # json处理【固定】
import requests
from urllib.parse import quote  # url编码，搜索关键词编码【固定】
sys.path.append('..')  # 导入上级模块路径【固定】
from base.spider import Spider  # 继承壳爬虫基类【固定】
# 类名Spider：壳识别爬虫脚本入口【类名固定，禁止修改】
class Spider(Spider):
    # ===================== 接口1：init 初始化接口【函数名、参数固定】 =====================
    def init(self, extend=""):
        #接口用途：源初始化，打开该爬虫源时执行一次；可修改字段：self.host、self.headers"""
        # 【可修改】请求头，模拟浏览器，反爬网站可修改UA、Accept等
        self.headers = {
            # 【可修改】浏览器标识，网站校验UA就改这里
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.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,en-US;q=0.8,en;q=0.7',
            'Upgrade-Insecure-Requests': '1',
            'sec-ch-ua': '"Not A;Brand";v="99", "Chromium";v="140", "Google Chrome";v="140"',
            'sec-ch-ua-mobile': '?1',
            'sec-ch-ua-platform': '"Android"',
            'x-requested-with': 'XBrowser',
            'Sec-Fetch-Site': 'none',
            'Sec-Fetch-Mode': 'navigate',
            'Sec-Fetch-User': '?1',
            'Sec-Fetch-Dest': 'document',
        }
        # 【已改】固定内容站域名，不再调用 _get_host() 从导航页获取
        self.host = 'https://4k01.pianku.online'
        # 解析接口固定域名
        self.auth_host = "https://svip.qlplayer.cyou"
        # 初始化访问首页建立会话，全局搜索不经过homeContent，必须在此建立
        try:
            self.fetch(self.host, headers=self.headers, timeout=15, verify=False)
        except Exception:
            pass

    # ===================== 接口2：getName 源名称接口【函数名固定】 =====================
    def getName(self):
        #接口用途：返回源名称，展示在壳源列表；可修改字段：return返回的字符串"""
        # 【可修改】修改引号内文字，更改源显示名字
        return '片库影院'
    # ===================== 壳内部回调钩子【全部固定，禁止改动】 =====================
    def isVideoFormat(self, url):
        return False
    def manualVideoCheck(self):
        return True

    # ===================== 解析列表公共工具函数 _parse_vodlist =====================
    def _parse_vodlist(self, html):
        #工具用途：首页/分类/搜索 共用解析影片列表；可修改：re.findall正则，适配不同网站的影片列表html"""
        videos = []
        # 【可修改】正则，匹配单条影片：链接、标题、封面，根据网页标签改写
        items = re.findall(
            r'<a href="(/voddetail/\d+\.html)"\s+title="([^"]*)"[^>]*>\s*<div class="vod-pic"[^>]*>\s*<img src="([^"]*)"[^>]*>\s*<span class="remarks">([^<]*)</span>',
            html
        )
        for href, title, pic, remark in items:
            # 【可修改】正则提取影片id，根据网站详情链接格式改写
            vod_id_match = re.search(r'/voddetail/(\d+)\.html', href)
            if not vod_id_match:
                continue
            vod_id = vod_id_match.group(1)
            # 补全相对图片地址【逻辑固定】
            if pic.startswith('/'):
                pic = self.host + pic
            # 返回字典key壳固定，value为解析数据
            videos.append({
                'vod_id': vod_id,
                'vod_name': title,
                'vod_pic': pic,
                'vod_remarks': remark,
            })
        return videos
    def homeVideoContent(self):
        """壳钩子，本源未使用，保留占位"""
        return ''
    # ===================== 接口3：homeContent 首页接口【函数名、参数固定】 =====================
    def homeContent(self, filter):
        #接口用途：加载源首页，返回分类列表 + 首页影片列表；可修改：首页url、分类抓取正则、异常兜底分类；返回格式固定 {"class":分类数组,"list":影片数组}"""
        try:
            # 【可修改】首页网页地址
            r = self.fetch(self.host, headers=self.headers, timeout=15, verify=False)
            html = r.text
            # 【可修改】抓取导航整块html正则，匹配导航标签
            nav_match = re.search(r'<ul class="nav-list">(.*?)</ul>', html, re.DOTALL)
            if nav_match:
                nav_html = nav_match.group(1)
                # 【可修改】提取分类id、分类名称正则，适配网站a标签
                categories = re.findall(r'<li[^>]*><a href="/vodtype/(\d+)\.html"[^>]*>([^<]+)</a></li>', nav_html)
            else:
                categories = []
            seen = set()
            classes = []
            for tid, name in categories:
                if tid not in seen:
                    seen.add(tid)
                    classes.append({'type_name': name, 'type_id': tid})
            if not classes:
                raise Exception('No categories found')
            video_list = self._parse_vodlist(html)
        except Exception:
            # 兜底写死分类，防止源直接空白崩溃【可修改按需修改】
            classes = [
                {'type_name': '电影', 'type_id': '20'},
                {'type_name': '剧集', 'type_id': '37'},
                {'type_name': '动漫', 'type_id': '43'},
                {'type_name': '综艺', 'type_id': '45'}
            ]
            video_list = []

        # ========= CAT分组绑定式筛选【筛选值为独立分类id，按大类tid分组绑定】 =========
        filters = {
            "20": [
                {
                    "key": "cateId",
                    "name": "类型",
                    "value": [
                        {"n": "全部", "v": "20"},
                        {"n": "动作片", "v": "21"},
                        {"n": "喜剧片", "v": "22"},
                        {"n": "爱情片", "v": "23"},
                        {"n": "科幻片", "v": "24"},
                        {"n": "恐怖片", "v": "25"},
                        {"n": "剧情片", "v": "26"},
                        {"n": "战争片", "v": "27"},
                        {"n": "惊悚片", "v": "28"},
                        {"n": "犯罪片", "v": "29"},
                        {"n": "冒险篇", "v": "30"},
                        {"n": "动画片", "v": "31"},
                        {"n": "悬疑片", "v": "32"},
                        {"n": "武侠片", "v": "33"},
                        {"n": "奇幻片", "v": "34"},
                        {"n": "纪录片", "v": "35"},
                        {"n": "其他片", "v": "36"}
                    ]
                }
            ],
            "37": [
                {
                    "key": "cateId",
                    "name": "类型",
                    "value": [
                        {"n": "全部", "v": "37"},
                        {"n": "国产剧", "v": "38"},
                        {"n": "港台剧", "v": "39"},
                        {"n": "欧美剧", "v": "40"},
                        {"n": "日韩剧", "v": "41"},
                        {"n": "其他剧", "v": "42"}
                    ]
                }
            ]
        }
        return {"class": classes, "list": video_list, "filters": filters}

    # ===================== 接口4：categoryContent 分类接口【函数名、参数固定】 =====================
    def categoryContent(self, tid, pg, filter, extend):
        #接口用途：点击分类，加载分类页面数据；tid：分类id，pg：页码；可修改：分类页面url模板；返回key全部壳固定不可修改"""
        pg = int(pg)
        # 【可修改】分类页面url模板，未翻页 vodtype/{tid}.html，已翻页 vodtype/{tid}-{pg}.html
        # CAT协议：筛选值存放在extend，key为cateId；无筛选沿用原始tid
        cateId = extend.get("cateId", tid) if extend else tid
        url = f'{self.host}/vodtype/{cateId}.html' if pg == 1 else f'{self.host}/vodtype/{cateId}-{pg}.html'
        try:
            r = self.fetch(url, headers=self.headers, timeout=15, verify=False)
            html = r.text
            video_list = self._parse_vodlist(html)
            return {
                "list": video_list,
                "page": pg,
                "pagecount": 9999, # 9999=无限分页【可修改，设置真实总页数】
                "limit": 30,       # 每页条数【可修改】
                "total": len(video_list),
            }
        except Exception:
            return {"list": [], "page": pg, "pagecount": 1, "limit": 90, "total": 0}

    # ===================== 接口5：detailContent 详情接口【函数名、参数固定】 =====================
    def detailContent(self, ids):
        #接口用途：点击影片，加载详情页面，获取标题、演员、简介、播放线路集数；ids：壳传入影片id列表；可修改：详情url、各个字段截取正则；返回格式固定 {"list":[详情字典]}"""
        # ----------------------【可修改 线路配置区】----------------------
        # 【已移除】line_rename手动重命名字典，改为下方自动编号逻辑，无需逐个写原始名映射
        del_line_list = [""] # 【可修改】需要屏蔽的线路原始名称
        sort_order = [""]    # 【可修改】自定义线路显示排序，优先顺序
        # ----------------------------------------------------------------
        vod_id = ids[0] if isinstance(ids, list) else ids
        try:
            # 【可修改】详情页面url模板
            url = f'{self.host}/voddetail/{vod_id}.html'
            r = self.fetch(url, headers=self.headers, timeout=15, verify=False)
            html = r.text
            # 【可修改】截取影片标题正则
            title_match = re.search(r'<h1 class="detail-title"[^>]*>([^<]+)', html)
            vod_name = title_match.group(1).strip() if title_match else ''
            # 【可修改】截取封面图片正则
            pic_match = re.search(r'<div class="detail-poster"[^>]*>\s*<img src="([^"]+)"', html, re.DOTALL)
            vod_pic = pic_match.group(1) if pic_match else ''
            if vod_pic and vod_pic.startswith('/'):
                vod_pic = self.host + vod_pic
            # 【可修改】截取类型、地区、年份正则（优化匹配规则，兼容页面结构）
            info_match = re.search(r'分类：<a[^>]*>([^<]+)</a></span>\s*<span[^>]*>地区：([^<]+)</span>\s*<span[^>]*>年份：(\d{4})</span>', html, re.S|re.DOTALL)
            type_name = info_match.group(1).strip() if info_match else ''
            vod_area = info_match.group(2).strip() if info_match else ''
            vod_year = info_match.group(3).strip() if info_match else ''
            # 【可修改】截取导演正则
            director_match = re.search(r'<span[^>]*>导演：(.*?)</span>', html, re.DOTALL)
            vod_director = ''
            if director_match:
                vod_director = re.sub(r'<[^>]+>', '', director_match.group(1)).strip()
                vod_director = vod_director.replace('&nbsp;', '').strip()
            # 【可修改】截取主演正则（彻底清理&nbsp;空格实体）
            actor_match = re.search(r'<span[^>]*>主演：(.*?)</span>', html, re.DOTALL)
            vod_actor = ''
            if actor_match:
                vod_actor = re.sub(r'<[^>]+>', '', actor_match.group(1)).strip()
                vod_actor = vod_actor.replace('&nbsp;', ' ').strip()
            # 【可修改】截取剧情简介正则
            desc_match = re.search(r'<div class="detail-desc"[^>]*>.*?<p[^>]*>(.*?)</p>', html, re.DOTALL)
            vod_content = desc_match.group(1).strip() if desc_match else ''
            # =====================线路抓取=====================
            # 正则抓取线路序号和名称，序号取自data-target，与显示顺序无关
            source_tabs = re.findall(r'<span class="source-tab-item[^>]*" data-target="playlist-(\d+)"[^>]*>([^<]+)</span>', html)
            temp_line_data = {}     # 存放 线路名:集数串
            for source_id, source_name in source_tabs:
                # 命中屏蔽的线路跳过
                if source_name in del_line_list:
                    continue
                # 正则抓取该线路的集数列表整块，id属性前有class等属性，需从div开头放宽匹配
                play_block = re.search(
                    f'<div[^>]*id="playlist-{source_id}"[^>]*>.*?<div class="url-grid-play[^"]*"[^>]*>(.*?)</div>',
                    html, re.DOTALL
                )
                if play_block:
                    play_html = play_block.group(1)
                    # 正则抓取每一集的链接和名称
                    ep_items = re.findall(r'<a\s+href="([^"]+)"[^>]*>([^<]*)</a>', play_html)
                    ep_list = []
                    for ep_url, ep_name in ep_items:
                        full_url = self.host + ep_url   # 补全集数完整地址
                        # $ 分隔集名和地址
                        ep_list.append(f'{ep_name}${full_url}')
                    # # 分隔集与集
                    temp_line_data[source_name] = '#'.join(ep_list)

            # =========【原始名称层面做sort_order排序，填网站原始线路名称】=========
            play_from_raw = []      # 线路名列表
            play_url_raw = []       # 集数串列表
            temp_raw_keys = list(temp_line_data.keys())
            for raw_name in sort_order:      # 先排优先线路
                if raw_name in temp_raw_keys:
                    play_from_raw.append(raw_name)
                    play_url_raw.append(temp_line_data[raw_name])
                    temp_raw_keys.remove(raw_name)
            for remain_raw_name in temp_raw_keys:   # 剩余线路追加在后
                play_from_raw.append(remain_raw_name)
                play_url_raw.append(temp_line_data[remain_raw_name])

            # =========【原站名称自动命名】=========
            renamed_play_from = []
            renamed_play_url = []
            for raw_name, ep_str in zip(play_from_raw, play_url_raw):
                # 空集数跳过，不占位
                if not (ep_str and len(ep_str.strip()) > 0):
                    continue
                # 【可修改】展示名模板，直接用原站线路名
                new_name = f'📡{raw_name}┃广告勿信！'
                renamed_play_from.append(new_name)
                renamed_play_url.append(ep_str)     # 地址原样不动

            # $$$ 分隔线路与线路
            vod_play_from = '$$$'.join(renamed_play_from) if renamed_play_from else ''
            vod_play_url = '$$$'.join(renamed_play_url) if renamed_play_url else ''
            vod_info = {
                'vod_id': vod_id, # 影片id
                'vod_name': vod_name, # 影片名
                'vod_pic': vod_pic,  # 封面
                'vod_year': vod_year, # 年份
                'vod_area': vod_area,  # 地区
                'type_name': type_name,  # 类型
                'vod_director': vod_director, # 导演
                'vod_actor': vod_actor,  # 主演
                'vod_content': vod_content,  # 简介
                'vod_play_from': vod_play_from,  # 线路名
                'vod_play_url': vod_play_url,  # 各线路集数
            }
            return {"list": [vod_info]}
        except Exception:
            return {"list": []}
    
             # ===================== 接口6：searchContent 搜索接口【函数名、参数固定】 =====================
    def searchContent(self, key, quick, pg="1"):
        #接口用途：执行搜索，返回搜索影片列表；key：搜索关键词，pg：搜索页码；可修改：搜索页面url地址；返回key全部壳固定不可修改"""
        pg = int(pg)
        # 【可修改】搜索url模板，quote(key)关键词编码不可删除 # 走联想搜索接口（站点搜索框下拉提示入口，无验证码），mid=1 为视频模型，limit=50 单次返回上限
        url = f'{self.host}/index.php/ajax/suggest?mid=1&wd={quote(key)}&limit=50'
        try:
            # 搜索请求头，照抓包补全浏览器指纹，UA与sec-ch-ua版本对齐，会话cookie由壳自动携带
            search_headers = {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.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,en-US;q=0.8,en;q=0.7',
                'Upgrade-Insecure-Requests': '1',
                'sec-ch-ua': '"Not A;Brand";v="99", "Chromium";v="140", "Google Chrome";v="140"',
                'sec-ch-ua-mobile': '?1',
                'sec-ch-ua-platform': '"Android"',
                'X-Requested-With': 'com.mmbox.xbrowser',
                'Sec-Fetch-Site': 'same-origin',
                'Sec-Fetch-Mode': 'navigate',
                'Sec-Fetch-User': '?1',
                'Sec-Fetch-Dest': 'document',
                'Referer': f'{self.host}/',
            }
            video_list = []
            try:
                import json as _json
                sug_headers = dict(search_headers)
                sug_headers['X-Requested-With'] = 'XMLHttpRequest'
                sug_headers['Accept'] = 'application/json, text/javascript, */*; q=0.01'
                r = self.fetch(url, headers=sug_headers, timeout=15, verify=False)
                data = _json.loads(r.text)
                for it in (data or {}).get('list') or []:
                    vid = it.get('id')
                    name = (it.get('name') or '').strip()
                    if not (vid and name):
                        continue
                    video_list.append({
                        "vod_id": str(vid),
                        "vod_name": name,
                        "vod_pic": it.get('pic') or '',
                        "vod_remarks": '',
                    })
            except Exception:
                video_list = []
            return {
                "list": video_list,
                "page": pg,
                "pagecount": 1,
                "limit": 50,
                "total": len(video_list),
            }
        except Exception:
            return {"list": [], "page": pg, "pagecount": 1, "limit": 50, "total": 0}

    # ===================== 接口7：playerContent 播放解析接口 =====================
    def playerContent(self, flag, id, vipFlags):
        result = {"parse": 0, "url": "", "header": {}}
        try:
            # 1.请求播放页，用壳fetch带cookie，提取播放源url
            play_page_resp = self.fetch(id, headers=self.headers, timeout=15, verify=False)
            play_page_html = play_page_resp.text
            player_aaaa_match = re.search(r'player_aaaa\s*=\s*\{.*?"url"\s*:\s*"([^"]+)"', play_page_html, re.S)
            if not player_aaaa_match:
                raise Exception("播放页未匹配到player_aaaa.url")
            raw_qq_url = player_aaaa_match.group(1)
            qq_real_url = raw_qq_url.replace(r'\/', '/')

            # 下面三个接口统一用的请求头
            parse_api_headers = {
                "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",
                "Origin": "https://4k01.pianku.online",   # 与内容站域名保持一致
                "Accept": "*/*",
            }

            # 2.中转接口，拿apiToken，裸请求不带cookie
            mid_url = f"{self.auth_host}/?url={quote(qq_real_url)}"
            mid_resp = self.fetch(mid_url, headers=parse_api_headers, cookies={}, timeout=15, verify=False)
            mid_html = mid_resp.text
            token_match = re.search(r'apiToken\s*:\s*"([^"]+)"', mid_html)
            if not token_match:
                raise Exception("中转页面未提取apiToken")
            api_token = token_match.group(1)

            # 3.鉴权接口，传内容站域名，裸请求不带cookie
            auth_param_host = '4k01.pianku.online'
            auth_url = f"{self.auth_host}/api/auth.php?host={auth_param_host}"
            self.fetch(auth_url, headers=parse_api_headers, cookies={}, timeout=15, verify=False)

            # 4.解析接口，用token换直链，裸请求不带cookie
            resolve_url = f"{self.auth_host}/api/resolve.php?token={quote(api_token)}"
            resolve_resp = self.fetch(resolve_url, headers=parse_api_headers, cookies={}, timeout=15, verify=False)
            resolve_data = json.loads(resolve_resp.text)
            if resolve_data.get("code") != 200:
                raise Exception("解析接口返回失败")
            play_url = resolve_data.get("url")
            if not play_url:
                raise Exception("未获取有效播放地址")

            result = {
                "parse": 0,  # 0=直链播放，1=嗅探播放
                "url": play_url,
                "header": parse_api_headers
            }
        except Exception as e:
            # 异常统一捕获，返回空result
            pass
        return result

    # 壳必备占位函数，禁止删除修改
    def localProxy(self, param):
        return [200, {}, ""]
    def destroy(self):
        pass