0
Fork 0
mirror of https://github.com/Silent1566/webhtv.git synced 2026-11-15 19:30:54 +00:00

Compare commits

..

29 commits

Author SHA1 Message Date
Silent1566
1d611acd54 支持:默认 EXO 内核执行结构化去广告规则
此前 HlsManifestCleaner 只在 IJK 的 MpvHlsProxy 与 nano /m3u8 代理上执行,
EXO(默认内核)与系统内核完全不跑结构化规则 —— 用户和接口下发的 HLS 规则在
多数用户的设备上是死的,只有 fork 版 media3 里的 legacy 启发式在工作。

新增 HlsPlaylistCleaningDataSource 挂在 MediaSourceFactory 的数据源链上:
嗅探 #EXTM3U 识别 playlist、缓冲整份、跑 HlsAdblockPipeline、交付净化结果。
放在缓存之上,好让改规则下次加载即生效而不必等淘汰。

三件必须一起做的事,否则接上比不接更糟:

- 关掉 fork parser 的启发式(ExoUtil setAdblock(false)),由 pipeline 保证
  两套引擎互斥。HlsAdsParser.findMinorityGroup 的判据是「最大分组须超切片总数
  50%」且删除量无上限,而结构化净化会缩小它比对的分母。实测正片分布在两个 CDN
  (260+240 片)+ 广告 25 片时,原始 playlist 上启发式弃权(520<=525),净化后
  520>500 闸门打开,删掉那 240 片正片 —— 48% 的正片静默消失。

- 只处理 VOD(#EXT-X-ENDLIST 判据,与 MpvHlsProxy.isVodPlaylist 一致)。
  cleaner 会按删掉的头部片数抬高 #EXT-X-MEDIA-SEQUENCE,但它的回退闸门会在
  相邻两次 reload 间翻转,回退时发出未抬高的原始序号。滑动窗口里新广告块总是
  先出现在尾部(删除点不连续 → 回退),随窗口滑动才变头部连续,于是序号来回跳。
  media3 的 isNewerThan 会静默丢弃「变旧」的 reload,targetDuration×3.5 后抛
  PlaylistStuckException 卡播。判据刻意大小写敏感,与 cleaner 保持一致 ——
  不一致会让门判 VOD 放行而 cleaner 判直播走抬高分支,缺陷原地复活。

- 隐式 IV 的加密流两套引擎都不许动(新增 HlsEncryptionGuard,放在
  HlsAdblockPipeline 入口)。#EXT-X-KEY 省略 IV= 时解密 IV 由每片的 media
  sequence 推导,删片会让后续每片 IV 错位,画面直接花屏。这道检查曾放在
  HlsManifestCleaner 里,那是错的:cleaner 唯一的拒绝方式是 Result.fallback,
  而 fallback 恰是 pipeline 判定「最该由启发式接手」的信号 —— 等于把 playlist
  递给启发式,IV 错位一分不少还多删 200 片正片。判据逐字镜像 media3 的
  REGEX_METHOD/REGEX_IV(大小写敏感、锚定属性名)、TAG_KEY 不含冒号、按
  \r\n|\n|\r 切行,覆盖 URI 里出现 ?method=none / ?iv=1、KEYFORMAT 里出现
  iv=、标签与冒号间有空格、CR-only 换行这五种会骗过朴素子串匹配的形状。

测试 27 条,每条都用反向插桩确认非空转。夹具刻意做成 legacy 启发式会弃权的
均匀命名 —— 此前用独立广告域名,启发式的输出与结构化引擎逐字节相同,导致
4 条测试把规则换成 List.of() 也照样绿。另外本模块没配 returnDefaultValues,
未 mock 的 android.util.Log 会让 HlsAdsParser 第一句就抛而被 pipeline 吞掉,
启发式实际从未执行过;用 @BeforeClass 关掉 media3 日志让它真的跑起来,
并 @AfterClass 还原(进程级静态状态,无 fork 会泄漏给别的测试类)。

已知覆盖缺口:ExoUtil 的 setAdblock(false) 与 getDataSourceFactory 的挂载点
纯 JVM 都构造不出来,改回去不会有红灯,只能靠注释与人工复查。
2026-08-31 17:38:02 +08:00
Silent1566
1a0f44f091 修复:新规则叠加已启用规则后越过回退闸门会连带丢掉原有净化
HlsManifestCleaner 的三道回退闸门(全删 / 删除比例 > 35% / 删除时长 > 90s)
作用于所有规则合并后的删除总量,而自检此前只验证新规则单独生效的效果。一条
单独安全的规则叠加到已生效规则上可能越过闸门,导致整份 manifest 回退 —— 不只
新规则无效,连该站原有的结构化净化也一起丢掉,比不加规则更差。

把已启用规则接进自检:AdFeedbackDataSource.activeHlsRules 经 Host 与三个分类器
传到 RuleSelfCheck,合并后跑一遍真实 cleaner,回退即拒绝。

几个刻意的取舍:

- 不追究回退是谁造成的。「已启用规则自己就回退时放行」看似合理(那时生产上已
  整体不净化),实则错误:判据只对证据里这一份 manifest 成立,而规则一旦保存就
  对该站所有剧集长期生效。实测同一站点另一集里已启用规则并未越界(删 10/30),
  叠加后越界回退,原本正常工作的 10 片删除全丢。且合并后回退时新规则在该
  manifest 上删 0 片,对它是否有效没有任何证据。

- activeHlsRules 用 null 表示「无法确定」,与空列表(确无规则生效)区分。
  removedCount 对规则集单调,于是用子集判出「会回退」是安全的,判出「不回退」
  却对全集一无所知。带未锚定 segmentUrlRegex 的已启用规则在合成 manifest 上
  命中 0 片(合成 URI 不含 query/fragment),此类规则存在时整体返回 null 而非
  把它剔除后预测。Host.activeHlsRules 因此不给默认实现,读取失败也传播 null。

- 不做「区间外切片逐一比对」。合并后的删除集恒等于各自删除集的并集,而单独校验
  已钉住「新规则只删区间内」,那种比对恒真。

顺带堵上一个既有洞:区间内外存在同一 URI 时,「删反了」(留区间内、删区间外)
与正确结果的 URI 次数和删除总数完全相同,两道判据同时被抵消而放行,真实运行删
正片且 fallback=false。真实 URL 靠 query 区分而证据已去敏,补判据无解,只能弃权。
代价是也拒掉一部分本来安全的重名场景,注释里记了权衡与将来的救回思路。

测试:新增 RuleStackingTest;补上此前完全缺失的控制器串接覆盖(Host 方法曾是
默认实现,串接断开不会有红灯);每条新测试都用反向插桩确认非空转。
2026-08-30 21:05:57 +08:00
Silent1566
c5acc64d9a 修复:既有规则带未锚定切片正则时会静默误删正片
第八轮评审复现出上一轮修复未覆盖的链路:RuleSelfCheck 的合成 manifest 用
SegmentFact.path() 拼 URL,而那是 URI.getPath() 的产物 —— 按去敏要求 query 与
fragment 都被丢弃。于是合成 URL 永不带 ? 和 #,而真实 matchesPattern 匹配的是
含两者的完整 URL。

上一轮给自己生成的正则加 ^[^?#]* 前缀对齐了这个差异,但既有规则的正则是第三方
写的,不会有这个锚定。实测一条 segmentUrlRegex=["/ads/"] 的既有规则:
自检放行,真实 cleaner removed=5(应为 3),超删 5/40=12.5% 低于 35% 闸门故
fallback=false —— 错误不被回退兜住,用户直接看到正片跳帧。

这个场景的现实性不低:HlsRuleConfig.compileExternal 对外部规则用
Boolean.TRUE.equals(enabled) 判定启用状态,即接口下发的 hlsRules 只要没显式写
"enabled":true 就落地为 disabled+valid,正是「启用已有规则」通道的主要来源。

- compiledOf 新增 hasUnanchoredSegmentRegex 前置检查:凡带 segmentUrlRegex 且
  未以 ^[^?#]* 开头的规则一律返回 null,不为它背书。解析失败也保守判为未锚定。
- compiledOf 从 App.gson() 改为本地 Gson:前者在纯 JVM 单测里 App.get() 为 null
  会抛 NPE,被 catch 吞掉后静默让整个通道弃权,这条路径因此既没测也测不了。
  评审实测确认两者的 @SerializedName 映射与 disableHtmlEscaping 往返完全等价。
- 行数预判补 16 行 header 余量:total*5 在 total=4000 时放行,但真实 manifest 的
  #EXTM3U / #EXT-X-VERSION / #EXT-X-ENDLIST 等会让 lineCount 越过 20000 而回退。
- RuleState 的 6 参简化构造器从 public 收为包级:生产误用会静默关掉整条通道。
- 清掉重复 Javadoc 与死常量 LINES_PER_SEGMENT_SYNTHETIC。

评审确认无回归:全仓 7 个 RuleState 构造点,生产侧唯一一处已传 compiledOf,
测试侧 3 处用简化构造器的都是纯诊断断言。三处修复逐项回退实测均有测试捕获。

未修(记录在案):自检只跑 List.of(rule),不校验与站内已启用规则叠加后是否
越过 35% 闸门。这是 HLS_STRUCTURED_RULE 路径的既有局限,需要给
RuleSelfCheck 传入当前已启用规则列表,改动面较大,留待后续。

Verification: testMobileArm64_v8aDebugUnitTest ad.feedback 150 tests 0 failures (CompiledRuleGateTest 6 new); full suite 4047 tests with the same 5 pre-existing failures as baseline; both flavors compile
Task-Guard: AF5m-review8
2026-08-30 15:27:52 +08:00
Silent1566
3ff99a8cc7 修复:路径正则跨 fragment 误删正片,启用已有规则补上自检
第七轮评审用 30 万次随机对抗给出分层结论:RuleSelfCheck 作为独立 API 仍有
3% 假阳性(全部是同 URI 一删一留的计数抵消),但两个分类器实际能产出的载荷
形状下闭合(0/7571)—— 因为 payloadOf 恒置 requireDiscontinuity=false,
同 URI 又必然让路径条件对两侧同时成立,凑不够 minimumSignals。第六轮的
contains 子串遮蔽已确认修掉。

本次修三个端到端可达的问题:

1. pathOnlyPattern 的 ^[^?]* 只挡 query,不挡 fragment。证据侧用
   URI.getPath(),fragment 被丢弃,所以 pathAbsentOutside 与 RuleSelfCheck
   都看不见它。实测正片 URL 形如 /seg/99.ts#/ads/x 会被跨 # 命中,
   真实 cleaner removedSegments=3(应为 2)且 fallback=false —— 错误不被回退
   兜住。在专门生成该形状的 20 万轮里,放行载荷 87.7% 会误删正片。
   改为 ^[^?#]*。

2. ENABLE_EXISTING_RULE 完全绕过 RuleSelfCheck,而它的 score 0.825 高于
   HLS 规则通道,30 万轮里被选为首选 63984 次。判据只看「hostSuffixes 与
   区间内某片同域」,不看该规则的其余条件:一条 minimumSignals=1 的既有规则
   在「广告与正片共用同一 CDN」时命中全部切片,回退并连带停掉 legacy。
   RuleState 新增 compiled 字段携带编译产物,findDisabledMatch 用
   RuleSelfCheck.isSafe 实跑验证;拿不到编译产物时不建议启用。

3. 行密度系数 3 双向都不准。真实 KEY+PDT+DISCONTINUITY 是 5 行/片且实测可
   正常净化,系数 3 会让 5000..6666 片的 playlist 误放行 —— 规则存下来却因
   行数超限永久无效。提到 5:误拒方向落在 6667..9998 片(4s/片约 7.4 小时),
   正片剧集触达不到,代价可忽略;误放行才是实际损失。

原有测试的 RuleState fixture 都用简化构造器(compiled=null)且区间外只给
一两片对照,删除比例天然超 35%,新逻辑正确地拒绝了它们,一并补足。

Verification: testMobileArm64_v8aDebugUnitTest ad.feedback 144 tests 0 failures; full suite 4041 tests with the same 5 pre-existing failures as baseline; both flavors compile
Task-Guard: AF5l-review7
2026-08-30 13:41:30 +08:00
Silent1566
b4e644d593 修复:规则自检的 contains 判据被别名遮蔽,改为 URI 次数比对
第六轮评审用 30 万次随机对抗证伪了上一轮的自检:放行的 5252 条载荷里有
147 条(2.8%)在真实 cleaner 上会误删区间外正片。URI 全局唯一化后假阳性为 0,
确认根因就是别名。

两处判据都不够:
- removedAnyOutside 用 cleaned.contains(uri) 是子串查找而非「该片仍存在」。
  删掉 /s/1 后保留的 /s/12 让判断恒为真。触发形态不止一种:无扩展名序号切片、
  代理式内嵌完整 URL 的 path(AdEvidenceCollector.pathOf 会原样保留)、
  同一 promo 切片在片头片尾复用。
- removedSegments() == inside.size() 只比总数。「漏删一片区间内 + 误删一片
  区间外」数量抵消后照样通过,而这在用户框选多含一片同域正片时自然发生。
  两个弱判据合成,等于整道闸门只剩前者。

改为按切片 URI 出现次数比对:区间外每个 URI 的保留次数必须与预期完全相等,
少一次即误删。次数相等也顺带覆盖了「区间内外同 URI 被一并删除」的情形。

中途试过给每片注入哨兵注释以精确回读下标,实测不可行:HlsManifestCleaner
对未知 tag 整份回退(fallback=true),注入后自检永远拒绝。已在 synthesize
的注释里记下这个约束,避免后续重走。

同时修 P1 闸门错位:真实 manifest 每片常带 #EXT-X-KEY 或
#EXT-X-PROGRAM-DATE-TIME,行密度高于合成的 2 行/片。按 3 行/片的上界预判
MAX_MANIFEST_LINES,否则真实 manifest 会因行数超限回退而自检看不到。

RuleSelfCheckAliasTest 锁定四类遮蔽场景与规模预判,外加一条健康场景必须
放行 —— 上一版哨兵方案正是被这条测出来的。

Verification: testMobileArm64_v8aDebugUnitTest ad.feedback 141 tests 0 failures; full suite 4038 tests with the same 5 pre-existing failures as baseline; both flavors compile
Task-Guard: AF5k-selfcheck-commit
2026-08-30 11:50:36 +08:00
Silent1566
ed0a4994d8 修复:规则自检用真实 cleaner 兜底,并让 legacy 在回退时仍然运行
第五轮评审在真实 HlsManifestCleaner 上跑了 19 个边界场景,找出第五、六类
有害规则,其中两类会静默误删正片而不触发任何回退:
- 广告与正片共用同一第三方 CDN、只靠路径区分 → host 条件误删 5 片正片;
- 正片时长恰好落入广告 ±0.1s 窗口 → duration + requireCrossDomain 在区间外
  同时成立,凑够 minimumSignals=2 → 误删正片。
另有四类因删除比例 >35% 或删除时长 >90s 而整体回退。

前五轮的模式一致:每轮为某一类条件补对照,下一轮暴露另一类。根因是那些修法
都在验证「条件」,而不变量说的是「规则的实际效果」。这次改用执行引擎自身做
判据:

- 新增 RuleSelfCheck:把候选载荷编译成真实 HlsAdRule,交给真实
  HlsManifestCleaner 跑一遍由证据合成的 manifest,只有「区间内全删、区间外
  一片不删、且未触发回退」才放行。两个通道在返回结论前都过这道闸。
  好处是对未来新增的任何条件类型自动生效,并顺带预判了 cleaner 的三道回退
  闸门 —— 那些闸门即使条件完全有区分度也会让规则整体失效。

- HlsAdblockPipeline 去掉 clean.fallback() 短路:fallback 的语义是「这份
  manifest 我处理不了」,恰恰最该由 legacy 启发式接手。此前把它并入短路条件,
  等于一条过宽规则会连带停掉该站原有去广能力。触发源不止用户规则,内置与接口
  下发的规则同样会让 cleaner 回退。摘掉后「比不加规则更差」这个后果从架构上
  消失,规则缺陷最坏退化为误删或空转。

RuleSelfCheckTest 锁定五类被拒场景 + 一类健康场景必须放行(否则自检等于把
功能关掉)。原有测试的 fixture 区间外只有两三片,删除比例天然超过 35%,
一并改为 30 片的真实规模。

Verification: testMobileArm64_v8aDebugUnitTest ad.feedback 136 tests + HlsAdblockPipelineTest 3 tests 0 failures; full suite 4033 tests with the same 5 pre-existing failures as baseline; both flavors compile
Task-Guard: AF5i-closure
2026-08-30 09:56:03 +08:00
Silent1566
157da95745 修复:区间外采样截断使对照判据失效,收敛为统一不变量
第四轮评审指出前三轮都没触及根因:AdEvidenceCollector 只保留前 60 个区间外
切片,而两个分类器却用 anyMatch/noneMatch 把这份样本当成整个 playlist 的结论。

可复现的两种有害规则:
- playlist 首片同域、其后 299 片全在 cdn.site-x.com,用户标记第 100 片附近,
  采样里恰好含那一个同域片,hasSameDomainOutside 放行,规则命中全站 CDN;
- 前 60 片路径都是 /seg/,第 200 片起正片路径也变成 /ads/,outsideNone 成立,
  生成的路径规则命中后半段全部正片。
两者都会让 cleaner 回退并连带跳过 legacy HlsAdsParser。

修复:
1. AdEvidenceCollector 不再截断区间外切片。上游 M3u8Evidence 本身受
   HlsManifestCleaner 同样的 2 MiB / 20,000 行上限约束,无需二次采样。
2. 新增 SegmentContrast 作为对照判据的单一真源,把「任何写进规则的条件必须
   对块内每片成立、对块外每片不成立」这条不变量集中表达。四轮里换了四个入口
   反复出问题,根因就是这条判据被拆在多处各写一份。
3. hasSameDomainOutside 从「存在一个同域切片」改为「同域切片占区间外多数」,
   单片同域无法证明区间外整体不是同一 CDN。
4. detectPathHint 改用与 pathPatternsOf 相同的双向判据,消除信号成立但无法
   编码导致的空载荷归因。

同时补两处漏接的换 URL 路径:
- mobile/leanback 画质列表直点 onItemClick(Result) 不经 changeQuality;
- leanback 快速 TMDB 起播 startPendingFastTmdbPlayback 不经 getPlayer。

SitePlaylistHostBaseline:host 未变但站点访问顺序变化时仍要落盘,否则
accessOrder LinkedHashMap 的 LRU 顺序只存在内存里,重启后淘汰顺序失真。

Verification: testMobileArm64_v8aDebugUnitTest ad.feedback 130 tests 0 failures; ui.activity 0 failures; full suite 4026 tests with same 5 pre-existing failures as baseline; both flavors compile
Task-Guard: AF5h-invariant
2026-08-30 08:30:45 +08:00
Silent1566
b6f3353b72 修复:路径特征正则会误删 query 含 hint 的正片切片
自查时发现校验与执行的语义不一致:pathPatternsOf 的双向校验读
SegmentFact.path()(按去敏要求已去掉 query),而
HlsManifestCleaner.matchesPattern 匹配的是含 query 的完整 URL。

后果由 QueryHintMismatchTest 实测确认:正片带 ?ref=/ads/ 这类参数时,
双向校验认为区间外干净而放行 Pattern.quote("/ads/"),运行时该正则却命中
全部切片 → removed=0 fallback=true → HlsAdblockPipeline 连带跳过 legacy
启发式。这正是前三轮一直在堵的那个「比不加规则更差」的洞,只是入口换成了
路径通道。

改为 pathOnlyPattern():^[^?]*\Q…\E。[^?]* 无法跨过 ?,把匹配锚定在 path
内,与校验读 path 的语义对齐。修复后同一场景 removed=3 fallback=false。

PathOnlyPatternSafetyTest 锁定两件事:该正则能通过 HlsManifestCleaner 的
looksDangerous 与长度检查;且只匹配 path —— query、域名含 ads、无斜杠包围
的 ads1.ts 与 downloads/ 均不命中。

Verification: testMobileArm64_v8aDebugUnitTest ad.feedback: 127 tests 0 failures (QueryHintMismatchTest + PathOnlyPatternSafetyTest 新增); compileMobileArm64_v8aDebug + compileLeanbackArm64_v8aDebug OK
Task-Guard: AF5g-query-hint
2026-08-30 07:07:35 +08:00
Silent1566
b3ea90384b 修复:第三轮评审,HLS 通道仍会生成停掉全站去广的规则
上一轮只给域名通道补了对照校验,HLS 通道的同一漏洞没堵住 —— 评审实测
「playlist 在 api.site.com、切片全在 cdn.site-x.com、区间首片带断点且落在
片头」这个常见形态,置信度 0.30 恰好压线过关,产出 minimumSignals=1 +
全站 CDN 域名的规则,命中每一个切片。

1. payloadOf 的 hosts 现在只在 signals.crossDomain() 成立时编码。crossDomain
   自带对照校验(要求区间外存在同域切片),少了这道门槛就会把全站域名写进
   规则。此前只筛「不是 playlist 域名」,与 pathPatterns 的双向校验不对称。
2. pathPatternsOf 在区间外为空时弃权:用户框选覆盖整个 playlist 时无法完成
   对照,任何条件都命中全部切片。
3. hasSameDomainOutside 守卫下移到黑名单分支之后。黑名单分支返回的是
   NONE + 空载荷的纯诊断,不可能生成规则,不该被这道守卫拦掉 —— 而「整站
   切片在独立 CDN」恰是那条诊断最该出现的场景。
4. describe() 新增 describeRule(),逐项列出实际写进规则的条件。此前证据按
   signals 输出,与实际编码脱钩:用户看到「路径包含广告目录特征」却不知道
   规则其实按 host 生效,也看不到规则的杀伤范围。
5. 三端换播放器内核补 resetAdFeedback():它重新解析出可能不同的 URL,
   与换画质同理。此前只接了换画质与换解码。
6. recordAdFeedbackHost() 加开关判断,关闭去广告或播直播时不建基线,
   避免这些域名挤掉 LRU 里有用的条目。
7. SitePlaylistHostBaseline.record 在 host 已是最近一条时跳过写盘 ——
   STATE_READY 会因 seek、卡顿恢复反复触发。
8. Signals.encodableCount() 更名 signalCount() 并恢复计入全部信号:它现在
   只用于诊断展示,不再参与落地判断,避免下次又出现与 payloadOf 的语义漂移。

GenRuleE2ETest 补两条回归:全站 CDN 与区间覆盖整个 playlist,两个通道都
必须弃权或产出不可落地的结论。

Verification: testMobileArm64_v8aDebugUnitTest ad.feedback: 124 tests 0 failures (GenRuleE2ETest 5, +2 site-wide-CDN regressions); full suite 4020 tests with the same 5 pre-existing failures as baseline; compileMobileArm64_v8aDebug + compileLeanbackArm64_v8aDebug OK
Task-Guard: AF5f-review3
2026-08-30 06:55:55 +08:00
Silent1566
d79d7d3576 修复:TmdbDetail 断连恢复不应关掉用户正在看的反馈对话框
上一个提交把 resetAdFeedback() 放进 startInlinePlayer,但该方法同时承担
断连恢复(restoreInlinePlaybackIfNeeded / startPendingInlinePlayer)与
正常换集(playInline → onParseSuccess → startInlinePlayer)。后果是:

- 正常换集会重复作废两次(playInline 已经过 stopInlinePlayerForReload);
- 服务重连时会关掉用户此刻正开着的归因对话框。

改为只在 changeInlineQuality 里作废 —— 那才是「同集换 URL」的唯一入口。
换集路径已由 stopInlinePlayerForReload 覆盖,无需在汇聚点重复。

Verification: testMobileArm64_v8aDebugUnitTest ad.feedback + ui.activity: 0 failures; compileMobileArm64_v8aDebug OK
Task-Guard: AF5e-reset-guard
2026-08-30 05:21:01 +08:00
Silent1566
f19e81785a 修复:第二轮评审,域名通道会生成停掉全站去广的有害规则
复审发现上一轮声称修好的死代码仍是死的,另有一条会让情况变得更糟:

1. recordAdFeedbackHost() 写成 if (mAdFeedbackHost != null),而适配器只在
   用户首次按下「有广告」时懒创建 —— 首次反馈前基线永远为空,域名通道照旧
   因「无基线不断言」弃权。改为强制走 adFeedback() 初始化。

2. 域名通道缺 HLS 通道那道对照校验。playlist 在 api.site.com、切片全在
   cdn.site-x.com 是常见拆分,此时基线只有 playlist host,outsideBaseline
   恒真 → 生成命中全站切片的规则 → HlsManifestCleaner 因
   removedCount == segmentCount 回退原文,而 HlsAdblockPipeline 的
   fallback 会连带停掉 legacy 启发式,比不加规则更差。新增
   hasSameDomainOutside() 要求区间外存在同域切片作对照。

3. pathPatternsOf 只校验区间内每片都含目录段,未校验区间外不含。规则按切片
   独立匹配,没有「只在区间内生效」的概念,站内正片路径恰好含 hint 时会被
   一起删。改为双向校验。

4. classify 的守卫从 encodableCount() == 0 改为以实际载荷是否为空为准 ——
   信号命中不等于条件有区分度。同时把 discontinuity 从 encodableCount 摘掉,
   与 payloadOf 不编码它保持一致。

5. 换画质/换解码绕过 resetAdFeedback:这类「同集换 URL」不经 getPlayer 或
   stopInlinePlayerForReload,切片结构与在途归因都属于上一画质。三端补接。

6. resetAdFeedback 补 close() 对话框:invalidate() 后归因回调不再刷新界面,
   对话框会永久停在「分析中」。

7. durationMs() 补 try/catch 与 safePositionMs 对称;统计移到成交之后,
   未成立的反馈不再计入。

Verification: testMobileArm64_v8aDebugUnitTest ad.feedback: 122 tests 0 failures; full suite 4018 tests with the same 5 pre-existing failures as baseline; compileMobileArm64_v8aDebug + compileLeanbackArm64_v8aDebug OK
Task-Guard: AF5d-review2
2026-08-30 05:13:43 +08:00
Silent1566
756954e2c5 文档:区间反馈与多通道归因设计
本次功能的设计依据,含背景、数据契约、各通道判据、仲裁规则、
分阶段计划与架构决策摘要。实现过程中修正的两处设计缺口也已回写:
HlsAdRule 不支持位置信号、域名黑名单只在 WebView 层生效。
2026-08-30 04:17:48 +08:00
Silent1566
e881bc9115 测试:锁定片头钳位、位置不可用与换集作废三个边界
- 片头广告是最常见场景,位置远小于 90 秒回溯窗口,起点必须钳到 0;
  之前只测过 200 秒位置的常规路径。
- safePositionMs() 返回 -1 时必须被拒,不能当成合法的片头位置 0。
- invalidate() 后过期归因回来不得再刷新界面 —— 这是上一个提交修的
  换集串台问题,补测试防回归。

Verification: testMobileArm64_v8aDebugUnitTest AdFeedbackControllerTest: 16 tests 0 failures
Task-Guard: AF5c-preroll-test
2026-08-30 04:15:28 +08:00
Silent1566
b8ca2b3615 修复:评审发现的六个问题,其中两处使功能成为死代码
1. 三端 submitAdFeedback 直接调 player().getPosition() 无 null 保护。
   player() 在 service 断连时返回 null,PlayerManager.getPosition() 内部
   也无守卫。改为经 AdFeedbackHostAdapter.safePositionMs() 取值,取不到
   返回 -1;且先取位置再清 mAdMarkStartMs —— 原顺序会在异常时丢失标记。

2. AdFeedbackController.generation 非 volatile 但跨线程读写,加 volatile。

3. 换源换集不再作废在途归因。TmdbDetail 原来递增 mAdFeedbackGeneration
   作废旧回调,接线时替换成 invalidateEvidence() 却丢了作废语义:后台
   fetchEvidence() 返回后仍会用上一集的证据弹窗,用户按「保存规则」写入的
   是上一集的域名与切片条件。新增 AdFeedbackController.invalidate(),三端
   统一经 resetAdFeedback() 调用。mobile/leanback 原本连 invalidateEvidence()
   都没接,换集后短按会拿上一集的切片结构推断起点。

4. recordPlaybackHost() 全仓无生产调用,导致 siteBaselineHosts() 恒为空、
   DomainReputationClassifier 因「无基线不断言」而永久弃权 —— 整条域名通道
   是死代码。三端在播放地址确定后接线。

5. AdFeedbackDialog 未在 onDestroy 关闭。usableActivity() 已挡住销毁后弹窗,
   但 AlertDialog 强引用字段仍会经 dialog→Context 泄漏 Activity。三端
   onDestroy 加 close(),与 AdSkipPromptPresenter 的既有做法一致。

6. AdSkipCoordinator.onUserInterval 的 seek 失败恢复复查无字段污染
   (activeKey/activeCandidate/undoContext 在该方法内从未被写),保持现状。

TmdbDetailActivityLayoutTest 补四条断言锁定上述接线,防止回归。

Verification: testMobileArm64_v8aDebugUnitTest ad.feedback + ui.activity: 471 tests 0 failures; full suite 4013 tests with the same 5 pre-existing failures as baseline; compileMobileArm64_v8aDebug + compileLeanbackArm64_v8aDebug OK
Task-Guard: AF5b-review-fixes
2026-08-30 03:40:35 +08:00
Silent1566
75859b5de4 修复:生成的 HLS 规则实际删不掉广告切片
端到端测试(GenRuleE2ETest)把分类器载荷真的组装成 HlsAdRule 交给
HlsManifestCleaner 跑 30 片 manifest,三个场景全部一片未删。单元测试
只验证了载荷字段与 compile() 通过,覆盖不到运行时评分。

根因是误解了 HlsManifestCleaner 的信号语义:它对每个切片独立累加信号
(HlsManifestCleaner.java:143-149),达到 minimumSignals 即删。因此只能
编码对块内每一片都成立、且有区分度的条件:

- hostSuffixes 原本包含与 playlist 同域的域名,同域条件对每片正片都成立,
  会误删正片。现只保留非本站域名。
- requireDiscontinuity 只对广告块首片成立,块内后续片拿不到该信号,
  编码进规则会只删首片、留下其余广告。现不编码,断点只用于归因定位区间。
- 新增 pathPatterns:命中的广告目录段经 Pattern.quote 转正则,作为同域
  场景下唯一的区分条件。两类区分条件都没有时返回空载荷让通道弃权。
- minimumSignals 从 Math.max(2, ...) 改为 Math.min(2, encoded):
  超过实际可编码信号数会让 compile() 抛 Invalid minimum signals,
  规则永远写不进去且用户只看到「保存失败」。
- AdRulePlanApplier 不再二次抬高门限,同样原因。

Verification: testMobileArm64_v8aDebugUnitTest ad.feedback: 13 suites 117 tests 0 failures (GenRuleE2ETest 3 new)
Task-Guard: AF5a-e2e-fix
2026-08-30 03:15:52 +08:00
Silent1566
60cd50046d Merge remote-tracking branch 'origin/beta' into dev1 2026-08-30 02:24:33 +08:00
Silent1566
1bfa7e0ffc 接线:三端「有广告」按钮改用区间反馈与多通道归因
Phase 1 收尾,见 docs/ad-feedback-timerange-design.md 第 4、5.2 节。
这是本系列唯一改变用户可见行为的一步。

三端(leanback / mobile / TmdbDetail)各自那份近乎逐字重复的
submitAdFeedback / buildAdDetectionRequest / enrichRequestWithM3u8Evidence /
onAdDetectionResult 全部删除,改为共用 AdFeedbackController +
AdFeedbackHostAdapter,每端只留 Playback/Metadata/Ui 三个内部类适配。

行为变化:
- 按钮可见性放宽:去掉 isAiConfigReady() && isAiAdDetection() 硬门槛,
  改为 isAdblock() && 非直播 && 时长已知。不配 AI 也能用,因为本地归因
  通道(切片结构/域名/既有规则)不依赖 AI。
- 短按:以当前位置为终点,按 DISCONTINUITY → 跨域切换 → 音频候选 →
  固定窗口逐级回溯推断起点。
- 长按:进入标记模式打起点,再次长按取消,之后短按确定终点。
- 提交后立即跳过该区间并显示第一屏,归因完成后原地替换为第二屏。
- 确认保存时经 AdRulePlanApplier 落成真实规则,三态结果分别提示。
- TmdbDetail 换源换集时作废标记模式并清掉上一集的切片证据缓存。

同步更新 TmdbDetailActivityLayoutTest 的源码断言:原断言锁定了 AI 硬门槛
与 AiAdDetectionService 调用,现改为锁定新契约,并加反向断言确保重复的
per-activity 请求构造器不会回归。

Verification: compileMobileArm64_v8aDebug + compileLeanbackArm64_v8aDebug OK; testMobileArm64_v8aDebugUnitTest ui.activity 348 tests 0 failures; full suite 4002 tests with 5 pre-existing failures (MpvPreloadController/FfmpegVc1Support/MultiThreadProxyPlayerUiSource) verified present on base commit via stash
Task-Guard: AF4h-wiring
2026-08-30 01:55:21 +08:00
Silent1566
f5d80b1de0 新增:规则落地器与用户 HLS 规则库
Phase 1 第十二个逻辑单元,见 docs/ad-feedback-timerange-design.md 第 6.2 节。

写落地器时查证了一件关键事实:RuleConfig.getAds() 的唯一消费者是
CustomWebView,即域名黑名单只拦 WebView 请求,拦不住播放器直连的切片请求。
所以域名黑名单对区间广告是无效落地 —— 这正是设计文档 7.2 节把「已在黑名单
却仍被看到」标记为 ALREADY_HANDLED 的原因。

- UserHlsRuleStore:新增用户可写的 HLS 规则来源并接入 HlsRuleConfig。
  内置规则来自 APK assets、接口规则来自点播/直播配置,两者都不可写,
  而区间反馈需要产出真正能删除切片的规则。写入前先 compile 校验,
  非法规则直接拒绝,避免在 HlsRuleConfig 里留下永久报错条目。
- HlsAdRule.createUserRule():字段全私有无 setter,补一个工厂方法。
- DomainReputationClassifier 的落地机制从 HOST_BLACKLIST 改为
  HLS_STRUCTURED_RULE,携带 playlistHostSuffixes 收窄作用域;
  hostSuffixes + requireCrossDomain 两个信号刚好满足 minimumSignals=2。
  取不到 playlist 域名时才退回域名黑名单。
- AdRulePlanApplier:三条落地路径(启用已有规则/HLS 规则/用户规则),
  数据全部取自 RulePayload,不解析证据文本。返回 Outcome 三态而非
  boolean,让 UI 能区分「跳过」和「失败」。

RulePayloadCompileContractTest 锁定最易断的一环:分类器产出的载荷必须能
组装成 compile() 接受的规则。compile() 要求 minimumSignals 不超过实际信号
数,若不满足会出现「已保存但广告依旧」的静默失败。

Verification: testMobileArm64_v8aDebugUnitTest ad.feedback: 114 tests, 0 failures (RulePayloadCompileContractTest 5 new); compileMobileArm64_v8aDebug OK
Task-Guard: AF4g2-applier
2026-08-30 01:34:28 +08:00
Silent1566
9b2c31f1e0 新增:归因结论携带机器可读的规则载荷
Phase 1 第十一个逻辑单元,见 docs/ad-feedback-timerange-design.md 第 6.2、10 节。

接线 UI 时发现一个设计缺口:AdAttribution 只有给人看的 evidence 字符串,
没有落地规则所需的结构化数据。若从证据文本里正则提取域名,规则会随文案
和语言变化而失效 —— 这是不可接受的耦合。

- RulePayload:域名/正则/排除/规则键/时长范围/信号门限。三个分类器各自
  填充自己那部分,HLS 通道额外带 playlistHostSuffixes 把作用域收窄到当前
  站点,满足设计文档第 10 节「禁止生成全局生效的规则」。
- AdAttribution.actionable() 现在要求载荷非空,否则「保存规则」会写出空规则。
- 仲裁合并同类结论时载荷跟随被选中的机制,不能拿 A 的数据执行 B 的动作。
- 时长范围仅在时长离群信号成立时给出,避免只靠固定时长删片;
  minimumSignals 恒不低于 2。

Verification: testMobileArm64_v8aDebugUnitTest ad.feedback: 109 tests, 0 failures (RulePayloadTest 10 new); compileMobileArm64_v8aDebug OK
Task-Guard: AF4g1-payload
2026-08-30 01:02:35 +08:00
Silent1566
f19d4a0e97 新增:区间反馈两屏对话框与三语言文案
Phase 1 第十个逻辑单元,见 docs/ad-feedback-timerange-design.md 第 4.3 节。

AdFeedbackDialog 放在 main 源集三端共用,参照 AdSkipPromptPresenter 的模式:
用 MaterialAlertDialogBuilder + 代码搭建视图,不写 layout xml。理由是内容
本身是不定长的证据列表,用 layout 需要为 mobile/leanback 各维护一份文件,
而现有 dialog_ad_rule_preview.xml 已经是那样重复了两份。

- 第一屏提交瞬间即出:区间、时长、起点来源、是否已跳过。
- 第二屏归因完成后原地替换正文,同一 feedbackId 不重建对话框,
  避免 TV 上闪一下丢焦点;只有「保存规则」按钮的有无发生变化时才重建,
  因为 AlertDialog 不支持事后增删按钮。
- 起点来源逐一展示,让用户知道 02:15 是框选的还是推断的。
- 按钮在 TV 上居中,遥控器更容易命中。

新增 27 条文案,en/zh-rCN/zh-rTW 三份齐全,格式串统一用带位置索引的
%1$ 形式以通过 lint 的 StringFormat 检查。

对话框尚无调用方,无行为变化。

Verification: compileMobileArm64_v8aDebug + compileLeanbackArm64_v8aDebug OK; lintVitalMobileArm64_v8aRelease OK (no StringFormat/MissingTranslation)
Task-Guard: AF4f-feedback-dialog
2026-08-30 00:27:46 +08:00
Silent1566
48731cbbbf 新增:三端共用的反馈 Host 适配器
Phase 1 第九个逻辑单元,见 docs/ad-feedback-timerange-design.md 第 5.2 节。

AdFeedbackHostAdapter 实现 AdFeedbackController.Host,把 Activity 需要提供的
东西收窄成三个小接口:Playback(播放状态与跳过)、Metadata(剧集元数据)、
Ui(线程调度与展示)。读本地配置、抓 playlist、维护站点域名基线都在适配器内,
三端 Activity 因此不必各写一份。

- 配置读取全部经 Deps 注入,默认指向 AdFeedbackDataSource,测试可替换;
  每个读取点吞 RuntimeException 并降级为空集合。
- fetchEvidence 缓存最近一次成功抓取的证据供短按推断起点复用,
  invalidateEvidence() 供换源换集时清理,避免用上一集的切片结构。
- legacyHeuristicActive 对点播 HLS 恒为 true:HlsAdblockPipeline 两个调用点
  都传 legacyFallback=true 且内置规则默认全关,旧启发式实际总在生效。

尚未接线到播放器,无行为变化。

Verification: testMobileArm64_v8aDebugUnitTest ad.feedback: 99 tests, 0 failures (AdFeedbackHostAdapterTest 10 new); compileMobileArm64_v8aDebug + compileLeanbackArm64_v8aDebug OK
Task-Guard: AF4e-host-adapter
2026-08-30 00:08:24 +08:00
Silent1566
d95c5537fc 新增:归因数据源与站点域名基线的应用级落地
Phase 1 第八个逻辑单元,见 docs/ad-feedback-timerange-design.md 第 5.1、8.3 节。

- AdFeedbackDataSource:把 RuleConfig / UserAdRuleStore /
  ImportedAdRuleCandidateStore / HlsRuleConfig 的静态单例数据读成分类器入参。
  三个分类器刻意做成纯函数以便单测,读 SharedPreferences 集中在这里,
  每个读取点都吞 RuntimeException —— 归因是辅助功能,不能因为读配置失败
  影响播放。
- SiteHostBaselineStore:SitePlaylistHostBaseline 的 Prefers 实现与单例。

hostSuffixesOf 从 HlsRuleConfig.Entry.detail() 的 JSON 里取切片域名条件:
HlsAdRule 的字段全部私有且无 getter,而 Entry.detail() 已经是它的完整
JSON 序列化,不必为此给上游 bean 加访问器。解析失败一律返回空列表。

尚未接线到播放器,无行为变化。

Verification: testMobileArm64_v8aDebugUnitTest ad.feedback: 89 tests, 0 failures (AdFeedbackDataSourceTest 5 new); compileMobileArm64_v8aDebug + compileLeanbackArm64_v8aDebug OK
Task-Guard: AF4d-android-bridge
2026-08-29 23:59:05 +08:00
Silent1566
2822ea1f9e 新增:区间反馈证据采集与归因编排
Verification: testMobileArm64_v8aDebugUnitTest: ad.feedback 84 tests, 0 failures (AdEvidenceCollector 12, AdFeedbackController 13 new)
Task-Guard: AF4c-controller
2026-08-29 23:51:45 +08:00
Silent1566
d8ef2aed1f 新增:区间跳过的运行时接线,不依赖音频指纹开关
Phase 1 第六个逻辑单元,见 docs/ad-feedback-timerange-design.md 第 9 节。

- AdAudioRuntimeController.skipUserInterval():UI 已绑定但 coordinator 尚未
  建立时按需创建。bindUi() 只在音频通道启用路径上被调用,不能作为区间跳过
  的前置条件 —— 指纹与语音都关闭、规则库为空时,用户框选广告仍须能跳过。
- PlayerManager.skipUserAdInterval():对外入口,isReleased() 时安全返回 false。

区间反馈单独跟踪 intervalSessionId/intervalGeneration 并在提交前对齐
coordinator:timeline reset 只经 PCM provider 的回调转发,音频通道关闭时
那条路径不存在,coordinator 会永远卡在上一次跳过留下的 UNDO_WINDOW 上导致
后续每次提交都被拒。基线取 seek 之后的实际状态,因为 seek 会推进 generation。

刻意不复用 activeSessionId:该字段决定音频管线是否需要重建,从区间路径改写
它会让 refreshLocked 漏掉一次必要的重建。

Verification: testMobileArm64_v8aDebugUnitTest --tests com.fongmi.android.tv.ad.* : 350 tests, 0 failures (AdAudioRuntimeControllerUserIntervalTest 8 new); compileMobileArm64_v8aDebugJavaWithJavac OK
Task-Guard: AF4b-skip-plumbing
2026-08-29 23:41:53 +08:00
Silent1566
7c843fd05f 新增:站点域名基线、反馈会话状态与诊断计数
Phase 1 第五个逻辑单元,见 docs/ad-feedback-timerange-design.md 第 8.3、11 节。

- SitePlaylistHostBaseline:每站点最近 8 个域名、全局 200 站点的双层 LRU,
  供 DomainReputationClassifier 判断「非当前适配的域名」。只存 host 字符串,
  不存 URL 全文、query 或 token。持久化经 Storage 接口注入,紧凑文本格式
  而非 JSON,避免依赖 Gson 以便纯 JVM 单测;siteKey 中的分隔符做转义。
- AdFeedbackSession:驱动对话框两屏的状态,withVerdict() 从第一屏推进到第二屏。
- AdFeedbackDiagnostics:12 个固定枚举计数,沿用 AdAudioDiagnostics 的
  「不记录 URL、规则正文、Cookie、Authorization」约定。

修了一处 record() 的缺陷:全部输入为空白时 computeIfAbsent 会留下空条目
白占站点配额,现改为移除。尚未接线到播放器,无行为变化。

Verification: testMobileArm64_v8aDebugUnitTest --tests com.fongmi.android.tv.ad.feedback.* : 59 tests (10 new baseline), 0 failures, 0 errors
Task-Guard: AF4a-feedback-controller
2026-08-29 23:13:17 +08:00
Silent1566
2f1b919325 新增:AdSkipCoordinator 支持用户框选区间的即时跳过
Phase 1 第四个逻辑单元,见 docs/ad-feedback-timerange-design.md 第 9 节。

onUserInterval() 与音频候选路径的两个关键区别:
- 区间是媒体时间,直接用作 seek 目标,不经 clock().mapCaptureToMediaMs()
  二次映射(音频候选的 endTimeMs 是采集时间,必须映射)。
- 授权来自用户框选动作本身,不经 PROMPT_PENDING 确认,直接进 SEEKING。

安全校验与撤销窗口保持一致:session/generation 匹配、可 seek、非直播、
时长已知;终点超出时长则钳制;终点已播过(短按场景广告已放完)则记录
SEEK_REJECTED 并返回 false,由调用方提示「已记录,未跳过」;撤销窗口内
同一区间重复提交不再 seek。

seek 失败时恢复调用前状态而非 clearState():音频候选的 suppressedKey 与
PROMPT_PENDING 不属于本次区间反馈,连带清掉会让同一广告再次弹提示。
音频路径不受此影响(onAutoCandidate 进 seek 前已主动清 suppressedKey),
这条只在新增的用户区间路径上成立,已由测试锁定。

Verification: testMobileArm64_v8aDebugUnitTest --tests com.fongmi.android.tv.ad.* : 37 suites, 332 tests, 0 failures (AdSkipCoordinatorUserIntervalTest 15 new, AdSkipCoordinatorTest 9 unchanged)
Task-Guard: AF3-user-interval-skip
2026-08-29 23:03:17 +08:00
Silent1566
39bbdf2510 新增:域名信誉与既有规则归因通道,以及跨通道仲裁器
Phase 1 第三个逻辑单元,见 docs/ad-feedback-timerange-design.md 第 7.2、7.3、8 节。

- DomainReputationClassifier:三层比对。命中现有黑名单时输出
  ALREADY_HANDLED 诊断(黑名单只在 WebView 层生效,不拦播放器直连切片);
  不在本站基线内则判 THIRD_PARTY_CDN_SEGMENT;接口学习候选命中加 0.15。
  无基线数据时弃权,避免首次播放该站即误判。
- ExistingRuleClassifier:优先建议启用已存在但默认关闭的规则,这是成本
  最低的修复;编译失败的规则只报诊断不建议启用。另报旧启发式引擎生效、
  正片保护规则可能误保护。
- AdAttributionArbiter:同类别按概率或合并置信度并取成本最低机制,跨类别
  按 score() 排序(置信度 0.7 + 机制优先级 0.3)。不可落地的结论一律降级
  为诊断,只剩诊断时兜底为 SESSION_SKIP_ONLY 并合并证据供 UI 展示。

三个分类器均以 record Input 接收外部数据,不触碰 SharedPreferences,
可在纯 JVM 下单测。尚未接线到播放器,无行为变化。

Verification: testMobileArm64_v8aDebugUnitTest --tests com.fongmi.android.tv.ad.feedback.* : 49 tests (11 mapper + 15 hls + 7 domain + 7 existing + 9 arbiter), 0 failures, 0 errors
Task-Guard: AF2b-domain-arbiter
2026-08-29 22:50:37 +08:00
Silent1566
8ce6437487 新增:HLS 切片结构归因通道
Phase 1 第二个逻辑单元,见 docs/ad-feedback-timerange-design.md 第 7.1、8.1 节。

- HlsSegmentClassifier:五种信号加权判定(跨域 0.35 / 断点 0.25 /
  时长离群 0.20 / 路径特征 0.15 / 头部位置 0.05),置信度低于 0.30
  或无可编码信号时弃权。跨域要求区间内外形成对照,整条 playlist 同域
  不算证据;时长离群要求区间内一致且与区间外众数差异超过 0.5s。
- AdAttribution:单通道归因结论,score() 按置信度 0.7 + 机制优先级 0.3
  加权,ALREADY_HANDLED 强制归零。
- RemediationKind:枚举顺序即成本-风险优先级,priorityWeight() 供仲裁使用。
- AdCategory / RiskLevel:归因类别与风险等级。

位置信号不被 HlsAdRule 支持,encodableCount() 将其排除,避免生成
无法被现有引擎执行的规则。尚未接线到播放器,无行为变化。

Verification: testMobileArm64_v8aDebugUnitTest --tests com.fongmi.android.tv.ad.feedback.* : 26 tests (11 mapper + 15 classifier), 0 failures, 0 errors
Task-Guard: AF2a-hls-classifier
2026-08-29 22:40:03 +08:00
Silent1566
f012214a98 新增:广告区间反馈的证据契约与切片映射
Phase 1 第一个逻辑单元,见 docs/ad-feedback-timerange-design.md 第 6.1、7.1 节。

- AdIntervalMapper:播放时间↔切片下标映射(durations 前缀和,半开区间
  相交),以及短按场景的起点推断(DISCONTINUITY → 跨域切换 → 音频候选
  → 固定窗口逐级降级)。纯计算,无新增网络请求。
- AdIntervalEvidence / SegmentFact / AudioIntervalFact / SpeechIntervalFact:
  区间证据快照契约,只保留 host 与去参数 path,沿用 AdDetectionRequest
  的去敏原则。
- StartOrigin:起点来源枚举,携带证据权重供后续仲裁使用。

尚未接线到播放器,无行为变化。

Verification: testMobileArm64_v8aDebugUnitTest --tests com.fongmi.android.tv.ad.feedback.* : 11 tests, 0 failures, 0 errors
Task-Guard: AF1-evidence
2026-08-29 22:27:06 +08:00
2985 changed files with 45263 additions and 359533 deletions

315
.codex/scripts/task_guard.sh Executable file →

7
.gitignore vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Some files were not shown because too many files have changed in this diff Show more