metadata读取能力已达成,接下来会顺着路径完成部分缓存指针设计
This commit is contained in:
@@ -15,3 +15,4 @@ GeneratedPluginRegistrant.java
|
|||||||
key.properties
|
key.properties
|
||||||
**/*.keystore
|
**/*.keystore
|
||||||
**/*.jks
|
**/*.jks
|
||||||
|
.settings
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user