content: package com.acgist.snail.protocol.hls;
  
  import java.io.File;
  import java.io.IOException;
  import java.nio.file.Files;
  import java.security.InvalidAlgorithmParameterException;
  import java.security.InvalidKeyException;
  import java.security.NoSuchAlgorithmException;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.stream.Collectors;
  
  import javax.crypto.Cipher;
  import javax.crypto.NoSuchPaddingException;
  import javax.crypto.spec.IvParameterSpec;
  import javax.crypto.spec.SecretKeySpec;
  
  import com.acgist.snail.config.SymbolConfig;
  import com.acgist.snail.context.wrapper.KeyValueWrapper;
  import com.acgist.snail.logger.Logger;
  import com.acgist.snail.logger.LoggerFactory;
  import com.acgist.snail.net.DownloadException;
  import com.acgist.snail.net.NetException;
  import com.acgist.snail.net.hls.M3u8;
  import com.acgist.snail.net.hls.M3u8.Type;
  import com.acgist.snail.net.http.HttpClient;
  import com.acgist.snail.utils.CollectionUtils;
  import com.acgist.snail.utils.StringUtils;
  import com.acgist.snail.utils.UrlUtils;
  
  /**
   * M3U8 Builder
   * 
   * 多级M3U8列表：
   * 
   * #EXTM3U
   * #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=800000,RESOLUTION=1080x608
   * 1000k/hls/index.m3u8
   * 
   * M3U8文件列表：
   * 
   * #EXTM3U
   * #EXT-X-VERSION:3
   * #EXT-X-KEY:METHOD=AES-128,URI="https://www.acgist.com/key",IV=0x00000000
   * #EXT-X-TARGETDURATION:8
   * #EXT-X-MEDIA-SEQUENCE:0
   * #EXTINF:7.200000,
   * 7a9cb1ab891000000.ts
   * #EXTINF:0.920000,
   * 7a9cb1ab891000001.ts
   * ...
   * #EXTINF:4.000000,
   * 7a9cb1ab891000616.ts
   * #EXTINF:2.320000,
   * 7a9cb1ab891000617.ts
   * #EXT-X-ENDLIST
   * 
   * @author acgist
   */
  public final class M3u8Builder {
      
      private static final Logger LOGGER = LoggerFactory.getLogger(M3u8Builder.class);
  
      /**
       * IV长度：{@value}
       */
      private static final int IV_LENGTH = 32;
      /**
       * 16进制IV开头：{@value}
       */
      private static final String IV_HEX_PREFIX = "0x";
      /**
       * 类型标签：{@value}
       */
      private static final String LABEL_EXTM3U = "EXTM3U";
      /**
       * 视频分片：{@value}
       */
      private static final String LABEL_EXTINF = "EXTINF";
      /**
       * 视频分片（码率）：{@value}
       */
      private static final String LABEL_EXT_X_BITRATE = "EXT-X-BITRATE";
      /**
       * 数据加密信息：{@value}
       * 
       * #EXT-X-KEY:METHOD=AES-128,URI="https://www.acgist.com/key",IV=0x00000000
       */
      private static final String LABEL_EXT_X_KEY = "EXT-X-KEY";
      /**
       * 视频结束标记：{@value}
       * 没有结束标记：直播流媒体
       */
      private static final String LABEL_EXT_X_ENDLIST = "EXT-X-ENDLIST";
      /**
       * 多级M3U8列表：{@value}
       */
      private static final String LABEL_EXT_X_STREAM_INF = "EXT-X-STREAM-INF";
      /**
       * 序列号：{@value}
       * 没有数据加密IV使用序列号
       * 
       * @see #LABEL_EXT_X_KEY
       */
      private static final String LABEL_EXT_X_MEDIA_SEQUENCE = "EXT-X-MEDIA-SEQUENCE";
      /**
       * 码率：{@value}
       */
      private static final String ATTR_BANDWIDTH = "BANDWIDTH";
      /**
       * 加密信息：{@value}
       * 
       * @see #LABEL_EXT_X_KEY
       */
      private static final String ATTR_IV = "IV";
      /**
       * 加密信息：{@value}
       * 
       * @see #LABEL_EXT_X_KEY
       */
      private static final String ATTR_URI = "URI";
      /**
       * 加密信息：{@value}
       * 
       * @see #LABEL_EXT_X_KEY
       */
      private static final String ATTR_METHOD = "METHOD";
      
      /**
       * 原始链接地址
       */
      private final String source;
      /**
       * M3U8描述信息
       */
      private final List<String> lines;
      /**
       * 标签
       */
      private final List<Label> labels;
      
      /**
       * @param source 原始链接地址
       * @param lines  M3U8描述信息
       */
      private M3u8Builder(String source, List<String> lines) {
          this.source = source;
          this.lines = lines;
          this.labels = new ArrayList<>();
      }
      
      /**
       * 新建M3U8解析器
       * 
       * @param file   描述文件
       * @param source 原始链接地址
       * 
       * @return {@link M3u8Builder}
       * 
       * @throws DownloadException 下载异常
       */
      public static final M3u8Builder newInstance(File file, String source) throws DownloadException {
          try {
              return new M3u8Builder(source, Files.readAllLines(file.toPath()));
          } catch (IOException e) {
              throw new DownloadException("M3U8文件解析失败", e);
          }
      }
      
      /**
       * 新建M3U8解析器
       * 
       * @param content 描述信息
       * @param source  原始链接地址
       * 
       * @return {@link M3u8Builder}
       */
      public static final M3u8Builder newInstance(String content, String source) {
          return new M3u8Builder(source, StringUtils.readLines(content));
      }
      
      /**
       * 解析M3U8
       * 
       * @return {@link M3u8}
       * 
       * @throws NetException      网络异常
       * @throws DownloadException 下载异常
       */
      public M3u8 build() throws NetException, DownloadException {
          if(CollectionUtils.isEmpty(this.lines)) {
              throw new DownloadException("M3U8文件解析失败（没有描述信息）");
          }
          this.buildLabels();
          this.checkLabels();
          return this.buildM3u8();
      }
      
      /**
       * 解析标签
       * 
       * 空行：忽略
       * URL：文件地址、M3U8地址
       * 标签、注释：#开头
       * 属性：跟在标签后面
       */
      private void buildLabels() {
          int jndex;
          String line;
          Label label = null;
          // 解析标签
          for (int index = 0; index < this.lines.size(); index++) {
              line = this.lines.get(index).strip();
              if(StringUtils.isEmpty(line)) {
                  // 空行跳过
                  continue;
              }
              if(line.indexOf(SymbolConfig.Symbol.POUND.toChar()) == 0) {
                  // 标签
                  label = new Label();
                  this.labels.add(label);
                  jndex = line.indexOf(SymbolConfig.Symbol.COLON.toChar());
                  if(jndex < 0) {
                      // 没有属性
                      label.setName(line.substring(1).strip());
                  } else {
                      // 含有属性
                      label.setName(line.substring(1, jndex).strip());
                      label.setValue(line.substring(jndex + 1).strip());
                  }
              } else {
                  // URL
                  if(label == null) {
                      LOGGER.warn("链接没有标签信息：{}-{}", this.source, line);
                  } else {
                      label.setUrl(line);
                  }
              }
          }
      }
      
      /**
       * 标签校验
       * 
       * @throws DownloadException 下载异常
       */
      private void checkLabels() throws DownloadException {
          if(this.labels.isEmpty()) {
              throw new DownloadException("M3U8格式错误（没有标签）");
          }
          // 验证类型
          final String type = this.labels.get(0).getName();
          if(!LABEL_EXTM3U.equalsIgnoreCase(type)) {
              throw new DownloadException("M3U8格式错误（头部错误）");
          }
      }
      
      /**
       * 新建M3U8信息
       * 
       * @return {@link M3u8}
       * 
       * @throws NetException 网络异常
       */
      private M3u8 buildM3u8() throws NetException {
          final M3u8.Type type = this.buildType();
          final Cipher cipher = this.buildCipher();
          List<String> links;
          if(type == Type.M3U8) {
              links = this.buildM3u8Links();
          } else {
              // 获取LABEL_EXTINF标签数据
              links = this.buildFileLinks(LABEL_EXTINF);
              // 没有LABEL_EXTINF标签数据：获取LABEL_EXT_X_BITRATE标签数据
              if(CollectionUtils.isEmpty(links)) {
                  links = this.buildFileLinks(LABEL_EXT_X_BITRATE);
              }
              if(CollectionUtils.isEmpty(links)) {
                  throw new NetException("没有下载文件");
              }
          }
          return new M3u8(type, cipher, links);
      }
  
      /**
       * 新建M3U8类型
       * 
       * @return {@link Type}
       */
      private Type buildType() {
          // 注意顺序：多级M3U8列表、直播流媒体、文件列表
          // 判断是否是多级M3U8列表
          final boolean multiM3u8 = this.labels.stream()
              .anyMatch(label -> LABEL_EXT_X_STREAM_INF.equalsIgnoreCase(label.getName()));
          if(multiM3u8) {
              return Type.M3U8;
          }
          // 判断是否是直播流媒体
          final boolean streamM3u8 = this.labels.stream()
              .noneMatch(label -> LABEL_EXT_X_ENDLIST.equalsIgnoreCase(label.getName()));
          if(streamM3u8) {
              return Type.STREAM;
          }
          return Type.FILE;
      }
  
      /**
       * 新建加密套件
       * 
       * @return 加密套件
       * 
       * @throws NetException 网络异常
       */
      private Cipher buildCipher() throws NetException {
          final var optional = this.labels.stream()
              .filter(label -> LABEL_EXT_X_KEY.equalsIgnoreCase(label.getName()))
              .findFirst();
          if(optional.isEmpty()) {
              // 没有加密标签：不用解密
              return null;
          }
          final KeyValueWrapper wrapper = optional.get().attrs();
          final String method = wrapper.getIgnoreCase(ATTR_METHOD);
          final M3u8.Protocol protocol = M3u8.Protocol.of(method);
          LOGGER.debug("HLS加密算法：{}", method);
          if(protocol == null || protocol == M3u8.Protocol.NONE) {
              throw new NetException("不支持的HLS加密算法：" + method);
          }
          if(protocol == M3u8.Protocol.AES_128) {
              return this.buildCipherAes128(wrapper.getIgnoreCase(ATTR_IV), wrapper.getIgnoreCase(ATTR_URI));
          } else {
              throw new NetException("不支持的HLS加密算法：" + method);
          }
      }
      
      /**
       * 新建加密套件
       * 
       * @param iv  IV
       * @param uri URI
       * 
       * @return 加密套件
       * 
       * @throws NetException 网络异常
       */
      private Cipher buildCipherAes128(String iv, String uri) throws NetException {
          final byte[] secret = HttpClient
              .newInstance(UrlUtils.redirect(this.source, uri))
              .get()
              .responseToBytes();
          try {
              return this.buildCipher(this.buildCipherIv(iv), secret, "AES", "AES/CBC/NoPadding");
          } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
              throw new NetException("获取加密套件失败", e);
          }
      }
      
      /**
       * 新建IV
       * 
       * @param iv IV
       * 
       * @return IV
       * 
       * @see #LABEL_EXT_X_MEDIA_SEQUENCE
       */
      private byte[] buildCipherIv(String iv) {
          if(iv == null) {
              iv = this.buildCiperSequenceIv();
          }
          if(
              iv.startsWith(IV_HEX_PREFIX) ||
              iv.startsWith(IV_HEX_PREFIX.toUpperCase())
          ) {
              // 去掉十六进制前缀
              iv = iv.substring(IV_HEX_PREFIX.length());
          }
          final int length = iv.length();
          if(length == IV_LENGTH) {
              return StringUtils.unhex(iv);
          } else if(length < IV_LENGTH) {
              final String padding = SymbolConfig.Symbol.ZERO.toString().repeat(IV_LENGTH - length);
              return StringUtils.unhex(padding + iv);
          } else {
              return StringUtils.unhex(iv.substring(length - IV_LENGTH));
          }
      }
      
      /**
       * 新建序列号IV
       * 
       * @return IV
       */
      private String buildCiperSequenceIv() {
          final var optional = this.labels.stream()
              .filter(label -> LABEL_EXT_X_MEDIA_SEQUENCE.equalsIgnoreCase(label.getName()))
              .findFirst();
          if(optional.isEmpty()) {
              return SymbolConfig.Symbol.ZERO.toString().repeat(IV_LENGTH);
          } else {
              final String sequence = optional.get().getValue();
              final int length = sequence.length();
              if(length < IV_LENGTH) {
                  final String padding = SymbolConfig.Symbol.ZERO.toString().repeat(IV_LENGTH - length);
                  return padding + sequence;
              } else if(length > IV_LENGTH) {
                  return sequence.substring(length - IV_LENGTH);
              } else {
                  return sequence;
              }
          }
      }
      
      /**
       * 新建加密套件
       * 
       * @param iv             IV
       * @param secret         密钥
       * @param algorithm      加密算法名称
       * @param transformation 算法描述
       * 
       * @return 加密套件
       * 
       * @throws InvalidKeyException                密钥异常
       * @throws NoSuchPaddingException             填充异常
       * @throws NoSuchAlgorithmException           算法异常
       * @throws InvalidAlgorithmParameterException 参数异常
       */
      private Cipher buildCipher(byte[] iv, byte[] secret, String algorithm, String transformation) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
          final SecretKeySpec secretKeySpec = new SecretKeySpec(secret, algorithm);
          final IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
          final Cipher cipher = Cipher.getInstance(transformation);
          cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
          return cipher;
      }
      
      /**
       * 新建多级M3U8链接
       * 
       * @return 多级M3U8链接
       */
      private List<String> buildM3u8Links() {
          return this.labels.stream()
              .filter(label -> LABEL_EXT_X_STREAM_INF.equalsIgnoreCase(label.getName()))
              .sorted((sourceLabel, targetLabel) -> {
                  // 码率排序
                  final String sourceBandwidth = sourceLabel.attrs().getIgnoreCase(ATTR_BANDWIDTH);
                  final String targetBandwidth = targetLabel.attrs().getIgnoreCase(ATTR_BANDWIDTH);
                  if(sourceBandwidth == null || targetBandwidth == null) {
                      return 0;
                  } else {
                      return Integer.valueOf(sourceBandwidth).compareTo(Integer.valueOf(targetBandwidth));
                  }
              })
              .map(label -> UrlUtils.redirect(this.source, label.getUrl()))
              .collect(Collectors.toList());
      }
      
      /**
       * @return 文件链接
       */
      private List<String> buildFileLinks(String labelName) {
          return this.labels.stream()
              .filter(label -> labelName.equalsIgnoreCase(label.getName()))
              .filter(label -> StringUtils.isNotEmpty(label.getUrl()))
              .map(label -> UrlUtils.redirect(this.source, label.getUrl()))
              .collect(Collectors.toList());
      }
      
      /**
       * 标签
       * 
       * @author acgist
       */
      public static final class Label {
          
          /**
           * 标签名称
           */
          private String name;
          /**
           * 标签值
           */
          private String value;
          /**
           * 标签链接
           */
          private String url;
          /**
           * 属性Key-Value包装器
           */
          private KeyValueWrapper wrapper;
  
          /**
           * @return 标签名称
           */
          public String getName() {
              return this.name;
          }
  
          /**
           * @param name 标签名称
           */
          public void setName(String name) {
              this.name = name;
          }
  
          /**
           * @return 标签值
           */
          public String getValue() {
              return this.value;
          }
  
          /**
           * @param value 标签值
           */
          public void setValue(String value) {
              this.value = value;
          }
  
          /**
           * @return 标签链接
           */
          public String getUrl() {
              return this.url;
          }
  
          /**
           * @param url 标签链接
           */
          public void setUrl(String url) {
              this.url = url;
          }
  
          /**
           * @return 属性Key-Value包装器
           */
          public KeyValueWrapper attrs() {
              if(this.wrapper == null) {
                  this.wrapper = KeyValueWrapper.newInstance(SymbolConfig.Symbol.COMMA.toChar(), SymbolConfig.Symbol.EQUALS.toChar(), this.value);
                  this.wrapper.decode();
              }
              return this.wrapper;
          }
          
      }
      
  }
  