mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-05 16:15:43 +00:00
feat: support custom GitHub mirror config (#1191)
This commit is contained in:
7 files changed
+449
-53
No files matched your search
@@ -86,6 +86,7 @@ class ExtraConfig {
|
||||
List<DownloadCategory> downloadCategories;
|
||||
|
||||
ExtraConfigBt bt = ExtraConfigBt();
|
||||
ExtraConfigGithubMirror githubMirror = ExtraConfigGithubMirror();
|
||||
|
||||
ExtraConfig({
|
||||
this.themeMode = '',
|
||||
@@ -109,7 +110,8 @@ class DownloadCategory {
|
||||
String path;
|
||||
bool isBuiltIn;
|
||||
String? nameKey; // i18n key for built-in categories (e.g., 'categoryMusic')
|
||||
bool isDeleted; // Mark built-in categories as deleted instead of removing them
|
||||
bool
|
||||
isDeleted; // Mark built-in categories as deleted instead of removing them
|
||||
|
||||
DownloadCategory({
|
||||
required this.name,
|
||||
@@ -181,3 +183,46 @@ class ExtraConfigBt {
|
||||
|
||||
Map<String, dynamic> toJson() => _$ExtraConfigBtToJson(this);
|
||||
}
|
||||
|
||||
enum GithubMirrorType {
|
||||
jsdelivr,
|
||||
ghProxy,
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class GithubMirror {
|
||||
GithubMirrorType type;
|
||||
String url;
|
||||
bool isBuiltIn;
|
||||
bool isDeleted;
|
||||
|
||||
GithubMirror({
|
||||
required this.type,
|
||||
required this.url,
|
||||
this.isBuiltIn = false,
|
||||
this.isDeleted = false,
|
||||
});
|
||||
|
||||
factory GithubMirror.fromJson(Map<String, dynamic> json) =>
|
||||
_$GithubMirrorFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$GithubMirrorToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class ExtraConfigGithubMirror {
|
||||
bool enabled;
|
||||
List<GithubMirror> mirrors;
|
||||
|
||||
ExtraConfigGithubMirror({
|
||||
this.enabled = true,
|
||||
this.mirrors = const [],
|
||||
});
|
||||
|
||||
factory ExtraConfigGithubMirror.fromJson(Map<String, dynamic>? json) =>
|
||||
json == null
|
||||
? ExtraConfigGithubMirror()
|
||||
: _$ExtraConfigGithubMirrorFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ExtraConfigGithubMirrorToJson(this);
|
||||
}
|
||||
@@ -15,7 +15,8 @@ DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
|
||||
json['protocolConfig'] as Map<String, dynamic>?)
|
||||
..extra = ExtraConfig.fromJson(json['extra'] as Map<String, dynamic>?)
|
||||
..proxy = ProxyConfig.fromJson(json['proxy'] as Map<String, dynamic>)
|
||||
..webhook = WebhookConfig.fromJson(json['webhook'] as Map<String, dynamic>?);
|
||||
..webhook =
|
||||
WebhookConfig.fromJson(json['webhook'] as Map<String, dynamic>?);
|
||||
|
||||
Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
|
||||
<String, dynamic>{
|
||||
@@ -81,7 +82,10 @@ ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
|
||||
?.map((e) => DownloadCategory.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
|
||||
)
|
||||
..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>)
|
||||
..githubMirror = ExtraConfigGithubMirror.fromJson(
|
||||
json['githubMirror'] as Map<String, dynamic>?);
|
||||
|
||||
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
||||
<String, dynamic>{
|
||||
@@ -94,6 +98,7 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
||||
'downloadCategories':
|
||||
instance.downloadCategories.map((e) => e.toJson()).toList(),
|
||||
'bt': instance.bt.toJson(),
|
||||
'githubMirror': instance.githubMirror.toJson(),
|
||||
};
|
||||
|
||||
DownloadCategory _$DownloadCategoryFromJson(Map<String, dynamic> json) =>
|
||||
@@ -126,10 +131,9 @@ Map<String, dynamic> _$DownloadCategoryToJson(DownloadCategory instance) {
|
||||
WebhookConfig _$WebhookConfigFromJson(Map<String, dynamic> json) =>
|
||||
WebhookConfig(
|
||||
enable: json['enable'] as bool? ?? false,
|
||||
urls: (json['urls'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const [],
|
||||
urls:
|
||||
(json['urls'] as List<dynamic>?)?.map((e) => e as String).toList() ??
|
||||
const [],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$WebhookConfigToJson(WebhookConfig instance) =>
|
||||
@@ -191,3 +195,40 @@ Map<String, dynamic> _$ExtraConfigBtToJson(ExtraConfigBt instance) {
|
||||
val['customTrackers'] = instance.customTrackers;
|
||||
return val;
|
||||
}
|
||||
|
||||
GithubMirror _$GithubMirrorFromJson(Map<String, dynamic> json) => GithubMirror(
|
||||
type: $enumDecode(_$GithubMirrorTypeEnumMap, json['type']),
|
||||
url: json['url'] as String,
|
||||
isBuiltIn: json['isBuiltIn'] as bool? ?? false,
|
||||
isDeleted: json['isDeleted'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$GithubMirrorToJson(GithubMirror instance) =>
|
||||
<String, dynamic>{
|
||||
'type': _$GithubMirrorTypeEnumMap[instance.type]!,
|
||||
'url': instance.url,
|
||||
'isBuiltIn': instance.isBuiltIn,
|
||||
'isDeleted': instance.isDeleted,
|
||||
};
|
||||
|
||||
const _$GithubMirrorTypeEnumMap = {
|
||||
GithubMirrorType.jsdelivr: 'jsdelivr',
|
||||
GithubMirrorType.ghProxy: 'ghProxy',
|
||||
};
|
||||
|
||||
ExtraConfigGithubMirror _$ExtraConfigGithubMirrorFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
ExtraConfigGithubMirror(
|
||||
enabled: json['enabled'] as bool? ?? true,
|
||||
mirrors: (json['mirrors'] as List<dynamic>?)
|
||||
?.map((e) => GithubMirror.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ExtraConfigGithubMirrorToJson(
|
||||
ExtraConfigGithubMirror instance) =>
|
||||
<String, dynamic>{
|
||||
'enabled': instance.enabled,
|
||||
'mirrors': instance.mirrors.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
@@ -526,17 +526,22 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
|
||||
// Initialize default download categories if empty
|
||||
if (extra.downloadCategories.isEmpty) {
|
||||
await _initDefaultDownloadCategories();
|
||||
_initDefaultDownloadCategories();
|
||||
}
|
||||
|
||||
// Initialize default GitHub mirrors if empty
|
||||
if (extra.githubMirror.mirrors.isEmpty) {
|
||||
_initDefaultGithubMirrors();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initDefaultDownloadCategories() async {
|
||||
void _initDefaultDownloadCategories() {
|
||||
final extra = downloaderConfig.value.extra;
|
||||
final downloadDir = downloaderConfig.value.downloadDir;
|
||||
|
||||
// Add default built-in categories with i18n keys
|
||||
// No need to set initial name value, it will be retrieved via nameKey
|
||||
extra.downloadCategories.addAll([
|
||||
extra.downloadCategories = [
|
||||
DownloadCategory(
|
||||
name: '',
|
||||
path: path.join(downloadDir, 'Music'),
|
||||
@@ -561,7 +566,25 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
isBuiltIn: true,
|
||||
nameKey: 'categoryProgram',
|
||||
),
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
void _initDefaultGithubMirrors() {
|
||||
final extra = downloaderConfig.value.extra;
|
||||
|
||||
// Add default built-in GitHub mirrors
|
||||
extra.githubMirror.mirrors = [
|
||||
GithubMirror(
|
||||
type: GithubMirrorType.jsdelivr,
|
||||
url: 'https://fastly.jsdelivr.net/gh',
|
||||
isBuiltIn: true,
|
||||
),
|
||||
GithubMirror(
|
||||
type: GithubMirrorType.ghProxy,
|
||||
url: 'https://fastgit.cc',
|
||||
isBuiltIn: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _initLaunchAtStartup() async {
|
||||
|
||||
@@ -884,6 +884,136 @@ class SettingView extends GetView<SettingController> {
|
||||
},
|
||||
);
|
||||
|
||||
// advanced config GitHub mirror items start
|
||||
final buildGithubMirror = _buildConfigItem(
|
||||
'githubMirror',
|
||||
() => downloaderCfg.value.extra.githubMirror.enabled ? 'on'.tr : 'off'.tr,
|
||||
(Key key) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'githubMirrorEnable'.tr,
|
||||
style: Theme.of(Get.context!).textTheme.bodyMedium,
|
||||
),
|
||||
const Spacer(),
|
||||
Switch(
|
||||
value: downloaderCfg.value.extra.githubMirror.enabled,
|
||||
onChanged: (value) {
|
||||
downloaderCfg.update((val) {
|
||||
val!.extra.githubMirror.enabled = value;
|
||||
});
|
||||
debounceSave();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
_padding,
|
||||
Text(
|
||||
'githubMirrorDesc'.tr,
|
||||
style: Theme.of(Get.context!).textTheme.bodySmall,
|
||||
),
|
||||
_padding,
|
||||
// List of existing GitHub mirrors
|
||||
...downloaderCfg.value.extra.githubMirror.mirrors
|
||||
.where((m) => !m.isDeleted)
|
||||
.toList()
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) {
|
||||
// Get the original index in the full list
|
||||
final mirror = entry.value;
|
||||
final index = downloaderCfg.value.extra.githubMirror.mirrors
|
||||
.indexOf(mirror);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
mirror.url,
|
||||
style:
|
||||
Theme.of(Get.context!).textTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, size: 20),
|
||||
tooltip: 'edit'.tr,
|
||||
onPressed: () {
|
||||
_showGithubMirrorDialog(
|
||||
index: index, initialMirror: mirror);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, size: 20),
|
||||
tooltip: 'delete'.tr,
|
||||
onPressed: () async {
|
||||
// Show confirmation dialog
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('tip'.tr),
|
||||
content: Text('confirmDelete'.tr),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(false),
|
||||
child: Text('cancel'.tr),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(true),
|
||||
child: Text('confirm'.tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
|
||||
if (mirror.isBuiltIn) {
|
||||
// Mark built-in mirror as deleted (logical delete)
|
||||
downloaderCfg.update((val) {
|
||||
mirror.isDeleted = true;
|
||||
});
|
||||
} else {
|
||||
// Remove custom mirrors completely (physical delete)
|
||||
final mirrors = List<GithubMirror>.from(
|
||||
downloaderCfg.value.extra.githubMirror.mirrors);
|
||||
mirrors.removeAt(index);
|
||||
downloaderCfg.update((val) {
|
||||
val!.extra.githubMirror.mirrors = mirrors;
|
||||
});
|
||||
}
|
||||
await debounceSave();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
_padding,
|
||||
// Add button
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
_showGithubMirrorDialog();
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text('add'.tr),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// advanced config API items start
|
||||
final buildApiProtocol = _buildConfigItem(
|
||||
'protocol',
|
||||
@@ -1092,7 +1222,8 @@ class SettingView extends GetView<SettingController> {
|
||||
tooltip: 'delete'.tr,
|
||||
onPressed: () async {
|
||||
// Create new list to avoid unmodifiable list error
|
||||
final urls = List<String>.from(downloaderCfg.value.webhook.urls);
|
||||
final urls =
|
||||
List<String>.from(downloaderCfg.value.webhook.urls);
|
||||
urls.removeAt(index);
|
||||
downloaderCfg.update((val) {
|
||||
val!.webhook.urls = urls;
|
||||
@@ -1247,7 +1378,10 @@ class SettingView extends GetView<SettingController> {
|
||||
Text('network'.tr),
|
||||
Card(
|
||||
child: Column(
|
||||
children: _addDivider([buildProxy()]),
|
||||
children: _addDivider([
|
||||
buildProxy(),
|
||||
buildGithubMirror(),
|
||||
]),
|
||||
)),
|
||||
const Text('API'),
|
||||
Card(
|
||||
@@ -1307,12 +1441,12 @@ class SettingView extends GetView<SettingController> {
|
||||
final url = value.trim().toLowerCase();
|
||||
if (!url.startsWith('http://') &&
|
||||
!url.startsWith('https://')) {
|
||||
return 'webhookUrlInvalid'.tr;
|
||||
return 'urlInvalid'.tr;
|
||||
}
|
||||
try {
|
||||
Uri.parse(value.trim());
|
||||
} catch (e) {
|
||||
return 'webhookUrlInvalid'.tr;
|
||||
return 'urlInvalid'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -1366,7 +1500,8 @@ class SettingView extends GetView<SettingController> {
|
||||
saveController.start();
|
||||
try {
|
||||
// Create new list to avoid unmodifiable list error
|
||||
final urls = List<String>.from(downloaderCfg.value.webhook.urls);
|
||||
final urls =
|
||||
List<String>.from(downloaderCfg.value.webhook.urls);
|
||||
if (isEdit) {
|
||||
urls[index] = url;
|
||||
} else {
|
||||
@@ -1394,6 +1529,125 @@ class SettingView extends GetView<SettingController> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showGithubMirrorDialog({int? index, GithubMirror? initialMirror}) {
|
||||
final isEdit = index != null;
|
||||
GithubMirrorType selectedType =
|
||||
initialMirror?.type ?? GithubMirrorType.jsdelivr;
|
||||
final urlController = TextEditingController(text: initialMirror?.url ?? '');
|
||||
final saveController = TextButtonLoadingController();
|
||||
final appController = Get.find<AppController>();
|
||||
final downloaderCfg = appController.downloaderConfig;
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: Text(isEdit ? 'edit'.tr : 'add'.tr),
|
||||
content: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DropdownButtonFormField<GithubMirrorType>(
|
||||
value: selectedType,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'githubMirrorType'.tr,
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
selectedType = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
items: GithubMirrorType.values
|
||||
.map((type) => DropdownMenuItem(
|
||||
value: type,
|
||||
child: Text(type.name),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: urlController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'githubMirrorUrl'.tr,
|
||||
hintText: 'githubMirrorUrlHint'.tr,
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'required'.tr;
|
||||
}
|
||||
final url = value.trim().toLowerCase();
|
||||
if (!url.startsWith('http://') &&
|
||||
!url.startsWith('https://')) {
|
||||
return 'urlInvalid'.tr;
|
||||
}
|
||||
try {
|
||||
Uri.parse(value.trim());
|
||||
} catch (e) {
|
||||
return 'urlInvalid'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text('cancel'.tr),
|
||||
),
|
||||
TextButtonLoading(
|
||||
controller: saveController,
|
||||
onPressed: () async {
|
||||
if (!formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
final url = urlController.text.trim();
|
||||
if (url.isEmpty) return;
|
||||
|
||||
saveController.start();
|
||||
try {
|
||||
// Create new list to avoid unmodifiable list error
|
||||
final mirrors = List<GithubMirror>.from(
|
||||
downloaderCfg.value.extra.githubMirror.mirrors);
|
||||
|
||||
final newMirror = GithubMirror(
|
||||
type: selectedType,
|
||||
url: url,
|
||||
);
|
||||
|
||||
if (isEdit) {
|
||||
mirrors[index] = newMirror;
|
||||
} else {
|
||||
mirrors.add(newMirror);
|
||||
}
|
||||
|
||||
downloaderCfg.update((val) {
|
||||
val!.extra.githubMirror.mirrors = mirrors;
|
||||
});
|
||||
await appController.saveConfig();
|
||||
if (dialogContext.mounted) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
} finally {
|
||||
saveController.stop();
|
||||
}
|
||||
},
|
||||
child: Text('confirm'.tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _tapInputWidget(GlobalKey key) {
|
||||
if (key.currentContext == null) {
|
||||
return;
|
||||
|
||||
@@ -73,7 +73,7 @@ const enUS = {
|
||||
'webhookTest': 'Test',
|
||||
'webhookTestSuccess': 'Webhook test successful',
|
||||
'webhookTestFail': 'Webhook test failed',
|
||||
'webhookUrlInvalid': 'Please enter a valid HTTP or HTTPS URL',
|
||||
'urlInvalid': 'Please enter a valid HTTP or HTTPS URL',
|
||||
'required': 'This field is required',
|
||||
'show': 'Show',
|
||||
'continue': 'Continue',
|
||||
@@ -153,5 +153,11 @@ const enUS = {
|
||||
'categoryPath': 'Category Path',
|
||||
'builtInCategory': 'Built-in category cannot be deleted',
|
||||
'selectCategory': 'Select Category',
|
||||
'githubMirror': 'GitHub Mirror',
|
||||
'githubMirrorEnable': 'Enable GitHub Mirror',
|
||||
'githubMirrorDesc': 'Use mirrors to accelerate GitHub content downloads',
|
||||
'githubMirrorType': 'Mirror Type',
|
||||
'githubMirrorUrl': 'Mirror URL',
|
||||
'githubMirrorUrlHint': 'Enter mirror URL',
|
||||
},
|
||||
};
|
||||
@@ -72,7 +72,7 @@ const zhCN = {
|
||||
'webhookTest': '测试',
|
||||
'webhookTestSuccess': 'Webhook 测试成功',
|
||||
'webhookTestFail': 'Webhook 测试失败',
|
||||
'webhookUrlInvalid': '请输入有效的 HTTP 或 HTTPS 链接',
|
||||
'urlInvalid': '请输入有效的 HTTP 或 HTTPS 链接',
|
||||
'required': '此项为必填项',
|
||||
'show': '显示',
|
||||
'continue': '继续',
|
||||
@@ -150,5 +150,11 @@ const zhCN = {
|
||||
'categoryPath': '分类路径',
|
||||
'builtInCategory': '内置分类不能删除',
|
||||
'selectCategory': '选择分类',
|
||||
'githubMirror': 'GitHub 镜像',
|
||||
'githubMirrorEnable': '启用 GitHub 镜像',
|
||||
'githubMirrorDesc': '使用镜像站点加速 GitHub 内容下载',
|
||||
'githubMirrorType': '镜像类型',
|
||||
'githubMirrorUrl': '镜像地址',
|
||||
'githubMirrorUrlHint': '请输入镜像地址',
|
||||
}
|
||||
};
|
||||
@@ -1,39 +1,41 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
// List of mirrors for GitHub
|
||||
const _sourceMirror = [
|
||||
// github.tbedu.top: https://github.tbedu.top/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
["https://github.tbedu.top/", r"(.*)"],
|
||||
// fastgit.cc: https://fastgit.cc/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
["https://fastgit.cc/", r"(.*)"],
|
||||
// gitproxy.click: https://gitproxy.click/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
["https://gitproxy.click/", r"(.*)"],
|
||||
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
|
||||
[
|
||||
"https://fastly.jsdelivr.net/gh",
|
||||
r".*raw.githubusercontent.com(/.*)(/.*)/(?:master|main)(/.*)"
|
||||
],
|
||||
];
|
||||
|
||||
// List of mirrors for GitHub release
|
||||
const _assertMirror = [
|
||||
// github.tbedu.top: https://https://github.tbedu.top/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||
["https://github.tbedu.top/", r"(.*)"],
|
||||
// fastgit.cc: https://fastgit.cc/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||
["https://fastgit.cc/", r"(.*)"],
|
||||
// gitproxy.click: https://gitproxy.click/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||
["https://gitproxy.click/", r"(.*)"],
|
||||
];
|
||||
import '../api/model/downloader_config.dart';
|
||||
import '../app/modules/app/controllers/app_controller.dart';
|
||||
|
||||
enum MirrorType {
|
||||
githubSource,
|
||||
githubRelease,
|
||||
}
|
||||
|
||||
/// Get the configured mirrors
|
||||
List<GithubMirror> _getConfiguredMirrors() {
|
||||
try {
|
||||
final appController = Get.find<AppController>();
|
||||
final config = appController.downloaderConfig.value.extra.githubMirror;
|
||||
|
||||
if (!config.enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Return configured mirrors (filtering not deleted ones)
|
||||
return config.mirrors.where((m) => !m.isDeleted).toList();
|
||||
} catch (e) {
|
||||
// Fallback to empty list if controller not found
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto detect the best mirror for the given [rawUrl] and [type]
|
||||
Future<String> githubAutoMirror(String rawUrl, MirrorType type) async {
|
||||
final mirrorUrls = githubMirrorUrls(rawUrl, type);
|
||||
|
||||
// If no mirrors, return original URL
|
||||
if (mirrorUrls.isEmpty) {
|
||||
return rawUrl;
|
||||
}
|
||||
|
||||
// Ping all mirrors and get the fastest one
|
||||
final pingResult = await Future.wait(mirrorUrls.map((e) async {
|
||||
final client = Dio()
|
||||
@@ -64,20 +66,39 @@ Future<String> githubAutoMirror(String rawUrl, MirrorType type) async {
|
||||
}
|
||||
|
||||
List<String> githubMirrorUrls(String rawUrl, MirrorType type) {
|
||||
final mirrors = switch (type) {
|
||||
MirrorType.githubSource => _sourceMirror,
|
||||
MirrorType.githubRelease => _assertMirror,
|
||||
};
|
||||
final mirrors = _getConfiguredMirrors();
|
||||
|
||||
final ret = <String>[];
|
||||
for (final cdn in mirrors) {
|
||||
final reg = RegExp(cdn[1]);
|
||||
final match = reg.firstMatch(rawUrl.toString());
|
||||
var matchStr = "";
|
||||
for (var i = 1; i <= match!.groupCount; i++) {
|
||||
matchStr += match.group(i)!;
|
||||
for (final mirror in mirrors) {
|
||||
String? mirrorUrl;
|
||||
|
||||
if (mirror.type == GithubMirrorType.jsdelivr) {
|
||||
// jsdelivr only supports source files
|
||||
if (type != MirrorType.githubSource) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Transform: https://raw.githubusercontent.com/user/repo/master/path
|
||||
// To: https://fastly.jsdelivr.net/gh/user/repo/path
|
||||
final jsDelivrPattern = RegExp(
|
||||
r'.*raw\.githubusercontent\.com(/[^/]+)(/[^/]+)/(?:master|main)(/.*)');
|
||||
final match = jsDelivrPattern.firstMatch(rawUrl);
|
||||
if (match != null) {
|
||||
final user = match.group(1);
|
||||
final repo = match.group(2);
|
||||
final path = match.group(3);
|
||||
mirrorUrl = '${mirror.url}$user$repo$path';
|
||||
}
|
||||
} else if (mirror.type == GithubMirrorType.ghProxy) {
|
||||
// gh-proxy supports both source and release
|
||||
// Simply prepend the mirror URL to the original URL
|
||||
mirrorUrl = '${mirror.url}/$rawUrl';
|
||||
}
|
||||
|
||||
if (mirrorUrl != null) {
|
||||
ret.add(mirrorUrl);
|
||||
}
|
||||
ret.add("${cdn[0]}$matchStr");
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
Reference in new issue
Block a user