diff --git a/lib/services/api/nexusphp_adapter.dart b/lib/services/api/nexusphp_adapter.dart index 2924535..6386830 100644 --- a/lib/services/api/nexusphp_adapter.dart +++ b/lib/services/api/nexusphp_adapter.dart @@ -14,6 +14,7 @@ import 'nexusphp_helper.dart'; class NexusPHPAdapter with NexusPHPHelper implements SiteAdapter { late SiteConfig _siteConfig; late Dio _dio; + static final RegExp _whitespaceRegExp = RegExp(r'[\s\u200B-\u200D\uFEFF]'); Map? _discountMapping; Map? _tagMapping; static final Logger _logger = Logger(); @@ -39,7 +40,9 @@ class NexusPHPAdapter with NexusPHPHelper implements SiteAdapter { await _loadTagMapping(); swDiscount.stop(); if (kDebugMode) { - _logger.d('NexusPHPAdapter.init: 加载优惠映射耗时=${swDiscount.elapsedMilliseconds}ms'); + _logger.d( + 'NexusPHPAdapter.init: 加载优惠映射耗时=${swDiscount.elapsedMilliseconds}ms', + ); } _dio = Dio( @@ -75,7 +78,9 @@ class NexusPHPAdapter with NexusPHPHelper implements SiteAdapter { ); swInterceptors.stop(); if (kDebugMode) { - _logger.d('NexusPHPAdapter.init: 配置Dio与拦截器耗时=${swInterceptors.elapsedMilliseconds}ms'); + _logger.d( + 'NexusPHPAdapter.init: 配置Dio与拦截器耗时=${swInterceptors.elapsedMilliseconds}ms', + ); } swTotal.stop(); if (kDebugMode) { @@ -91,9 +96,7 @@ class NexusPHPAdapter with NexusPHPHelper implements SiteAdapter { SiteType.nexusphp, ); if (template?.discountMapping != null) { - _discountMapping = Map.from( - template!.discountMapping, - ); + _discountMapping = Map.from(template!.discountMapping); } final specialMapping = await SiteConfigService.getDiscountMapping( _siteConfig.baseUrl, @@ -283,8 +286,6 @@ class NexusPHPAdapter with NexusPHPHelper implements SiteAdapter { } } - - final name = item['name'] as String; final smallDescr = item['small_descr'] as String? ?? ''; @@ -311,8 +312,6 @@ class NexusPHPAdapter with NexusPHPHelper implements SiteAdapter { } } - - return TorrentItem( id: (item['id'] as int).toString(), name: name, @@ -369,7 +368,11 @@ class NexusPHPAdapter with NexusPHPHelper implements SiteAdapter { } @override - Future fetchComments(String id, {int pageNumber = 1, int pageSize = 20}) async { + Future fetchComments( + String id, { + int pageNumber = 1, + int pageSize = 20, + }) async { try { final response = await _dio.get( '/api/v1/comments', @@ -506,9 +509,7 @@ class NexusPHPAdapter with NexusPHPHelper implements SiteAdapter { Future> getSearchCategories() async { // 通过baseUrl匹配预设配置 final defaultCategories = - await SiteConfigService.getDefaultSearchCategories( - _siteConfig.baseUrl, - ); + await SiteConfigService.getDefaultSearchCategories(_siteConfig.baseUrl); // 如果获取到默认分类配置,则直接返回 if (defaultCategories.isNotEmpty) { @@ -536,13 +537,13 @@ class NexusPHPAdapter with NexusPHPHelper implements SiteAdapter { for (final section in sectionsData) { final sectionName = section['name'] as String; final sectionDisplayName = (section['display_name'] as String) - .replaceAll(RegExp(r'[\s\u200B-\u200D\uFEFF]'), ''); + .replaceAll(_whitespaceRegExp, ''); final categoriesData = section['categories'] as List; for (final category in categoriesData) { final categoryId = category['id']; final categoryName = (category['name'] as String).replaceAll( - RegExp(r'[\s\u200B-\u200D\uFEFF]'), + _whitespaceRegExp, '', ); categories.add( diff --git a/lib/services/api/nexusphp_web_adapter.dart b/lib/services/api/nexusphp_web_adapter.dart index de866d1..34ca1c2 100644 --- a/lib/services/api/nexusphp_web_adapter.dart +++ b/lib/services/api/nexusphp_web_adapter.dart @@ -41,6 +41,12 @@ class ParseSearchParams { }); } +final RegExp _sizeRegExp = RegExp(r'([\d.]+)\s*(\w+)'); +final RegExp _relativeUrlRegExp = RegExp( + r'(src|href)="((?!https?://|//|data:|javascript:|#)[^"]+)"', + caseSensitive: false, +); + /// 解析结果对象 class ParsedTorrentResult { final List items; @@ -1147,7 +1153,7 @@ class NexusPHPWebAdapter extends SiteAdapter // 解析文件大小为字节数 int sizeInBytes = 0; if (sizeText.isNotEmpty) { - final sizeMatch = RegExp(r'([\d.]+)\s*(\w+)').firstMatch(sizeText); + final sizeMatch = _sizeRegExp.firstMatch(sizeText); if (sizeMatch != null) { final sizeValue = double.tryParse(sizeMatch.group(1) ?? '0') ?? 0; final unit = sizeMatch.group(2)?.toUpperCase() ?? 'B'; @@ -1303,10 +1309,7 @@ class NexusPHPWebAdapter extends SiteAdapter } else { // HTML 模式:处理相对URL extractedContent = extractedContent.replaceAllMapped( - RegExp( - r'(src|href)="((?!https?://|//|data:|javascript:|#)[^"]+)"', - caseSensitive: false, - ), + _relativeUrlRegExp, (match) { final attr = match.group(1); final path = match.group(2)!; diff --git a/lib/services/downloader/qbittorrent_client.dart b/lib/services/downloader/qbittorrent_client.dart index ceaffb4..bec1da4 100644 --- a/lib/services/downloader/qbittorrent_client.dart +++ b/lib/services/downloader/qbittorrent_client.dart @@ -11,6 +11,9 @@ import 'downloader_config.dart'; import 'downloader_models.dart'; import 'torrent_file_downloader_mixin.dart'; +final RegExp _httpRegExp = RegExp(r'https?://'); +final RegExp _sidRegExp = RegExp(r'SID=([^;]+)'); + /// qBittorrent下载器客户端实现 class QbittorrentClient with TorrentFileDownloaderMixin @@ -65,7 +68,7 @@ class QbittorrentClient String _buildBase(QbittorrentConfig c) { var urlStr = c.host.trim(); // 补全协议 - if (!urlStr.startsWith(RegExp(r'https?://'))) { + if (!urlStr.startsWith(_httpRegExp)) { urlStr = 'http://$urlStr'; } @@ -223,7 +226,7 @@ class QbittorrentClient // 从响应头中提取会话ID final cookies = response.headers['set-cookie']; if (cookies != null && cookies.isNotEmpty) { - final sidMatch = RegExp(r'SID=([^;]+)').firstMatch(cookies.first); + final sidMatch = _sidRegExp.firstMatch(cookies.first); if (sidMatch != null) { _sessionId = sidMatch.group(1); return; @@ -308,7 +311,6 @@ class QbittorrentClient forceRelay = true; } - final useRelay = config.useLocalRelay || forceRelay; if (!useRelay) { body['urls'] = url; @@ -339,8 +341,6 @@ class QbittorrentClient await _request('POST', '/torrents/add', body: body); } - - /// 根据版本获取暂停任务的 API 路径 String _getPauseApiPath(String? version) { if (version == null) return '/torrents/pause'; // 默认使用 4.x 的路径 diff --git a/lib/services/downloader/rutorrent_client.dart b/lib/services/downloader/rutorrent_client.dart index d065d58..27a5b0c 100644 --- a/lib/services/downloader/rutorrent_client.dart +++ b/lib/services/downloader/rutorrent_client.dart @@ -12,6 +12,8 @@ import 'downloader_config.dart'; import 'downloader_models.dart'; import 'torrent_file_downloader_mixin.dart'; +final RegExp _httpRegExp = RegExp(r'https?://'); + /// ruTorrent下载器客户端实现 class RuTorrentClient with TorrentFileDownloaderMixin @@ -66,7 +68,7 @@ class RuTorrentClient String _buildBase(RuTorrentConfig c) { var urlStr = c.host.trim(); // 补全协议 - if (!urlStr.startsWith(RegExp(r'https?://'))) { + if (!urlStr.startsWith(_httpRegExp)) { urlStr = 'http://$urlStr'; } @@ -291,8 +293,6 @@ class RuTorrentClient return FormatUtil.parseInt(val) ?? 0; } - - @override Future testConnection() async { try { diff --git a/lib/services/downloader/transmission_client.dart b/lib/services/downloader/transmission_client.dart index bc32e35..20d6a3f 100644 --- a/lib/services/downloader/transmission_client.dart +++ b/lib/services/downloader/transmission_client.dart @@ -10,6 +10,8 @@ import 'downloader_config.dart'; import 'downloader_models.dart'; import 'torrent_file_downloader_mixin.dart'; +final RegExp _httpRegExp = RegExp(r'https?://'); + /// Transmission下载器客户端实现 class TransmissionClient with TorrentFileDownloaderMixin @@ -32,17 +34,19 @@ class TransmissionClient required this.password, Function(TransmissionConfig)? onConfigUpdated, }) : _onConfigUpdated = onConfigUpdated { - _dio = Dio(BaseOptions( - connectTimeout: const Duration(seconds: 10), - receiveTimeout: const Duration(seconds: 30), - headers: { + _dio = Dio( + BaseOptions( + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 30), + headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36', - 'Content-Type': 'application/json', - }, + 'Content-Type': 'application/json', + }, followRedirects: true, maxRedirects: 5, - )); + ), + ); } /// 获取基础URL @@ -52,7 +56,7 @@ class TransmissionClient String _buildBase(TransmissionConfig c) { var urlStr = c.host.trim(); // 补全协议 - if (!urlStr.startsWith(RegExp(r'https?://'))) { + if (!urlStr.startsWith(_httpRegExp)) { urlStr = 'http://$urlStr'; } @@ -85,18 +89,15 @@ class TransmissionClient }) async { final url = '$_baseUrl$_rpcPath'; - final requestBody = { - 'method': method, - 'arguments': ?arguments, - }; + final requestBody = {'method': method, 'arguments': ?arguments}; - final requestHeaders = { - 'Content-Type': 'application/json', - }; + final requestHeaders = {'Content-Type': 'application/json'}; // 添加认证信息 if (config.username.isNotEmpty) { - final credentials = base64Encode(utf8.encode('${config.username}:$password')); + final credentials = base64Encode( + utf8.encode('${config.username}:$password'), + ); requestHeaders['Authorization'] = 'Basic $credentials'; } @@ -129,7 +130,9 @@ class TransmissionClient // 检查是否需要会话ID if (e.response?.statusCode == 409) { // 从响应头中提取会话ID - final sessionId = e.response?.headers.value('X-Transmission-Session-Id'); + final sessionId = e.response?.headers.value( + 'X-Transmission-Session-Id', + ); if (sessionId != null) { _sessionId = sessionId; // 重试请求 @@ -142,15 +145,15 @@ class TransmissionClient } if (e.response?.statusCode != null && e.response!.statusCode! >= 400) { - throw HttpException('HTTP ${e.response!.statusCode}: ${e.response!.data}'); + throw HttpException( + 'HTTP ${e.response!.statusCode}: ${e.response!.data}', + ); } throw Exception('Request failed: ${e.message}'); } } - - @override Future testConnection() async { try { @@ -283,13 +286,16 @@ class TransmissionClient } @override - Future deleteTasks(List hashes, {bool deleteFiles = false}) async { + Future deleteTasks( + List hashes, { + bool deleteFiles = false, + }) async { final ids = await _hashesToIds(hashes); if (ids.isNotEmpty) { - await _rpcRequest('torrent-remove', arguments: { - 'ids': ids, - 'delete-local-data': deleteFiles, - }); + await _rpcRequest( + 'torrent-remove', + arguments: {'ids': ids, 'delete-local-data': deleteFiles}, + ); } } @@ -303,9 +309,12 @@ class TransmissionClient @override Future> getTags() async { // 获取所有种子的标签 - final response = await _rpcRequest('torrent-get', arguments: { - 'fields': ['labels'], - }); + final response = await _rpcRequest( + 'torrent-get', + arguments: { + 'fields': ['labels'], + }, + ); final List torrents = response['torrents'] as List? ?? []; final Set allLabels = {}; @@ -346,9 +355,12 @@ class TransmissionClient @override Future> getPaths() async { // 获取所有种子的下载路径 - final response = await _rpcRequest('torrent-get', arguments: { - 'fields': ['downloadDir'], - }); + final response = await _rpcRequest( + 'torrent-get', + arguments: { + 'fields': ['downloadDir'], + }, + ); final List torrents = response['torrents'] as List? ?? []; final Set allPaths = {}; @@ -386,9 +398,12 @@ class TransmissionClient Future> _hashesToIds(List hashes) async { if (hashes.isEmpty) return []; - final response = await _rpcRequest('torrent-get', arguments: { - 'fields': ['id', 'hashString'], - }); + final response = await _rpcRequest( + 'torrent-get', + arguments: { + 'fields': ['id', 'hashString'], + }, + ); final List torrents = response['torrents'] as List? ?? []; final List ids = []; @@ -457,7 +472,9 @@ class TransmissionClient addedOn: torrent['addedDate'] ?? 0, amountLeft: leftUntilDone, ratio: (torrent['uploadRatio'] as num? ?? 0).toDouble(), - timeActive: (torrent['activityDate'] as int? ?? 0) - (torrent['addedDate'] as int? ?? 0), + timeActive: + (torrent['activityDate'] as int? ?? 0) - + (torrent['addedDate'] as int? ?? 0), uploaded: torrent['uploadedEver'] ?? 0, ); } @@ -486,4 +503,4 @@ class TransmissionClient void dispose() { _dio.close(); } -} \ No newline at end of file +} diff --git a/lib/utils/format.dart b/lib/utils/format.dart index a3de6a6..091b209 100644 --- a/lib/utils/format.dart +++ b/lib/utils/format.dart @@ -46,6 +46,7 @@ class FormatUtil { } static final RegExp _nonDigitRegExp = RegExp(r'\D'); + static final RegExp _timezoneRegExp = RegExp(r'Z|[+-]\d{2}:?\d{2}$'); /// 解析字符串为整数 /// 先判断有没有小数点,有的话则截取整数部分,然后移除掉所有的非数字字符,最后返回int.tryParse的结果 @@ -193,7 +194,7 @@ class Formatters { normalizedDate = normalizedDate.replaceRange(10, 11, 'T'); } - if (normalizedDate.contains(RegExp(r'Z|[+-]\d{2}:?\d{2}$'))) { + if (normalizedDate.contains(FormatUtil._timezoneRegExp)) { return DateTime.parse(normalizedDate).toLocal(); } else { return DateTime.parse("$normalizedDate$actualZone").toLocal(); diff --git a/lib/widgets/web_login_widget.dart b/lib/widgets/web_login_widget.dart index a155474..808d717 100644 --- a/lib/widgets/web_login_widget.dart +++ b/lib/widgets/web_login_widget.dart @@ -4,6 +4,11 @@ import 'package:logger/logger.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:pt_mate/utils/notification_helper.dart'; +final RegExp _titleRegExp = RegExp( + r'(.*?)<\/title>', + caseSensitive: false, +); + class WebLoginWidget extends StatefulWidget { final String baseUrl; final String? loginPath; // 自定义登录页面路径,默认为 /login.php @@ -77,10 +82,7 @@ class _WebLoginWidgetState extends State<WebLoginWidget> { if (html == null || html.isEmpty) return; // 检查页面标题 - final titleMatch = RegExp( - r'<title>(.*?)<\/title>', - caseSensitive: false, - ).firstMatch(html); + final titleMatch = _titleRegExp.firstMatch(html); final title = titleMatch?.group(1) ?? ""; // 如果标题包含登录字样,说明尚未成功 @@ -311,7 +313,11 @@ class _WebLoginWidgetState extends State<WebLoginWidget> { _logger.w('CookieManager未获取到cookie'); } if (mounted) { - NotificationHelper.showInfo(context, '无法获取cookie,请检查登录状态', duration: const Duration(seconds: 3)); + NotificationHelper.showInfo( + context, + '无法获取cookie,请检查登录状态', + duration: const Duration(seconds: 3), + ); } return false; } catch (e) { @@ -319,7 +325,7 @@ class _WebLoginWidgetState extends State<WebLoginWidget> { _logger.e('提取cookie时出错: $e'); } if (mounted) { - NotificationHelper.showError(context, 'Cookie提取失败: $e'); + NotificationHelper.showError(context, 'Cookie提取失败: $e'); } return false; } diff --git a/pubspec.lock b/pubspec.lock index 507755d..2ed0eaf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -649,18 +649,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -1022,10 +1022,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" timezone: dependency: transitive description: