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 key.properties
**/*.keystore **/*.keystore
**/*.jks **/*.jks
.settings
-2
View File
@@ -187,8 +187,6 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
} }
_lifecycleListener = (status) { _lifecycleListener = (status) {
final audioService = context.read<AudioService>();
switch (status) { switch (status) {
case AppLifecycleStatus.paused: case AppLifecycleStatus.paused:
case AppLifecycleStatus.inactive: case AppLifecycleStatus.inactive:
+47 -11
View File
@@ -1,11 +1,57 @@
// lib/metadata/file_provider.dart // lib/metadata/file_provider.dart
import 'dart:io'; 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 { abstract class FileProvider {
/// 获取文件,如果是远程文件则先下载到本地缓存
Future<File?> getFile(String url); 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 { class LocalFileProvider implements FileProvider {
@override @override
Future<File?> getFile(String url) async { Future<File?> getFile(String url) async {
@@ -16,13 +62,3 @@ class LocalFileProvider implements FileProvider {
return null; 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 { class MetadataNormalizer {
NormalizedMetadata normalize(RawMetadata raw, NormalizedMetadata normalize(RawMetadata raw,
{String fileName = '', String filePath = ''}) { {String fileName = '', String filePath = ''}) {
// 1. 修剪空格 // 1. 修剪
final title = raw.title.trim(); final title = raw.title.trim();
final artist = raw.artist.trim(); final artist = raw.artist.trim();
final album = raw.album.trim();
// 2. 如果 title 为空,从文件名推断 // 2. 如果 title 为空,从文件名推断
final finalTitle = final finalTitle =
title.isNotEmpty ? title : _inferTitleFromFileName(fileName); title.isNotEmpty ? title : _inferTitleFromFileName(fileName);
// 3. 如果 artist 为空,从路径推断或使用默认值 // 3. 如果 artist 为空,从路径推断
final finalArtist = final finalArtist =
artist.isNotEmpty ? artist : _inferArtistFromPath(filePath); artist.isNotEmpty ? artist : _inferArtistFromPath(filePath);
// 4. 如果 performers 不为空且 artist 为空,用 performers 的第一个 // 4. 如果 performers 不为空且 artist 为空,使用 performers
final finalArtist2 = finalArtist.isNotEmpty final finalArtist2 = finalArtist.isNotEmpty
? finalArtist ? finalArtist
: (raw.performers.isNotEmpty ? raw.performers.first : '未知艺术家'); : (raw.performers.isNotEmpty ? raw.performers.first : '未知艺术家');
@@ -25,7 +24,7 @@ class MetadataNormalizer {
return NormalizedMetadata( return NormalizedMetadata(
title: finalTitle, title: finalTitle,
artist: finalArtist2, artist: finalArtist2,
album: album, album: raw.album,
genres: raw.genres, genres: raw.genres,
performers: raw.performers, performers: raw.performers,
year: raw.year, year: raw.year,
@@ -40,17 +39,16 @@ class MetadataNormalizer {
String _inferTitleFromFileName(String fileName) { String _inferTitleFromFileName(String fileName) {
final dotIndex = fileName.lastIndexOf('.'); final dotIndex = fileName.lastIndexOf('.');
if (dotIndex > 0) { return dotIndex > 0
return fileName.substring(0, dotIndex).trim(); ? fileName.substring(0, dotIndex).trim()
} : fileName.trim();
return fileName.trim();
} }
String _inferArtistFromPath(String filePath) { String _inferArtistFromPath(String filePath) {
final parts = filePath.split(RegExp(r'[/\\]')); final parts = filePath.split(RegExp(r'[/\\]'));
if (parts.length >= 2) { if (parts.length >= 2) {
final candidate = parts[parts.length - 2].trim(); final candidate = parts[parts.length - 2].trim();
if (candidate.isNotEmpty && !candidate.contains(' ')) { if (candidate.isNotEmpty && !candidate.startsWith('http')) {
return candidate; return candidate;
} }
} }
+3 -3
View File
@@ -1,5 +1,6 @@
// lib/metadata/metadata_reader.dart // lib/metadata/metadata_reader.dart
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; // ⭐ 添加
import 'package:audio_metadata_reader/audio_metadata_reader.dart' as amr; import 'package:audio_metadata_reader/audio_metadata_reader.dart' as amr;
import 'metadata_model.dart'; import 'metadata_model.dart';
@@ -12,7 +13,7 @@ class MetadataReader {
title: meta.title ?? '', title: meta.title ?? '',
artist: meta.artist ?? '', artist: meta.artist ?? '',
album: meta.album ?? '', album: meta.album ?? '',
// ⭐ genres 和 performers 是非空 List<String>直接使用 // ⭐ genres 和 performers 是非空 List<String>不需要 ??
genres: meta.genres, genres: meta.genres,
performers: meta.performers, performers: meta.performers,
year: _toInt(meta.year), year: _toInt(meta.year),
@@ -23,16 +24,15 @@ class MetadataReader {
duration: meta.duration, duration: meta.duration,
); );
} catch (e) { } catch (e) {
debugPrint('⚠️ [MetadataReader] read failed: $e');
return const RawMetadata(); return const RawMetadata();
} }
} }
/// 安全转换为 int,处理 Object 类型
int _toInt(dynamic value) { int _toInt(dynamic value) {
if (value == null) return 0; if (value == null) return 0;
if (value is int) return value; if (value is int) return value;
if (value is String) return int.tryParse(value) ?? 0; if (value is String) return int.tryParse(value) ?? 0;
// 有些字段可能是 double 或 num
if (value is num) return value.toInt(); if (value is num) return value.toInt();
return 0; return 0;
} }
+46 -8
View File
@@ -1,38 +1,76 @@
// lib/metadata/metadata_service.dart // lib/metadata/metadata_service.dart
// ⭐ 移除未使用的 import 'dart:io';
import 'package:flutter/foundation.dart'; // ⭐ 添加
import 'metadata_model.dart'; import 'metadata_model.dart';
import 'metadata_reader.dart';
import 'metadata_normalizer.dart';
import 'metadata_cache.dart'; import 'metadata_cache.dart';
import 'file_provider.dart';
class MetadataService { class MetadataService {
static final MetadataService _instance = MetadataService._internal(); static final MetadataService _instance = MetadataService._internal();
factory MetadataService() => _instance; factory MetadataService() => _instance;
MetadataService._internal(); MetadataService._internal();
final MetadataReader _reader = MetadataReader();
final MetadataNormalizer _normalizer = MetadataNormalizer();
final MetadataCache _cache = MetadataCache(); final MetadataCache _cache = MetadataCache();
/// 临时版本:不进行实际文件读取,只返回基于文件名的 fallback FileProvider _getProvider(String url) {
if (url.startsWith('http://') || url.startsWith('https://')) {
return WebDAVFileProvider();
}
return LocalFileProvider();
}
Future<FinalMetadata> getMetadata({ Future<FinalMetadata> getMetadata({
required String url, required String url,
required String fileName, required String fileName,
required String fileId, required String fileId,
bool forceRefresh = false, bool forceRefresh = false,
}) async { }) async {
// 1. 检查缓存(如果有) // 1. 检查缓存
if (!forceRefresh) { if (!forceRefresh) {
final cached = await _cache.get(fileId); final cached = await _cache.get(fileId);
if (cached != null && cached.isNotEmpty) { if (cached != null && cached.isNotEmpty) {
debugPrint('📦 [MetadataService] cache hit: ${cached.title}');
return cached; return cached;
} }
} }
// 2. 暂时不读取文件,直接 fallback // 2. 获取文件
final fallback = _fallbackFromFileName(fileName); final provider = _getProvider(url);
final file = await provider.getFile(url);
// 3. 缓存结果(即使 fallback 也缓存,避免频繁调用) if (file == null || !await file.exists()) {
if (fallback.isNotEmpty) { debugPrint(
await _cache.put(fileId, fallback); '⚠️ [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) { FinalMetadata _fallbackFromFileName(String fileName) {
+1 -1
View File
@@ -441,7 +441,7 @@ packages:
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_provider: path_provider:
dependency: transitive dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
+1
View File
@@ -42,6 +42,7 @@ dependencies:
flutter_cache_manager: ^3.3.1 flutter_cache_manager: ^3.3.1
sqflite: ^2.3.0 sqflite: ^2.3.0
audio_metadata_reader: ^1.7.1 audio_metadata_reader: ^1.7.1
path_provider: ^2.1.0
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.