diff --git a/lib/metadata/file_provider.dart b/lib/metadata/file_provider.dart new file mode 100644 index 0000000..d184457 --- /dev/null +++ b/lib/metadata/file_provider.dart @@ -0,0 +1,28 @@ +// lib/metadata/file_provider.dart +import 'dart:io'; + +abstract class FileProvider { + /// 获取文件,如果是远程文件则先下载到本地缓存 + Future getFile(String url); +} + +class LocalFileProvider implements FileProvider { + @override + Future getFile(String url) async { + final file = File(url); + if (await file.exists()) { + return file; + } + return null; + } +} + +class WebDAVFileProvider implements FileProvider { + @override + Future getFile(String url) async { + // 🔴 第一阶段:先不实现下载,返回 null + // 这样 WebDAV 文件会 fallback 到文件名 + // 等核心播放逻辑稳定后,再实现真正的下载缓存 + return null; + } +} diff --git a/lib/metadata/metadata_cache.dart b/lib/metadata/metadata_cache.dart index 80a7dd0..1103bae 100644 --- a/lib/metadata/metadata_cache.dart +++ b/lib/metadata/metadata_cache.dart @@ -1,35 +1,26 @@ // lib/metadata/metadata_cache.dart import 'metadata_model.dart'; -/// 本地缓存接口(先用内存缓存,后续升级为 SQLite) class MetadataCache { - // 内存缓存:Map final Map _cache = {}; - /// 获取缓存 Future get(String fileId) async { return _cache[fileId]; } - /// 存入缓存 Future put(String fileId, FinalMetadata metadata) async { _cache[fileId] = metadata; } - /// 更新缓存(同 put) Future update(String fileId, FinalMetadata metadata) async { _cache[fileId] = metadata; } - /// 根据 artist 获取历史记录(用于证据收集) - /// 实际项目中需要从数据库查询,目前返回空列表 Future>> getHistory(String artist, {int limit = 10}) async { - // TODO: 从 SQLite 查询 return []; } - /// 清空缓存 Future clear() async { _cache.clear(); } diff --git a/lib/metadata/metadata_model.dart b/lib/metadata/metadata_model.dart index fd88358..6a192f8 100644 --- a/lib/metadata/metadata_model.dart +++ b/lib/metadata/metadata_model.dart @@ -1,71 +1,74 @@ // lib/metadata/metadata_model.dart import 'dart:typed_data'; -/// 从音频文件读取的原始元数据(未经任何处理) +/// 从音频文件读取的原始元数据 class RawMetadata { final String title; final String artist; final String album; - final String albumArtist; - final String composer; - final String genre; + final List genres; // ⬅️ 改为 List + final List performers; // ⬅️ 新增 final int year; final int trackNumber; + final int trackTotal; final int discNumber; + final int totalDisc; + final Duration? duration; final Uint8List? artwork; const RawMetadata({ this.title = '', this.artist = '', this.album = '', - this.albumArtist = '', - this.composer = '', - this.genre = '', + this.genres = const [], + this.performers = const [], this.year = 0, this.trackNumber = 0, + this.trackTotal = 0, this.discNumber = 0, + this.totalDisc = 0, + this.duration, this.artwork, }); - /// 判断是否为空(没有任何有效信息) bool get isEmpty => title.isEmpty && artist.isEmpty && album.isEmpty && - albumArtist.isEmpty && - composer.isEmpty && - genre.isEmpty && - year == 0 && - trackNumber == 0 && - discNumber == 0 && - artwork == null; + genres.isEmpty && + performers.isEmpty && + year == 0; bool get isNotEmpty => !isEmpty; } -/// 经过标准化处理后的元数据 +/// 标准化后的元数据 class NormalizedMetadata { final String title; final String artist; final String album; - final String albumArtist; - final String composer; - final String genre; + final List genres; + final List performers; final int year; final int trackNumber; + final int trackTotal; final int discNumber; + final int totalDisc; + final Duration? duration; final Uint8List? artwork; const NormalizedMetadata({ this.title = '', this.artist = '', this.album = '', - this.albumArtist = '', - this.composer = '', - this.genre = '', + this.genres = const [], + this.performers = const [], this.year = 0, this.trackNumber = 0, + this.trackTotal = 0, this.discNumber = 0, + this.totalDisc = 0, + this.duration, this.artwork, }); @@ -73,54 +76,43 @@ class NormalizedMetadata { title.isEmpty && artist.isEmpty && album.isEmpty && - albumArtist.isEmpty && - composer.isEmpty && - genre.isEmpty && - year == 0 && - trackNumber == 0 && - discNumber == 0 && - artwork == null; + genres.isEmpty && + performers.isEmpty && + year == 0; bool get isNotEmpty => !isEmpty; } -/// 带置信度的候选元数据 -class CandidateMetadata { - final NormalizedMetadata metadata; - final double confidence; // 0.0 ~ 1.0 - final Map evidence; // 证据来源权重 - - const CandidateMetadata({ - required this.metadata, - required this.confidence, - this.evidence = const {}, - }); -} - /// 最终确认的元数据 class FinalMetadata { final String title; final String artist; final String album; - final String albumArtist; - final String composer; - final String genre; + final String genre; // ⬅️ 简化为单个流派(取第一个) + final List genres; // ⬅️ 保留完整列表 + final List performers; final int year; final int trackNumber; + final int trackTotal; final int discNumber; + final int totalDisc; + final Duration? duration; final Uint8List? artwork; - final String source; // 'file_tag', 'filename', 'cache', 'user_corrected' + final String source; const FinalMetadata({ this.title = '', this.artist = '', this.album = '', - this.albumArtist = '', - this.composer = '', this.genre = '', + this.genres = const [], + this.performers = const [], this.year = 0, this.trackNumber = 0, + this.trackTotal = 0, this.discNumber = 0, + this.totalDisc = 0, + this.duration, this.artwork, this.source = 'unknown', }); @@ -129,13 +121,23 @@ class FinalMetadata { title.isEmpty && artist.isEmpty && album.isEmpty && - albumArtist.isEmpty && - composer.isEmpty && genre.isEmpty && - year == 0 && - trackNumber == 0 && - discNumber == 0 && - artwork == null; + genres.isEmpty && + performers.isEmpty && + year == 0; bool get isNotEmpty => !isEmpty; } + +/// 候选元数据 +class CandidateMetadata { + final NormalizedMetadata metadata; + final double confidence; + final Map evidence; + + const CandidateMetadata({ + required this.metadata, + required this.confidence, + this.evidence = const {}, + }); +} diff --git a/lib/metadata/metadata_normalizer.dart b/lib/metadata/metadata_normalizer.dart index 0e1a02d..0b6694b 100644 --- a/lib/metadata/metadata_normalizer.dart +++ b/lib/metadata/metadata_normalizer.dart @@ -2,39 +2,43 @@ import 'metadata_model.dart'; class MetadataNormalizer { - /// 标准化原始数据(清洗空格、推断、fallback) NormalizedMetadata normalize(RawMetadata raw, {String fileName = '', String filePath = ''}) { // 1. 修剪空格 final title = raw.title.trim(); final artist = raw.artist.trim(); final album = raw.album.trim(); - final albumArtist = raw.albumArtist.trim(); - // 2. 如果 title 为空,尝试从文件名推断 + // 2. 如果 title 为空,从文件名推断 final finalTitle = title.isNotEmpty ? title : _inferTitleFromFileName(fileName); - // 3. 如果 artist 为空,尝试从路径推断或使用默认值 + // 3. 如果 artist 为空,从路径推断或使用默认值 final finalArtist = artist.isNotEmpty ? artist : _inferArtistFromPath(filePath); + // 4. 如果 performers 不为空且 artist 为空,用 performers 的第一个 + final finalArtist2 = finalArtist.isNotEmpty + ? finalArtist + : (raw.performers.isNotEmpty ? raw.performers.first : '未知艺术家'); + return NormalizedMetadata( title: finalTitle, - artist: finalArtist, + artist: finalArtist2, album: album, - albumArtist: albumArtist, - composer: raw.composer.trim(), - genre: raw.genre.trim(), + 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, ); } String _inferTitleFromFileName(String fileName) { - // 去掉扩展名 final dotIndex = fileName.lastIndexOf('.'); if (dotIndex > 0) { return fileName.substring(0, dotIndex).trim(); @@ -43,10 +47,8 @@ class MetadataNormalizer { } 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.contains(' ')) { return candidate; @@ -55,10 +57,8 @@ class MetadataNormalizer { return '未知艺术家'; } - /// 评估候选元数据(第一阶段:直接接受,不纠错) CandidateMetadata evaluate( NormalizedMetadata normalized, List> history) { - // 第一阶段:置信度固定为 0.8(文件名+标签混合) return CandidateMetadata( metadata: normalized, confidence: 0.8, @@ -66,18 +66,22 @@ class MetadataNormalizer { ); } - /// 决策是否修正(第一阶段:直接接受) FinalMetadata decide(CandidateMetadata candidate, {double threshold = 0.6}) { return FinalMetadata( title: candidate.metadata.title, artist: candidate.metadata.artist, album: candidate.metadata.album, - albumArtist: candidate.metadata.albumArtist, - composer: candidate.metadata.composer, - genre: candidate.metadata.genre, + 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: 'normalized', ); diff --git a/lib/metadata/metadata_reader.dart b/lib/metadata/metadata_reader.dart index 955221e..dc80765 100644 --- a/lib/metadata/metadata_reader.dart +++ b/lib/metadata/metadata_reader.dart @@ -1,30 +1,39 @@ // lib/metadata/metadata_reader.dart import 'dart:io'; +import 'package:audio_metadata_reader/audio_metadata_reader.dart' as amr; import 'metadata_model.dart'; -/// 底层元数据读取器(封装 audio_metadata_reader) class MetadataReader { - /// 从本地文件读取原始元数据 Future readRawMetadata(File file) async { try { - // TODO: 安装 audio_metadata_reader 后实现 - // final meta = await amr.readMetadata(file, getImage: false); - // return RawMetadata( - // title: meta.title ?? '', - // artist: meta.artist ?? '', - // album: meta.album ?? '', - // albumArtist: meta.albumArtist ?? '', - // composer: meta.composer ?? '', - // genre: meta.genre ?? '', - // year: meta.year ?? 0, - // trackNumber: meta.trackNumber ?? 0, - // discNumber: meta.discNumber ?? 0, - // ); + final meta = amr.readMetadata(file, getImage: false); - // 临时返回空,等待接入 audio_metadata_reader - return const RawMetadata(); + return RawMetadata( + title: meta.title ?? '', + artist: meta.artist ?? '', + album: meta.album ?? '', + // ⭐ genres 和 performers 是非空 List,直接使用 + genres: meta.genres, + performers: meta.performers, + year: _toInt(meta.year), + trackNumber: _toInt(meta.trackNumber), + trackTotal: _toInt(meta.trackTotal), + discNumber: _toInt(meta.discNumber), + totalDisc: _toInt(meta.totalDisc), + duration: meta.duration, + ); } catch (e) { return const RawMetadata(); } } + + /// 安全转换为 int,处理 Object 类型 + int _toInt(dynamic value) { + if (value == null) return 0; + if (value is int) return value; + if (value is String) return int.tryParse(value) ?? 0; + // 有些字段可能是 double 或 num + if (value is num) return value.toInt(); + return 0; + } } diff --git a/lib/metadata/metadata_service.dart b/lib/metadata/metadata_service.dart index 328b91a..1fef191 100644 --- a/lib/metadata/metadata_service.dart +++ b/lib/metadata/metadata_service.dart @@ -1,8 +1,5 @@ // lib/metadata/metadata_service.dart -import 'dart:io'; import 'metadata_model.dart'; -import 'metadata_reader.dart'; -import 'metadata_normalizer.dart'; import 'metadata_cache.dart'; class MetadataService { @@ -10,18 +7,16 @@ class MetadataService { factory MetadataService() => _instance; MetadataService._internal(); - final MetadataReader _reader = MetadataReader(); - final MetadataNormalizer _normalizer = MetadataNormalizer(); final MetadataCache _cache = MetadataCache(); - /// 获取歌曲的最终元数据(优先缓存,否则读取文件) + /// 临时版本:不进行实际文件读取,只返回基于文件名的 fallback Future getMetadata({ - required String filePath, + required String url, required String fileName, required String fileId, bool forceRefresh = false, }) async { - // 1. 尝试从缓存读取 + // 1. 检查缓存(如果有) if (!forceRefresh) { final cached = await _cache.get(fileId); if (cached != null && cached.isNotEmpty) { @@ -29,39 +24,17 @@ class MetadataService { } } - // 2. 检查文件是否存在 - final file = File(filePath); - if (!await file.exists()) { - // 文件不存在,返回基于文件名的推断 - return _fallbackFromFileName(fileName); + // 2. 暂时不读取文件,直接 fallback + final fallback = _fallbackFromFileName(fileName); + + // 3. 缓存结果(即使 fallback 也缓存,避免频繁调用) + if (fallback.isNotEmpty) { + await _cache.put(fileId, fallback); } - // 3. 从文件读取原始元数据 - final raw = await _reader.readRawMetadata(file); - - // 4. 标准化 - final normalized = _normalizer.normalize( - raw, - fileName: fileName, - filePath: filePath, - ); - - // 5. 评估(第一阶段直接接受) - final history = await _cache.getHistory(normalized.artist, limit: 10); - final candidate = _normalizer.evaluate(normalized, history); - - // 6. 决策 - final finalMetadata = _normalizer.decide(candidate); - - // 7. 写入缓存 - if (finalMetadata.isNotEmpty) { - await _cache.put(fileId, finalMetadata); - } - - return finalMetadata; + return fallback; } - /// Fallback:从文件名推断 FinalMetadata _fallbackFromFileName(String fileName) { final dotIndex = fileName.lastIndexOf('.'); final title = dotIndex > 0 ? fileName.substring(0, dotIndex) : fileName; diff --git a/lib/pages/player_page.dart b/lib/pages/player_page.dart index f20223a..32386c4 100644 --- a/lib/pages/player_page.dart +++ b/lib/pages/player_page.dart @@ -167,9 +167,11 @@ class _PlayerPageState extends State { } Widget _buildProgressSection(AudioService service, Duration duration) { + debugPrint('🎯 [PlayerPage] duration=$duration'); return ValueListenableBuilder( valueListenable: service.positionNotifier, builder: (context, position, _) { + debugPrint('🎯 [PlayerPage] position=$position, duration=$duration'); final progress = duration.inMilliseconds > 0 ? position.inMilliseconds / duration.inMilliseconds : 0.0; diff --git a/lib/services/audio_player_handler.dart b/lib/services/audio_player_handler.dart index c3e7fb5..5c0784b 100644 --- a/lib/services/audio_player_handler.dart +++ b/lib/services/audio_player_handler.dart @@ -39,6 +39,7 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler { }); _player.durationStream.listen((duration) { + debugPrint('🎯 [durationStream] duration=$duration'); _state.updateDuration(duration); if (_currentId != null && _currentTitle != null) { _updateMediaItem( diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart index 4e983dd..b69d33b 100644 --- a/lib/services/audio_service.dart +++ b/lib/services/audio_service.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:media_kit/media_kit.dart'; import 'playback_service.dart'; -import '../metadata/metadata_service.dart'; // ⭐ 新增 +import '../metadata/metadata_service.dart'; enum PlayMode { sequential, @@ -30,7 +30,7 @@ class AudioService extends ChangeNotifier { factory AudioService() => _instance; AudioService._internal(); - // ---- 基础状态(低频,触发 UI 重建) ---- + // ---- 基础状态 ---- Song? _currentSong; bool _isPlaying = false; PlayMode _playMode = PlayMode.sequential; @@ -41,7 +41,7 @@ class AudioService extends ChangeNotifier { List _shuffledIndices = []; int _shuffledIndex = -1; - // ---- ⭐ 高频进度(用 ValueNotifier,不触发全局重建) ---- + // ---- 高频进度 ---- final ValueNotifier positionNotifier = ValueNotifier(Duration.zero); final ValueNotifier durationNotifier = ValueNotifier(Duration.zero); final ValueNotifier bufferedNotifier = ValueNotifier(Duration.zero); @@ -49,13 +49,10 @@ class AudioService extends ChangeNotifier { bool _listening = false; final List _subscriptions = []; - // ⭐ 防重入标志 bool _handlingCompletion = false; - - // ⭐ 歌曲切换回调(用于通知 Handler 更新 MediaItem) void Function(Song)? _onSongChanged; - // ---- Getter(高频字段不走 ChangeNotifier) ---- + // ---- Getter ---- Song? get currentSong => _currentSong; bool get isPlaying => _isPlaying; PlayMode get playMode => _playMode; @@ -63,7 +60,6 @@ class AudioService extends ChangeNotifier { int get currentIndex => _currentIndex; bool get hasQueue => _queue.isNotEmpty; - // ---- 兼容旧代码:提供 getter 返回 ValueNotifier 的值 ---- Duration get position => positionNotifier.value; Duration get duration => durationNotifier.value; Duration get bufferedPosition => bufferedNotifier.value; @@ -79,12 +75,10 @@ class AudioService extends ChangeNotifier { } } - // ---- 注册歌曲切换回调 ---- void setOnSongChanged(void Function(Song) callback) { _onSongChanged = callback; } - // ---- 切换播放模式 ---- void togglePlayMode() { switch (_playMode) { case PlayMode.sequential: @@ -100,7 +94,6 @@ class AudioService extends ChangeNotifier { notifyListeners(); } - // ---- 设置播放队列 ---- void setQueue(List queue, {int startIndex = 0}) { if (queue.isEmpty) { _clearQueue(); @@ -129,7 +122,6 @@ class AudioService extends ChangeNotifier { stopPlay(); } - // ---- 播放指定歌曲 ---- Future playSong(Song song) async { if (_queue.isEmpty || _queue[_currentIndex].id != song.id) { setQueue([song], startIndex: 0); @@ -138,6 +130,9 @@ class AudioService extends ChangeNotifier { } } + // ============================================================ + // 核心播放逻辑 + // ============================================================ void _playCurrent() { if (_currentIndex < 0 || _currentIndex >= _queue.length) { stopPlay(); @@ -147,13 +142,8 @@ class AudioService extends ChangeNotifier { final song = _queue[_currentIndex]; _currentSong = song; - // ⭐ 切歌时触发回调(通知 Handler 更新 MediaItem) - _onSongChanged?.call(song); - - // ⭐ 重置进度(用 ValueNotifier) - positionNotifier.value = Duration.zero; - durationNotifier.value = Duration.zero; - bufferedNotifier.value = Duration.zero; + // ⭐ 不重置任何 Notifier,完全依赖 stream 推送 + // position / duration / buffered 由播放器自然更新 _startListening(); notifyListeners(); @@ -164,27 +154,27 @@ class AudioService extends ChangeNotifier { PlaybackService().play(song.url!); - // ⭐ 异步加载 metadata(不阻塞播放) + // ⭐ 兜底:主动同步一次播放器状态(解决首次加载时 stream 未推送的问题) + _syncPlayerStateDelayed(); + _loadMetadataForCurrentSong(); } - // ⭐ 新增:加载当前歌曲的 metadata + // ============================================================ + // Metadata 加载 + // ============================================================ Future _loadMetadataForCurrentSong() async { if (_currentIndex < 0 || _currentIndex >= _queue.length) return; - final song = _queue[_currentIndex]; if (song.url == null || song.url!.isEmpty) return; try { - // 注意:filePath 需要是本地文件路径 - // WebDAV 文件可能需要先缓存到本地 final metadata = await MetadataService().getMetadata( - filePath: song.url!, + url: song.url!, fileName: song.title, fileId: song.id, ); - // 如果 metadata 有效且与当前不同,更新 Song if (metadata.isNotEmpty) { final updatedSong = Song( id: song.id, @@ -198,12 +188,13 @@ class AudioService extends ChangeNotifier { _onSongChanged?.call(updatedSong); } } catch (e) { - // 读取失败,保持原有信息 debugPrint('⚠️ [AudioService] metadata load failed: $e'); } } - // ---- 下一首 ---- + // ============================================================ + // 播放控制 + // ============================================================ void next() { if (_queue.isEmpty) return; @@ -221,7 +212,6 @@ class AudioService extends ChangeNotifier { _playCurrent(); } - // ---- 上一首 ---- void previous() { if (_queue.isEmpty) return; @@ -247,7 +237,6 @@ class AudioService extends ChangeNotifier { _playCurrent(); } - // ---- 播放/暂停 ---- void togglePlay() { if (_currentSong == null) return; @@ -282,7 +271,38 @@ class AudioService extends ChangeNotifier { notifyListeners(); } - // ---- 监听 media_kit 状态 ---- + // ============================================================ + // 播放器状态同步(兜底机制) + // ============================================================ + void _syncPlayerStateDelayed() { + _syncPlayerStateNow(); + Future.delayed(const Duration(milliseconds: 200), () { + _syncPlayerStateNow(); + }); + Future.delayed(const Duration(milliseconds: 500), () { + _syncPlayerStateNow(); + }); + } + + void _syncPlayerStateNow() { + final player = PlaybackService().player; + final dur = player.state.duration; + final buf = player.state.buffer; + + if (dur.inMilliseconds > 0 && durationNotifier.value.inMilliseconds == 0) { + durationNotifier.value = dur; + debugPrint('🎯 [AudioService] sync duration: $dur'); + } + + if (buf.inMilliseconds > 0 && bufferedNotifier.value.inMilliseconds == 0) { + bufferedNotifier.value = buf; + debugPrint('🎯 [AudioService] sync buffer: $buf'); + } + } + + // ============================================================ + // media_kit 状态监听 + // ============================================================ void _startListening() { if (_listening) return; _listening = true; @@ -306,7 +326,11 @@ class AudioService extends ChangeNotifier { _subscriptions.add( player.stream.duration.listen((duration) { - durationNotifier.value = duration; + debugPrint('🎯 [AudioService] durationStream: $duration'); + // 只接受 > 0 的值,避免 0 覆盖正确值 + if (duration.inMilliseconds > 0) { + durationNotifier.value = duration; + } }), ); @@ -331,7 +355,9 @@ class AudioService extends ChangeNotifier { _subscriptions.clear(); } - // ⭐ 防重入的完成事件处理 + // ============================================================ + // 播放完成处理 + // ============================================================ void _onPlaybackCompleted() { if (_handlingCompletion) { debugPrint('⚠️ [service] completed ignored: already handling');