流媒体试看时长破解

现在的很多视频网站都采用流媒体分片技术进行分发视频,目前主流的是HLS技术

在一些视频网站中会限制视频时长,只能播放前15s

分析m3u8文件结构可以发现类似下面这种有规律的信息结构,而且不存在资源校验。

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:11
#EXTINF:10.000,
url_590/1_mp4_h264_aac_fhd_7.ts
#EXTINF:10.000,
url_591/1_mp4_h264_aac_fhd_7.ts
#EXTINF:10.000,
....

那么就可以通过二分查找总视频分片长度,从而得到完整的m3u8视频文件,配合N_m3u8DL-RE.exe 下载到本地。

import re
import requests
from requests.adapters import HTTPAdapter
from urllib.parse import urlparse, urljoin


class HttpClient:
    def __init__(self, retries=3, timeout=5):
        self.adapter = HTTPAdapter(max_retries=retries)
        self.session = requests.Session()
        self.session.mount("http://", self.adapter)
        self.session.mount("https://", self.adapter)
        self.timeout = timeout

    def head(self, url):
        """发送 HEAD 请求检测资源是否存在"""
        try:
            resp = self.session.head(url, timeout=self.timeout)
            return resp.status_code
        except requests.RequestException:
            return 404

    def get_text(self, url):
        """获取文本内容 (M3U8)"""
        try:
            resp = self.session.get(url, timeout=self.timeout)
            resp.raise_for_status()
            return resp.text
        except requests.RequestException as e:
            print(f"Error fetching URL: {e}")
            return None


class SegmentFinder:
    def __init__(self, http_client: HttpClient, base_url_pattern: str):
        self.http = http_client
        self.url_pattern = base_url_pattern  # 格式如: https://.../segment_{}.ts

    def _check_exists(self, index):
        """检查特定索引的 TS 切片是否存在"""
        url = self.url_pattern.format(index)
        status = self.http.head(url)
        # print(f"Checking {index} -> {status}") # 调试用
        return status == 200

    def find_last_segment_index(self, start_guess=3000):
        """
        组合策略:
        1. 指数搜索 (Exponential Search) 确定上界
        2. 二分搜索 (Binary Search) 确定精确值
        """
        low = 0
        high = start_guess

        print(">>> 正在探测最大边界...")
        # 1. 指数扩容寻找上界 (如果3000存在,就查6000,以此类推)
        while self._check_exists(high):
            print(f"索引 {high} 存在,扩大搜索范围...")
            low = high
            high *= 2
            if high > 100000:  # 防止死循环的安全熔断
                print("警告:切片数量异常巨大,强制停止扩容")
                break

        print(f">>> 锁定范围: {low} - {high},开始二分查找...")

        # 2. 二分查找精确值
        final_index = low
        while low <= high:
            mid = (low + high) // 2
            if self._check_exists(mid):
                final_index = mid
                low = mid + 1
            else:
                high = mid - 1

        return final_index


class M3U8Restorer:
    def __init__(self, original_url):
        self.original_url = original_url
        self.http = HttpClient()
        self.base_url = original_url.rsplit('/', 1)[0] + '/'
        self.content = ""
        self.headers = []
        self.ts_pattern = ""  # URL 格式化字符串
        self.ts_duration = ""  # 单片时长
        self.ts_prefix = ""  # 文件名前缀
        self.ts_suffix = ""  # 文件名后缀
        self.start_index = 0  # 起始索引

    def _parse_original(self):
        """解析原始 M3U8,提取头部信息、加密Key、文件名模式"""
        self.content = self.http.get_text(self.original_url)
        if not self.content:
            raise Exception("无法下载原始 M3U8")

        lines = self.content.strip().split('\n')
        self.headers = []

        ts_examples = []

        for line in lines:
            line = line.strip()
            if line.startswith('#EXTINF'):
                if not self.ts_duration:
                    self.ts_duration = line  # 暂且使用第一个切片的时长作为默认
            elif line.startswith('#') and not line.startswith('#EXT-X-ENDLIST'):
                self.headers.append(line)
            elif line and not line.startswith('#'):
                ts_examples.append(line)

        # 分析文件名模式 (假设类似 123_i0.ts)
        if ts_examples:
            last_ts = ts_examples[-1]  # 取最后一个做分析
            # 正则提取: (前缀)(数字)(后缀)
            # 例如: "data005.ts" -> "data", "005", ".ts"
            match = re.search(r'^(.*?)(\d+)(\.[a-zA-Z0-9]+)$', last_ts)
            if match:
                self.ts_prefix = match.group(1)
                num_str = match.group(2)
                self.ts_suffix = match.group(3)

                # 记录原始的数字长度(如果是001, 002这种填充格式)
                # 这里为了简化,我们假设是用 format 生成
                self.ts_pattern = f"{self.base_url}{self.ts_prefix}{{}}{self.ts_suffix}"
                self.start_index = int(num_str)  # 预览版最后一个切片的索引
            else:
                # 简单回退策略
                self.ts_pattern = urljoin(self.base_url, last_ts.replace(str(self.start_index), "{}"))

    def run(self):
        print(f"正在分析: {self.original_url}")
        self._parse_original()

        print(">>> 开始探测完整分片数量...")
        finder = SegmentFinder(self.http, self.ts_pattern)

        # 从预览版最后一个索引开始往后找
        total_last_index = finder.find_last_segment_index(start_guess=3000)
        print(f">>> 探测结束,最大索引为: {total_last_index}")

        return self._build_full_m3u8(total_last_index)

    def _build_full_m3u8(self, last_index):
        """重组完整的 M3U8 字符串"""
        lines = []
        # 1. 写入头部 (Version, TargetDuration, Key 等)
        for h in self.headers:
            lines.append(h)

        # 2. 循环生成切片列表
        # 注意:这里假设从 0 开始。如果原始视频不是从 0 开始,需要调整 range
        for i in range(0, last_index + 1):
            lines.append(self.ts_duration)  # 使用提取的时长
            # 构造完整 URL (有些播放器需要完整 URL,有些只需要相对路径)
            # 这里为了保险,直接放完整 URL
            full_ts_url = self.ts_pattern.format(i)
            lines.append(full_ts_url)

        # 3. 写入结尾
        lines.append("#EXT-X-ENDLIST")

        return "\n".join(lines)


if __name__ == "__main__":
    target_url = "https://xxxxx/a9f51d22edef4618a8600a0e263b2771_preview.m3u8"
    try:
        restorer = M3U8Restorer(target_url)
        full_m3u8_content = restorer.run()

        # 输出结果(可以写入文件)
        print("\n" + "=" * 20 + " 恢复结果 " + "=" * 20)
        # 仅打印前10行和后10行
        preview_lines = full_m3u8_content.split('\n')
        print("\n".join(preview_lines[:10]))
        print("...")
        print("\n".join(preview_lines[-5:]))

        # 保存到文件
        with open("full_movie.m3u8", "w", encoding="utf-8") as f:
            f.write(full_m3u8_content)
        print(f"\n已保存完整文件到 full_movie.m3u8,共 {len(preview_lines) // 2} 个切片")

    except Exception as e:
        print(f"处理出错: {e}")
posted @ 2026-03-08 20:43  Naihe\  阅读(99)  评论(0)    收藏  举报
// 音乐播放器