# -*- coding: utf-8 -*-
# !/usr/bin/env python
# Software: PyCharm
# __author__ == "YU HAIPENG"
# fileName: VideoHelp.py
# Month: 五月
# time: 2021/5/22 17:06
"""
https://ffmpeg.org/ffmpeg-filters.html 文档
https://blog.csdn.net/weixin_42081389/article/details/100543007
ffmpeg -y -i 待转换mp4文件路径 -vcodec copy -acodec copy -vbsf h264_mp4toannexb 目标ts文件
ffmpeg -i 待转换ts文件路径 -c copy -map 0 -f segment -segment_list 目标m3u8文件 -segment_time 单个切片时长 目标ts切片文件名称

通用选项
    -L license

    -h 帮助

    -fromats 显示可用的格式,编解码的,协议的。。。

    -f fmt 强迫采用格式fmt

    -I filename 输入文件

    -y 覆盖输出文件

    -t duration 设置纪录时间 hh:mm:ss[.xxx]格式的记录时间也支持

    -ss position 搜索到指定的时间 [-]hh:mm:ss[.xxx]的格式也支持

    -title string 设置标题

    -author string 设置作者

    -copyright string 设置版权

    -comment string 设置评论

    -target type 设置目标文件类型(vcd,svcd,dvd) 所有的格式选项(比特率,编解码以及缓冲区大小)自动设置 ,只需要输入如下的就可以了:
    ffmpeg -i myfile.avi -target vcd /tmp/vcd.mpg

    -hq 激活高质量设置

    -itsoffset offset 设置以秒为基准的时间偏移,该选项影响所有后面的输入文件。
        该偏移被加到输入文件的时戳,定义一个正偏移意味着相应的流被延迟了 offset秒。 [-]hh:mm:ss[.xxx]的格式也支持

视频选项
    -b bitrate 设置比特率,缺省200kb/s

    -r fps 设置帧频 缺省25

    -s size 设置帧大小 格式为WXH 缺省160X128.下面的简写也可以直接使用:
    Sqcif 128X96 qcif 176X144 cif 252X288 4cif 704X576

    -aspect aspect 设置横纵比 4:3 16:9 或 1.3333 1.7777

    -croptop size 设置顶部切除带大小 像素单位

    -cropbottom size –cropleft size –cropright size

    -padtop size 设置顶部补齐的大小 像素单位

    -padbottom size –padleft size –padright size –padcolor color 设置补齐条颜色(hex,6个16进制的数,红:绿:兰排列,比如 000000代表黑色)

    -vn 不做视频记录

    -bt tolerance 设置视频码率容忍度kbit/s

    -maxrate bitrate设置最大视频码率容忍度

    -minrate bitreate 设置最小视频码率容忍度

    -bufsize size 设置码率控制缓冲区大小

    -vcodec codec 强制使用codec编解码方式。 如果用copy表示原始编解码数据必须被拷贝。

    -sameq 使用同样视频质量作为源(VBR)

    -pass n 选择处理遍数(1或者2)。两遍编码非常有用。第一遍生成统计信息,第二遍生成精确的请求的码率

    -passlogfile file 选择两遍的纪录文件名为file

高级视频选项
    -g gop_size 设置图像组大小

    -intra 仅适用帧内编码

    -qscale q 使用固定的视频量化标度(VBR)

    -qmin q 最小视频量化标度(VBR)

    -qmax q 最大视频量化标度(VBR)

    -qdiff q 量化标度间最大偏差 (VBR)

    -qblur blur 视频量化标度柔化(VBR)

    -qcomp compression 视频量化标度压缩(VBR)

    -rc_init_cplx complexity 一遍编码的初始复杂度

    -b_qfactor factor 在p和b帧间的qp因子

    -i_qfactor factor 在p和i帧间的qp因子

    -b_qoffset offset 在p和b帧间的qp偏差

    -i_qoffset offset 在p和i帧间的qp偏差

    -rc_eq equation 设置码率控制方程 默认tex^qComp

    -rc_override override 特定间隔下的速率控制重载

    -me method 设置运动估计的方法 可用方法有 zero phods log x1 epzs(缺省) full

    -dct_algo algo 设置dct的算法 可用的有 0 FF_DCT_AUTO 缺省的DCT 1 FF_DCT_FASTINT 2 FF_DCT_INT 3 FF_DCT_MMX 4 FF_DCT_MLIB 5 FF_DCT_ALTIVEC

    -idct_algo algo 设置idct算法。可用的有 0 FF_IDCT_AUTO 缺省的IDCT 1 FF_IDCT_INT 2 FF_IDCT_SIMPLE 3 FF_IDCT_SIMPLEMMX 4 FF_IDCT_LIBMPEG2MMX 5 FF_IDCT_PS2 6 FF_IDCT_MLIB 7 FF_IDCT_ARM 8 FF_IDCT_ALTIVEC 9 FF_IDCT_SH4 10 FF_IDCT_SIMPLEARM

    -er n 设置错误残留为n 1 FF_ER_CAREFULL 缺省 2 FF_ER_COMPLIANT 3 FF_ER_AGGRESSIVE 4 FF_ER_VERY_AGGRESSIVE

    -ec bit_mask 设置错误掩蔽为bit_mask,该值为如下值的位掩码 1 FF_EC_GUESS_MVS (default=enabled) 2 FF_EC_DEBLOCK (default=enabled)

    -bf frames 使用frames B 帧,支持mpeg1,mpeg2,mpeg4

    -mbd mode 宏块决策 0 FF_MB_DECISION_SIMPLE 使用mb_cmp 1 FF_MB_DECISION_BITS 2 FF_MB_DECISION_RD

    -4mv 使用4个运动矢量 仅用于mpeg4

    -part 使用数据划分 仅用于mpeg4

    -bug param 绕过没有被自动监测到编码器的问题

    -strict strictness 跟标准的严格性

    -aic 使能高级帧内编码 h263+

    -umv 使能无限运动矢量 h263+

    -deinterlace 不采用交织方法

    -interlace 强迫交织法编码 仅对mpeg2和mpeg4有效。当你的输入是交织的并且你想要保持交织以最小图像损失的时候采用该选项。可选的方法是不交织,但是损失更大

    -psnr 计算压缩帧的psnr

    -vstats 输出视频编码统计到vstats_hhmmss.log

    -vhook module 插入视频处理模块 module 包括了模块名和参数,用空格分开

音频选项
    -ab bitrate 设置音频码率

    -ar freq 设置音频采样率

    -ac channels 设置通道 缺省为1

    -an 不使能音频纪录

    -acodec codec 使用codec编解码
音频/视频捕获选项
    -vd device 设置视频捕获设备。比如/dev/video0

    -vc channel 设置视频捕获通道 DV1394专用

    -tvstd standard 设置电视标准 NTSC PAL(SECAM)

    -dv1394 设置DV1394捕获

    -av device 设置音频设备 比如/dev/dsp

高级选项
    -map file:stream 设置输入流映射

    -debug 打印特定调试信息

    -benchmark 为基准测试加入时间

    -hex 倾倒每一个输入包

    -bitexact 仅使用位精确算法 用于编解码测试

    -ps size 设置包大小,以bits为单位

    -re 以本地帧频读数据,主要用于模拟捕获设备

    -loop 循环输入流。只工作于图像流,用于ffserver测试
# todo example
1、avi转MP4命令:
    ffmpeg -i .\\Video.avi -c copy -map 0 video.mp4
    或
    ffmpeg -i .\\Video.avi -c:v libx264 -crf 19 -preset slow -c:a aac -b:a 192k -ac 2  video.mp4

    //剪切视频
    ffmpeg -ss 0:1:30 -t 0:0:50 -i 1.avi -vcodec copy -acodec copy 3.mp4
    //-r 提取图像的频率,-ss 开始时间,-t 持续时间
MP4转ts
    ffmpeg -i .\video.mp4 output.ts
视频压缩
    1)ffmpeg -i 123_ffmpeg.mp4  (压缩的文件更大更清晰,一般情况下不用)

     2)ffmpeg.exe -i 123.MP4 -b:v 700k 1231_ffmpeg.mp4(压缩的更小,相对模糊一些)
"""
import os
from re import compile
import filetype
import platform
import subprocess
import requests
from requests.utils import requote_uri
import time
import shutil
from concurrent.futures import ThreadPoolExecutor, as_completed
from requests.packages.urllib3.exceptions import InsecureRequestWarning


__all__ = ["main_path", "get_desktop", "get_home",
           "OtherFormatToMp4", "SliceM3u8ToMp4", "M3u8ToMp4",
           "MP4ToM3u8", "MP4ToTs"
           ]


requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

split_reg = compile(r'[\\|/]')
reg = compile(r".*%0+\dd\.ts")
center_num = 40


def main_path(path: str):
    """
    路径总函数
    @param path:
    @return:
    """
    if not isinstance(path, str):
        raise ValueError("path err")
    current_path = os.getcwd()
    if path.startswith('..'):
        path = _wne_path(_parse(path, current_path))
    elif path.startswith('.'):
        path = _wne_path(path[1:], current_path)
    elif path:
        path = _wne_path(path)
    else:
        raise ValueError('文件路径错误')
    return path


def _parse(path: str, current_path):
    """
    解析 .. 路径
    :param path:
    :param current_path:
    :return:
    """
    new_path_args = list(filter(lambda x: x != '', _get_path_params(path)))
    row = 0
    while row < len(new_path_args):
        if new_path_args[row] == '..':
            current_path = os.path.dirname(current_path)
            new_path_args.remove(new_path_args[row])
            row -= 1
        else:
            break
        row += 1
    return os.path.join(current_path, *new_path_args)


def _wne_path(new_path: str, current_path=None):
    new_path_args = _get_path_params(new_path)
    if current_path:
        path = os.path.join(current_path, *new_path_args)
    else:
        sys_str = get_sys()
        if sys_str == "Windows":
            if new_path_args[0].find(':') != -1:
                new_path_args[0] += os.sep
            path = os.path.join(*new_path_args)
        elif sys_str in ["Linux", "Mac", "Darwin"]:
            if new_path_args[0] == '':
                new_path_args[0] = os.sep
            path = os.path.join(*new_path_args)
        else:
            path = new_path
    return path


def get_sys():
    """
    平台
    @return:
    """
    sys_str = platform.system()
    return sys_str


def get_home(path='~'):
    """
    家目录
    @return:
    """
    return os.path.expanduser(path)


def _get_path_params(path):
    """
    路径参数
    @param path:
    @return:
    """

    return split_reg.split(path)


def get_desktop():
    """
    桌面路径
    @return:
    """
    return get_home(f"~{os.sep}DeskTop")


def guss_file_type(file_path):
    """
    file_path
    @param file_path:
    @return:
    """
    kind = filetype.guess_extension(file_path)
    return kind


def m3u8_to_mp4_slice_download(m3u8_urls: list, sep=100, **kwargs):
    """
    分片下载
    @param m3u8_urls:  urls 数组 过大时可用分片下载  传入的 m3u8_urls必须排好序 果ts文件名 或 ts 文件路径
    @param sep:
    @param kwargs: M3u8ToMp4 参数
    @return:
    """
    n = 1
    mp4_path_name = kwargs.pop("mp4_path_name", None) or "temp"
    if mp4_path_name.endswith(".mp4"):
        mp4_path_name = os.path.splitext(mp4_path_name)[0]
    m3u_pt, save_mp4_pt = (None, None)
    for start in range(0, len(m3u8_urls), sep):
        new_list = m3u8_urls[start:start + sep]
        m3u_pt, save_mp4_pt = M3u8ToMp4(
            new_list,
            mp4_path_name=f"{mp4_path_name}{str(n).zfill(5)}.mp4",
            **kwargs).run()
        n += 1
    return m3u_pt, save_mp4_pt


def get_log(self):
    if self.logger is None:
        import logging
        logging.basicConfig(
            format='PROCESS ID:%(process)d: %(asctime)s-%(name)s-%(levelname)s '
                   '-[line:%(lineno)d]: %(message)s', level=logging.INFO)
        logger = logging.getLogger('video_log')
        self.logger = logger


class OtherFormatToMp4:
    """
    别的格式转mp4
    todo 请先安装 ffmpeg 程序
    """

    def __init__(
            self,
            origin_video_path,
            save_mp4_path,
            logger=None,
            change_timeout=None):
        """

        @param origin_video_path: 原视频路径  ../xx or ./x or abs_path
        @param save_mp4_path: 生成文件路径
        @param change_timeout: 生成文件路径
        @param logger
        """
        self.origin_video_path = main_path(origin_video_path)
        self.save_mp4_path = main_path(save_mp4_path)
        self.logger = logger
        self.change_timeout = 180 if change_timeout is None else change_timeout
        get_log(self)

    def run(self):
        return self.to_mp4()

    def to_mp4(self):
        try:
            params = self.get_change_params()
            self.logger.info("change start %s", self.origin_video_path)
            self.__change_to_mp4(params)
            self.logger.info("change end %s", self.origin_video_path)

        finally:
            for handler in self.logger.handlers:
                if hasattr(handler, "close"):
                    handler.close()  # noqa
            self.logger.handlers.clear()
        self.logger.info("over")

    def get_change_params(self):
        if not self.save_mp4_path.endswith(".mp4"):
            self.save_mp4_path = f"{main_path(self.save_mp4_path)}.mp4"
        self.make_folder(self.save_mp4_path)
        cmd = F"ffmpeg -y -i {self.origin_video_path} " \
              F"-vcodec copy -acodec copy {self.save_mp4_path}"
        return cmd

    def __change_to_mp4(self, cmd):
        log_info = "other video to mp4 %s %s path %s"
        self.logger.info(
            log_info,
            "start",
            '--'.center(center_num, '-'),
            cmd.rsplit("-acodec copy")[-1]
        )
        subprocess.check_output(cmd, shell=True, timeout=self.change_timeout)
        self.logger.info(
            log_info,
            "end",
            '--'.center(center_num, '-'),
            cmd.rsplit("-acodec copy")[-1]
        )

    @staticmethod
    def make_folder(save_mp4_path):
        a, b = os.path.split(save_mp4_path)
        if not a:
            return
        if not os.path.isdir(a):
            os.makedirs(a)


class SliceM3u8ToMp4:

    # todo 命令太长导致错误,所以分片下载数据

    def __init__(
            self,
            m3u8_path,
            # 请求相关 --------
            request_bool=False,
            req_headers=None, req_method='GET',
            req_params: dict = None, req_data: dict = None,
            req_before_func=None, req_after_func=None,
            req_repeat_times=10, req_sleep=None,
            # 解密相关 --------
            decrypt_params: dict = None,
            decrypt_callback=None,
            decrypt_decode_before_func=None,
            decrypt_decode_after_func=None,
            # ---------------
            thread_pool=None,
            logger=None,
            sort_func=None,
            mp4_path_name=None,
            change_timeout=None,
            debug=False,
            sep=50,
            sep_mp4=50
            # ---------------
    ):
        self.m3u8_path = m3u8_path
        if 0 > sep or sep > 100:
            sep = 50
        self.params = dict(
            request_bool=request_bool,
            decrypt_params=decrypt_params,
            decrypt_callback=decrypt_callback,
            decrypt_decode_before_func=decrypt_decode_before_func,
            decrypt_decode_after_func=decrypt_decode_after_func,
            req_headers=req_headers,
            req_method=req_method,
            req_params=req_params,
            req_data=req_data,
            req_before_func=req_before_func,
            req_after_func=req_after_func,
            req_repeat_times=req_repeat_times,
            req_sleep=req_sleep,
            thread_pool=thread_pool,
            logger=logger,
            sort_func=sort_func,
            mp4_path_name=mp4_path_name,
            change_timeout=change_timeout,
            debug=debug,
            sep=sep
        )
        self.sep_mp4 = sep_mp4
        if self.sep_mp4 < 2:
            self.sep_mp4 = 2
        elif sep_mp4 > 100:
            self.sep_mp4 = 50
        self.__t_temp_save = None

    def run(self):
        return self.to_mp4()

    @property
    def t_temp_save(self):
        if self.__t_temp_save is None:
            self.__t_temp_save = f".{os.sep}temp_save_{self.get_uuid}"
        return self.__t_temp_save

    def to_mp4(self):
        if not self.m3u8_path or not isinstance(self.m3u8_path, list):
            return
        for x in self.m3u8_path:  # type:str
            if not x.startswith('http'):
                raise ValueError("must be url %s" % x)
        ori_mp4_name = self.params['mp4_path_name']
        if not ori_mp4_name:
            ori_mp4_name = f".{os.sep}temp_{self.get_uuid}.mp4"
        self.params['mp4_path_name'] = f"{self.t_temp_save}" \
                                       f"{os.sep}" \
                                       f"{os.path.split(ori_mp4_name)[-1]}"
        m3u8_path, mp4_path_name = m3u8_to_mp4_slice_download(
            self.m3u8_path,
            **self.params
        )
        dirname = os.path.split(mp4_path_name)[0]
        MP4ToTs(logger=self.params['logger']).run(True, dirname)
        self.save_mp3(dirname, ori_mp4_name)

    def mp3_slice_synthesis(self, dirname, dir_list, temp_mp4_save):
        name = dir_list[0]
        temp_mp4 = main_path(f"./temp_{self.get_uuid}")
        for x in dir_list:
            self.move_file(os.path.join(dirname, x), os.path.join(temp_mp4, x))
        M3u8ToMp4(
            temp_mp4, thread_pool=self.params['thread_pool'] or 30, change_timeout=60 * 30,
            logger=self.params['logger'],
            mp4_path_name=os.path.join(temp_mp4_save, os.path.splitext(name)[0]),
            sort_func=self.params['sort_func'], debug=self.params['debug']
        ).run(remove_ts=True)

    def save_mp3(self, dirname, ori_mp4_name):
        mp3_list = os.listdir(dirname)
        if len(os.listdir(dirname)) > self.sep_mp4:
            temp_mp4_save = main_path(f"./temp_{self.get_uuid}_save")
            for i in range(0, len(mp3_list), self.sep_mp4):
                self.mp3_slice_synthesis(dirname, mp3_list[i: i + self.sep_mp4], temp_mp4_save)
            MP4ToTs(logger=self.params['logger']).run(True, temp_mp4_save)
            for x in os.listdir(temp_mp4_save):
                self.move_file(os.path.join(temp_mp4_save, x), os.path.join(dirname, x))
            os.rmdir(temp_mp4_save)
            return self.save_mp3(dirname, ori_mp4_name)
        else:
            M3u8ToMp4(
                dirname, thread_pool=self.params['thread_pool'] or 30, change_timeout=60 * 30,
                mp4_path_name=ori_mp4_name, logger=self.params['logger'],
                sort_func=self.params['sort_func'], debug=self.params['debug']
            ).run(remove_ts=True)

    @staticmethod
    def move_file(src_file, dst_file):
        if os.path.isfile(src_file):
            f_path, _ = os.path.split(dst_file)  # 分离文件名和路径
            if not os.path.exists(f_path):
                os.makedirs(f_path)  # 创建路径
            shutil.move(src_file, dst_file)  # 移动文

    @property
    def get_uuid(self):
        import uuid
        if hasattr(self, "_uuid"):
            return self._uuid
        self._uuid = uuid.uuid4().hex[:7]  # noqa
        return self._uuid


class M3u8ToMp4:
    """ts_文件 m3u8文件转mp4"""

    def __init__(
            self,
            m3u8_path,
            request_bool=False,
            decrypt_params: dict = None,
            decrypt_callback=None,
            decrypt_decode_before_func=None,
            decrypt_decode_after_func=None,
            req_headers=None,
            req_method='GET',
            req_params: dict = None,
            req_data: dict = None,
            req_before_func=None,
            req_after_func=None,
            thread_pool=None,
            logger=None,
            sort_func=None,
            mp4_path_name=None,
            change_timeout=None,
            req_sleep=None,
            debug=False,
            req_repeat_times=10,
            write_error_info: bool = False
    ):
        """
        todo 请先安装 ffmpeg 程序 (本转化为调用ffmpeg程序转化)
        @param m3u8_path: [url, url] or folder
            todo if m3u8_path is urls -> request_bool must be True  urls must be sort
            todo 如果 m3u8_path url集合长度大于10000, 最好分片下载 调用 m3u8_to_mp4_slice_download
        @param request_bool:  让本程序发送请求 m3u8_path 必须是完整的 url 集合 bool
        @param req_method:   request_bool 为 true 生效  req 参数 - > dict
        @param req_params:  request_bool 为 true 生效 req 参数 - > dict
        @param req_data:  request_bool 为 true 生效 req 参数 - > dict
        @param req_headers:  请求头参数
        @param req_before_func request_bool=True 生效
               入参 req_params, req_data 必须返回处理后的 req_params, req_data
        @param req_after_func request_bool=True 生效
               入参 请求后的返回对象 response 必须返回处理后的 response

        @param decrypt_params: 解密函数所要的参数 字典格式
        @param decrypt_callback: 解密函数 必须返回 bytes 解码后的数据
        @param decrypt_decode_before_func:  request_bool=True 生效
               解密前的调用的函数  入参 data, index(当前请求标识), decrypt_params 必须返回 data, kwargs (解密参数)
        @param decrypt_decode_after_func:  request_bool=True 生效
            解密后的调用的函数 入参 解密后的 data 必须返回处理后的 data

        @param thread_pool: request_bool 为 true 时启用的线程池个数
        @param sort_func:  ts文件的 排序规则  返回排好序的list 默认排序sort
        @param mp4_path_name:  转化后mp4的存储路径 xxx/xxx.mp4
        @param logger:  日志记录器
        @param change_timeout:  超时设置 默认 360 s
        @param req_repeat_times:  req 重复请求次数
        @param write_error_info:  错误文件内容写入文本
        """
        self.change_timeout = 360 if change_timeout is None else change_timeout
        if not request_bool:
            if not isinstance(m3u8_path, str):
                raise FileNotFoundError(f"this path m3u8_path is not folder")
            m3u8_path = main_path(m3u8_path)
            assert ' ' not in m3u8_path
            if not os.path.isdir(m3u8_path):
                raise FileNotFoundError(f"this path m3u8_path is not folder")
        self.m3u8_path = m3u8_path
        self.decrypt_callback = decrypt_callback
        self.decrypt_params = decrypt_params
        self.sort_func = sort_func
        self.req_sleep = req_sleep
        self.logger = logger
        self.mp4_path_name = mp4_path_name.replace(
            " ", '') if mp4_path_name else mp4_path_name
        self.req_headers = req_headers or {
            "User-Agent":
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
                AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36"
        }
        self._remove_ts = False
        self.__ts_len = None
        self.request_bool = request_bool
        self.thread_pool = thread_pool
        self.req_method = req_method
        self.req_params = req_params
        self.req_data = req_data
        self.decrypt_decode_before_func = decrypt_decode_before_func
        self.decrypt_decode_after_func = decrypt_decode_after_func
        self.req_before_func = req_before_func
        self.req_after_func = req_after_func
        self.__debug = debug
        self.req_repeat_times = req_repeat_times
        self.__write_error_info = bool(write_error_info)
        get_log(self)

    def req(self, remove_ts=None):
        if not self.m3u8_path or not isinstance(self.m3u8_path, list):
            raise ValueError(f"this m3u8_path is not list or empty")
        self.logger.info("req start %s", "--".center(center_num, '-'))
        self._remove_ts = True if remove_ts is None else remove_ts
        if self.__debug is True:
            self.m3u8_path = self.m3u8_path[:1]
            self._remove_ts = False
        temp_path = main_path(f"./temp_{self.get_uuid}")
        self.logger.info("temp_path %s", temp_path)
        if not os.path.isdir(temp_path):
            os.makedirs(temp_path)
        with ThreadPoolExecutor(self.thread_pool) as thread:
            all_task = [
                thread.submit(
                    self._requests,
                    index,
                    url) for index, url in enumerate(
                    self.m3u8_path,
                    1)]
            all_t = list()
            for future in as_completed(all_task):
                data, index, _res_url = future.result()
                if callable(self.decrypt_callback):
                    new_decrypt_params = self.decrypt_params.copy() if self.decrypt_params else dict()
                    try:
                        if callable(self.decrypt_decode_before_func):
                            data, new_decrypt_params = self.decrypt_decode_before_func(
                                data, index, new_decrypt_params)
                        data = self.decrypt_callback(data, **new_decrypt_params)
                        if callable(self.decrypt_decode_after_func):
                            data = self.decrypt_decode_after_func(data)
                    except Exception as e:
                        self.logger.error("url requests error %s", _res_url)
                        all_t.append(thread.submit(self._save, data, index, temp_path, error={
                            "e": str(e),
                            'error_url': _res_url
                        }))
                        continue
                all_t.append(thread.submit(self._save, data, index, temp_path))
            for a in as_completed(all_t):
                self.logger.info(f"file %s download %s" % a.result())
            self.m3u8_path = temp_path

    def _save(self, data, index, temp_path, error=None):
        if self.__ts_len is None:
            self.__ts_len = 1
        name = self.get_ts_name(index)
        if error:
            if self.__write_error_info is False:
                return 'no path', "ok"
            name = name.rstrip(".ts") + ".txt"
        ts_path = os.path.join(temp_path, name)
        with open(ts_path, mode='wb') as f:
            f.write(data)
            if error:
                _num_ = 10 + len(error['error_url'])
                fm_t = f"{'-'.center(_num_, '-')}".encode()
                f.write(b'\r\n')
                f.write(b'\r\n')
                f.write(fm_t)
                f.write(b'\r\n---')
                f.write("***-err info-***".center(_num_ - 6, ' ').encode())
                f.write(b'---\r\n---')
                f.write(error['e'].center(_num_ - 6, ' ').encode())
                f.write(b'---\r\n---')
                f.write(error['error_url'].center(_num_ - 6, ' ').encode())
                f.write(b'---\r\n---')
                f.write("***-err info end-***".center(_num_ - 6, ' ').encode())
                f.write(b'---\r\n')
                f.write(fm_t)
        self.logger.info("save ts name %s", name)
        return os.path.basename(ts_path), "ok"

    def get_ts_name(self, index: int):
        return f"{self.get_uuid}_{str(index).zfill(7)}.ts"

    @property
    def get_uuid(self):
        import uuid
        if hasattr(self, "_uuid"):
            return self._uuid
        self._uuid = uuid.uuid4().hex[:7]  # noqa
        return self._uuid

    def _requests(self, index, url, connection_times=0, read_timeout_times=0):
        with requests.session() as session:
            session.headers.update(self.req_headers)
            params = self.req_params.copy() if self.req_params else dict()
            data = self.req_data.copy() if self.req_data else dict()
            if callable(self.req_before_func):
                params, data = self.req_before_func(params, data)
            self.logger.info("request url %s start", url)
            req_func = getattr(session, self.req_method.lower())
            if self.req_sleep and isinstance(self.req_sleep, (int, float)):
                time.sleep(self.req_sleep)
            try:
                res = req_func(
                    requote_uri(url),
                    verify=False,
                    timeout=30,
                    data=data,
                    params=params
                )
            except requests.exceptions.ConnectionError as e:
                self.logger.info("ConnectionError %s", connection_times + 1)
                if connection_times > self.req_repeat_times:
                    raise e
                time.sleep(1)
                return self._requests(index, url, connection_times=connection_times + 1)
            except requests.exceptions.ReadTimeout as e:
                self.logger.info("ReadTimeout %s", read_timeout_times + 1)
                if read_timeout_times > self.req_repeat_times:
                    raise e
                time.sleep(1)
                return self._requests(index, url, read_timeout_times=read_timeout_times + 1)
            except requests.exceptions.ChunkedEncodingError as e:
                self.logger.info("网络发生变化 %s", read_timeout_times + 1)
                if read_timeout_times > self.req_repeat_times:
                    raise e
                time.sleep(5)
                return self._requests(index, url, read_timeout_times=read_timeout_times + 1)

            self.logger.info("request url %s end", url)
            if callable(self.req_after_func):
                res = self.req_after_func(res)
            return res.content, index, res.url

    def run(self, **kwargs):
        return self.to_mp4(**kwargs)

    def to_mp4(self, **kwargs):
        self.logger.info("start %s", "--".center(center_num, '-'))
        try:
            remove_ts = kwargs.pop("remove_ts", None)
            if self.request_bool:
                self.req(remove_ts)
            else:
                self._remove_ts = remove_ts or False
            self.parse()
        finally:
            if self.logger:
                for handler in self.logger.handlers:
                    if hasattr(handler, "close"):
                        handler.close()  # noqa
                self.logger.handlers.clear()
        self.logger.info("end %s", "--".center(center_num, '-'))
        return self.m3u8_path, self.mp4_path_name

    def parse(self):
        ts_path_list = []
        if self.__ts_len is None:
            if callable(self.sort_func):
                ts_path_list = self.sort_func(ts_path_list)
            else:
                ts_path_list = self.default_sort_func
        else:
            ts_path_list = self.default_sort_func
        self.__change_to_mp4(ts_path_list)

    @property
    def default_sort_func(self):
        return list(
            filter(
                lambda x: x.endswith(".ts"),
                sorted(
                    os.listdir(
                        self.m3u8_path))))

    def __change_to_mp4(self, ts_path_list):
        p = None
        if self.mp4_path_name is None:

            self.mp4_path_name = main_path(
                os.path.join(
                    os.path.dirname(self.m3u8_path),
                    f"{os.path.basename(self.m3u8_path)}_{self.get_uuid}.mp4"
                )
            )
            self.logger.info(f"mp4_path_name {self.mp4_path_name}")
        else:
            if not self.mp4_path_name.endswith(".mp4"):
                self.mp4_path_name = f"{main_path(self.mp4_path_name)}.mp4"
            self.mp4_path_name = main_path(self.mp4_path_name)
            p, file_name = os.path.split(self.mp4_path_name)
            if p:
                if not os.path.isdir(p):
                    try:
                        os.makedirs(p)
                    except FileExistsError:
                        pass
            if file_name == ".mp4":
                file_name = f"{self.get_uuid}{file_name}"
            self.mp4_path_name = os.path.join(p, file_name)
            self.logger.info(f"mp4_path_name {self.mp4_path_name}")
        if not ts_path_list:
            if p and not os.listdir(p):
                os.rmdir(p)
            if not os.listdir(self.m3u8_path):
                os.rmdir(self.m3u8_path)
            self.logger.error("ts file is empty ")
            return
        # todo cmd 中不能带有 ffmpeg 不能解析的空格
        cmd = self._get_cmd(self.m3u8_path, ts_path_list, self.mp4_path_name)
        try:
            # todo 转化开始
            subprocess.check_output(
                cmd, shell=True, timeout=self.change_timeout)
        finally:
            if self._remove_ts:
                self.logger.info("原始文件删除中 %s", '--'.center(center_num, '-'))
                for n in ts_path_list:
                    os.unlink(os.path.join(self.m3u8_path, n))
                if not os.listdir(self.m3u8_path):
                    os.rmdir(self.m3u8_path)
                self.logger.info("原始文件删除成功 %s", '--'.center(center_num, '-'))
        return

    @staticmethod
    def _get_cmd(path, name_list, mp4_path_name, to_encoding=None):
        if not to_encoding:
            if get_sys() == "Windows":
                to_encoding = "gbk"
            else:
                to_encoding = 'utf8'
        out_list = 'concat:'
        for n in name_list:
            out_list += os.path.join(path,
                                     n).encode(to_encoding).decode(to_encoding) + '|'
        cmd = f'ffmpeg -i "{out_list.rstrip("|")}" -y -acodec copy -vcodec copy -absf aac_adtstoasc' \
              f' {mp4_path_name}'
        return cmd


class MP4ToM3u8:

    def __init__(
            self,
            ts_time=5,
            change_timeout=30,
            logger=None,
            *,
            encrypt=False,
            key_len=16
    ):
        """

        @param ts_time: ts 切片时长
        @param change_timeout: 转换超时时长
        @param encrypt:
        @param key_len:
        """
        self.ts_time = ts_time
        self.change_timeout = change_timeout
        self._enc_info_path = "enc.key"
        self.key_len = key_len
        self.encrypt = encrypt
        self.logger = logger
        get_log(self)

    @staticmethod
    def __get_key(length=16):
        b = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'
        import random
        return ''.join(random.sample(b, length)).encode('utf8')

    def run(self, *args, **kwargs):
        """
        路径中空格不要出现
        @param args:
        @param kwargs:
        @return:
        """
        return self.mp4_to_m3u8(*args, **kwargs)

    def mp4_to_m3u8(
            self,
            mp4_path: str,
            save_m3u8_path=None,
            ts_time=None,
            cmd=''):
        """

        @param mp4_path: mp4 路径
        @param save_m3u8_path: 保存
        @param ts_time:
        @param cmd:
        @return:
        """
        assert ' ' not in mp4_path
        if save_m3u8_path:
            assert ' ' not in save_m3u8_path

            if ".m3u8" not in save_m3u8_path:
                tp_a, tp_b = os.path.split(save_m3u8_path)
                old = save_m3u8_path
                save_m3u8_path = os.path.join(tp_a, tp_b, f"{tp_b}.m3u8")
            else:
                tp_a, tp_b = os.path.splitext(save_m3u8_path)
                old = tp_a
                tp_a, tp_b = os.path.split(tp_a)
                save_m3u8_path = os.path.join(tp_a, tp_b, f"{tp_b}.m3u8")
            if not os.path.isdir(old):
                os.makedirs(old)

        self.logger.info("start to m3u8 %s", '--'.center(center_num, '-'))
        mp4_path = main_path(mp4_path)
        kind = guss_file_type(mp4_path)
        create_dir = False
        if kind != "mp4":
            raise ValueError("file not mp4")
        if not save_m3u8_path:
            create_dir = True
            save_m3u8_path = os.path.splitext(mp4_path)[0]
        if not save_m3u8_path.endswith(".m3u8"):
            save_m3u8_path += ".m3u8"
        if create_dir:
            if not os.path.isdir(save_m3u8_path):
                os.makedirs(save_m3u8_path)
            save_m3u8_path = os.path.join(
                save_m3u8_path, os.path.split(save_m3u8_path)[-1])
            save_ts_file_path = os.path.splitext(save_m3u8_path)[0]
        else:
            save_ts_file_path = save_m3u8_path
        if not cmd:
            if not save_ts_file_path.endswith(".ts"):
                if save_ts_file_path.endswith(".m3u8"):
                    save_ts_file_path = os.path.splitext(save_m3u8_path)[0]
                save_ts_file_path += ".ts"
            f_path, f_name = os.path.split(save_ts_file_path)
            if not reg.search(f_name):
                f_na_li = list(os.path.splitext(f_name))
                f_na_li.insert(1, "%07d")
                f_name = ''.join(f_na_li)
                del f_na_li
            save_ts_file_path = os.path.join(f_path, f_name)
            del f_name
            del f_path
            cmd = F"ffmpeg -i " \
                  F"{mp4_path} " \
                  F"-f segment -segment_time " \
                  F"{ts_time or self.ts_time} " \
                  F"-segment_format mpegts -segment_list " \
                  F"{save_m3u8_path} " \
                  F"-c copy -bsf:v h264_mp4toannexb -map 0 " \
                  F"{save_ts_file_path} -y"
        if os.path.exists(save_m3u8_path):
            return
        # todo 转化开始
        subprocess.check_output(
            cmd, shell=True, timeout=self.change_timeout)
        if self.encrypt:
            with open(os.path.join(os.path.split(save_m3u8_path)[0], self._enc_info_path), 'w') as f:
                f.write(f"key:{self.__get_key(self.key_len)}\n")
                f.write(f"iv:{self.__get_key()}\n")
        self.logger.info("start to m3u8 end %s", '--'.center(center_num, '-'))


class MP4ToTs:

    def __init__(self, change_timeout=None, thread_pool=None, logger=None):
        self.change_timeout = change_timeout or 60 * 30
        self.save_ts_path = []
        self.thread_pool = thread_pool or 30
        self.logger = logger
        self._remove_ts = False
        get_log(self)

    def run(self, remove_ts=False, *args, **kwargs):
        self._remove_ts = remove_ts
        return self.mp4_to_ts(*args, **kwargs)

    def get_mp4_list(self, folder, save_ts_path, video_format='.mp4'):
        """

        @param folder:
        @param save_ts_path:
        @param video_format:
        @return:
        """
        li = []
        for index, file in enumerate(os.listdir(folder), 1):
            if os.path.splitext(file)[-1] != video_format:
                continue
            if save_ts_path:
                a, b = os.path.splitext(save_ts_path)
                self.save_ts_path.append(f"{a}{str(index).zfill(3)}{b}")
            else:
                a = os.path.splitext(file)[0]
                self.save_ts_path.append(f"{a}.ts")
            li.append(file)
        return li

    def mp4_to_ts(self, mp4_path: str, save_ts_path=None):
        """

        @param mp4_path:
        @param save_ts_path:
        @return:
        """
        assert ' ' not in mp4_path
        if save_ts_path:
            assert ' ' not in save_ts_path
            if ".ts" not in save_ts_path:
                save_ts_path += '.ts'
        self.logger.info("start to ts %s", '--'.center(center_num, '-'))
        mp4_path = main_path(mp4_path)
        if os.path.isdir(mp4_path):
            mp4_path_list = self.get_mp4_list(mp4_path, save_ts_path)
        else:
            if not save_ts_path:
                self.save_ts_path.append(os.path.splitext(mp4_path)[0] + ".ts")
            else:
                self.save_ts_path.append(save_ts_path)
            mp4_path_list = [mp4_path]
        with ThreadPoolExecutor(self.thread_pool) as thread:
            thread_list = []
            for index, mp4_file in enumerate(mp4_path_list):
                mp4_path_file = os.path.join(mp4_path, mp4_file)
                kind = guss_file_type(mp4_path_file)
                if kind != "mp4":
                    raise ValueError("file not mp4")
                save_ts_file_path = self.save_ts_path[index]
                folder_file = os.path.split(save_ts_file_path)[0]
                if not save_ts_path and not folder_file:
                    save_ts_file_path = os.path.join(mp4_path, save_ts_file_path)
                    folder_file = os.path.split(save_ts_file_path)[0]
                if folder_file and not os.path.isdir(folder_file):
                    os.makedirs(folder_file)
                cmd = F"ffmpeg -y -i {mp4_path_file} " \
                      F"-vcodec copy -acodec copy -vbsf " \
                      F"h264_mp4toannexb {save_ts_file_path}"

                if os.path.exists(save_ts_file_path):
                    continue

                # todo 转化开始
                _t_ = thread.submit(
                    self.__change,
                    cmd,
                    save_ts_file_path,
                    shell=True,
                    timeout=self.change_timeout
                )
                thread_list.append(_t_)
            for k in as_completed(thread_list):
                self.logger.info("file save ok %s ", k.result())

        if self._remove_ts:
            for mp4_file in mp4_path_list:
                mp4_path_file = os.path.join(mp4_path, mp4_file)
                os.unlink(mp4_path_file)
            if not os.listdir(mp4_path):
                os.rmdir(mp4_path)
        self.logger.info("start to ts end %s", '--'.center(center_num, '-'))

    @staticmethod
    def __change(cmd, mp4_path_file, **kwargs):
        subprocess.check_output(cmd, **kwargs)
        return mp4_path_file