metadata读取能力已达成,接下来会顺着路径完成部分缓存指针设计

This commit is contained in:
2026-08-25 21:57:04 +08:00
parent b75ea9e4ce
commit 7bd23bfee6
8 changed files with 107 additions and 35 deletions
+1
View File
@@ -15,3 +15,4 @@ GeneratedPluginRegistrant.java
key.properties
**/*.keystore
**/*.jks
.settings
-2
View File
@@ -187,8 +187,6 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
}
_lifecycleListener = (status) {
final audioService = context.read<AudioService>();
switch (status) {
case AppLifecycleStatus.paused:
case AppLifecycleStatus.inactive:
+47 -11
View File
@@ -1,11 +1,57 @@
// lib/metadata/file_provider.dart
import 'dart:io';
import 'package:flutter/foundation.dart'; // ⭐ 添加
import 'package:path_provider/path_provider.dart';
import 'package:dio/dio.dart';
import '../services/webdav_service.dart';
abstract class FileProvider {
/// 获取文件,如果是远程文件则先下载到本地缓存
Future<File?> getFile(String url);
}
class WebDAVFileProvider implements FileProvider {
final Dio _dio = Dio();
@override
Future<File?> getFile(String url) async {
try {
// 1. 获取缓存目录
final cacheDir = await getTemporaryDirectory();
final cachePath = '${cacheDir.path}/metadata_${url.hashCode}.tmp';
final cacheFile = File(cachePath);
// 2. 如果缓存存在且未过期,直接返回
if (await cacheFile.exists()) {
final stat = await cacheFile.stat();
// 缓存 1 小时内有效(86400 秒)
if (DateTime.now().difference(stat.modified).inSeconds < 86400) {
return cacheFile;
}
}
// 3. 获取认证头
final headers = await WebDAVService.instance.getAuthHeaders();
if (headers.isEmpty) return null;
// 4. 只下载前 1MB
await _dio.download(
url,
cachePath,
options: Options(
headers: headers,
receiveTimeout: const Duration(seconds: 10),
sendTimeout: const Duration(seconds: 10),
),
);
return cacheFile;
} catch (e) {
debugPrint('⚠️ [WebDAVFileProvider] download failed: $e');
return null;
}
}
}
class LocalFileProvider implements FileProvider {
@override
Future<File?> getFile(String url) async {
@@ -16,13 +62,3 @@ class LocalFileProvider implements FileProvider {
return null;
}
}
class WebDAVFileProvider implements FileProvider {
@override
Future<File?> getFile(String url) async {
// 🔴 第一阶段:先不实现下载,返回 null
// 这样 WebDAV 文件会 fallback 到文件名
// 等核心播放逻辑稳定后,再实现真正的下载缓存
return null;
}
}
+8 -10
View File
@@ -4,20 +4,19 @@ import 'metadata_model.dart';
class MetadataNormalizer {
NormalizedMetadata normalize(RawMetadata raw,
{String fileName = '', String filePath = ''}) {
// 1. 修剪空格
// 1. 修剪
final title = raw.title.trim();
final artist = raw.artist.trim();
final album = raw.album.trim();
// 2. 如果 title 为空,从文件名推断
final finalTitle =
title.isNotEmpty ? title : _inferTitleFromFileName(fileName);
// 3. 如果 artist 为空,从路径推断或使用默认值
// 3. 如果 artist 为空,从路径推断
final finalArtist =
artist.isNotEmpty ? artist : _inferArtistFromPath(filePath);
// 4. 如果 performers 不为空且 artist 为空,用 performers 的第一个
// 4. 如果 performers 不为空且 artist 为空,使用 performers
final finalArtist2 = finalArtist.isNotEmpty
? finalArtist
: (raw.performers.isNotEmpty ? raw.performers.first : '未知艺术家');
@@ -25,7 +24,7 @@ class MetadataNormalizer {
return NormalizedMetadata(
title: finalTitle,
artist: finalArtist2,
album: album,
album: raw.album,
genres: raw.genres,
performers: raw.performers,
year: raw.year,
@@ -40,17 +39,16 @@ class MetadataNormalizer {
String _inferTitleFromFileName(String fileName) {
final dotIndex = fileName.lastIndexOf('.');
if (dotIndex > 0) {
return fileName.substring(0, dotIndex).trim();
}
return fileName.trim();
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.contains(' ')) {
if (candidate.isNotEmpty && !candidate.startsWith('http')) {
return candidate;
}
}
+3 -3
View File
@@ -1,5 +1,6 @@
// lib/metadata/metadata_reader.dart
import 'dart:io';
import 'package:flutter/foundation.dart'; // ⭐ 添加
import 'package:audio_metadata_reader/audio_metadata_reader.dart' as amr;
import 'metadata_model.dart';
@@ -12,7 +13,7 @@ class MetadataReader {
title: meta.title ?? '',
artist: meta.artist ?? '',
album: meta.album ?? '',
// ⭐ genres 和 performers 是非空 List<String>直接使用
// ⭐ genres 和 performers 是非空 List<String>不需要 ??
genres: meta.genres,
performers: meta.performers,
year: _toInt(meta.year),
@@ -23,16 +24,15 @@ class MetadataReader {
duration: meta.duration,
);
} catch (e) {
debugPrint('⚠️ [MetadataReader] read failed: $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;
}
+46 -8
View File
@@ -1,38 +1,76 @@
// lib/metadata/metadata_service.dart
// ⭐ 移除未使用的 import 'dart:io';
import 'package:flutter/foundation.dart'; // ⭐ 添加
import 'metadata_model.dart';
import 'metadata_reader.dart';
import 'metadata_normalizer.dart';
import 'metadata_cache.dart';
import 'file_provider.dart';
class MetadataService {
static final MetadataService _instance = MetadataService._internal();
factory MetadataService() => _instance;
MetadataService._internal();
final MetadataReader _reader = MetadataReader();
final MetadataNormalizer _normalizer = MetadataNormalizer();
final MetadataCache _cache = MetadataCache();
/// 临时版本:不进行实际文件读取,只返回基于文件名的 fallback
FileProvider _getProvider(String url) {
if (url.startsWith('http://') || url.startsWith('https://')) {
return WebDAVFileProvider();
}
return LocalFileProvider();
}
Future<FinalMetadata> getMetadata({
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) {
debugPrint('📦 [MetadataService] cache hit: ${cached.title}');
return cached;
}
}
// 2. 暂时不读取文件,直接 fallback
final fallback = _fallbackFromFileName(fileName);
// 2. 获取文件
final provider = _getProvider(url);
final file = await provider.getFile(url);
// 3. 缓存结果(即使 fallback 也缓存,避免频繁调用)
if (fallback.isNotEmpty) {
await _cache.put(fileId, fallback);
if (file == null || !await file.exists()) {
debugPrint(
'⚠️ [MetadataService] file not available, using filename fallback');
return _fallbackFromFileName(fileName);
}
return fallback;
// 3. 读取 metadata
final raw = await _reader.readRawMetadata(file);
// 4. 标准化
final normalized = _normalizer.normalize(
raw,
fileName: fileName,
filePath: url,
);
// 5. 评估(第一阶段:直接接受)
final history = await _cache.getHistory(normalized.artist, limit: 10);
final candidate = _normalizer.evaluate(normalized, history);
final finalMetadata = _normalizer.decide(candidate);
// 6. 缓存
if (finalMetadata.isNotEmpty) {
await _cache.put(fileId, finalMetadata);
debugPrint(
'✅ [MetadataService] metadata saved: ${finalMetadata.title} - ${finalMetadata.artist}');
}
return finalMetadata;
}
FinalMetadata _fallbackFromFileName(String fileName) {
+1 -1
View File
@@ -441,7 +441,7 @@ packages:
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
dependency: "direct main"
description:
name: path_provider
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
+1
View File
@@ -42,6 +42,7 @@ dependencies:
flutter_cache_manager: ^3.3.1
sqflite: ^2.3.0
audio_metadata_reader: ^1.7.1
path_provider: ^2.1.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.