Files
app/lib/services/custom_line_service.dart

224 lines
6.1 KiB
Dart

import 'dart:convert';
import 'package:media_kit/media_kit.dart';
import '../core/constants/storage_keys.dart';
import '../core/utils/app_logger.dart';
import 'host_mapping_proxy_service.dart';
import 'server_service.dart';
import 'storage_service.dart';
class CustomLineService {
CustomLineService._();
static final CustomLineService instance = CustomLineService._();
HostMappingProxyEndpoint? get currentProxyEndpoint =>
HostMappingProxyService.instance.endpoint;
Future<SelectedCustomLineNode?> getSelectedNode() async {
final raw = await StorageService.instance.getString(
StorageKeys.selectedCustomLineNode,
);
if (raw == null || raw.isEmpty) return null;
try {
final decoded = jsonDecode(raw);
if (decoded is! Map<String, dynamic>) return null;
final node = SelectedCustomLineNode.fromJson(decoded);
if (!node.canRewriteDownloadUrl) return null;
return node;
} catch (_) {
return null;
}
}
Future<void> setSelectedNode(SelectedCustomLineNode node) async {
if (!node.canRewriteDownloadUrl) {
throw ArgumentError('The selected custom line node has no usable IP.');
}
await StorageService.instance.setString(
StorageKeys.selectedCustomLineNode,
jsonEncode(node.toJson()),
);
}
Future<void> clearSelectedNode() async {
await StorageService.instance.remove(StorageKeys.selectedCustomLineNode);
await HostMappingProxyService.instance.stop();
}
Future<CustomLineDownloadRoute> routeDownloadUrl(String url) async {
if (!_canUseCustomLine()) {
await HostMappingProxyService.instance.stop();
return CustomLineDownloadRoute(url: url);
}
final node = await getSelectedNode();
if (node == null) return CustomLineDownloadRoute(url: url);
final uri = Uri.tryParse(url);
if (uri == null || uri.host.isEmpty) {
return CustomLineDownloadRoute(url: url);
}
final endpoint = await activateSelectedNodeForHost(uri.host);
if (endpoint == null) {
return CustomLineDownloadRoute(url: url);
}
return CustomLineDownloadRoute(
url: url,
nodeName: node.name,
originalHost: uri.host,
proxy: endpoint,
);
}
Future<HostMappingProxyEndpoint?> activateSelectedNodeForHost(String host) {
return activateSelectedNodeForHosts([host]);
}
Future<HostMappingProxyEndpoint?> activateSelectedNodeForHosts(
Iterable<String> hosts,
) async {
if (!_canUseCustomLine()) {
await HostMappingProxyService.instance.stop();
return null;
}
final node = await getSelectedNode();
if (node == null) {
await HostMappingProxyService.instance.stop();
return null;
}
final hostMap = <String, String>{};
void addHost(String value) {
final host = value.trim();
if (host.isNotEmpty) hostMap[host] = node.ip;
}
addHost(node.host);
for (final host in hosts) {
addHost(host);
}
if (hostMap.isEmpty) {
await HostMappingProxyService.instance.stop();
return null;
}
return HostMappingProxyService.instance.configure(hostMap);
}
Future<HostMappingProxyEndpoint?> activateSelectedNodeForUrl(String url) {
final uri = Uri.tryParse(url);
return activateSelectedNodeForHost(uri?.host ?? '');
}
Future<HostMappingProxyEndpoint?> activateSelectedNodeForUrls(
Iterable<String> urls,
) {
final hosts = urls
.map((url) => Uri.tryParse(url)?.host ?? '')
.where((host) => host.trim().isNotEmpty);
return activateSelectedNodeForHosts(hosts);
}
Future<void> configureMediaPlayerProxy(Player player, String url) async {
final endpoint = await activateSelectedNodeForUrl(url);
final platform = player.platform;
if (platform is! NativePlayer) return;
try {
await platform.setProperty('http-proxy', endpoint?.proxyUri ?? '');
AppLogger.d('媒体播放器自定义线路代理已配置: ${endpoint?.proxyUri ?? 'DIRECT'}');
} catch (e) {
AppLogger.d('媒体播放器自定义线路代理配置失败: $e');
}
}
bool _canUseCustomLine() {
final user = ServerService.instance.currentServer?.user;
final groupName = user?.group?.name.trim().toLowerCase();
return user != null &&
groupName != null &&
groupName.isNotEmpty &&
groupName != 'user';
}
}
class SelectedCustomLineNode {
final String id;
final String name;
final String ip;
final String host;
final String protocol;
final int? port;
const SelectedCustomLineNode({
required this.id,
required this.name,
required this.ip,
required this.host,
this.protocol = '',
this.port,
});
bool get canRewriteDownloadUrl =>
ip.trim().isNotEmpty && host.trim().isNotEmpty;
String routeScheme(String fallback) {
final value = protocol.trim().toLowerCase();
return value == 'http' || value == 'https' ? value : fallback;
}
int? get routePort => port != null && port! > 0 ? port : null;
factory SelectedCustomLineNode.fromJson(Map<String, dynamic> json) {
return SelectedCustomLineNode(
id: json['id']?.toString() ?? '',
name: json['name']?.toString() ?? '',
ip: json['ip']?.toString() ?? '',
host: json['host']?.toString() ?? '',
protocol: json['protocol']?.toString() ?? '',
port: _asNullableInt(json['port']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'ip': ip,
'host': host,
if (protocol.trim().isNotEmpty) 'protocol': protocol,
if (routePort != null) 'port': routePort,
};
}
static int? _asNullableInt(Object? value) {
if (value is int) return value;
if (value is num) return value.round();
return int.tryParse(value?.toString() ?? '');
}
}
class CustomLineDownloadRoute {
final String url;
final Map<String, String> headers;
final String? nodeName;
final String? originalHost;
final HostMappingProxyEndpoint? proxy;
const CustomLineDownloadRoute({
required this.url,
this.headers = const {},
this.nodeName,
this.originalHost,
this.proxy,
});
bool get changed => proxy != null || headers.isNotEmpty;
}