diff --git a/lib/api/guangya_api.dart b/lib/api/guangya_api.dart index 281407d..981d925 100644 --- a/lib/api/guangya_api.dart +++ b/lib/api/guangya_api.dart @@ -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> 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> cloudAssets() async { + final response = await Http.apiRequest('/assets/v1/get_assets'); + final data = response['data']; + return data is Map + ? data + : data is Map + ? Map.from(data) + : const {}; } // ── Cloud downloads ──────────────────────────────────────────────── diff --git a/lib/core/http/http.dart b/lib/core/http/http.dart index 4032832..64d190b 100644 --- a/lib/core/http/http.dart +++ b/lib/core/http/http.dart @@ -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 = { + ..._accountHeaders(), 'did': _getDeviceID(), 'dt': '4', ...?extraHeaders, @@ -163,9 +144,7 @@ class Http { static void _checkTokenExpiry() { final expiresAt = StorageManager.get(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 _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() { diff --git a/lib/core/http/interceptors/app_log_interceptor.dart b/lib/core/http/interceptors/app_log_interceptor.dart index a08788e..2d307a0 100644 --- a/lib/core/http/interceptors/app_log_interceptor.dart +++ b/lib/core/http/interceptors/app_log_interceptor.dart @@ -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 '云盘服务请求'; + } } diff --git a/lib/core/http/interceptors/auth_interceptor.dart b/lib/core/http/interceptors/auth_interceptor.dart index e75e6a6..e2834a9 100644 --- a/lib/core/http/interceptors/auth_interceptor.dart +++ b/lib/core/http/interceptors/auth_interceptor.dart @@ -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); diff --git a/lib/core/http/interceptors/response_interceptor.dart b/lib/core/http/interceptors/response_interceptor.dart index 35628db..6f43754 100644 --- a/lib/core/http/interceptors/response_interceptor.dart +++ b/lib/core/http/interceptors/response_interceptor.dart @@ -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); } diff --git a/lib/core/logging/app_logger.dart b/lib/core/logging/app_logger.dart index b9f4f13..59e0c3d 100644 --- a/lib/core/logging/app_logger.dart +++ b/lib/core/logging/app_logger.dart @@ -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 diff --git a/lib/main.dart b/lib/main.dart index 735cc1b..8a8f7c0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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, diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart index 9756fd2..c091437 100644 --- a/lib/providers/auth_provider.dart +++ b/lib/providers/auth_provider.dart @@ -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? json, List keys) { + Map? json, + List 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) { - final found = - _findStringDeep(entry.value as Map, keys); + final found = _findStringDeep( + entry.value as Map, + keys, + ); if (found != null) return found; } } return null; } - static int? _findInt64Deep( - Map? json, List keys) { + static int? _findInt64Deep(Map? json, List 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 { 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 { } Future 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 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((_) => {}); + 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 { Future 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 { 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 { 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 { 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 { 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 { Future _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( diff --git a/lib/providers/media_library_provider.dart b/lib/providers/media_library_provider.dart index 44d4bc0..2253748 100644 --- a/lib/providers/media_library_provider.dart +++ b/lib/providers/media_library_provider.dart @@ -546,7 +546,7 @@ class MediaLibraryNotifier extends StateNotifier { void cancelDetailSync() { _cancelDetailSync = true; - _appendScanLog('[同步识别][DEBUG] 已请求取消同步识别'); + _appendScanLog('[同步识别][调试] 已请求取消同步识别'); } Future refreshGlobalCloudIndex({bool force = false}) async { @@ -574,6 +574,7 @@ class MediaLibraryNotifier extends StateNotifier { } _refreshingCloudIndex = true; try { + AppLogger.info('CloudIndex', '开始刷新全盘文件索引'); final files = await _allGlobalRemoteFiles(); final folders = >{}; for (final file in files) { @@ -584,6 +585,7 @@ class MediaLibraryNotifier extends StateNotifier { 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 { 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 { 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 { } 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 { 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 { 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 { 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 { const pageSize = 10000; final values = []; 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 { ); 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 { 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 { 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 { } } 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 { 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 { if (located != null) return located; } _appendScanLog( - '[同步识别][DEBUG] 未定位到当前文件目录中的 GCID;停止自动查找,建议重新扫描媒体库', + '[同步识别][调试] 未定位到当前文件目录中的 GCID;停止自动查找,建议重新扫描媒体库', isError: true, ); return null; @@ -1965,7 +1976,7 @@ class MediaLibraryNotifier extends StateNotifier { }) async { final gcid = knownFile.gcid?.trim(); if (gcid == null || gcid.isEmpty) return null; - _appendScanLog('[同步识别][DEBUG] 从当前文件目录扫描:parentId=$parentID,gcid=$gcid'); + _appendScanLog('[同步识别][调试] 从当前文件目录扫描:父目录 ID=$parentID,GCID=$gcid'); final parentPath = _parentPath(knownFile.cloudPath); List children; try { @@ -1998,7 +2009,7 @@ class MediaLibraryNotifier extends StateNotifier { 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 { } } _appendScanLog( - '[同步识别][DEBUG] 当前文件目录未命中:已检查 ${children.length} 个条目,详情查询不超过 12 次', + '[同步识别][调试] 当前文件目录未命中:已检查 ${children.length} 个条目,详情查询不超过 12 次', ); return null; }