# 脚本名称：片库影院
# 作者：傻柱子
# 创建时间：2026‑09‑11
# 文件编码utf‑8，防止中文乱码【固定，不要改】
# -*- coding: utf-8 -*-
import sys # 系统模块【固定】
import re  # 正则，解析html【固定】
import json # json处理【固定】
import time
from urllib.parse import quote  # url编码，搜索关键词编码【固定】
sys.path.append('..') # 导入上级模块路径【固定】
from base.spider import Spider  # 继承壳爬虫基类【固定】

# 类名Spider：壳识别爬虫脚本入口【类名固定，禁止修改】
class Spider(Spider):
    # ===================== 接口1：init 初始化接口【函数名、参数固定】 =====================
    # 接口用途：源初始化，打开该爬虫源时执行一次；可修改字段：self.host、self.headers
    def init(self, extend=""):
        # 【可修改】请求头，模拟浏览器，反爬网站可修改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',
        }
        # 【已改】固定内容站域名，不再从导航发布页获取地址
        self.host = "https://shdy2.com"
        # 初始化访问首页建立会话获取cookie，全局搜索不经过homeContent，必须在此建立
        try:
            self.fetch(f'{self.host}/', headers=self.headers, timeout=15, verify=False)
        except Exception:
            pass

    # ===================== 接口2：getName 源名称接口【函数名固定】 =====================
    # 接口用途：返回源名称，展示在壳源列表；可修改字段：return返回的字符串
    def getName(self):
        # 【可修改】修改引号内文字，更改源显示名字
        return '花火电影'

    # ===================== 壳内部回调钩子【全部固定，禁止改动】 =====================
    def isVideoFormat(self, url):
        return False

    def manualVideoCheck(self):
        return True

    # ===================== 公共工具函数 _parse_vodlist =====================
    # 工具用途：首页/分类/搜索 共用解析影片列表；可修改：re.findall正则，适配不同网站的影片列表html
    def _parse_vodlist(self, html):
        videos = []
        # 【可修改】正则，匹配单条影片：链接、标题、封面，根据网页标签改写
        items = re.findall(
            r'<li>.*?<div class="v_img">.*?<a href="([^"]+)" title="([^"]+)".*?<img class="lazyload".*?data-original="([^"]+)".*?</div>',
            html,
            re.DOTALL
        )
        for href, title, pic in items:
            # 【可修改】正则提取影片id，根据网站详情链接格式改写
            vod_id_match = re.search(r'/movie/(\d+)\.html', href)
            if not vod_id_match:
                continue
            vod_id = vod_id_match.group(1)
            # 【可修改】截取更新备注正则，匹配更新状态标签
            remark_match = re.search(
                r'<div class="v_note">([^<]+)</div>',
                html[html.find(href):html.find(href)+600]
            )
            remark = remark_match.group(1).strip() if remark_match else ""
            # 补全相对图片地址【逻辑固定】
            if pic and 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

    # ===================== 接口3：homeContent 首页接口【函数名、参数固定】 =====================
    # 接口用途：加载源首页，返回分类列表 + 首页影片列表；可修改：首页url、分类抓取正则、异常兜底分类；返回格式固定 {"class":分类数组,"list":影片列表}
    def homeContent(self, filter):
        try:
            # 【可修改】首页网页地址
            r = self.fetch(self.host, headers=self.headers, timeout=15, verify=False)
            html = r.text
            # 【可修改】抓取导航整块html正则，匹配导航标签
            nav_result = re.search(r'<nav id="nav" class="top_bar clearfix">(.*?)</nav>', html, re.DOTALL)
            if nav_result:
                nav_html = nav_result.group(1)
                # 【可修改】提取分类id、分类名称正则，适配网站a标签
                cat_list = re.findall(r'<a href="/list/(\d+)\.html">([^<]+)</a>', nav_html)
            else:
                cat_list = []
            seen = set()
            classes = []
            for tid, name in cat_list:
                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': '1'},
                {'type_name': '连续剧', 'type_id': '2'},
            ]
            video_list = []

        # ========= CAT分组绑定式筛选【100%复刻骚火结构，写在homeContent内部，局部变量filters】 =========
        filters = {
            "1": [
                {
                    "key": "cateId",
                    "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":"13"},
                        {"n":"奇幻","v":"14"},
                        {"n":"剧情","v":"15"},
                        {"n":"冒险","v":"16"},
                        {"n":"悬疑","v":"17"},
                        {"n":"惊悚","v":"18"},
                        {"n":"其它","v":"19"}
                    ]
                }
            ],
            "2": [
                {
                    "key": "cateId",
                    "name": "类型",
                    "value": [
                        {"n":"全部","v":"2"},
                        {"n":"大陆剧","v":"20"},
                        {"n":"港剧","v":"21"},
                        {"n":"韩剧","v":"22"},
                        {"n":"美剧","v":"23"},
                        {"n":"日剧","v":"24"},
                        {"n":"英剧","v":"25"},
                        {"n":"台剧","v":"26"},
                        {"n":"其它","v":"27"}
                    ]
                }
            ]
        }
        return {"class": classes, "list": video_list, "filters": filters}

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

    # ===================== 接口5：detailContent 详情接口【函数名、参数固定】 =====================
    # 接口用途：点击影片，加载详情页面，获取标题、演员、简介、播放线路集数；ids：壳传入影片id列表；可修改：详情url、各个字段截取正则、线路配置区；返回格式固定 {"list":[详情字典]}
    def detailContent(self, ids):
        try:
            # ----------------------【可修改 线路配置区】----------------------
            # 【已移除】line_rename手动重命名字典，改为下方自动编号逻辑，无需再逐个写原始名映射
            del_line_list = ["线路3"] # 【可修改】需要屏蔽的线路原始名称
            sort_order = ["线路1","线路2"]    # 【可修改】自定义线路显示排序，优先顺序
            # ----------------------------------------------------------------
            vod_id = ids[0] if isinstance(ids, list) else ids
            # 【可修改】详情页面url模板
            url = f'{self.host}/movie/{vod_id}.html'
            resp = self.fetch(url, headers=self.headers, timeout=15, verify=False)
            html = resp.text
            # 【可修改】截取影片标题正则
            title_res = re.search(r'<h1 class="v_title"><a[^>]+>([^<]+)</a>', html)
            vod_name = title_res.group(1).strip() if title_res else ""
            # 【可修改】截取封面图片正则
            pic_res = re.search(r'<div class="m_background" style="background-image:url\(([^)]+)\)', html)
            vod_pic = pic_res.group(1).strip() if pic_res else ""
            if vod_pic and vod_pic.startswith('/'):
                vod_pic = self.host + vod_pic
            # 【可修改】截取主演正则
            actor_res = re.search(r'主演:([^<]+)', html)
            vod_actor = actor_res.group(1).strip() if actor_res else ""
            # 【可修改】截取导演正则
            director_res = re.search(r'导演:([^<]+)', html)
            vod_director = director_res.group(1).strip() if director_res else ""
            # 【可修改】截取地区、年份、影片类型正则（兼容电影评分前缀版本）
            info_match = re.search(r'<p>(?:[^/]+?\s*/\s*)?([^/]+?)\s*/\s*(\d{4})\s*/\s*([^/]+?)(?=\s*/|$)', html, re.S)
            vod_area = info_match.group(1).strip() if info_match else ""
            vod_year = info_match.group(2).strip() if info_match else ""
            type_name = info_match.group(3).strip() if info_match else ""
            # 【可修改】截取剧情简介正则，re.DOTALL开启多行匹配
            content_res = re.search(r'<p class="p_txt show_part">(.*?)<a href="/movie/', html, re.DOTALL)
            vod_content = content_res.group(1).strip() if content_res else ""
            
            # =====================线路抓取=====================
            source_tabs = re.findall(r'<li.*?>(线路\d+).*?</li>', html, re.DOTALL)
            temp_line_data = {}
            # 抓取集数外层容器
            play_block = re.search(r'<ul class="play_list" id="play_link">(.*?)</ul>', html, re.DOTALL)
            if play_block:
                play_html = play_block.group(1)
                # 拆分每条线路对应的集数li块
                ep_li_list = re.findall(r'<li.*?>(.*?)</li>', play_html, re.DOTALL)
                # 下标配对线路和集数，加越界防护
                for idx, source_name in enumerate(source_tabs):
                    # 【标准：原始线路名阶段执行屏蔽】
                    if source_name in del_line_list:
                        continue
                    if idx >= len(ep_li_list):
                        continue
                    ep_one_html = ep_li_list[idx]
                    # 提取集数链接和名称
                    ep_items = re.findall(r'<a href="([^"]+)">(.*?)</a>', ep_one_html, re.DOTALL)

                    episode_list = []
                    for j, (href, title) in enumerate(ep_items):
                        # 提取标题中的数字，等价js text.replace(/[^0-9]/g,'')
                        num_match = re.search(r'(\d+)', title)
                        if num_match:
                            num = int(num_match.group(1))
                        else:
                            # 没有数字就使用下标兜底，等价js j+1
                            num = j + 1
                        episode_list.append({"title": title, "href": href, "num": num})

                    # ✅按集数数字升序排序，等价js episodes.sort((a,b)=>a.num‑b.num)
                    episode_list.sort(key=lambda x: x["num"])

                    ep_list = []
                    for ep in episode_list:
                        full_url = self.host + ep["href"]
                        ep_list.append(f"{ep['title']}${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_actor": vod_actor, # 主演
                "vod_director": vod_director, # 导演
                "vod_year": vod_year, # 年份
                "vod_area": vod_area, # 地区
                "type_name": type_name, # 类型
                "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 搜索接口【函数名、参数固定】 =====================
    # 接口用途：执行搜索，返回搜索影片列表；key：搜索关键词，pg：搜索页码；可修改：搜索页面url地址；返回key全部壳固定不可修改
    def searchContent(self, key, quick, pg="1"):
        pg = int(pg)
        try:
            # 【可修改】搜索url模板，quote(key)关键词编码不可删除
            url = f'{self.host}/s-{quote(key)}---------{pg}.html'
            resp = self.fetch(url, headers=self.headers, timeout=15, verify=False)
            html = resp.text
            video_list = self._parse_vodlist(html)
            return {
                "list": video_list,
                "page": pg,
                "pagecount": 9999,
                "limit": 36,
                "total": len(video_list),
            }
        except Exception:
            return {"list": [], "page": pg, "pagecount": 1, "limit": 90, "total": 0}

        # ===================== 接口7：playerContent 播放解析接口 =====================
    # 播放地址解析；flag：线路标识，id：集数链接，vipFlags：vip标识
    def playerContent(self, flag, id, vipFlags):
        result = {"parse": 0, "url": "", "header": {}}
        try:
            t_start = time.time()
            # 拆分集名字与播放中转页面url
            if '$' in id:
                _, play_url = id.split('$', 1)
            else:
                play_url = id
            # 请求播放中转页面
            resp = self.fetch(play_url, headers=self.headers, timeout=20, verify=False)
            html = resp.text
            # 提取页面内iframe播放器嵌入地址
            iframe_res = re.search(r'<iframe[^>]+src=[\'"]([^\'"]+)[\'"]', html, re.IGNORECASE)
            if not iframe_res:
                raise Exception("页面找不到iframe播放标签")
            iframe_link = iframe_res.group(1).strip()
            # 补全iframe为完整http链接
            if not iframe_link.startswith("http"):
                iframe_link = self.host + iframe_link
            # 请求iframe播放器页面获取boot配置参数
            iframe_headers = self.headers.copy()
            iframe_resp = self.fetch(iframe_link, headers=iframe_headers, timeout=20, verify=False)
            iframe_html = iframe_resp.text
            # 抓取页面内window.__HHJX_BOOTSTRAP__ json配置
            boot_match = re.search(r'window\.__HHJX_BOOTSTRAP__\s*=\s*(\{[\s\S]*?\});', iframe_html)
            if not boot_match:
                raise Exception("iframe页面找不到boot配置")
            boot_str = boot_match.group(1)
            # 将字符串转为json对象
            try:
                boot_json = json.loads(boot_str)
            except Exception as e:
                raise Exception(f"boot json解析异常:{str(e)}")
            # 组装解析接口post请求参数
            post_data = {
                "url": boot_json["url"],
                "t": boot_json["t"],
                "key": boot_json["key"],
                "client_fallback": False
            }
            # 解析接口需要的额外请求头
            parse_api_extra_headers = {
                "Content-Type": "application/json",
                "Origin": "https://hhjx.hhplayer.com",
                "X-Requested-With": "com.mmbox.xbrowser",
                "sec-ch-ua-platform": "\"Android\"",
                "sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Android WebView\";v=\"138\"",
                "sec-ch-ua-mobile": "?1",
                "Accept": "*/*",
                "Sec-Fetch-Site": "same-origin",
                "Sec-Fetch-Mode": "cors",
                "Sec-Fetch-Dest": "empty",
                "Accept-Encoding": "gzip, deflate, br, zstd"
            }
            # 合并基础headers与接口专用headers
            api_headers = self.headers.copy()
            api_headers.update(parse_api_extra_headers)
            # post调用hhjx解析接口获取真实播放直链
            parse_resp = self.post(
                url="https://hhjx.hhplayer.com/api/parse",
                headers=api_headers,
                data=json.dumps(post_data),
                timeout=20,
                verify=False
            )
            # 解析接口返回的json结果
            try:
                parse_result = parse_resp.json()
            except Exception as e:
                raise Exception(f"解析接口返回非json:{str(e)}")
            # 判断接口返回状态码是否成功
            if parse_result.get("code") != 200:
                msg = parse_result.get('msg','无错误提示')
                raise Exception(f"播放器接口失败:{msg}")
            # 取出真实视频直链返回给壳，parse=0代表直链不嗅探
            real_video_url = parse_result["url"]
            # 播放头按直链域名分流，1ljx节点校验Origin与UA，不能沿用首页文档头
            play_headers = self.headers
            if '1ljx.com' in real_video_url or 'cibn-edge' in real_video_url:
                play_headers = {
                    "User-Agent": "Mozilla/5.0 (Linux; Android 16; V2509A Build/BP2A.250605.031.A3_V000L1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.179 Mobile Safari/537.36",
                    "Accept": "*/*",
                    "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
                    "Origin": "https://hhjx.hhplayer.com",
                    "X-Requested-With": "com.hiker.youtoo",
                    "sec-ch-ua-platform": "\"Android\"",
                    "sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Android WebView\";v=\"138\"",
                    "sec-ch-ua-mobile": "?1",
                    "Sec-Fetch-Site": "cross-site",
                    "Sec-Fetch-Mode": "cors",
                    "Sec-Fetch-Dest": "empty",
                }
            result = {
                "parse": 0,  # 0=直链播放，1=嗅探播放
                "url": real_video_url,
                "header": play_headers
            }
        except Exception:
            pass
        return result

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

    def destroy(self):
        pass