171 lines
5.5 KiB
Dart
171 lines
5.5 KiB
Dart
// 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_cache.dart';
|
|
import 'file_provider.dart';
|
|
import '../utils/artwork_helper.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, int fileSize, int modifiedTime) {
|
|
final raw = '$url|$fileSize|$modifiedTime';
|
|
return raw.hashCode.toString();
|
|
}
|
|
|
|
FileProvider _getProvider(String url) {
|
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
return WebDAVFileProvider();
|
|
}
|
|
return LocalFileProvider();
|
|
}
|
|
|
|
/// 公开给 AudioService 使用(用于清除缓存时获取文件)
|
|
FileProvider getProviderForUrl(String url) {
|
|
return _getProvider(url);
|
|
}
|
|
|
|
/// 清除内存缓存
|
|
Future<void> clearCache(String fileId) async {
|
|
await _memoryCache.remove(fileId);
|
|
debugPrint('🗑️ [MetadataService] memory cache cleared: $fileId');
|
|
}
|
|
|
|
Future<FinalMetadata> 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. 获取文件
|
|
final provider = _getProvider(url);
|
|
final file = await provider.getFile(url);
|
|
|
|
if (file == null || !await file.exists()) {
|
|
debugPrint('⚠️ [MetadataService] file not available, using fallback');
|
|
return _fallbackFromFileName(fileName);
|
|
}
|
|
|
|
final stat = await file.stat();
|
|
final songKey =
|
|
_generateSongKey(url, stat.size, stat.modified.millisecondsSinceEpoch);
|
|
|
|
// 3. SQLite 查询(高置信度直接使用)
|
|
if (!forceRefresh) {
|
|
final dbSong = await _db.getSong(songKey);
|
|
if (dbSong != null) {
|
|
final confidence = (dbSong['confidence'] as num?)?.toDouble() ?? 0.0;
|
|
if (confidence >= 0.6) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. 读取原始 metadata
|
|
final raw = await _reader.readRawMetadata(file);
|
|
|
|
// 5. 标准化
|
|
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);
|
|
debugPrint(
|
|
'📢 [MetadataService] finalMetadata.artwork is ${finalMetadata.artwork != null ? 'not null (${finalMetadata.artwork!.length} bytes)' : 'null'}');
|
|
|
|
// 7. 保存封面图到本地
|
|
String? artworkPath;
|
|
if (finalMetadata.artwork != null && finalMetadata.artwork!.isNotEmpty) {
|
|
artworkPath =
|
|
await ArtworkHelper.saveArtwork(finalMetadata.artwork!, songKey);
|
|
}
|
|
|
|
// 8. 保存到 SQLite
|
|
await _db.insertSong({
|
|
'song_key': songKey,
|
|
'remote_path': url,
|
|
'file_size': stat.size,
|
|
'modified_time': stat.modified.millisecondsSinceEpoch,
|
|
'title': finalMetadata.title,
|
|
'artist': finalMetadata.artist,
|
|
'album': finalMetadata.album,
|
|
'genre': finalMetadata.genre,
|
|
'artwork_path': artworkPath,
|
|
'confidence': candidate.confidence,
|
|
'validation_count': 1,
|
|
'first_scan': DateTime.now().millisecondsSinceEpoch,
|
|
'last_scan': DateTime.now().millisecondsSinceEpoch,
|
|
'metadata_status': candidate.confidence >= 0.6 ? 'verified' : 'pending',
|
|
});
|
|
|
|
// 9. 内存缓存
|
|
await _memoryCache.put(fileId, finalMetadata);
|
|
|
|
debugPrint(
|
|
'✅ [MetadataService] saved: ${finalMetadata.title} - ${finalMetadata.artist} (confidence: ${candidate.confidence})');
|
|
return finalMetadata;
|
|
}
|
|
|
|
FinalMetadata _fallbackFromFileName(String fileName) {
|
|
final dotIndex = fileName.lastIndexOf('.');
|
|
final title = dotIndex > 0 ? fileName.substring(0, dotIndex) : fileName;
|
|
return FinalMetadata(
|
|
title: title,
|
|
artist: '未知艺术家',
|
|
source: 'filename',
|
|
);
|
|
}
|
|
}
|