Files
QingTing-Player/lib/metadata/file_provider.dart
T
lxh2875931338 a5849969d3 perf(metadata): 元数据探测从 2MB 降到 512KB
1Panel 访问日志显示:2MB 的探测请求在当前链路上要 10~18 秒,
期间 libmpv 的 206 吞吐被压到 ~1Mbps,直接拖慢起播。

- _maxBytes 2MB -> 512KB:常见标签与封面都在此范围内
- 读不到标签时仍由 80% 重试兜底
- 日志文案同步更新

另:访问日志 CSV 属于本地参考,加入 .gitignore
2026-10-23 23:16:07 +08:00

131 lines
4.0 KiB
Dart

// 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';
import '../utils/song_key.dart';
abstract class FileProvider {
Future<File?> getFile(String url);
}
class WebDAVFileProvider implements FileProvider {
/// 只为读标签取文件头部,绝不整文件下载。
/// 之前用 dio.download 会把整首歌拉到临时目录,和 mpv 的流式播放抢带宽,
/// 20Mbps 的 FRP 链路上尤其明显(表现为流量很高但缓冲一直为 0)。
///
/// 从访问日志看:2MB 的探测请求在这条链路上要 10~18 秒,
/// 期间 libmpv 的 206 吞吐会被压到 1Mbps 左右,所以压到 512KB。
/// 常见标签 + 封面都在这范围内;读不到时由上层在 80% 处重试。
static const int _maxBytes = 512 * 1024;
/// 切歌时取消上一次未完成的下载,避免多个下载叠加占满带宽。
static CancelToken? _activeToken;
final Dio _dio = Dio();
@override
Future<File?> getFile(String url) async {
try {
final cacheDir = await getTemporaryDirectory();
final cachePath = '${cacheDir.path}/metadata_${songKeyForUrl(url)}.part';
final cacheFile = File(cachePath);
// 1. 命中本地缓存(头部文件同样按 24h 过期)
if (await cacheFile.exists()) {
final stat = await cacheFile.stat();
if (stat.size > 0 &&
DateTime.now().difference(stat.modified).inSeconds < 86400) {
return cacheFile;
}
}
// 2. 认证头
final headers = await WebDAVService.instance.getAuthHeaders();
if (headers.isEmpty) return null;
// 3. 取消上一次未完成的下载
_activeToken?.cancel('superseded');
final token = CancelToken();
_activeToken = token;
// 4. 只请求前 2MB
final response = await _dio.get<ResponseBody>(
url,
cancelToken: token,
options: Options(
headers: {
...headers,
'Range': 'bytes=0-${_maxBytes - 1}',
},
responseType: ResponseType.stream,
receiveTimeout: const Duration(seconds: 15),
sendTimeout: const Duration(seconds: 10),
followRedirects: true,
),
);
final body = response.data;
if (body == null) return null;
// 5. 写入临时文件(最多 _maxBytes)
final sink = cacheFile.openWrite();
var written = 0;
try {
await for (final chunk in body.stream) {
final remain = _maxBytes - written;
if (remain <= 0) break;
final data =
chunk.lengthInBytes > remain ? chunk.sublist(0, remain) : chunk;
sink.add(data);
written += data.lengthInBytes;
if (written >= _maxBytes) break;
}
} finally {
await sink.flush();
await sink.close();
}
if (written == 0) {
try {
await cacheFile.delete();
} catch (_) {}
return null;
}
debugPrint(
'📦 [WebDAVFileProvider] 取头部 ${(written / 1024).toStringAsFixed(0)}KB'
'(上限 ${_maxBytes ~/ 1024}KB,不再整文件下载)');
return cacheFile;
} on DioException catch (e) {
if (CancelToken.isCancel(e)) {
debugPrint('↩️ [WebDAVFileProvider] 下载已取消(切歌触发)');
return null;
}
debugPrint('⚠️ [WebDAVFileProvider] download failed: $e');
return null;
} catch (e) {
debugPrint('⚠️ [WebDAVFileProvider] download failed: $e');
return null;
}
}
/// 主动取消进行中的元数据下载(切歌时可由上层调用)。
static void cancelActive() {
_activeToken?.cancel('superseded');
_activeToken = null;
}
}
class LocalFileProvider implements FileProvider {
@override
Future<File?> getFile(String url) async {
final file = File(url);
if (await file.exists()) {
return file;
}
return null;
}
}