metadata读取能力已达成,接下来会顺着路径完成部分缓存指针设计
This commit is contained in:
@@ -15,3 +15,4 @@ GeneratedPluginRegistrant.java
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
.settings
|
||||
|
||||
@@ -187,8 +187,6 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
|
||||
}
|
||||
|
||||
_lifecycleListener = (status) {
|
||||
final audioService = context.read<AudioService>();
|
||||
|
||||
switch (status) {
|
||||
case AppLifecycleStatus.paused:
|
||||
case AppLifecycleStatus.inactive:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -441,7 +441,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user