// lib/metadata/metadata_normalizer.dart import 'metadata_model.dart'; class MetadataNormalizer { NormalizedMetadata normalize(RawMetadata raw, {String fileName = '', String filePath = ''}) { final tagTitle = raw.title.trim(); final tagArtist = raw.artist.trim(); final tagAlbum = raw.album.trim(); final finalTitle = tagTitle.isNotEmpty ? tagTitle : _inferTitleFromFileName(fileName); final artistFromPath = _inferArtistFromPath(filePath); final finalArtist = tagArtist.isNotEmpty ? tagArtist : (raw.performers.isNotEmpty ? raw.performers.first : artistFromPath); return NormalizedMetadata( title: finalTitle, artist: finalArtist, album: tagAlbum, genres: raw.genres, performers: raw.performers, year: raw.year, trackNumber: raw.trackNumber, trackTotal: raw.trackTotal, discNumber: raw.discNumber, totalDisc: raw.totalDisc, duration: raw.duration, artwork: raw.artwork, titleFromTag: tagTitle.isNotEmpty, artistFromTag: tagArtist.isNotEmpty || raw.performers.isNotEmpty, albumFromTag: tagAlbum.isNotEmpty, ); } String _inferTitleFromFileName(String fileName) { final dotIndex = fileName.lastIndexOf('.'); return dotIndex > 0 ? fileName.substring(0, dotIndex).trim() : fileName.trim(); } String _inferArtistFromPath(String filePath) { final parts = filePath.split(RegExp(r'[/\\]')); if (parts.length >= 2) { final candidate = parts[parts.length - 2].trim(); if (candidate.isNotEmpty && !candidate.startsWith('http')) { return candidate; } } return '未知艺术家'; } /// 置信度评估(元数据引擎第 2 步)。 /// /// 设计原则: /// 1. 标签 >= 推断:来自标签的字段永远比文件名/目录推断更可信; /// 2. 字段齐全度:标题、艺术家、专辑依次加分; /// 3. 历史一致性:同一艺人/专辑已有多条可信记录时额外加权, /// 这是"自学习"的入口——库越大,判断越稳; /// 4. 可解释:每个加分项都写进 evidence,方便日志与后续调参。 /// /// 分数区间大致: /// 0.35 只有文件名 /// 0.55 标题来自标签、艺术家靠推断 /// 0.80 标题 + 艺术家都来自标签 /// 0.90 标题 + 艺术家 + 专辑 /// 0.95 以上再加年份/封面/历史一致性 CandidateMetadata evaluate( NormalizedMetadata normalized, List> history) { final evidence = {}; var confidence = 0.25; evidence['base'] = 0.25; if (normalized.title.isNotEmpty) { confidence += 0.20; evidence['title'] = 0.20; } if (normalized.titleFromTag) { confidence += 0.10; evidence['title_from_tag'] = 0.10; } else if (normalized.title.isNotEmpty) { // 拿文件名当标题:明确扣分 confidence -= 0.10; evidence['title_inferred_penalty'] = -0.10; } if (normalized.artist.isNotEmpty && normalized.artist != '未知艺术家') { confidence += 0.15; evidence['artist'] = 0.15; } if (normalized.artistFromTag) { confidence += 0.10; evidence['artist_from_tag'] = 0.10; } if (normalized.album.isNotEmpty) { confidence += 0.10; evidence['album'] = 0.10; } if (normalized.year > 0) { confidence += 0.03; evidence['year'] = 0.03; } if (normalized.artwork != null && normalized.artwork!.isNotEmpty) { confidence += 0.02; evidence['artwork'] = 0.02; } // ---- 历史一致性(自学习的核心)---- if (history.isNotEmpty) { final sameArtist = history .where((row) => (row['artist'] as String?) == normalized.artist) .length; if (sameArtist >= 2) { confidence += 0.05; evidence['artist_history'] = 0.05; } if (normalized.album.isNotEmpty) { final sameAlbum = history .where((row) => (row['album'] as String?) == normalized.album) .length; if (sameAlbum >= 2) { confidence += 0.03; evidence['album_history'] = 0.03; } } } confidence = confidence.clamp(0.05, 0.98); return CandidateMetadata( metadata: normalized, confidence: double.parse(confidence.toStringAsFixed(2)), evidence: evidence, ); } /// 按阈值决定最终元数据。 /// 低于阈值时仍返回数据(用于展示),但 source 标成 pending, /// 调用方据此累计 validation_count、下次播放重新确认。 FinalMetadata decide(CandidateMetadata candidate, {double threshold = 0.6}) { return FinalMetadata( title: candidate.metadata.title, artist: candidate.metadata.artist, album: candidate.metadata.album, genre: candidate.metadata.genres.isNotEmpty ? candidate.metadata.genres.first : '', genres: candidate.metadata.genres, performers: candidate.metadata.performers, year: candidate.metadata.year, trackNumber: candidate.metadata.trackNumber, trackTotal: candidate.metadata.trackTotal, discNumber: candidate.metadata.discNumber, totalDisc: candidate.metadata.totalDisc, duration: candidate.metadata.duration, artwork: candidate.metadata.artwork, source: candidate.confidence >= threshold ? 'normalized' : 'pending', ); } }