Skip to content

去广告功能增强 #481

Description

@talefou

功能说明
去广告功能增强。

使用场景
自定义去广告脚本。

AI 整合适配代码如下:

/**
 * MoonTVPlus 超级M3U8广告过滤脚本
 * 
 * 使用说明:
 * - 此脚本用于在管理配置中自定义M3U8播放列表的去广告逻辑
 * - 保存到数据库中的 SiteConfig.CustomAdFilterCode 字段
 * - 客户端会自动缓存代码,只在版本更新时重新获取
 * - 函数签名必须为: filterAdsFromM3U8(type, m3u8Content)
 * - type 参数为视频源类型(可选),m3u8Content 为播放列表内容
 * - 函数需要返回处理后的M3U8内容
 * 
 * 工作流程:
 * 1. 解析M3U8结构(提取片段信息和元数据)
 * 2. 多维度分析(时长异常、位置异常、不连续标记)
 * 3. 科学统计(Z-score、标准差、百分位数)
 * 4. 智能过滤(动态阈值、综合评分)
 * 5. 完善重建(保留关键标签、更新头部信息)
 */

function filterAdsFromM3U8(type, m3u8Content) {
  if (!m3u8Content) return '';
  
  try {
    // 使用超级算法
    return SuperFilterAdsFromM3U8(m3u8Content, null);
  } catch (error) {
    console.error('[MoonTVPlus AdFilter] 超级过滤异常,降级使用基础过滤:', error);
    // 降级方案
    return filterAdsFromM3U8Basic(m3u8Content);
  }
}

/**
 * 基础M3U8广告过滤(降级方案)
 */
function filterAdsFromM3U8Basic(m3u8Content) {
  if (!m3u8Content) return '';
  
  const adKeywords = [
    'sponsor',
    '/ad/',
    '/ads/',
    'advert',
    'advertisement',
    '/adjump',
    'redtraffic'
  ];
  
  const lines = m3u8Content.split('\n');
  const filteredLines = [];
  let i = 0;

  while (i < lines.length) {
    const line = lines[i];

    if (line.includes('#EXT-X-DISCONTINUITY')) {
      i++;
      continue;
    }

    if (line.includes('#EXTINF:')) {
      if (i + 1 < lines.length) {
        const nextLine = lines[i + 1];
        const containsAdKeyword = adKeywords.some(keyword =>
          nextLine.toLowerCase().includes(keyword.toLowerCase())
        );

        if (containsAdKeyword) {
          i += 2;
          continue;
        }
      }
    }

    filteredLines.push(line);
    i++;
  }

  return filteredLines.join('\n');
}

/**
 * 超级M3U8广告过滤
 */
function SuperFilterAdsFromM3U8(m3u8Content, regexFilter) {
  if (!m3u8Content) return '';
  
  // 第一阶段:预处理
  let processedContent = regexFilter 
    ? applyRegexFilter(m3u8Content, regexFilter) 
    : m3u8Content;
  
  // 解析M3U8结构
  const parsed = parseM3U8Structure(processedContent);
  if (!parsed.segments || parsed.segments.length === 0) {
    return processedContent;
  }
  
  // 第二阶段:科学分析
  const stats = calculateSegmentStats(parsed.segments);
  const analyzedSegments = analyzeSegments(parsed.segments, stats);
  const filteredSegments = applyFilterDecision(analyzedSegments, stats);
  
  // 第三阶段:重建M3U8
  return rebuildM3U8(parsed.headers, filteredSegments, processedContent);
}

/**
 * 应用正则过滤
 */
function applyRegexFilter(content, regexFilter) {
  try {
    const regex = new RegExp(regexFilter, 'gi');
    return content.replace(regex, '');
  } catch (e) {
    console.warn('[MoonTVPlus AdFilter] 正则过滤失败:', e);
    return content;
  }
}

/**
 * 深度解析M3U8结构
 */
function parseM3U8Structure(content) {
  const lines = content.split('\n');
  const segments = [];
  const headers = { main: [], other: [] };
  let currentDiscontinuity = false;
  let currentMap = null;
  let segmentIndex = 0;

  for (let i = 0; i < lines.length; i++) {
    const line = (lines[i] || '').trim();
    
    // 收集头部信息
    if (i < 10 && line.startsWith('#EXT')) {
      headers.main.push(line);
      continue;
    }
    
    // 处理关键标签
    if (line.startsWith('#EXT-X-MAP:')) {
      currentMap = line;
      continue;
    }
    
    if (line.includes('#EXT-X-DISCONTINUITY')) {
      currentDiscontinuity = true;
      continue;
    }
    
    // 解析片段
    if (line.startsWith('#EXTINF:')) {
      const durationMatch = line.match(/#EXTINF:([\d.]+)/);
      if (durationMatch && lines[i + 1] && !(lines[i + 1] || '').startsWith('#')) {
        const duration = parseFloat(durationMatch[1]);
        const url = (lines[i + 1] || '').trim();
        
        segments.push({
          index: segmentIndex++,
          startLine: i,
          endLine: i + 1,
          duration: duration,
          url: url,
          hasDiscontinuity: currentDiscontinuity,
          hasMap: currentMap !== null,
          content: currentMap 
            ? [currentMap, line, lines[i + 1]].join('\n')
            : [line, lines[i + 1]].join('\n'),
          isAd: false,
          adScore: 0
        });
        
        currentDiscontinuity = false;
        currentMap = null;
        i++;
      }
    } else if (line.startsWith('#')) {
      headers.other.push(line);
    }
  }
  
  return { segments, headers };
}

/**
 * 计算高级统计量
 */
function calculateSegmentStats(segments) {
  const durations = segments.map(s => s.duration);
  const totalDuration = durations.reduce((sum, d) => sum + d, 0);
  const avgDuration = totalDuration / durations.length;
  
  // 计算标准差
  const squaredDiffs = durations.map(d => Math.pow(d - avgDuration, 2));
  const stdDev = Math.sqrt(squaredDiffs.reduce((sum, sd) => sum + sd, 0) / durations.length);
  
  // 排序后的时长数组用于百分位计算
  const sortedDurations = durations.slice().sort((a, b) => a - b);
  const p10 = sortedDurations[Math.floor(durations.length * 0.1)];
  const p90 = sortedDurations[Math.floor(durations.length * 0.9)];
  
  return {
    avgDuration: avgDuration,
    stdDev: stdDev,
    p10: p10,
    p90: p90,
    totalDuration: totalDuration,
    segmentCount: segments.length,
    durationRange: [sortedDurations[0], sortedDurations[sortedDurations.length - 1]]
  };
}

/**
 * 多维度片段分析
 */
function analyzeSegments(segments, stats) {
  const avgDuration = stats.avgDuration;
  const stdDev = stats.stdDev;
  const p10 = stats.p10;
  
  return segments.map(segment => {
    const deviation = Math.abs(segment.duration - avgDuration);
    const zScore = stdDev === 0 ? 0 : deviation / stdDev;
    
    // 1. 时长异常检测
    const durationAbnormality = Math.min(1, zScore / 3);
    
    // 2. 位置异常检测
    let positionFactor = 0;
    if (segment.index < 3 && segment.duration < p10) {
      positionFactor = 0.8;
    } else if (segment.index > segments.length - 3 && segment.duration < p10) {
      positionFactor = 0.5;
    }
    
    // 3. 不连续标记检测
    const discontinuityFactor = segment.hasDiscontinuity ? 0.3 : 0;
    
    // 综合广告概率
    const adScore = Math.min(1, 
      (durationAbnormality * 0.6) + 
      (positionFactor * 0.3) + 
      (discontinuityFactor * 0.1)
    );
    
    return Object.assign({}, segment, {
      adScore: adScore,
      isAd: adScore > 0.65,
      stats: { deviation: deviation, zScore: zScore }
    });
  });
}

/**
 * 智能过滤决策
 */
function applyFilterDecision(segments, stats) {
  const avgDuration = stats.avgDuration;
  const stdDev = stats.stdDev;
  
  // 动态调整阈值
  const baseThreshold = 0.65;
  const dynamicThreshold = Math.min(0.8, Math.max(0.5, 
    baseThreshold - (stdDev / avgDuration) * 0.2
  ));
  
  return segments.filter(segment => {
    // 明确广告标记
    if (segment.isAd && segment.adScore > dynamicThreshold) {
      return false;
    }
    
    // 极短片段过滤
    if (segment.duration < 1.0 && segment.index > 3) {
      return false;
    }
    
    // 保留关键片段
    if (segment.hasMap) {
      return true;
    }
    
    return true;
  });
}

/**
 * 完美重建M3U8
 */
function rebuildM3U8(headers, segments, originalContent) {
  const keepLines = new Set();
  
  // 保留头部
  headers.main.forEach((_, i) => keepLines.add(i));
  
  // 保留片段
  segments.forEach(segment => {
    for (let i = segment.startLine; i <= segment.endLine; i++) {
      keepLines.add(i);
    }
  });
  
  // 处理关键标签
  const lines = originalContent.split('\n');
  const criticalTags = [
    '#EXT-X-VERSION',
    '#EXT-X-TARGETDURATION',
    '#EXT-X-MEDIA-SEQUENCE',
    '#EXT-X-PLAYLIST-TYPE',
    '#EXT-X-ENDLIST'
  ];
  
  for (let i = 0; i < lines.length; i++) {
    const line = (lines[i] || '').trim();
    if (criticalTags.some(tag => line.startsWith(tag))) {
      keepLines.add(i);
    }
  }
  
  // 重建
  const filteredLines = lines.filter((_, i) => keepLines.has(i));
  updateM3U8Headers(filteredLines, segments);
  
  return filteredLines.join('\n');
}

/**
 * 更新M3U8头部信息
 */
function updateM3U8Headers(lines, segments) {
  if (segments.length === 0) return;
  
  // 更新TARGETDURATION
  const maxDuration = Math.max.apply(null, segments.map(s => s.duration));
  for (let i = 0; i < lines.length; i++) {
    if (lines[i].startsWith('#EXT-X-TARGETDURATION')) {
      lines[i] = '#EXT-X-TARGETDURATION:' + Math.ceil(maxDuration);
      break;
    }
  }
  
  // 更新MEDIA-SEQUENCE
  if (segments[0].index > 0) {
    for (let i = 0; i < lines.length; i++) {
      if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE')) {
        lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + segments[0].index;
        break;
      }
    }
  }
}

其他内容
其他可供参考的内容,如:

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions