// lib/metadata/metadata_service.dart import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import '../database/song_database.dart'; import 'metadata_model.dart'; import 'metadata_reader.dart'; import 'metadata_normalizer.dart'; import 'metadata_stability.dart'; import 'metadata_cache.dart'; import 'file_provider.dart'; import '../utils/artwork_helper.dart'; import '../utils/song_key.dart'; class MetadataService { static final MetadataService _instance = MetadataService._internal(); factory MetadataService() => _instance; MetadataService._internal(); final MetadataReader _reader = MetadataReader(); final MetadataNormalizer _normalizer = MetadataNormalizer(); final MetadataCache _memoryCache = MetadataCache(); final SongDatabase _db = SongDatabase(); String _generateSongKey(String url) { return songKeyFor(url); } FileProvider _getProvider(String url) { if (url.startsWith('http://') || url.startsWith('https://')) { return WebDAVFileProvider(); } return LocalFileProvider(); } /// 公开给 AudioService 使用(用于清除缓存时获取文件) FileProvider getProviderForUrl(String url) { return _getProvider(url); } /// 清除内存缓存 Future clearCache(String fileId) async { await _memoryCache.remove(fileId); debugPrint('🗑️ [MetadataService] memory cache cleared: $fileId'); } Future getMetadata({ required String url, required String fileName, required String fileId, bool forceRefresh = false, }) async { // 1. 内存缓存 if (!forceRefresh) { final cached = await _memoryCache.get(fileId); if (cached != null && cached.isNotEmpty) { debugPrint('📦 [MetadataService] memory hit: ${cached.title}'); return cached; } } // 2. 分阶段取头部:先 128KB 拿 title/artist,读不到再补到 512KB。 // 弱网下首包更小,标签能更快出来,也少占带宽。 final provider = _getProvider(url); var file = await provider.getFile(url, maxBytes: 128 * 1024); if (file == null || !await file.exists()) { debugPrint('⚠️ [MetadataService] file not available, using fallback'); return _fallbackFromFileName(fileName); } var raw = await _reader.readRawMetadata(file); if (raw.title.isEmpty && raw.artist.isEmpty) { debugPrint('📦 [MetadataService] 128KB 没读到标签,补取到 512KB'); final bigger = await provider.getFile(url, maxBytes: 512 * 1024); if (bigger != null && await bigger.exists()) { file = bigger; raw = await _reader.readRawMetadata(file); } } final stat = await file.stat(); final songKey = _generateSongKey(url); // 3. SQLite 查询:只有「已验证」的记录才直接返回。 // pending 记录会继续往下走,重新读取并累计 validation_count, // 这就是自修复:置信度不足的记录会在下次播放时被重新确认。 Map? dbSong; if (!forceRefresh) { dbSong = await _db.getSong(songKey); if (dbSong != null) { final confidence = (dbSong['confidence'] as num?)?.toDouble() ?? 0.0; final status = dbSong['metadata_status'] as String? ?? 'pending'; if (confidence >= 0.6 && status == 'verified') { debugPrint( '📦 [MetadataService] SQLite hit: ${dbSong['title']} - ${dbSong['artist']}'); // 读取封面图 final artworkPath = dbSong['artwork_path'] as String?; Uint8List? artwork; if (artworkPath != null && artworkPath.isNotEmpty) { try { final artFile = File(artworkPath); if (await artFile.exists()) { artwork = await artFile.readAsBytes(); } } catch (e) { // ignore } } final metadata = FinalMetadata( title: dbSong['title'] as String? ?? '', artist: dbSong['artist'] as String? ?? '', album: dbSong['album'] as String? ?? '', genre: dbSong['genre'] as String? ?? '', artwork: artwork, source: 'database', ); await _memoryCache.put(fileId, metadata); return metadata; } debugPrint('🔁 [MetadataService] 记录未验证' '(conf=${confidence.toStringAsFixed(2)}, status=$status),重新确认'); } } // 4. 标准化(raw 已在上面分阶段读取) final normalized = _normalizer.normalize( raw, fileName: fileName, filePath: url, ); // 6. 评估(含历史一致性) final history = await _db.getSongsByArtist(normalized.artist, limit: 10); final candidate = _normalizer.evaluate(normalized, history); final finalMetadata = _normalizer.decide(candidate); // 7. 与已有记录合并(一致性 + 次数由状态机决定) final prevConfidence = (dbSong?['confidence'] as num?)?.toDouble() ?? 0.0; final decision = MetadataStability.decide( previous: dbSong, candidate: candidate, title: finalMetadata.title, artist: finalMetadata.artist, ); final validationCount = decision.validationCount; final useNew = dbSong == null || candidate.confidence >= prevConfidence; final effectiveConfidence = useNew ? candidate.confidence : prevConfidence; // 8. 保存封面图(只有采用新结果且确实读到图时才写) String? artworkPath = dbSong?['artwork_path'] as String?; if (useNew && finalMetadata.artwork != null && finalMetadata.artwork!.isNotEmpty) { artworkPath = await ArtworkHelper.saveArtwork(finalMetadata.artwork!, songKey); } final status = decision.status; debugPrint('🧭 [MetadataService] 状态机: ${decision.reason} → $status' '(validations=$validationCount)'); final row = { 'song_key': songKey, // remote_path 必须存应用内路径(fileId / song.id),不能存完整 URL 'remote_path': fileId, 'file_size': stat.size, 'modified_time': stat.modified.millisecondsSinceEpoch, 'title': useNew ? finalMetadata.title : (dbSong?['title'] ?? finalMetadata.title), 'artist': useNew ? finalMetadata.artist : (dbSong?['artist'] ?? finalMetadata.artist), 'album': useNew ? finalMetadata.album : (dbSong?['album'] ?? finalMetadata.album), 'genre': useNew ? finalMetadata.genre : (dbSong?['genre'] ?? finalMetadata.genre), 'artwork_path': artworkPath, 'confidence': effectiveConfidence, 'validation_count': validationCount, 'first_scan': dbSong?['first_scan'] ?? DateTime.now().millisecondsSinceEpoch, 'last_scan': DateTime.now().millisecondsSinceEpoch, 'metadata_status': status, }; if (dbSong == null) { await _db.insertSong(row); } else { await _db.updateSong(songKey, row); } final merged = FinalMetadata( title: row['title'] as String? ?? '', artist: row['artist'] as String? ?? '', album: row['album'] as String? ?? '', genre: row['genre'] as String? ?? '', genres: finalMetadata.genres, performers: finalMetadata.performers, year: finalMetadata.year, duration: finalMetadata.duration, artwork: finalMetadata.artwork, source: status, ); // 9. 内存缓存 await _memoryCache.put(fileId, merged); debugPrint('📊 [MetadataService] $status ' 'conf=${effectiveConfidence.toStringAsFixed(2)} ' 'validations=$validationCount ' 'evidence=${candidate.evidence}'); debugPrint('✅ [MetadataService] saved: ${merged.title} - ${merged.artist}'); return merged; } FinalMetadata _fallbackFromFileName(String fileName) { final dotIndex = fileName.lastIndexOf('.'); final title = dotIndex > 0 ? fileName.substring(0, dotIndex) : fileName; return FinalMetadata( title: title, artist: '未知艺术家', source: 'filename', ); } }