fix account profile and improve operation logs

This commit is contained in:
ngfchl
2026-07-18 22:35:47 +08:00
parent 6b202475d1
commit dbacdb854f
9 changed files with 255 additions and 126 deletions
+32 -2
View File
@@ -1,12 +1,13 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import '../core/http/dio_client.dart';
import '../core/http/http.dart';
import '../core/logging/app_logger.dart';
import '../core/http/http_error.dart';
import '../core/config/app_config.dart';
@@ -153,7 +154,36 @@ class GuangyaAPI {
}
Future<Map<String, dynamic>> userInfo() async {
return Http.accountRequest('/v1/user/me', body: null, authenticated: true);
// The current account gateway exposes this REST resource as GET. Older
// deployments accepted POST, so retain it as a compatibility fallback.
try {
return await Http.accountRequest(
'/v1/user/me',
method: 'GET',
body: null,
authenticated: true,
);
} on DioException catch (error) {
final status = error.response?.statusCode;
if (status != 404 && status != 405 && status != 501) rethrow;
AppLogger.warning('Auth', '账户资料 GET 接口不可用($status),正在尝试兼容请求');
return Http.accountRequest(
'/v1/user/me',
body: null,
authenticated: true,
);
}
}
/// 云盘总容量、已用容量、会员与直链流量信息。
Future<Map<String, dynamic>> cloudAssets() async {
final response = await Http.apiRequest('/assets/v1/get_assets');
final data = response['data'];
return data is Map<String, dynamic>
? data
: data is Map
? Map<String, dynamic>.from(data)
: const {};
}
// ── Cloud downloads ────────────────────────────────────────────────
+41 -26
View File
@@ -1,4 +1,5 @@
import 'dart:developer';
import 'dart:io';
import 'dart:math';
import 'package:dio/dio.dart';
@@ -28,10 +29,6 @@ class Http {
headers: headers,
);
final stopwatch = Stopwatch()..start();
final endpoint = useAccountDio ? 'account' : 'api';
log('[HTTP:$endpoint] -> $method $path');
try {
final res = await client.request(
path,
@@ -40,23 +37,10 @@ class Http {
cancelToken: cancelToken,
options: mergedOptions,
);
stopwatch.stop();
log(
'[HTTP:$endpoint] <- $method $path status=${res.statusCode} '
'elapsed=${stopwatch.elapsedMilliseconds}ms',
);
return res.data as T;
} on DioException catch (e) {
stopwatch.stop();
final status = e.response?.statusCode;
log(
'[HTTP:$endpoint] !! $method $path status=${status ?? '-'} '
'type=${e.type.name} elapsed=${stopwatch.elapsedMilliseconds}ms',
);
// 将业务错误信息包装为 ApiException
final status = e.response?.statusCode;
final errorMsg = e.error is String ? (e.error as String) : '';
if (errorMsg.isNotEmpty) {
throw ApiException(status: status, message: errorMsg);
@@ -64,10 +48,6 @@ class Http {
rethrow;
} catch (e) {
stopwatch.stop();
log(
'[HTTP:$endpoint] !! $method $path elapsed=${stopwatch.elapsedMilliseconds}ms',
);
rethrow;
}
}
@@ -121,6 +101,7 @@ class Http {
bool authenticated = false,
}) async {
final headers = <String, dynamic>{
..._accountHeaders(),
'did': _getDeviceID(),
'dt': '4',
...?extraHeaders,
@@ -163,9 +144,7 @@ class Http {
static void _checkTokenExpiry() {
final expiresAt = StorageManager.get<int>(StorageKeys.tokenExpiresAt);
if (expiresAt == null) return;
if (DateTime.now().millisecondsSinceEpoch > expiresAt) {
log('[HTTP] token expired, will refresh on next 401');
}
if (DateTime.now().millisecondsSinceEpoch > expiresAt) {}
}
static String _getDeviceID() {
@@ -173,6 +152,42 @@ class Http {
'flutter-${DateTime.now().millisecondsSinceEpoch}';
}
/// 账号网关会校验网页客户端的设备标识;与源客户端保持一致。
static Map<String, String> _accountHeaders() {
final deviceID = _getDeviceID();
return {
'x-client-id': 'aMe-8VSlkrbQXpUR',
'x-client-version': '0.0.1',
'x-device-id': deviceID,
'x-device-model': 'chrome%2F150.0.0.0',
'x-device-name': 'PC-Chrome',
'x-device-sign': 'wdi10.$deviceID${_randomHex(16)}',
'x-net-work-type': 'NONE',
'x-os-version': _webPlatformName(),
'x-platform-version': '1',
'x-protocol-version': '301',
'x-provider-name': 'NONE',
'x-sdk-version': '9.0.2',
};
}
static String _webPlatformName() {
if (Platform.isMacOS) return 'MacIntel';
if (Platform.isWindows) return 'Win32';
if (Platform.isLinux) return 'Linux x86_64';
if (Platform.isIOS) return 'iPhone';
if (Platform.isAndroid) return 'Linux armv8l';
return Platform.operatingSystem;
}
static String _randomHex(int bytes) {
final random = Random.secure();
return List.generate(
bytes,
(_) => random.nextInt(256).toRadixString(16).padLeft(2, '0'),
).join();
}
// ── Helpers ─────────────────────────────────────────────────────
static String _generateTraceparent() {
@@ -1,34 +1,52 @@
import 'package:dio/dio.dart';
import '../http_error.dart';
import '../../logging/app_logger.dart';
class AppLogInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
AppLogger.debug(
'HTTP',
'${options.method} ${options.uri.path}${options.uri.hasQuery ? '?${options.uri.query}' : ''}',
);
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
AppLogger.debug(
'HTTP',
'${response.statusCode ?? 0} ${response.requestOptions.method} ${response.requestOptions.uri.path}',
);
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
final status = err.response?.statusCode;
final serverMessage = err.response?.data is Map
? extractHttpMessage(err.response?.data)
: null;
final detail = serverMessage?.trim().isNotEmpty == true
? serverMessage!
: (err.error?.toString().trim().isNotEmpty == true
? err.error.toString()
: err.type.name);
AppLogger.error(
'HTTP',
'${err.requestOptions.method} ${err.requestOptions.uri.path} 失败',
error: err.message,
stackTrace: err.stackTrace,
'${_actionName(err.requestOptions.path)}失败(状态码 ${status ?? '-'},原因:$detail',
);
handler.next(err);
}
static String _actionName(String path) {
if (path.contains('/v1/user/me')) return '获取账户资料';
if (path.contains('/assets/v1/get_assets')) return '获取云盘空间信息';
if (path.contains('/search_files')) return '搜索云盘文件';
if (path.contains('/get_file_list')) return '读取文件列表';
if (path.contains('/get_file_detail')) return '读取文件详情';
if (path.contains('/create_dir')) return '新建文件夹';
if (path.contains('/move_file')) return '移动文件';
if (path.contains('/copy_file')) return '复制文件';
if (path.contains('/rename')) return '重命名文件';
if (path.contains('/recycle_file')) return '移入回收站';
if (path.contains('/delete_file')) return '删除文件';
if (path.contains('/auth/token')) return '刷新登录凭据';
if (path.contains('/auth/')) return '登录认证';
if (path.contains('/tmdb')) return '获取影视资料';
return '云盘服务请求';
}
}
@@ -1,8 +1,7 @@
import 'dart:async';
import 'dart:developer';
import 'package:dio/dio.dart';
import '../../logging/app_logger.dart';
import '../../config/app_config.dart';
import '../../storage/storage_manager.dart';
import '../http_error.dart';
@@ -60,10 +59,6 @@ class AuthInterceptor extends Interceptor {
final status = err.response?.statusCode;
final alreadyLoggedOut = status == 401 && _isAlreadyLoggedOut;
if (!alreadyLoggedOut) {
log('[Auth] error path=${err.requestOptions.path} status=$status');
}
// 免认证路径的错误直接放行
if (_isAuthExemptPath(err.requestOptions.path)) {
return handler.next(err);
@@ -127,7 +122,7 @@ class AuthInterceptor extends Interceptor {
// 开始刷新
_isRefreshing = true;
log('[Auth] refreshing access token');
AppLogger.info('Auth', '登录凭据已过期,正在刷新');
try {
final dio = Dio(
@@ -158,7 +153,7 @@ class AuthInterceptor extends Interceptor {
),
);
log('[Auth] access token refreshed');
AppLogger.info('Auth', '登录凭据刷新完成');
final data = res.data;
final newAccess = _findStringDeep(data, ['access_token', 'accessToken']);
@@ -202,7 +197,7 @@ class AuthInterceptor extends Interceptor {
final response = await DioClient.dio.fetch(request);
return handler.resolve(response);
} catch (e) {
log('[Auth] token refresh failed: $e');
AppLogger.warning('Auth', '登录凭据刷新失败,将退出当前账号');
for (var c in _waitQueue) {
c.complete();
@@ -228,7 +223,7 @@ class AuthInterceptor extends Interceptor {
if (_logoutScheduled) return;
_logoutScheduled = true;
log('[Auth] logout scheduled');
AppLogger.info('Auth', '登录状态已失效,正在退出当前账号');
await StorageManager.delete(StorageKeys.accessToken);
await StorageManager.delete(StorageKeys.refreshToken);
await StorageManager.delete(StorageKeys.tokenExpiresAt);
@@ -1,6 +1,4 @@
import 'dart:convert';
import 'dart:developer';
import 'package:dio/dio.dart';
import '../http_error.dart';
@@ -72,27 +70,6 @@ class ResponseInterceptor extends Interceptor {
return handler.next(err);
}
// 网络错误
if (err.type == DioExceptionType.connectionError ||
err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.sendTimeout ||
err.type == DioExceptionType.receiveTimeout) {
if (!suppressErrorToast(err.requestOptions)) {
log(
'[HTTP] ${requestToastMessage(err.requestOptions, '请求超时或连接失败,请检查网络')}',
);
}
return handler.next(err);
}
// 其他错误
if (!suppressErrorToast(err.requestOptions)) {
final msg = err.response?.data is Map
? extractHttpMessage(err.response?.data) ?? '请求失败'
: '请求失败';
log('[HTTP] ${requestToastMessage(err.requestOptions, msg)}');
}
return handler.next(err);
}
+20 -2
View File
@@ -22,8 +22,26 @@ class AppLogEntry {
});
String get text =>
'${timestamp.toIso8601String()} [${level.name.toUpperCase()}] '
'[$scope] $message';
'${timestamp.toIso8601String()} [${_levelLabel(level)}] '
'[${_scopeLabel(scope)}] $message';
static String _levelLabel(AppLogLevel level) => switch (level) {
AppLogLevel.debug => '调试',
AppLogLevel.info => '信息',
AppLogLevel.warning => '警告',
AppLogLevel.error => '错误',
};
static String _scopeLabel(String scope) => switch (scope) {
'HTTP' => '网络',
'Auth' => '认证',
'Media' => '媒体库',
'CloudIndex' => '全盘索引',
'App' => '应用',
'Flutter' => '界面',
'Dart' => '运行时',
_ => scope,
};
}
/// One log sink for debug console, release file diagnostics and the in-app log
+2 -2
View File
@@ -35,8 +35,8 @@ void main() async {
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
await windowManager.ensureInitialized();
const options = WindowOptions(
size: Size(1280, 820),
minimumSize: Size(980, 640),
size: Size(1600, 900),
minimumSize: Size(1200, 800),
center: true,
backgroundColor: Colors.transparent,
titleBarStyle: TitleBarStyle.hidden,
+96 -31
View File
@@ -1,5 +1,8 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/legacy.dart';
import '../core/logging/app_logger.dart';
import '../core/http/http_error.dart';
import '../core/storage/storage_manager.dart';
import '../api/guangya_api.dart';
@@ -75,12 +78,19 @@ class AuthState {
String get memberLevel =>
_findStringDeep(userInfo, ['vipName', 'memberName', 'memberLevelName']) ??
'普通会员';
_memberLevelFromAssets;
String get _memberLevelFromAssets {
final svipStatus = _findInt64Deep(userInfo, ['svipStatus']) ?? 0;
if (svipStatus > 0) return '超级会员';
final vipStatus = _findInt64Deep(userInfo, ['vipStatus']) ?? 0;
return vipStatus > 0 ? '会员' : '普通会员';
}
int? get capacity =>
_findInt64Deep(userInfo, ['capacity', 'totalCapacity']);
_findInt64Deep(userInfo, ['totalSpaceSize', 'capacity', 'totalCapacity']);
int? get usedCapacity =>
_findInt64Deep(userInfo, ['usedCapacity', 'usedSpace']);
_findInt64Deep(userInfo, ['usedSpaceSize', 'usedCapacity', 'usedSpace']);
String get capacityText {
if (capacity == null) return '空间信息暂不可用';
@@ -90,7 +100,9 @@ class AuthState {
// ── Static helpers ─────────────────────────────────────────────
static String? _findStringDeep(
Map<String, dynamic>? json, List<String> keys) {
Map<String, dynamic>? json,
List<String> keys,
) {
if (json == null) return null;
for (final key in keys) {
final v = json[key];
@@ -98,16 +110,17 @@ class AuthState {
}
for (final entry in json.entries) {
if (entry.value is Map<String, dynamic>) {
final found =
_findStringDeep(entry.value as Map<String, dynamic>, keys);
final found = _findStringDeep(
entry.value as Map<String, dynamic>,
keys,
);
if (found != null) return found;
}
}
return null;
}
static int? _findInt64Deep(
Map<String, dynamic>? json, List<String> keys) {
static int? _findInt64Deep(Map<String, dynamic>? json, List<String> keys) {
if (json == null) return null;
for (final key in keys) {
final v = json[key];
@@ -122,7 +135,13 @@ class AuthState {
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
if (bytes < 1024 * 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
if (bytes < 1024 * 1024 * 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024 * 1024 * 1024)).toStringAsFixed(1)} TB';
}
return '${(bytes / (1024 * 1024 * 1024 * 1024 * 1024)).toStringAsFixed(1)} PB';
}
}
@@ -149,11 +168,14 @@ class AuthNotifier extends StateNotifier<AuthState> {
if (access.isNotEmpty || refresh != null) {
try {
AppLogger.info('Auth', '正在恢复登录状态');
await _api.refreshAccessToken();
await _saveTokens();
state = state.copyWith(isSignedIn: true);
await loadAccount();
AppLogger.info('Auth', '登录状态恢复完成');
} catch (_) {
AppLogger.warning('Auth', '登录状态恢复失败,已清除本地令牌');
state = state.copyWith(isSignedIn: false);
_api.clearTokens();
await _clearTokens();
@@ -163,10 +185,33 @@ class AuthNotifier extends StateNotifier<AuthState> {
}
Future<void> loadAccount() async {
AppLogger.info('Auth', '正在获取账户资料与云盘空间信息');
final userInfoFuture = _api.userInfo();
final assetsFuture = _api.cloudAssets();
try {
final userInfo = await _api.userInfo();
state = state.copyWith(userInfo: userInfo);
final userInfo = await userInfoFuture;
Map<String, dynamic> assets = const {};
try {
assets = await assetsFuture;
} catch (_) {
AppLogger.warning('Auth', '云盘空间信息暂时不可用');
}
state = state.copyWith(userInfo: {...userInfo, 'assets': assets});
AppLogger.info('Auth', '账户资料与云盘空间信息获取完成');
} on DioException catch (error) {
await assetsFuture.catchError((_) => <String, dynamic>{});
final status = error.response?.statusCode;
if (status == 404 || status == 501) {
AppLogger.warning('Auth', '账户信息接口暂不可用 ($status),已保留登录态');
return;
}
final message = error.response?.data is Map
? extractHttpMessage(error.response?.data) ?? error.type.name
: error.type.name;
AppLogger.error('Auth', '加载账户信息失败', error: message);
state = state.copyWith(errorMessage: message);
} catch (e) {
AppLogger.error('Auth', '加载账户信息失败', error: e);
state = state.copyWith(errorMessage: e.toString());
}
}
@@ -183,8 +228,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
Future<void> sendVerificationCode() async {
state = state.copyWith(clearError: true);
final cleanPhone =
state.phoneNumber.replaceAll(RegExp(r'[^\d]'), '');
final cleanPhone = state.phoneNumber.replaceAll(RegExp(r'[^\d]'), '');
if (cleanPhone.length < 8) {
state = state.copyWith(errorMessage: '请输入有效的手机号');
return;
@@ -192,8 +236,10 @@ class AuthNotifier extends StateNotifier<AuthState> {
try {
final initResult = await _api.loginSMSInit(cleanPhone);
final captcha =
AuthState._findStringDeep(initResult, ['captcha_token', 'captchaToken']);
final captcha = AuthState._findStringDeep(initResult, [
'captcha_token',
'captchaToken',
]);
if (captcha == null) {
if (AuthState._findStringDeep(initResult, ['url', 'verify_url']) !=
null) {
@@ -227,11 +273,14 @@ class AuthNotifier extends StateNotifier<AuthState> {
state = state.copyWith(clearError: true);
try {
final verifyResult = await _api.loginSMSVerify(
state.verificationID, state.verificationCode);
final vToken = AuthState._findStringDeep(
verifyResult, ['verification_token', 'verificationToken']);
final code =
AuthState._findStringDeep(verifyResult, ['code']);
state.verificationID,
state.verificationCode,
);
final vToken = AuthState._findStringDeep(verifyResult, [
'verification_token',
'verificationToken',
]);
final code = AuthState._findStringDeep(verifyResult, ['code']);
if (vToken == null || code == null) throw Exception('验证码验证失败');
await _api.loginSMSSignIn(
@@ -254,11 +303,24 @@ class AuthNotifier extends StateNotifier<AuthState> {
state = state.copyWith(clearError: true);
try {
final result = await _api.loginQRInit();
final token = AuthState._findStringDeep(
result, ['device_code', 'deviceCode']) ??
final token =
AuthState._findStringDeep(result, ['device_code', 'deviceCode']) ??
'';
final payload = AuthState._findStringDeep(
result, ['verification_uri_complete', 'verificationUriComplete', 'qr_url', 'qrUrl', 'url', 'verification_uri', 'verificationUri', 'qrcode', 'qrCode', 'code', 'qr_payload', 'qrPayload']) ??
final payload =
AuthState._findStringDeep(result, [
'verification_uri_complete',
'verificationUriComplete',
'qr_url',
'qrUrl',
'url',
'verification_uri',
'verificationUri',
'qrcode',
'qrCode',
'code',
'qr_payload',
'qrPayload',
]) ??
token;
state = state.copyWith(
qrPayload: payload,
@@ -273,24 +335,24 @@ class AuthNotifier extends StateNotifier<AuthState> {
void _startQRPolling() {
_qrPollingTimer?.cancel();
_qrPollingTimer =
Timer.periodic(const Duration(seconds: 5), (timer) async {
_qrPollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async {
if (state.qrToken.isEmpty) {
timer.cancel();
return;
}
try {
final result = await _api.loginQRPoll(state.qrToken);
final accessToken = AuthState._findStringDeep(
result, ['access_token', 'accessToken']);
final accessToken = AuthState._findStringDeep(result, [
'access_token',
'accessToken',
]);
if (accessToken != null) {
timer.cancel();
state = state.copyWith(isSignedIn: true);
await _saveTokens();
await loadAccount();
} else {
final error =
AuthState._findStringDeep(result, ['error']);
final error = AuthState._findStringDeep(result, ['error']);
String? newStatus;
if (error == 'slow_down') {
newStatus = '轮询过快,请稍候';
@@ -322,7 +384,10 @@ class AuthNotifier extends StateNotifier<AuthState> {
Future<void> _saveTokens() async {
await StorageManager.set(StorageKeys.accessToken, _api.accessToken);
if (_api.refreshTokenValue != null) {
await StorageManager.set(StorageKeys.refreshToken, _api.refreshTokenValue!);
await StorageManager.set(
StorageKeys.refreshToken,
_api.refreshTokenValue!,
);
}
if (_api.tokenExpiresAt != null) {
await StorageManager.set(
+30 -19
View File
@@ -546,7 +546,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
void cancelDetailSync() {
_cancelDetailSync = true;
_appendScanLog('[同步识别][DEBUG] 已请求取消同步识别');
_appendScanLog('[同步识别][调试] 已请求取消同步识别');
}
Future<void> refreshGlobalCloudIndex({bool force = false}) async {
@@ -574,6 +574,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
}
_refreshingCloudIndex = true;
try {
AppLogger.info('CloudIndex', '开始刷新全盘文件索引');
final files = await _allGlobalRemoteFiles();
final folders = <String?, List<CloudFile>>{};
for (final file in files) {
@@ -584,6 +585,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
StorageKeys.cloudIndexLastUpdatedAt,
DateTime.now().millisecondsSinceEpoch.toString(),
);
AppLogger.info('CloudIndex', '全盘文件索引刷新完成,共 ${files.length}');
_appendScanLog('[云盘索引] 已刷新 ${files.length} 个文件与目录缓存');
} catch (error) {
_appendScanLog('[云盘索引] 刷新失败:$error', isError: true);
@@ -736,7 +738,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
if (_cancelDetailSync) break;
try {
_appendScanLog(
'[同步识别][DEBUG] 开始:${original.file.name} '
'[同步识别][调试] 开始:${original.file.name} '
'(fileId=${original.id}, gcid=${original.file.gcid ?? '-'})',
);
final latestFile = await _resolveCurrentCloudFile(
@@ -747,7 +749,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
throw StateError('云盘中未找到该文件');
}
_appendScanLog(
'[同步识别][DEBUG] 云盘已同步:fileId ${original.id} -> '
'[同步识别][调试] 云盘已同步:文件 ID ${original.id} -> '
'${latestFile.id},名称=${latestFile.name}gcid=${latestFile.gcid ?? '-'}',
);
synced.add(
@@ -763,7 +765,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
} catch (error) {
failures.add('${original.file.name}: $error');
_appendScanLog(
'[同步识别][DEBUG] 同步失败:${original.file.name}$error',
'[同步识别][调试] 同步失败:${original.file.name}$error',
isError: true,
);
}
@@ -796,7 +798,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
directoryName: _parentDirectoryName(fallback.file.cloudPath),
);
_appendScanLog(
'[同步识别][DEBUG] 解析:${fallback.file.name} -> '
'[同步识别][调试] 解析:${fallback.file.name} -> '
'标题=${parsed.title},年份=${parsed.year ?? '-'}'
'类型=${fallback.mediaKind?.name ?? 'automatic'}',
);
@@ -825,19 +827,19 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
MediaTMDBMatchRequest(items: [fallback], candidates: candidates),
);
_appendScanLog(
'[同步识别][DEBUG] 存在 ${candidates.length} 个 TMDB 候选,等待用户选择',
'[同步识别][调试] 存在 ${candidates.length} 个 TMDB 候选,等待用户选择',
);
}
}
if (updated.tmdbID != null) {
recognizedCount++;
_appendScanLog(
'[同步识别][DEBUG] TMDB 命中:${updated.title} '
'[同步识别][调试] TMDB 命中:${updated.title} '
'(tmdbId=${updated.tmdbID}, ${updated.year.isEmpty ? '年份未知' : updated.year})',
);
} else {
_appendScanLog(
'[同步识别][DEBUG] TMDB 未命中:${parsed.title} '
'[同步识别][调试] TMDB 未命中:${parsed.title} '
'(年份=${parsed.year ?? '-'})',
);
}
@@ -854,17 +856,17 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
if (updated.file.name != beforeName) {
renamedCount++;
_appendScanLog(
'[同步识别][DEBUG] 已规范命名:$beforeName -> ${updated.file.name}',
'[同步识别][调试] 已规范命名:$beforeName -> ${updated.file.name}',
);
} else if (updated.tmdbID != null) {
_appendScanLog('[同步识别][DEBUG] 跳过规范命名:未解析到分辨率或名称已符合规则');
_appendScanLog('[同步识别][调试] 跳过规范命名:未解析到分辨率或名称已符合规则');
}
updates.add(updated);
replacements['${original.libraryID}:${original.id}'] = updated;
} catch (error) {
failures.add('${original.file.name}: $error');
_appendScanLog(
'[同步识别][DEBUG] 识别失败:${original.file.name}$error',
'[同步识别][调试] 识别失败:${original.file.name}$error',
isError: true,
);
}
@@ -1584,6 +1586,8 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
const pageSize = 10000;
final values = <CloudFile>[];
for (var page = 0; !_cancelScan; page++) {
final typeLabel = resType == 2 ? '目录' : '文件';
AppLogger.info('CloudIndex', '正在获取全盘$typeLabel索引,第 ${page + 1}');
final response = await _api!.fsFiles(
parentID: '*',
page: page,
@@ -1594,6 +1598,10 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
);
final batch = _extractFiles(response);
values.addAll(batch);
AppLogger.info(
'CloudIndex',
'全盘$typeLabel索引第 ${page + 1} 页完成,获取 ${batch.length} 项,累计 ${values.length}',
);
final total = _findIntDeep(response, const [
'total',
'totalCount',
@@ -1667,6 +1675,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
if (cachedFolder != null) {
files = cachedFolder;
} else {
AppLogger.info('Media', '正在读取目录「${folder.path}」第 ${page + 1}');
final response = await _api!.fsFiles(
parentID: folder.id,
page: page,
@@ -1675,6 +1684,10 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
sortType: 0,
);
files = _extractFiles(response);
AppLogger.info(
'Media',
'目录「${folder.path}」第 ${page + 1} 页读取完成,获取 ${files.length}',
);
files = await _enrichAndCacheFiles(files);
folderSnapshot.addAll(files);
}
@@ -1867,9 +1880,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
}
} catch (error) {
// A rename can replace the cloud record. Locate its new ID below.
_appendScanLog(
'[同步识别][DEBUG] 旧 File ID 查询失败:${knownFile.id}$error;开始回退定位',
);
_appendScanLog('[同步识别][调试] 旧文件 ID 查询失败:${knownFile.id}$error;开始回退定位');
}
final cached = await FileMetadataCache.file(knownFile.id);
@@ -1881,7 +1892,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
cached.gcid == gcid &&
cached.id != knownFile.id) {
_appendScanLog(
'[同步识别][DEBUG] 从 GCID 缓存恢复新文件:${knownFile.id} -> ${cached.id}',
'[同步识别][调试] 从 GCID 缓存恢复新文件:${knownFile.id} -> ${cached.id}',
);
return _cacheResolvedCloudFile(
knownFile,
@@ -1953,7 +1964,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
if (located != null) return located;
}
_appendScanLog(
'[同步识别][DEBUG] 未定位到当前文件目录中的 GCID;停止自动查找,建议重新扫描媒体库',
'[同步识别][调试] 未定位到当前文件目录中的 GCID;停止自动查找,建议重新扫描媒体库',
isError: true,
);
return null;
@@ -1965,7 +1976,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
}) async {
final gcid = knownFile.gcid?.trim();
if (gcid == null || gcid.isEmpty) return null;
_appendScanLog('[同步识别][DEBUG] 从当前文件目录扫描:parentId=$parentIDgcid=$gcid');
_appendScanLog('[同步识别][调试] 从当前文件目录扫描:父目录 ID=$parentIDGCID=$gcid');
final parentPath = _parentPath(knownFile.cloudPath);
List<CloudFile> children;
try {
@@ -1998,7 +2009,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
if (resolved.gcid == gcid) {
final exact = _withPath(resolved, '$parentPath/${resolved.name}');
_appendScanLog(
'[同步识别][DEBUG] 当前目录定位成功:${knownFile.id} -> '
'[同步识别][调试] 当前目录定位成功:${knownFile.id} -> '
'${exact.id},路径=${exact.cloudPath}',
);
return _cacheResolvedCloudFile(
@@ -2009,7 +2020,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
}
}
_appendScanLog(
'[同步识别][DEBUG] 当前文件目录未命中:已检查 ${children.length} 个条目,详情查询不超过 12 次',
'[同步识别][调试] 当前文件目录未命中:已检查 ${children.length} 个条目,详情查询不超过 12 次',
);
return null;
}