Show subscription metadata in dashboard
This commit is contained in:
@@ -3,25 +3,25 @@
|
||||
"call": "✔ Running",
|
||||
"key": "✔ Running",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/initController.ts:387",
|
||||
"src/podkop/tabs/dashboard/initController.ts:398",
|
||||
"src/podkop/tabs/dashboard/initController.ts:411"
|
||||
"src/podkop/tabs/dashboard/initController.ts:391",
|
||||
"src/podkop/tabs/dashboard/initController.ts:402",
|
||||
"src/podkop/tabs/dashboard/initController.ts:415"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "✘ Not installed",
|
||||
"key": "✘ Not installed",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/initController.ts:409"
|
||||
"src/podkop/tabs/dashboard/initController.ts:413"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "✘ Stopped",
|
||||
"key": "✘ Stopped",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/initController.ts:388",
|
||||
"src/podkop/tabs/dashboard/initController.ts:399",
|
||||
"src/podkop/tabs/dashboard/initController.ts:412"
|
||||
"src/podkop/tabs/dashboard/initController.ts:392",
|
||||
"src/podkop/tabs/dashboard/initController.ts:403",
|
||||
"src/podkop/tabs/dashboard/initController.ts:416"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -64,7 +64,7 @@
|
||||
"call": "Active Connections",
|
||||
"key": "Active Connections",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/initController.ts:349"
|
||||
"src/podkop/tabs/dashboard/initController.ts:353"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -142,7 +142,7 @@
|
||||
"call": "Auto refresh",
|
||||
"key": "Auto refresh",
|
||||
"places": [
|
||||
"src/partials/modal/renderModal.ts:219"
|
||||
"src/partials/modal/renderModal.ts:218"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -296,7 +296,7 @@
|
||||
"call": "Close",
|
||||
"key": "Close",
|
||||
"places": [
|
||||
"src/partials/modal/renderModal.ts:196"
|
||||
"src/partials/modal/renderModal.ts:195"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -353,8 +353,8 @@
|
||||
"call": "Copy proxy link",
|
||||
"key": "Copy proxy link",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/partials/renderSections.ts:93",
|
||||
"src/podkop/tabs/dashboard/partials/renderSections.ts:94"
|
||||
"src/podkop/tabs/dashboard/partials/renderSections.ts:235",
|
||||
"src/podkop/tabs/dashboard/partials/renderSections.ts:236"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -375,7 +375,7 @@
|
||||
"call": "Dashboard currently unavailable",
|
||||
"key": "Dashboard currently unavailable",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/partials/renderSections.ts:24"
|
||||
"src/podkop/tabs/dashboard/partials/renderSections.ts:29"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -578,8 +578,8 @@
|
||||
"call": "Downlink",
|
||||
"key": "Downlink",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/initController.ts:283",
|
||||
"src/podkop/tabs/dashboard/initController.ts:317"
|
||||
"src/podkop/tabs/dashboard/initController.ts:287",
|
||||
"src/podkop/tabs/dashboard/initController.ts:321"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -776,6 +776,13 @@
|
||||
"../luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop/podkop.js:45"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Expires",
|
||||
"key": "Expires",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/partials/renderSections.ts:110"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Expose this section as a local HTTP+SOCKS proxy",
|
||||
"key": "Expose this section as a local HTTP+SOCKS proxy",
|
||||
@@ -813,9 +820,9 @@
|
||||
"call": "Fastest",
|
||||
"key": "Fastest",
|
||||
"places": [
|
||||
"src/podkop/methods/custom/getDashboardSections.ts:225",
|
||||
"src/podkop/methods/custom/getDashboardSections.ts:257",
|
||||
"src/podkop/methods/custom/getDashboardSections.ts:295",
|
||||
"src/podkop/methods/custom/getDashboardSections.ts:308",
|
||||
"src/podkop/methods/custom/getDashboardSections.ts:344",
|
||||
"src/podkop/methods/custom/getDashboardSections.ts:385",
|
||||
"src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:59"
|
||||
]
|
||||
},
|
||||
@@ -1348,7 +1355,7 @@
|
||||
"call": "Memory Usage",
|
||||
"key": "Memory Usage",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/initController.ts:353"
|
||||
"src/podkop/tabs/dashboard/initController.ts:357"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1637,7 +1644,7 @@
|
||||
"call": "Proxy link is unavailable",
|
||||
"key": "Proxy link is unavailable",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/initController.ts:202"
|
||||
"src/podkop/tabs/dashboard/initController.ts:205"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1654,6 +1661,13 @@
|
||||
"src/podkop/tabs/diagnostic/checks/runFakeIPCheck.ts:67"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Refill",
|
||||
"key": "Refill",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/partials/renderSections.ts:111"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Regional options cannot be used together",
|
||||
"key": "Regional options cannot be used together",
|
||||
@@ -1938,7 +1952,7 @@
|
||||
"call": "Services info",
|
||||
"key": "Services info",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/initController.ts:382"
|
||||
"src/podkop/tabs/dashboard/initController.ts:386"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2083,6 +2097,13 @@
|
||||
"../luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop/section.js:1782"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Subscription info:",
|
||||
"key": "Subscription info:",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/partials/renderSections.ts:125"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Subscription update interval",
|
||||
"key": "Subscription update interval",
|
||||
@@ -2115,7 +2136,7 @@
|
||||
"call": "System info",
|
||||
"key": "System info",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/initController.ts:346"
|
||||
"src/podkop/tabs/dashboard/initController.ts:350"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2136,7 +2157,7 @@
|
||||
"call": "Test latency",
|
||||
"key": "Test latency",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/partials/renderSections.ts:139"
|
||||
"src/podkop/tabs/dashboard/partials/renderSections.ts:283"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2206,14 +2227,15 @@
|
||||
"call": "Traffic",
|
||||
"key": "Traffic",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/initController.ts:280"
|
||||
"src/podkop/tabs/dashboard/initController.ts:284",
|
||||
"src/podkop/tabs/dashboard/partials/renderSections.ts:106"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call": "Traffic Total",
|
||||
"key": "Traffic Total",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/initController.ts:310"
|
||||
"src/podkop/tabs/dashboard/initController.ts:314"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2326,8 +2348,8 @@
|
||||
"call": "Uplink",
|
||||
"key": "Uplink",
|
||||
"places": [
|
||||
"src/podkop/tabs/dashboard/initController.ts:282",
|
||||
"src/podkop/tabs/dashboard/initController.ts:313"
|
||||
"src/podkop/tabs/dashboard/initController.ts:286",
|
||||
"src/podkop/tabs/dashboard/initController.ts:317"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP PLUS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-13 21:19+0300\n"
|
||||
"PO-Revision-Date: 2026-05-13 21:19+0300\n"
|
||||
"POT-Creation-Date: 2026-05-14 01:44+0300\n"
|
||||
"PO-Revision-Date: 2026-05-14 01:44+0300\n"
|
||||
"Last-Translator: ushan0v <142658494+ushan0v@users.noreply.github.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
@@ -16,19 +16,19 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:387
|
||||
#: src/podkop/tabs/dashboard/initController.ts:398
|
||||
#: src/podkop/tabs/dashboard/initController.ts:411
|
||||
#: src/podkop/tabs/dashboard/initController.ts:391
|
||||
#: src/podkop/tabs/dashboard/initController.ts:402
|
||||
#: src/podkop/tabs/dashboard/initController.ts:415
|
||||
msgid "✔ Running"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:409
|
||||
#: src/podkop/tabs/dashboard/initController.ts:413
|
||||
msgid "✘ Not installed"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:388
|
||||
#: src/podkop/tabs/dashboard/initController.ts:399
|
||||
#: src/podkop/tabs/dashboard/initController.ts:412
|
||||
#: src/podkop/tabs/dashboard/initController.ts:392
|
||||
#: src/podkop/tabs/dashboard/initController.ts:403
|
||||
#: src/podkop/tabs/dashboard/initController.ts:416
|
||||
msgid "✘ Stopped"
|
||||
msgstr ""
|
||||
|
||||
@@ -53,7 +53,7 @@ msgstr ""
|
||||
msgid "Action"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:349
|
||||
#: src/podkop/tabs/dashboard/initController.ts:353
|
||||
msgid "Active Connections"
|
||||
msgstr ""
|
||||
|
||||
@@ -98,7 +98,7 @@ msgstr ""
|
||||
msgid "At least one valid IP or subnet must be specified."
|
||||
msgstr ""
|
||||
|
||||
#: src/partials/modal/renderModal.ts:219
|
||||
#: src/partials/modal/renderModal.ts:218
|
||||
msgid "Auto refresh"
|
||||
msgstr ""
|
||||
|
||||
@@ -192,7 +192,7 @@ msgstr ""
|
||||
msgid "CIDR must be between 0 and 32"
|
||||
msgstr ""
|
||||
|
||||
#: src/partials/modal/renderModal.ts:196
|
||||
#: src/partials/modal/renderModal.ts:195
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
@@ -225,8 +225,8 @@ msgstr ""
|
||||
msgid "Copy"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:93
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:94
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:235
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:236
|
||||
msgid "Copy proxy link"
|
||||
msgstr ""
|
||||
|
||||
@@ -238,7 +238,7 @@ msgstr ""
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:24
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:29
|
||||
msgid "Dashboard currently unavailable"
|
||||
msgstr ""
|
||||
|
||||
@@ -357,8 +357,8 @@ msgstr ""
|
||||
msgid "Dont Touch My DHCP!"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:283
|
||||
#: src/podkop/tabs/dashboard/initController.ts:317
|
||||
#: src/podkop/tabs/dashboard/initController.ts:287
|
||||
#: src/podkop/tabs/dashboard/initController.ts:321
|
||||
msgid "Downlink"
|
||||
msgstr ""
|
||||
|
||||
@@ -475,6 +475,10 @@ msgstr ""
|
||||
msgid "Expecting: %s"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:110
|
||||
msgid "Expires"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop/section.js:2145
|
||||
msgid "Expose this section as a local HTTP+SOCKS proxy"
|
||||
msgstr ""
|
||||
@@ -496,9 +500,9 @@ msgstr ""
|
||||
msgid "Failed to execute!"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/methods/custom/getDashboardSections.ts:225
|
||||
#: src/podkop/methods/custom/getDashboardSections.ts:257
|
||||
#: src/podkop/methods/custom/getDashboardSections.ts:295
|
||||
#: src/podkop/methods/custom/getDashboardSections.ts:308
|
||||
#: src/podkop/methods/custom/getDashboardSections.ts:344
|
||||
#: src/podkop/methods/custom/getDashboardSections.ts:385
|
||||
#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:59
|
||||
msgid "Fastest"
|
||||
msgstr ""
|
||||
@@ -806,7 +810,7 @@ msgstr ""
|
||||
msgid "Maximum response time delta in milliseconds"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:353
|
||||
#: src/podkop/tabs/dashboard/initController.ts:357
|
||||
msgid "Memory Usage"
|
||||
msgstr ""
|
||||
|
||||
@@ -978,7 +982,7 @@ msgstr ""
|
||||
msgid "Proxy Configuration URL"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:202
|
||||
#: src/podkop/tabs/dashboard/initController.ts:205
|
||||
msgid "Proxy link is unavailable"
|
||||
msgstr ""
|
||||
|
||||
@@ -990,6 +994,10 @@ msgstr ""
|
||||
msgid "Proxy traffic is routed via FakeIP"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:111
|
||||
msgid "Refill"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop/section.js:2289
|
||||
msgid "Regional options cannot be used together"
|
||||
msgstr ""
|
||||
@@ -1150,7 +1158,7 @@ msgstr ""
|
||||
msgid "Service list"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:382
|
||||
#: src/podkop/tabs/dashboard/initController.ts:386
|
||||
msgid "Services info"
|
||||
msgstr ""
|
||||
|
||||
@@ -1236,6 +1244,10 @@ msgstr ""
|
||||
msgid "Subscription"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:125
|
||||
msgid "Subscription info:"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop/section.js:1857
|
||||
msgid "Subscription update interval"
|
||||
msgstr ""
|
||||
@@ -1252,7 +1264,7 @@ msgstr ""
|
||||
msgid "Successfully copied!"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:346
|
||||
#: src/podkop/tabs/dashboard/initController.ts:350
|
||||
msgid "System info"
|
||||
msgstr ""
|
||||
|
||||
@@ -1264,7 +1276,7 @@ msgstr ""
|
||||
msgid "Table exist"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:139
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:283
|
||||
msgid "Test latency"
|
||||
msgstr ""
|
||||
|
||||
@@ -1304,11 +1316,12 @@ msgstr ""
|
||||
msgid "Time in seconds for DNS record caching (default: 60)"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:280
|
||||
#: src/podkop/tabs/dashboard/initController.ts:284
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:106
|
||||
msgid "Traffic"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:310
|
||||
#: src/podkop/tabs/dashboard/initController.ts:314
|
||||
msgid "Traffic Total"
|
||||
msgstr ""
|
||||
|
||||
@@ -1376,8 +1389,8 @@ msgstr ""
|
||||
msgid "Update interval"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:282
|
||||
#: src/podkop/tabs/dashboard/initController.ts:313
|
||||
#: src/podkop/tabs/dashboard/initController.ts:286
|
||||
#: src/podkop/tabs/dashboard/initController.ts:317
|
||||
msgid "Uplink"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP PLUS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-13 00:19+0300\n"
|
||||
"PO-Revision-Date: 2026-05-13 00:19+0300\n"
|
||||
"POT-Creation-Date: 2026-05-14 04:44+0300\n"
|
||||
"PO-Revision-Date: 2026-05-14 04:44+0300\n"
|
||||
"Last-Translator: ushan0v\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -335,6 +335,9 @@ msgstr "Исключите трафик протокола NTP из туннел
|
||||
msgid "Expecting: %s"
|
||||
msgstr "Ожидается: %s"
|
||||
|
||||
msgid "Expires"
|
||||
msgstr "Истекает"
|
||||
|
||||
msgid "Expose this section as a local HTTP+SOCKS proxy"
|
||||
msgstr "Открыть эту секцию как локальный HTTP+SOCKS прокси"
|
||||
|
||||
@@ -698,6 +701,9 @@ msgstr "Прокси-трафик не маршрутизируется чере
|
||||
msgid "Proxy traffic is routed via FakeIP"
|
||||
msgstr "Прокси-трафик направляется через FakeIP"
|
||||
|
||||
msgid "Refill"
|
||||
msgstr "Сброс"
|
||||
|
||||
msgid "Regional options cannot be used together"
|
||||
msgstr "Нельзя использовать несколько региональных опций одновременно"
|
||||
|
||||
@@ -881,6 +887,9 @@ msgstr "Остановить Podkop Plus"
|
||||
msgid "Subscription"
|
||||
msgstr "Подписка"
|
||||
|
||||
msgid "Subscription info:"
|
||||
msgstr "Информация о подписке:"
|
||||
|
||||
msgid "Subscription update interval"
|
||||
msgstr "Интервал обновления подписки"
|
||||
|
||||
|
||||
@@ -17,3 +17,4 @@ export * from './renderCogIcon24';
|
||||
export * from './renderSearchIcon24';
|
||||
export * from './renderBookOpenTextIcon24';
|
||||
export * from './renderCopyIcon24';
|
||||
export * from './renderLinkIcon24';
|
||||
|
||||
@@ -179,7 +179,7 @@ export function renderModal(
|
||||
stopRefreshTimer();
|
||||
};
|
||||
|
||||
const footerChildren = [
|
||||
const footerChildren: HTMLElement[] = [
|
||||
renderButton({
|
||||
classNames: ['cbi-button-apply'],
|
||||
text: _('Download'),
|
||||
@@ -188,8 +188,7 @@ export function renderModal(
|
||||
renderButton({
|
||||
classNames: ['cbi-button-apply'],
|
||||
text: _('Copy'),
|
||||
onClick: () =>
|
||||
copyToClipboard(`\`\`\`${name}\n${currentText}\n\`\`\``),
|
||||
onClick: () => copyToClipboard(`\`\`\`${name}\n${currentText}\n\`\`\``),
|
||||
}),
|
||||
renderButton({
|
||||
classNames: ['cbi-button-remove'],
|
||||
|
||||
@@ -107,6 +107,20 @@ async function markSubscriptionCopyableOutbounds(
|
||||
);
|
||||
}
|
||||
|
||||
async function getSubscriptionMetadata(sectionName: string) {
|
||||
const response = await PodkopShellMethods.getSubscriptionMetadata(sectionName);
|
||||
|
||||
if (
|
||||
response.success &&
|
||||
response.data &&
|
||||
Object.keys(response.data).length > 1
|
||||
) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function getDashboardSections(): Promise<IGetDashboardSectionsResponse> {
|
||||
const configSections = await getConfigSections();
|
||||
const clashProxies = await PodkopShellMethods.getClashApiProxies();
|
||||
@@ -309,6 +323,9 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
|
||||
const fallbackUrltest = proxies.find(
|
||||
(proxy) => proxy.code === `${section['.name']}-urltest-out`,
|
||||
);
|
||||
const subscriptionMetadata = await getSubscriptionMetadata(
|
||||
section['.name'],
|
||||
);
|
||||
|
||||
const selectorOutbounds = (selector?.value?.all ?? []).flatMap(
|
||||
(code) => {
|
||||
@@ -361,6 +378,7 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
|
||||
code: selector?.code || section['.name'],
|
||||
sectionName: section['.name'],
|
||||
displayName,
|
||||
subscriptionMetadata,
|
||||
outbounds: [
|
||||
{
|
||||
code: fallbackUrltest?.code || '',
|
||||
@@ -383,6 +401,7 @@ export async function getDashboardSections(): Promise<IGetDashboardSectionsRespo
|
||||
code: selector?.code || section['.name'],
|
||||
sectionName: section['.name'],
|
||||
displayName,
|
||||
subscriptionMetadata,
|
||||
outbounds: await markSubscriptionCopyableOutbounds(
|
||||
section['.name'],
|
||||
outbounds,
|
||||
|
||||
@@ -25,6 +25,11 @@ export const PodkopShellMethods = {
|
||||
Podkop.AvailableMethods.GET_OUTBOUND_LINK,
|
||||
[section, tag],
|
||||
),
|
||||
getSubscriptionMetadata: async (section: string) =>
|
||||
callBaseMethod<Podkop.SubscriptionMetadata>(
|
||||
Podkop.AvailableMethods.GET_SUBSCRIPTION_METADATA,
|
||||
[section],
|
||||
),
|
||||
checkSingBox: async () =>
|
||||
callBaseMethod<Podkop.SingBoxCheckResult>(
|
||||
Podkop.AvailableMethods.CHECK_SING_BOX,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { renderCopyIcon24 } from '../../../../icons';
|
||||
import {
|
||||
renderCopyIcon24,
|
||||
renderLinkIcon24,
|
||||
} from '../../../../icons';
|
||||
import { isCopyableProxyLink } from '../../../../helpers';
|
||||
import { prettyBytes } from '../../../../helpers/prettyBytes';
|
||||
import { Podkop } from '../../../types';
|
||||
|
||||
interface IRenderSectionsProps {
|
||||
@@ -34,6 +38,144 @@ function renderLoadingState() {
|
||||
});
|
||||
}
|
||||
|
||||
function isValidHttpUrl(url?: string) {
|
||||
return Boolean(url && /^https?:\/\/\S+$/i.test(url));
|
||||
}
|
||||
|
||||
function formatBytes(value?: number) {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return prettyBytes(value);
|
||||
}
|
||||
|
||||
function formatDate(seconds?: number) {
|
||||
if (typeof seconds !== 'number' || !Number.isFinite(seconds) || seconds <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const date = new Date(seconds * 1000);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return date.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function renderMetadataAction(label: string, url?: string) {
|
||||
if (!isValidHttpUrl(url)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return E(
|
||||
'a',
|
||||
{
|
||||
class: 'btn pdk_dashboard-page__subscription-meta__action',
|
||||
href: url,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
title: label,
|
||||
'aria-label': label,
|
||||
},
|
||||
renderLinkIcon24(),
|
||||
);
|
||||
}
|
||||
|
||||
function renderSubscriptionMetadata(metadata?: Podkop.SubscriptionMetadata) {
|
||||
if (!metadata || Object.keys(metadata).length <= 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const title = metadata.title || metadata.fileName;
|
||||
const traffic = metadata.traffic;
|
||||
const used = formatBytes(traffic?.used) || '0 B';
|
||||
const total = traffic?.isUnlimited
|
||||
? '∞'
|
||||
: formatBytes(traffic?.total) || '0 B';
|
||||
const expire = formatDate(metadata.expire);
|
||||
const refillDate = formatDate(metadata.refillDate);
|
||||
|
||||
const rows = [
|
||||
traffic
|
||||
? {
|
||||
label: _('Traffic'),
|
||||
value: `${used} / ${total}`,
|
||||
}
|
||||
: undefined,
|
||||
expire ? { label: _('Expires'), value: expire } : undefined,
|
||||
refillDate ? { label: _('Refill'), value: refillDate } : undefined,
|
||||
].filter(Boolean) as { label: string; value: string }[];
|
||||
|
||||
const actions = [
|
||||
renderMetadataAction('Profile', metadata.webPageUrl),
|
||||
renderMetadataAction('Support', metadata.supportUrl),
|
||||
renderMetadataAction('More details', metadata.announceUrl),
|
||||
].filter(Boolean) as HTMLElement[];
|
||||
|
||||
return E('div', { class: 'pdk_dashboard-page__subscription-meta' }, [
|
||||
E('div', { class: 'pdk_dashboard-page__subscription-meta__main' }, [
|
||||
E(
|
||||
'div',
|
||||
{ class: 'pdk_dashboard-page__subscription-meta__heading' },
|
||||
_('Subscription info:'),
|
||||
),
|
||||
title
|
||||
? E(
|
||||
'div',
|
||||
{ class: 'pdk_dashboard-page__subscription-meta__title' },
|
||||
title,
|
||||
)
|
||||
: '',
|
||||
rows.length
|
||||
? E(
|
||||
'div',
|
||||
{ class: 'pdk_dashboard-page__subscription-meta__facts' },
|
||||
rows.map((row) =>
|
||||
E(
|
||||
'div',
|
||||
{ class: 'pdk_dashboard-page__subscription-meta__fact' },
|
||||
[
|
||||
E(
|
||||
'span',
|
||||
{ class: 'pdk_dashboard-page__subscription-meta__fact-key' },
|
||||
row.label,
|
||||
),
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
class:
|
||||
'pdk_dashboard-page__subscription-meta__fact-value',
|
||||
},
|
||||
row.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: '',
|
||||
actions.length
|
||||
? E(
|
||||
'div',
|
||||
{ class: 'pdk_dashboard-page__subscription-meta__actions' },
|
||||
actions,
|
||||
)
|
||||
: '',
|
||||
]),
|
||||
metadata.announce
|
||||
? E(
|
||||
'blockquote',
|
||||
{ class: 'pdk_dashboard-page__subscription-meta__announce' },
|
||||
metadata.announce,
|
||||
)
|
||||
: '',
|
||||
]);
|
||||
}
|
||||
|
||||
export function renderDefaultState({
|
||||
section,
|
||||
onChooseOutbound,
|
||||
@@ -118,6 +260,8 @@ export function renderDefaultState({
|
||||
);
|
||||
}
|
||||
|
||||
const metadataNode = renderSubscriptionMetadata(section.subscriptionMetadata);
|
||||
|
||||
return E('div', { class: 'pdk_dashboard-page__outbound-section' }, [
|
||||
// Title with test latency
|
||||
E('div', { class: 'pdk_dashboard-page__outbound-section__title-section' }, [
|
||||
@@ -142,7 +286,10 @@ export function renderDefaultState({
|
||||
E(
|
||||
'div',
|
||||
{ class: 'pdk_dashboard-page__outbound-grid' },
|
||||
section.outbounds.map((outbound) => renderOutbound(outbound)),
|
||||
[
|
||||
...(metadataNode ? [metadataNode] : []),
|
||||
...section.outbounds.map((outbound) => renderOutbound(outbound)),
|
||||
],
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -75,6 +75,134 @@ export const styles = `
|
||||
grid-gap: 10px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta {
|
||||
--subscription-meta-action-size: 28px;
|
||||
--subscription-meta-action-gap: 6px;
|
||||
grid-column: 1 / -1;
|
||||
border: 2px var(--background-color-low, lightgray) solid;
|
||||
border-radius: 4px;
|
||||
padding: 8px 10px;
|
||||
background: var(--background-color-high, transparent);
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__heading {
|
||||
flex: 0 0 auto;
|
||||
color: var(--text-color-high);
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__title {
|
||||
flex: 0 1 auto;
|
||||
width: max-content;
|
||||
max-width: min(28ch, 30%);
|
||||
min-width: min-content;
|
||||
color: var(--text-color-high);
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__facts {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 5px 12px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__fact {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__fact-key {
|
||||
color: var(--text-color-medium);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__fact-value {
|
||||
color: var(--text-color-high);
|
||||
font-weight: 600;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__actions {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--subscription-meta-action-gap);
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__action {
|
||||
width: var(--subscription-meta-action-size);
|
||||
height: var(--subscription-meta-action-size);
|
||||
min-width: var(--subscription-meta-action-size);
|
||||
min-height: var(--subscription-meta-action-size);
|
||||
padding: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__action svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__announce {
|
||||
margin: 6px 0 0;
|
||||
border-left: 3px solid var(--primary-color-medium, dodgerblue);
|
||||
padding: 4px 8px;
|
||||
background: var(--background-color-low, rgba(0, 0, 0, 0.04));
|
||||
color: var(--text-color-medium);
|
||||
font-style: italic;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.pdk_dashboard-page__subscription-meta__main {
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__heading,
|
||||
.pdk_dashboard-page__subscription-meta__title {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__actions {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__facts {
|
||||
order: 3;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__title {
|
||||
max-width: calc(100% - 42px);
|
||||
}
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__outbound-grid__item {
|
||||
border: 2px var(--background-color-low, lightgray) solid;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -56,6 +56,7 @@ export namespace Podkop {
|
||||
CHECK_ZAPRET_RUNTIME = 'check_zapret_runtime',
|
||||
GET_STATUS = 'get_status',
|
||||
GET_OUTBOUND_LINK = 'get_outbound_link',
|
||||
GET_SUBSCRIPTION_METADATA = 'get_subscription_metadata',
|
||||
CHECK_SING_BOX = 'check_sing_box',
|
||||
GET_SING_BOX_STATUS = 'get_sing_box_status',
|
||||
GET_ZAPRET_STATUS = 'get_zapret_status',
|
||||
@@ -95,9 +96,32 @@ export namespace Podkop {
|
||||
code: string;
|
||||
sectionName: string;
|
||||
displayName: string;
|
||||
subscriptionMetadata?: SubscriptionMetadata;
|
||||
outbounds: Outbound[];
|
||||
}
|
||||
|
||||
export interface SubscriptionTraffic {
|
||||
upload?: number;
|
||||
download?: number;
|
||||
used?: number;
|
||||
total?: number;
|
||||
remaining?: number;
|
||||
isUnlimited?: boolean;
|
||||
}
|
||||
|
||||
export interface SubscriptionMetadata {
|
||||
version?: number;
|
||||
title?: string;
|
||||
traffic?: SubscriptionTraffic;
|
||||
expire?: number;
|
||||
refillDate?: number;
|
||||
webPageUrl?: string;
|
||||
supportUrl?: string;
|
||||
announce?: string;
|
||||
announceUrl?: string;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
export type RuleAction = 'proxy' | 'vpn' | 'direct' | 'block' | 'zapret';
|
||||
export type LegacyConnectionType = 'proxy' | 'vpn' | 'block' | 'exclusion';
|
||||
export type ProxyConfigType =
|
||||
|
||||
@@ -732,6 +732,7 @@ var Podkop;
|
||||
AvailableMethods2["CHECK_ZAPRET_RUNTIME"] = "check_zapret_runtime";
|
||||
AvailableMethods2["GET_STATUS"] = "get_status";
|
||||
AvailableMethods2["GET_OUTBOUND_LINK"] = "get_outbound_link";
|
||||
AvailableMethods2["GET_SUBSCRIPTION_METADATA"] = "get_subscription_metadata";
|
||||
AvailableMethods2["CHECK_SING_BOX"] = "check_sing_box";
|
||||
AvailableMethods2["GET_SING_BOX_STATUS"] = "get_sing_box_status";
|
||||
AvailableMethods2["GET_ZAPRET_STATUS"] = "get_zapret_status";
|
||||
@@ -776,6 +777,10 @@ var PodkopShellMethods = {
|
||||
Podkop.AvailableMethods.GET_OUTBOUND_LINK,
|
||||
[section, tag]
|
||||
),
|
||||
getSubscriptionMetadata: async (section) => callBaseMethod(
|
||||
Podkop.AvailableMethods.GET_SUBSCRIPTION_METADATA,
|
||||
[section]
|
||||
),
|
||||
checkSingBox: async () => callBaseMethod(
|
||||
Podkop.AvailableMethods.CHECK_SING_BOX
|
||||
),
|
||||
@@ -903,6 +908,13 @@ async function markSubscriptionCopyableOutbounds(sectionName, outbounds) {
|
||||
}))
|
||||
);
|
||||
}
|
||||
async function getSubscriptionMetadata(sectionName) {
|
||||
const response = await PodkopShellMethods.getSubscriptionMetadata(sectionName);
|
||||
if (response.success && response.data && Object.keys(response.data).length > 1) {
|
||||
return response.data;
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
async function getDashboardSections() {
|
||||
const configSections = await getConfigSections();
|
||||
const clashProxies = await PodkopShellMethods.getClashApiProxies();
|
||||
@@ -1073,6 +1085,9 @@ async function getDashboardSections() {
|
||||
const fallbackUrltest = proxies.find(
|
||||
(proxy) => proxy.code === `${section[".name"]}-urltest-out`
|
||||
);
|
||||
const subscriptionMetadata = await getSubscriptionMetadata(
|
||||
section[".name"]
|
||||
);
|
||||
const selectorOutbounds = (selector?.value?.all ?? []).flatMap(
|
||||
(code) => {
|
||||
const item = proxies.find((proxy) => proxy.code === code);
|
||||
@@ -1114,6 +1129,7 @@ async function getDashboardSections() {
|
||||
code: selector?.code || section[".name"],
|
||||
sectionName: section[".name"],
|
||||
displayName,
|
||||
subscriptionMetadata,
|
||||
outbounds: [
|
||||
{
|
||||
code: fallbackUrltest?.code || "",
|
||||
@@ -1135,6 +1151,7 @@ async function getDashboardSections() {
|
||||
code: selector?.code || section[".name"],
|
||||
sectionName: section[".name"],
|
||||
displayName,
|
||||
subscriptionMetadata,
|
||||
outbounds: await markSubscriptionCopyableOutbounds(
|
||||
section[".name"],
|
||||
outbounds
|
||||
@@ -2532,6 +2549,45 @@ function renderCopyIcon24() {
|
||||
);
|
||||
}
|
||||
|
||||
// src/icons/renderLinkIcon24.ts
|
||||
function renderLinkIcon24() {
|
||||
return svgEl(
|
||||
"svg",
|
||||
{
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
width: "24",
|
||||
height: "24",
|
||||
viewBox: "0 0 24 24",
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round",
|
||||
class: "lucide lucide-link-icon lucide-link"
|
||||
},
|
||||
[
|
||||
svgEl("path", {
|
||||
d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"
|
||||
}),
|
||||
svgEl("path", {
|
||||
d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"
|
||||
})
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// src/helpers/prettyBytes.ts
|
||||
function prettyBytes(n) {
|
||||
const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
if (n < 1e3) {
|
||||
return n + " B";
|
||||
}
|
||||
const exponent = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1);
|
||||
n = Number((n / Math.pow(1e3, exponent)).toPrecision(3));
|
||||
const unit = UNITS[exponent];
|
||||
return n + " " + unit;
|
||||
}
|
||||
|
||||
// src/podkop/tabs/dashboard/partials/renderSections.ts
|
||||
function renderFailedState() {
|
||||
return E(
|
||||
@@ -2550,6 +2606,118 @@ function renderLoadingState() {
|
||||
style: "height: 127px"
|
||||
});
|
||||
}
|
||||
function isValidHttpUrl(url) {
|
||||
return Boolean(url && /^https?:\/\/\S+$/i.test(url));
|
||||
}
|
||||
function formatBytes(value) {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
||||
return void 0;
|
||||
}
|
||||
return prettyBytes(value);
|
||||
}
|
||||
function formatDate(seconds) {
|
||||
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) {
|
||||
return void 0;
|
||||
}
|
||||
const date = new Date(seconds * 1e3);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return void 0;
|
||||
}
|
||||
return date.toLocaleDateString(void 0, {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit"
|
||||
});
|
||||
}
|
||||
function renderMetadataAction(label, url) {
|
||||
if (!isValidHttpUrl(url)) {
|
||||
return void 0;
|
||||
}
|
||||
return E(
|
||||
"a",
|
||||
{
|
||||
class: "btn pdk_dashboard-page__subscription-meta__action",
|
||||
href: url,
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
title: label,
|
||||
"aria-label": label
|
||||
},
|
||||
renderLinkIcon24()
|
||||
);
|
||||
}
|
||||
function renderSubscriptionMetadata(metadata) {
|
||||
if (!metadata || Object.keys(metadata).length <= 1) {
|
||||
return void 0;
|
||||
}
|
||||
const title = metadata.title || metadata.fileName;
|
||||
const traffic = metadata.traffic;
|
||||
const used = formatBytes(traffic?.used) || "0 B";
|
||||
const total = traffic?.isUnlimited ? "\u221E" : formatBytes(traffic?.total) || "0 B";
|
||||
const expire = formatDate(metadata.expire);
|
||||
const refillDate = formatDate(metadata.refillDate);
|
||||
const rows = [
|
||||
traffic ? {
|
||||
label: _("Traffic"),
|
||||
value: `${used} / ${total}`
|
||||
} : void 0,
|
||||
expire ? { label: _("Expires"), value: expire } : void 0,
|
||||
refillDate ? { label: _("Refill"), value: refillDate } : void 0
|
||||
].filter(Boolean);
|
||||
const actions = [
|
||||
renderMetadataAction("Profile", metadata.webPageUrl),
|
||||
renderMetadataAction("Support", metadata.supportUrl),
|
||||
renderMetadataAction("More details", metadata.announceUrl)
|
||||
].filter(Boolean);
|
||||
return E("div", { class: "pdk_dashboard-page__subscription-meta" }, [
|
||||
E("div", { class: "pdk_dashboard-page__subscription-meta__main" }, [
|
||||
E(
|
||||
"div",
|
||||
{ class: "pdk_dashboard-page__subscription-meta__heading" },
|
||||
_("Subscription info:")
|
||||
),
|
||||
title ? E(
|
||||
"div",
|
||||
{ class: "pdk_dashboard-page__subscription-meta__title" },
|
||||
title
|
||||
) : "",
|
||||
rows.length ? E(
|
||||
"div",
|
||||
{ class: "pdk_dashboard-page__subscription-meta__facts" },
|
||||
rows.map(
|
||||
(row) => E(
|
||||
"div",
|
||||
{ class: "pdk_dashboard-page__subscription-meta__fact" },
|
||||
[
|
||||
E(
|
||||
"span",
|
||||
{ class: "pdk_dashboard-page__subscription-meta__fact-key" },
|
||||
row.label
|
||||
),
|
||||
E(
|
||||
"span",
|
||||
{
|
||||
class: "pdk_dashboard-page__subscription-meta__fact-value"
|
||||
},
|
||||
row.value
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
) : "",
|
||||
actions.length ? E(
|
||||
"div",
|
||||
{ class: "pdk_dashboard-page__subscription-meta__actions" },
|
||||
actions
|
||||
) : ""
|
||||
]),
|
||||
metadata.announce ? E(
|
||||
"blockquote",
|
||||
{ class: "pdk_dashboard-page__subscription-meta__announce" },
|
||||
metadata.announce
|
||||
) : ""
|
||||
]);
|
||||
}
|
||||
function renderDefaultState({
|
||||
section,
|
||||
onChooseOutbound,
|
||||
@@ -2620,6 +2788,7 @@ function renderDefaultState({
|
||||
]
|
||||
);
|
||||
}
|
||||
const metadataNode = renderSubscriptionMetadata(section.subscriptionMetadata);
|
||||
return E("div", { class: "pdk_dashboard-page__outbound-section" }, [
|
||||
// Title with test latency
|
||||
E("div", { class: "pdk_dashboard-page__outbound-section__title-section" }, [
|
||||
@@ -2642,7 +2811,10 @@ function renderDefaultState({
|
||||
E(
|
||||
"div",
|
||||
{ class: "pdk_dashboard-page__outbound-grid" },
|
||||
section.outbounds.map((outbound) => renderOutbound(outbound))
|
||||
[
|
||||
...metadataNode ? [metadataNode] : [],
|
||||
...section.outbounds.map((outbound) => renderOutbound(outbound))
|
||||
]
|
||||
)
|
||||
]);
|
||||
}
|
||||
@@ -2812,18 +2984,6 @@ function copyToClipboard(text) {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
// src/helpers/prettyBytes.ts
|
||||
function prettyBytes(n) {
|
||||
const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
if (n < 1e3) {
|
||||
return n + " B";
|
||||
}
|
||||
const exponent = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1);
|
||||
n = Number((n / Math.pow(1e3, exponent)).toPrecision(3));
|
||||
const unit = UNITS[exponent];
|
||||
return n + " " + unit;
|
||||
}
|
||||
|
||||
// src/podkop/fetchers/fetchServicesInfo.ts
|
||||
var latestServicesInfoRequestId = 0;
|
||||
var latestPodkopStatusRequestId = 0;
|
||||
@@ -3397,6 +3557,134 @@ var styles = `
|
||||
grid-gap: 10px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta {
|
||||
--subscription-meta-action-size: 28px;
|
||||
--subscription-meta-action-gap: 6px;
|
||||
grid-column: 1 / -1;
|
||||
border: 2px var(--background-color-low, lightgray) solid;
|
||||
border-radius: 4px;
|
||||
padding: 8px 10px;
|
||||
background: var(--background-color-high, transparent);
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__heading {
|
||||
flex: 0 0 auto;
|
||||
color: var(--text-color-high);
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__title {
|
||||
flex: 0 1 auto;
|
||||
width: max-content;
|
||||
max-width: min(28ch, 30%);
|
||||
min-width: min-content;
|
||||
color: var(--text-color-high);
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__facts {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 5px 12px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__fact {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__fact-key {
|
||||
color: var(--text-color-medium);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__fact-value {
|
||||
color: var(--text-color-high);
|
||||
font-weight: 600;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__actions {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--subscription-meta-action-gap);
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__action {
|
||||
width: var(--subscription-meta-action-size);
|
||||
height: var(--subscription-meta-action-size);
|
||||
min-width: var(--subscription-meta-action-size);
|
||||
min-height: var(--subscription-meta-action-size);
|
||||
padding: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__action svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__announce {
|
||||
margin: 6px 0 0;
|
||||
border-left: 3px solid var(--primary-color-medium, dodgerblue);
|
||||
padding: 4px 8px;
|
||||
background: var(--background-color-low, rgba(0, 0, 0, 0.04));
|
||||
color: var(--text-color-medium);
|
||||
font-style: italic;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.pdk_dashboard-page__subscription-meta__main {
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__heading,
|
||||
.pdk_dashboard-page__subscription-meta__title {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__actions {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__facts {
|
||||
order: 3;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__subscription-meta__title {
|
||||
max-width: calc(100% - 42px);
|
||||
}
|
||||
}
|
||||
|
||||
.pdk_dashboard-page__outbound-grid__item {
|
||||
border: 2px var(--background-color-low, lightgray) solid;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP PLUS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-13 00:19+0300\n"
|
||||
"PO-Revision-Date: 2026-05-13 00:19+0300\n"
|
||||
"POT-Creation-Date: 2026-05-14 04:44+0300\n"
|
||||
"PO-Revision-Date: 2026-05-14 04:44+0300\n"
|
||||
"Last-Translator: ushan0v\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -335,6 +335,9 @@ msgstr "Исключите трафик протокола NTP из туннел
|
||||
msgid "Expecting: %s"
|
||||
msgstr "Ожидается: %s"
|
||||
|
||||
msgid "Expires"
|
||||
msgstr "Истекает"
|
||||
|
||||
msgid "Expose this section as a local HTTP+SOCKS proxy"
|
||||
msgstr "Открыть эту секцию как локальный HTTP+SOCKS прокси"
|
||||
|
||||
@@ -698,6 +701,9 @@ msgstr "Прокси-трафик не маршрутизируется чере
|
||||
msgid "Proxy traffic is routed via FakeIP"
|
||||
msgstr "Прокси-трафик направляется через FakeIP"
|
||||
|
||||
msgid "Refill"
|
||||
msgstr "Сброс"
|
||||
|
||||
msgid "Regional options cannot be used together"
|
||||
msgstr "Нельзя использовать несколько региональных опций одновременно"
|
||||
|
||||
@@ -881,6 +887,9 @@ msgstr "Остановить Podkop Plus"
|
||||
msgid "Subscription"
|
||||
msgstr "Подписка"
|
||||
|
||||
msgid "Subscription info:"
|
||||
msgstr "Информация о подписке:"
|
||||
|
||||
msgid "Subscription update interval"
|
||||
msgstr "Интервал обновления подписки"
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PODKOP PLUS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-05-13 21:19+0300\n"
|
||||
"PO-Revision-Date: 2026-05-13 21:19+0300\n"
|
||||
"POT-Creation-Date: 2026-05-14 01:44+0300\n"
|
||||
"PO-Revision-Date: 2026-05-14 01:44+0300\n"
|
||||
"Last-Translator: ushan0v <142658494+ushan0v@users.noreply.github.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
@@ -16,19 +16,19 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:387
|
||||
#: src/podkop/tabs/dashboard/initController.ts:398
|
||||
#: src/podkop/tabs/dashboard/initController.ts:411
|
||||
#: src/podkop/tabs/dashboard/initController.ts:391
|
||||
#: src/podkop/tabs/dashboard/initController.ts:402
|
||||
#: src/podkop/tabs/dashboard/initController.ts:415
|
||||
msgid "✔ Running"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:409
|
||||
#: src/podkop/tabs/dashboard/initController.ts:413
|
||||
msgid "✘ Not installed"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:388
|
||||
#: src/podkop/tabs/dashboard/initController.ts:399
|
||||
#: src/podkop/tabs/dashboard/initController.ts:412
|
||||
#: src/podkop/tabs/dashboard/initController.ts:392
|
||||
#: src/podkop/tabs/dashboard/initController.ts:403
|
||||
#: src/podkop/tabs/dashboard/initController.ts:416
|
||||
msgid "✘ Stopped"
|
||||
msgstr ""
|
||||
|
||||
@@ -53,7 +53,7 @@ msgstr ""
|
||||
msgid "Action"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:349
|
||||
#: src/podkop/tabs/dashboard/initController.ts:353
|
||||
msgid "Active Connections"
|
||||
msgstr ""
|
||||
|
||||
@@ -98,7 +98,7 @@ msgstr ""
|
||||
msgid "At least one valid IP or subnet must be specified."
|
||||
msgstr ""
|
||||
|
||||
#: src/partials/modal/renderModal.ts:219
|
||||
#: src/partials/modal/renderModal.ts:218
|
||||
msgid "Auto refresh"
|
||||
msgstr ""
|
||||
|
||||
@@ -192,7 +192,7 @@ msgstr ""
|
||||
msgid "CIDR must be between 0 and 32"
|
||||
msgstr ""
|
||||
|
||||
#: src/partials/modal/renderModal.ts:196
|
||||
#: src/partials/modal/renderModal.ts:195
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
@@ -225,8 +225,8 @@ msgstr ""
|
||||
msgid "Copy"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:93
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:94
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:235
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:236
|
||||
msgid "Copy proxy link"
|
||||
msgstr ""
|
||||
|
||||
@@ -238,7 +238,7 @@ msgstr ""
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:24
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:29
|
||||
msgid "Dashboard currently unavailable"
|
||||
msgstr ""
|
||||
|
||||
@@ -357,8 +357,8 @@ msgstr ""
|
||||
msgid "Dont Touch My DHCP!"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:283
|
||||
#: src/podkop/tabs/dashboard/initController.ts:317
|
||||
#: src/podkop/tabs/dashboard/initController.ts:287
|
||||
#: src/podkop/tabs/dashboard/initController.ts:321
|
||||
msgid "Downlink"
|
||||
msgstr ""
|
||||
|
||||
@@ -475,6 +475,10 @@ msgstr ""
|
||||
msgid "Expecting: %s"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:110
|
||||
msgid "Expires"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop/section.js:2145
|
||||
msgid "Expose this section as a local HTTP+SOCKS proxy"
|
||||
msgstr ""
|
||||
@@ -496,9 +500,9 @@ msgstr ""
|
||||
msgid "Failed to execute!"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/methods/custom/getDashboardSections.ts:225
|
||||
#: src/podkop/methods/custom/getDashboardSections.ts:257
|
||||
#: src/podkop/methods/custom/getDashboardSections.ts:295
|
||||
#: src/podkop/methods/custom/getDashboardSections.ts:308
|
||||
#: src/podkop/methods/custom/getDashboardSections.ts:344
|
||||
#: src/podkop/methods/custom/getDashboardSections.ts:385
|
||||
#: src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts:59
|
||||
msgid "Fastest"
|
||||
msgstr ""
|
||||
@@ -806,7 +810,7 @@ msgstr ""
|
||||
msgid "Maximum response time delta in milliseconds"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:353
|
||||
#: src/podkop/tabs/dashboard/initController.ts:357
|
||||
msgid "Memory Usage"
|
||||
msgstr ""
|
||||
|
||||
@@ -978,7 +982,7 @@ msgstr ""
|
||||
msgid "Proxy Configuration URL"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:202
|
||||
#: src/podkop/tabs/dashboard/initController.ts:205
|
||||
msgid "Proxy link is unavailable"
|
||||
msgstr ""
|
||||
|
||||
@@ -990,6 +994,10 @@ msgstr ""
|
||||
msgid "Proxy traffic is routed via FakeIP"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:111
|
||||
msgid "Refill"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop/section.js:2289
|
||||
msgid "Regional options cannot be used together"
|
||||
msgstr ""
|
||||
@@ -1150,7 +1158,7 @@ msgstr ""
|
||||
msgid "Service list"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:382
|
||||
#: src/podkop/tabs/dashboard/initController.ts:386
|
||||
msgid "Services info"
|
||||
msgstr ""
|
||||
|
||||
@@ -1236,6 +1244,10 @@ msgstr ""
|
||||
msgid "Subscription"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:125
|
||||
msgid "Subscription info:"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-podkop-plus/htdocs/luci-static/resources/view/podkop/section.js:1857
|
||||
msgid "Subscription update interval"
|
||||
msgstr ""
|
||||
@@ -1252,7 +1264,7 @@ msgstr ""
|
||||
msgid "Successfully copied!"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:346
|
||||
#: src/podkop/tabs/dashboard/initController.ts:350
|
||||
msgid "System info"
|
||||
msgstr ""
|
||||
|
||||
@@ -1264,7 +1276,7 @@ msgstr ""
|
||||
msgid "Table exist"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:139
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:283
|
||||
msgid "Test latency"
|
||||
msgstr ""
|
||||
|
||||
@@ -1304,11 +1316,12 @@ msgstr ""
|
||||
msgid "Time in seconds for DNS record caching (default: 60)"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:280
|
||||
#: src/podkop/tabs/dashboard/initController.ts:284
|
||||
#: src/podkop/tabs/dashboard/partials/renderSections.ts:106
|
||||
msgid "Traffic"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:310
|
||||
#: src/podkop/tabs/dashboard/initController.ts:314
|
||||
msgid "Traffic Total"
|
||||
msgstr ""
|
||||
|
||||
@@ -1376,8 +1389,8 @@ msgstr ""
|
||||
msgid "Update interval"
|
||||
msgstr ""
|
||||
|
||||
#: src/podkop/tabs/dashboard/initController.ts:282
|
||||
#: src/podkop/tabs/dashboard/initController.ts:313
|
||||
#: src/podkop/tabs/dashboard/initController.ts:286
|
||||
#: src/podkop/tabs/dashboard/initController.ts:317
|
||||
msgid "Uplink"
|
||||
msgstr ""
|
||||
|
||||
|
||||
+75
-10
@@ -44,6 +44,7 @@ PODKOP_PENDING_RELOAD_FILE="$PODKOP_RUNTIME_STATE_DIR/reload.pending"
|
||||
PODKOP_LIST_UPDATE_STATE_FILE="$PODKOP_RUNTIME_STATE_DIR/list-update.timestamp"
|
||||
PODKOP_SUBSCRIPTION_UPDATE_STATE_DIR="$PODKOP_RUNTIME_STATE_DIR/subscription-update"
|
||||
PODKOP_SUBSCRIPTION_LINKS_DIR="$PODKOP_RUNTIME_STATE_DIR/subscription-links"
|
||||
PODKOP_SUBSCRIPTION_METADATA_DIR="$PODKOP_RUNTIME_STATE_DIR/subscription-metadata"
|
||||
PODKOP_SUBSCRIPTION_UPDATE_LOCK_DIR="$PODKOP_RUNTIME_STATE_DIR/subscription-update.lock"
|
||||
PODKOP_RELOAD_LOCK_DIR="/var/run/podkop-plus.reload.lock"
|
||||
PODKOP_LIST_UPDATE_CRON_MARKER="# podkop-plus-list-update"
|
||||
@@ -67,6 +68,7 @@ ensure_runtime_dirs() {
|
||||
mkdir -p "$PODKOP_RUNTIME_STATE_DIR"
|
||||
mkdir -p "$PODKOP_SUBSCRIPTION_UPDATE_STATE_DIR"
|
||||
mkdir -p "$PODKOP_SUBSCRIPTION_LINKS_DIR"
|
||||
mkdir -p "$PODKOP_SUBSCRIPTION_METADATA_DIR"
|
||||
}
|
||||
|
||||
get_subscription_json_path() {
|
||||
@@ -93,6 +95,12 @@ get_subscription_links_path() {
|
||||
echo "$PODKOP_SUBSCRIPTION_LINKS_DIR/${section}.json"
|
||||
}
|
||||
|
||||
get_subscription_metadata_path() {
|
||||
local section="$1"
|
||||
|
||||
echo "$PODKOP_SUBSCRIPTION_METADATA_DIR/${section}.json"
|
||||
}
|
||||
|
||||
write_subscription_outbound_links() {
|
||||
local section="$1"
|
||||
local links_json="$2"
|
||||
@@ -109,6 +117,24 @@ write_subscription_outbound_links() {
|
||||
fi
|
||||
}
|
||||
|
||||
write_subscription_metadata() {
|
||||
local section="$1"
|
||||
local metadata_path="$2"
|
||||
local target_path tmpfile
|
||||
|
||||
mkdir -p "$PODKOP_SUBSCRIPTION_METADATA_DIR"
|
||||
target_path="$(get_subscription_metadata_path "$section")"
|
||||
tmpfile="${target_path}.$$"
|
||||
|
||||
if [ -n "$metadata_path" ] &&
|
||||
[ -s "$metadata_path" ] &&
|
||||
jq -e 'type == "object" and (keys | length > 1)' "$metadata_path" >/dev/null 2>&1; then
|
||||
cp "$metadata_path" "$tmpfile" && mv "$tmpfile" "$target_path"
|
||||
else
|
||||
rm -f "$target_path" "$tmpfile"
|
||||
fi
|
||||
}
|
||||
|
||||
subscription_url_is_current() {
|
||||
local section="$1"
|
||||
local subscription_url="$2"
|
||||
@@ -155,51 +181,63 @@ download_subscription_into_cache() {
|
||||
local subscription_json_path="$3"
|
||||
local subscription_url_cache_path="$4"
|
||||
local service_proxy_address="$5"
|
||||
local raw_tmpfile normalized_tmpfile
|
||||
local raw_tmpfile headers_tmpfile normalized_tmpfile metadata_tmpfile
|
||||
|
||||
mkdir -p "$TMP_SUBSCRIPTION_FOLDER"
|
||||
raw_tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${section}.download.XXXXXX")" || return 1
|
||||
normalized_tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${section}.normalized.XXXXXX")" || {
|
||||
headers_tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${section}.headers.XXXXXX")" || {
|
||||
rm -f "$raw_tmpfile"
|
||||
return 1
|
||||
}
|
||||
normalized_tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${section}.normalized.XXXXXX")" || {
|
||||
rm -f "$raw_tmpfile" "$headers_tmpfile"
|
||||
return 1
|
||||
}
|
||||
metadata_tmpfile="$(mktemp "$TMP_SUBSCRIPTION_FOLDER/${section}.metadata.XXXXXX")" || {
|
||||
rm -f "$raw_tmpfile" "$headers_tmpfile" "$normalized_tmpfile"
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! download_subscription "$subscription_url" "$raw_tmpfile" "$service_proxy_address" 3 2 10; then
|
||||
rm -f "$raw_tmpfile" "$normalized_tmpfile"
|
||||
if ! download_subscription "$subscription_url" "$raw_tmpfile" "$service_proxy_address" 3 2 10 "$headers_tmpfile"; then
|
||||
rm -f "$raw_tmpfile" "$headers_tmpfile" "$normalized_tmpfile" "$metadata_tmpfile"
|
||||
return 1
|
||||
fi
|
||||
|
||||
subscription_extract_ui_metadata "$headers_tmpfile" "$raw_tmpfile" "$metadata_tmpfile" >/dev/null 2>&1 || rm -f "$metadata_tmpfile"
|
||||
|
||||
if ! normalize_subscription_file "$raw_tmpfile" "$normalized_tmpfile" "$section"; then
|
||||
log "Downloaded subscription for rule '$section' is invalid" "error"
|
||||
rm -f "$raw_tmpfile" "$normalized_tmpfile"
|
||||
rm -f "$raw_tmpfile" "$headers_tmpfile" "$normalized_tmpfile" "$metadata_tmpfile"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! validate_subscription_file "$normalized_tmpfile"; then
|
||||
log "Normalized subscription for rule '$section' is invalid" "error"
|
||||
rm -f "$raw_tmpfile" "$normalized_tmpfile"
|
||||
rm -f "$raw_tmpfile" "$headers_tmpfile" "$normalized_tmpfile" "$metadata_tmpfile"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! subscription_url_is_current "$section" "$subscription_url"; then
|
||||
log "Subscription URL changed while updating rule '$section'; discarding superseded download" "warn"
|
||||
rm -f "$raw_tmpfile" "$normalized_tmpfile"
|
||||
rm -f "$raw_tmpfile" "$headers_tmpfile" "$normalized_tmpfile" "$metadata_tmpfile"
|
||||
return 4
|
||||
fi
|
||||
|
||||
if [ -f "$subscription_json_path" ] && cmp -s "$normalized_tmpfile" "$subscription_json_path"; then
|
||||
rm -f "$raw_tmpfile" "$normalized_tmpfile"
|
||||
write_subscription_metadata "$section" "$metadata_tmpfile"
|
||||
rm -f "$raw_tmpfile" "$headers_tmpfile" "$normalized_tmpfile" "$metadata_tmpfile"
|
||||
printf '%s' "$subscription_url" > "$subscription_url_cache_path"
|
||||
log "Subscription for rule '$section' is unchanged" "info"
|
||||
return 2
|
||||
fi
|
||||
|
||||
mv "$normalized_tmpfile" "$subscription_json_path" || {
|
||||
rm -f "$raw_tmpfile" "$normalized_tmpfile"
|
||||
rm -f "$raw_tmpfile" "$headers_tmpfile" "$normalized_tmpfile" "$metadata_tmpfile"
|
||||
return 1
|
||||
}
|
||||
|
||||
rm -f "$raw_tmpfile"
|
||||
write_subscription_metadata "$section" "$metadata_tmpfile"
|
||||
rm -f "$raw_tmpfile" "$headers_tmpfile" "$metadata_tmpfile"
|
||||
printf '%s' "$subscription_url" > "$subscription_url_cache_path"
|
||||
return 0
|
||||
}
|
||||
@@ -6220,6 +6258,28 @@ get_outbound_link() {
|
||||
jq -cn --arg link "$link" '{link: $link}'
|
||||
}
|
||||
|
||||
get_subscription_metadata() {
|
||||
local section="$1"
|
||||
local metadata_path
|
||||
|
||||
ensure_runtime_dirs
|
||||
|
||||
case "$section" in
|
||||
"" | */* | *..*)
|
||||
printf '{}\n'
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
metadata_path="$(get_subscription_metadata_path "$section")"
|
||||
if [ -f "$metadata_path" ]; then
|
||||
jq -c 'if type == "object" then . else {} end' "$metadata_path" 2>/dev/null || printf '{}\n'
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '{}\n'
|
||||
}
|
||||
|
||||
validate_nfqws_strategy_json() {
|
||||
local raw_opt="${1:-}"
|
||||
|
||||
@@ -7011,6 +7071,8 @@ Available commands:
|
||||
show_system_info Show system information
|
||||
get_status Get Podkop Plus service status
|
||||
get_outbound_link Get source proxy link for a generated outbound
|
||||
get_subscription_metadata
|
||||
Get UI-only metadata for a subscription rule
|
||||
get_sing_box_status Get sing-box service status
|
||||
get_zapret_status Get zapret provider and Podkop-managed nfqws status
|
||||
get_system_info Get system information in JSON format
|
||||
@@ -7109,6 +7171,9 @@ get_status)
|
||||
get_outbound_link)
|
||||
get_outbound_link "$2" "$3"
|
||||
;;
|
||||
get_subscription_metadata)
|
||||
get_subscription_metadata "$2"
|
||||
;;
|
||||
get_sing_box_status)
|
||||
get_sing_box_status
|
||||
;;
|
||||
|
||||
@@ -299,6 +299,10 @@ get_sing_box_version() {
|
||||
echo "${version:-1.0}"
|
||||
}
|
||||
|
||||
get_subscription_user_agent() {
|
||||
printf 'clash.meta; sing-box/%s; PodkopPlus/OpenWrt' "$(get_sing_box_version)"
|
||||
}
|
||||
|
||||
generate_hwid() {
|
||||
local mac="" model="" raw_hash=""
|
||||
|
||||
@@ -325,16 +329,65 @@ download_subscription() {
|
||||
local retries="${4:-3}"
|
||||
local wait="${5:-2}"
|
||||
local timeout="${6:-10}"
|
||||
local tmpfile attempt
|
||||
local headers_filepath="${7:-}"
|
||||
local tmpfile headers_tmpfile attempt wget_status
|
||||
|
||||
tmpfile="${filepath}.part.$$"
|
||||
headers_tmpfile=""
|
||||
[ -n "$headers_filepath" ] && headers_tmpfile="${headers_filepath}.part.$$"
|
||||
rm -f "$tmpfile"
|
||||
[ -n "$headers_tmpfile" ] && rm -f "$headers_tmpfile"
|
||||
|
||||
for attempt in $(seq 1 "$retries"); do
|
||||
if [ -n "$http_proxy_address" ]; then
|
||||
http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \
|
||||
if [ -n "$headers_tmpfile" ]; then
|
||||
curl -fL -sS \
|
||||
--connect-timeout "$timeout" \
|
||||
--speed-time "$timeout" \
|
||||
--speed-limit 1 \
|
||||
-x "http://$http_proxy_address" \
|
||||
-D "$headers_tmpfile" \
|
||||
-o "$tmpfile" \
|
||||
-H "User-Agent: $(get_subscription_user_agent)" \
|
||||
-H "X-HWID: $(generate_hwid)" \
|
||||
-H "X-Device-OS: OpenWrt Linux" \
|
||||
-H "X-Device-Model: $(get_device_model)" \
|
||||
-H "X-Ver-OS: $(get_kernel_version)" \
|
||||
-H "Accept-Language: ru-RU,en,*" \
|
||||
-H "X-Device-Locale: EN" \
|
||||
"$url"
|
||||
else
|
||||
http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \
|
||||
wget -T "$timeout" -O "$tmpfile" \
|
||||
--header "User-Agent: $(get_subscription_user_agent)" \
|
||||
--header "X-HWID: $(generate_hwid)" \
|
||||
--header "X-Device-OS: OpenWrt Linux" \
|
||||
--header "X-Device-Model: $(get_device_model)" \
|
||||
--header "X-Ver-OS: $(get_kernel_version)" \
|
||||
--header "Accept-Language: ru-RU,en,*" \
|
||||
--header "X-Device-Locale: EN" \
|
||||
"$url"
|
||||
fi
|
||||
wget_status=$?
|
||||
else
|
||||
if [ -n "$headers_tmpfile" ]; then
|
||||
curl -fL -sS \
|
||||
--connect-timeout "$timeout" \
|
||||
--speed-time "$timeout" \
|
||||
--speed-limit 1 \
|
||||
-D "$headers_tmpfile" \
|
||||
-o "$tmpfile" \
|
||||
-H "User-Agent: $(get_subscription_user_agent)" \
|
||||
-H "X-HWID: $(generate_hwid)" \
|
||||
-H "X-Device-OS: OpenWrt Linux" \
|
||||
-H "X-Device-Model: $(get_device_model)" \
|
||||
-H "X-Ver-OS: $(get_kernel_version)" \
|
||||
-H "Accept-Language: ru-RU,en,*" \
|
||||
-H "X-Device-Locale: EN" \
|
||||
"$url"
|
||||
else
|
||||
wget -T "$timeout" -O "$tmpfile" \
|
||||
--header "User-Agent: singbox/$(get_sing_box_version)" \
|
||||
--header "User-Agent: $(get_subscription_user_agent)" \
|
||||
--header "X-HWID: $(generate_hwid)" \
|
||||
--header "X-Device-OS: OpenWrt Linux" \
|
||||
--header "X-Device-Model: $(get_device_model)" \
|
||||
@@ -342,29 +395,30 @@ download_subscription() {
|
||||
--header "Accept-Language: ru-RU,en,*" \
|
||||
--header "X-Device-Locale: EN" \
|
||||
"$url"
|
||||
else
|
||||
wget -T "$timeout" -O "$tmpfile" \
|
||||
--header "User-Agent: singbox/$(get_sing_box_version)" \
|
||||
--header "X-HWID: $(generate_hwid)" \
|
||||
--header "X-Device-OS: OpenWrt Linux" \
|
||||
--header "X-Device-Model: $(get_device_model)" \
|
||||
--header "X-Ver-OS: $(get_kernel_version)" \
|
||||
--header "Accept-Language: ru-RU,en,*" \
|
||||
--header "X-Device-Locale: EN" \
|
||||
"$url"
|
||||
fi
|
||||
wget_status=$?
|
||||
fi
|
||||
|
||||
if [ $? -eq 0 ] && [ -s "$tmpfile" ]; then
|
||||
if [ "$wget_status" -eq 0 ] && [ -s "$tmpfile" ]; then
|
||||
mv "$tmpfile" "$filepath"
|
||||
if [ -n "$headers_tmpfile" ]; then
|
||||
if [ -s "$headers_tmpfile" ]; then
|
||||
mv "$headers_tmpfile" "$headers_filepath"
|
||||
else
|
||||
rm -f "$headers_filepath" "$headers_tmpfile"
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f "$tmpfile"
|
||||
[ -n "$headers_tmpfile" ] && rm -f "$headers_tmpfile"
|
||||
log "Attempt $attempt/$retries to download subscription failed" "warn"
|
||||
sleep "$wait"
|
||||
done
|
||||
|
||||
rm -f "$tmpfile"
|
||||
[ -n "$headers_tmpfile" ] && rm -f "$headers_tmpfile"
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -380,7 +434,7 @@ check_subscription_connectivity() {
|
||||
if [ -n "$http_proxy_address" ]; then
|
||||
http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" \
|
||||
wget -q -T "$timeout" -O /dev/null \
|
||||
--header "User-Agent: singbox/$(get_sing_box_version)" \
|
||||
--header "User-Agent: $(get_subscription_user_agent)" \
|
||||
--header "X-HWID: $(generate_hwid)" \
|
||||
--header "X-Device-OS: OpenWrt Linux" \
|
||||
--header "X-Device-Model: $(get_device_model)" \
|
||||
@@ -390,7 +444,7 @@ check_subscription_connectivity() {
|
||||
"$url" && return 0
|
||||
else
|
||||
wget -q -T "$timeout" -O /dev/null \
|
||||
--header "User-Agent: singbox/$(get_sing_box_version)" \
|
||||
--header "User-Agent: $(get_subscription_user_agent)" \
|
||||
--header "X-HWID: $(generate_hwid)" \
|
||||
--header "X-Device-OS: OpenWrt Linux" \
|
||||
--header "X-Device-Model: $(get_device_model)" \
|
||||
|
||||
@@ -125,6 +125,438 @@ subscription_try_base64_decode_file() {
|
||||
return 1
|
||||
}
|
||||
|
||||
subscription_metadata_headers_json() {
|
||||
local input="$1"
|
||||
|
||||
[ -s "$input" ] || {
|
||||
printf '{}\n'
|
||||
return 0
|
||||
}
|
||||
|
||||
awk '
|
||||
function trim(s) {
|
||||
gsub(/^[ \t\r\n]+/, "", s)
|
||||
gsub(/[ \t\r\n]+$/, "", s)
|
||||
return s
|
||||
}
|
||||
function allowed(k) {
|
||||
return k == "profile-title" ||
|
||||
k == "subscription-userinfo" ||
|
||||
k == "profile-web-page-url" ||
|
||||
k == "support-url" ||
|
||||
k == "announce" ||
|
||||
k == "announce-url" ||
|
||||
k == "subscription-refill-date" ||
|
||||
k == "content-disposition"
|
||||
}
|
||||
function json_escape(s) {
|
||||
gsub(/\\/, "\\\\", s)
|
||||
gsub(/"/, "\\\"", s)
|
||||
gsub(/\t/, "\\t", s)
|
||||
gsub(/\r/, "", s)
|
||||
return s
|
||||
}
|
||||
{
|
||||
line = trim($0)
|
||||
colon = index(line, ":")
|
||||
if (colon <= 1) {
|
||||
next
|
||||
}
|
||||
|
||||
key = tolower(trim(substr(line, 1, colon - 1)))
|
||||
value = trim(substr(line, colon + 1))
|
||||
if (!allowed(key) || value == "") {
|
||||
next
|
||||
}
|
||||
|
||||
if (!(key in seen)) {
|
||||
order[++count] = key
|
||||
}
|
||||
seen[key] = 1
|
||||
values[key] = value
|
||||
}
|
||||
END {
|
||||
printf "{"
|
||||
sep = ""
|
||||
for (i = 1; i <= count; i++) {
|
||||
key = order[i]
|
||||
if (key in values) {
|
||||
printf "%s\"%s\":\"%s\"", sep, key, json_escape(values[key])
|
||||
sep = ","
|
||||
}
|
||||
}
|
||||
printf "}\n"
|
||||
}
|
||||
' "$input"
|
||||
}
|
||||
|
||||
subscription_metadata_body_json_from_file() {
|
||||
local input="$1"
|
||||
|
||||
[ -s "$input" ] || {
|
||||
printf '{}\n'
|
||||
return 0
|
||||
}
|
||||
|
||||
awk '
|
||||
function trim(s) {
|
||||
gsub(/^[ \t\r\n]+/, "", s)
|
||||
gsub(/[ \t\r\n]+$/, "", s)
|
||||
return s
|
||||
}
|
||||
function allowed(k) {
|
||||
return k == "profile-title" ||
|
||||
k == "subscription-userinfo" ||
|
||||
k == "profile-web-page-url" ||
|
||||
k == "support-url" ||
|
||||
k == "announce" ||
|
||||
k == "announce-url" ||
|
||||
k == "subscription-refill-date" ||
|
||||
k == "content-disposition"
|
||||
}
|
||||
function json_escape(s) {
|
||||
gsub(/\\/, "\\\\", s)
|
||||
gsub(/"/, "\\\"", s)
|
||||
gsub(/\t/, "\\t", s)
|
||||
gsub(/\r/, "", s)
|
||||
return s
|
||||
}
|
||||
NR > 20 {
|
||||
exit
|
||||
}
|
||||
{
|
||||
line = trim($0)
|
||||
if (line ~ /^#/) {
|
||||
sub(/^#[ \t]*/, "", line)
|
||||
} else if (line ~ /^\/\//) {
|
||||
sub(/^\/\/[ \t]*/, "", line)
|
||||
} else {
|
||||
next
|
||||
}
|
||||
|
||||
colon = index(line, ":")
|
||||
if (colon <= 1) {
|
||||
next
|
||||
}
|
||||
|
||||
key = tolower(trim(substr(line, 1, colon - 1)))
|
||||
value = trim(substr(line, colon + 1))
|
||||
if (!allowed(key) || value == "") {
|
||||
next
|
||||
}
|
||||
|
||||
if (!(key in seen)) {
|
||||
order[++count] = key
|
||||
}
|
||||
seen[key] = 1
|
||||
values[key] = value
|
||||
}
|
||||
END {
|
||||
printf "{"
|
||||
sep = ""
|
||||
for (i = 1; i <= count; i++) {
|
||||
key = order[i]
|
||||
if (key in values) {
|
||||
printf "%s\"%s\":\"%s\"", sep, key, json_escape(values[key])
|
||||
sep = ","
|
||||
}
|
||||
}
|
||||
printf "}\n"
|
||||
}
|
||||
' "$input"
|
||||
}
|
||||
|
||||
subscription_metadata_body_json() {
|
||||
local input="$1"
|
||||
local raw_json decoded_tmp decoded_json
|
||||
|
||||
raw_json="$(subscription_metadata_body_json_from_file "$input")"
|
||||
if [ "$raw_json" != "{}" ]; then
|
||||
printf '%s\n' "$raw_json"
|
||||
return 0
|
||||
fi
|
||||
|
||||
decoded_tmp="$(mktemp)" || {
|
||||
printf '{}\n'
|
||||
return 0
|
||||
}
|
||||
|
||||
if subscription_try_base64_decode_file "$input" "$decoded_tmp"; then
|
||||
decoded_json="$(subscription_metadata_body_json_from_file "$decoded_tmp")"
|
||||
rm -f "$decoded_tmp"
|
||||
printf '%s\n' "$decoded_json"
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f "$decoded_tmp"
|
||||
printf '{}\n'
|
||||
}
|
||||
|
||||
subscription_metadata_trim() {
|
||||
sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
|
||||
}
|
||||
|
||||
subscription_metadata_lower() {
|
||||
awk '{ print tolower($0) }'
|
||||
}
|
||||
|
||||
subscription_metadata_clean_text() {
|
||||
local value="$1"
|
||||
local max="$2"
|
||||
local mode="${3:-plain}"
|
||||
local prefix cleaned
|
||||
|
||||
if [ "$mode" = "base64" ]; then
|
||||
prefix="$(printf '%s' "$value" | cut -c 1-7 | subscription_metadata_lower)"
|
||||
if [ "$prefix" = "base64:" ]; then
|
||||
value="$(subscription_base64_decode_string "$(printf '%s' "$value" | cut -c 8-)" 2>/dev/null || printf '')"
|
||||
fi
|
||||
fi
|
||||
|
||||
cleaned="$(
|
||||
printf '%s' "$value" |
|
||||
tr '\000-\037\177' ' ' |
|
||||
sed 's/[[:space:]][[:space:]]*/ /g;s/^[[:space:]]*//;s/[[:space:]]*$//'
|
||||
)"
|
||||
|
||||
[ -n "$cleaned" ] || return 1
|
||||
if [ "${#cleaned}" -gt "$max" ]; then
|
||||
cleaned="$(printf '%s' "$cleaned" | cut -c "1-$max")"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$cleaned"
|
||||
}
|
||||
|
||||
subscription_metadata_clean_url() {
|
||||
local value cleaned
|
||||
|
||||
value="$1"
|
||||
cleaned="$(printf '%s' "$value" | subscription_metadata_trim)"
|
||||
[ -n "$cleaned" ] || return 1
|
||||
[ "${#cleaned}" -le 2048 ] || return 1
|
||||
|
||||
case "$cleaned" in
|
||||
http://* | https://*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
if printf '%s' "$cleaned" | grep -q '[[:cntrl:][:space:]]'; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$cleaned"
|
||||
}
|
||||
|
||||
subscription_metadata_clean_number() {
|
||||
local value
|
||||
|
||||
value="$(printf '%s' "$1" | subscription_metadata_trim)"
|
||||
case "$value" in
|
||||
"" | *[!0-9]*) return 1 ;;
|
||||
esac
|
||||
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
subscription_metadata_content_disposition_filename() {
|
||||
local value filename
|
||||
|
||||
value="$1"
|
||||
case "$value" in
|
||||
*filename=\"*)
|
||||
filename="${value#*filename=\"}"
|
||||
filename="${filename%%\"*}"
|
||||
;;
|
||||
*filename=*)
|
||||
filename="${value#*filename=}"
|
||||
filename="${filename%%;*}"
|
||||
filename="${filename#\"}"
|
||||
filename="${filename%\"}"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
filename="$(subscription_metadata_clean_text "$filename" 120 plain)" || return 1
|
||||
filename="$(printf '%s' "$filename" | tr '/\\' '__')"
|
||||
[ -n "$filename" ] || return 1
|
||||
printf '%s\n' "$filename"
|
||||
}
|
||||
|
||||
subscription_metadata_raw_value() {
|
||||
local raw_json="$1"
|
||||
local key="$2"
|
||||
|
||||
printf '%s\n' "$raw_json" | jq -r --arg key "$key" '.[$key] // empty' 2>/dev/null
|
||||
}
|
||||
|
||||
subscription_normalize_ui_metadata_json() {
|
||||
local raw_json="$1"
|
||||
local title web_page_url support_url announce announce_url refill_date file_name
|
||||
local userinfo item key value upload download total expire has_traffic used remaining is_unlimited
|
||||
local upload_json download_json total_json expire_json refill_date_json remaining_json
|
||||
|
||||
title="$(subscription_metadata_clean_text "$(subscription_metadata_raw_value "$raw_json" "profile-title")" 120 base64 2>/dev/null || true)"
|
||||
web_page_url="$(subscription_metadata_clean_url "$(subscription_metadata_raw_value "$raw_json" "profile-web-page-url")" 2>/dev/null || true)"
|
||||
support_url="$(subscription_metadata_clean_url "$(subscription_metadata_raw_value "$raw_json" "support-url")" 2>/dev/null || true)"
|
||||
announce="$(subscription_metadata_clean_text "$(subscription_metadata_raw_value "$raw_json" "announce")" 500 base64 2>/dev/null || true)"
|
||||
announce_url="$(subscription_metadata_clean_url "$(subscription_metadata_raw_value "$raw_json" "announce-url")" 2>/dev/null || true)"
|
||||
refill_date="$(subscription_metadata_clean_number "$(subscription_metadata_raw_value "$raw_json" "subscription-refill-date")" 2>/dev/null || true)"
|
||||
file_name="$(subscription_metadata_content_disposition_filename "$(subscription_metadata_raw_value "$raw_json" "content-disposition")" 2>/dev/null || true)"
|
||||
|
||||
userinfo="$(subscription_metadata_raw_value "$raw_json" "subscription-userinfo")"
|
||||
upload=""
|
||||
download=""
|
||||
total=""
|
||||
expire=""
|
||||
|
||||
while IFS= read -r item || [ -n "$item" ]; do
|
||||
item="$(printf '%s' "$item" | subscription_metadata_trim)"
|
||||
case "$item" in
|
||||
*=*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
|
||||
key="$(printf '%s' "${item%%=*}" | subscription_metadata_trim | subscription_metadata_lower)"
|
||||
value="$(subscription_metadata_clean_number "${item#*=}" 2>/dev/null || true)"
|
||||
[ -n "$value" ] || continue
|
||||
|
||||
case "$key" in
|
||||
upload) upload="$value" ;;
|
||||
download) download="$value" ;;
|
||||
total) total="$value" ;;
|
||||
expire) expire="$value" ;;
|
||||
esac
|
||||
done <<EOF
|
||||
$(printf '%s' "$userinfo" | tr ';' '\n')
|
||||
EOF
|
||||
|
||||
has_traffic=false
|
||||
[ -n "$upload$download$total$expire" ] && has_traffic=true
|
||||
|
||||
upload_json="${upload:-null}"
|
||||
download_json="${download:-null}"
|
||||
total_json="${total:-null}"
|
||||
expire_json="${expire:-null}"
|
||||
refill_date_json="${refill_date:-null}"
|
||||
|
||||
used=$(( ${upload:-0} + ${download:-0} ))
|
||||
remaining_json="null"
|
||||
is_unlimited=true
|
||||
if [ -n "$total" ] && [ "$total" -gt 0 ]; then
|
||||
is_unlimited=false
|
||||
remaining=$((total - used))
|
||||
[ "$remaining" -lt 0 ] && remaining=0
|
||||
remaining_json="$remaining"
|
||||
fi
|
||||
|
||||
jq -cn \
|
||||
--arg title "$title" \
|
||||
--arg webPageUrl "$web_page_url" \
|
||||
--arg supportUrl "$support_url" \
|
||||
--arg announce "$announce" \
|
||||
--arg announceUrl "$announce_url" \
|
||||
--arg fileName "$file_name" \
|
||||
--argjson hasTraffic "$has_traffic" \
|
||||
--argjson upload "$upload_json" \
|
||||
--argjson download "$download_json" \
|
||||
--argjson used "$used" \
|
||||
--argjson total "$total_json" \
|
||||
--argjson remaining "$remaining_json" \
|
||||
--argjson isUnlimited "$is_unlimited" \
|
||||
--argjson expire "$expire_json" \
|
||||
--argjson refillDate "$refill_date_json" '
|
||||
{
|
||||
version: 1
|
||||
}
|
||||
+ (if $title != "" then {title: $title} else {} end)
|
||||
+ (if $hasTraffic then
|
||||
{
|
||||
traffic: (
|
||||
{}
|
||||
+ (if $upload != null then {upload: $upload} else {} end)
|
||||
+ (if $download != null then {download: $download} else {} end)
|
||||
+ {used: $used, isUnlimited: $isUnlimited}
|
||||
+ (if $total != null and $total > 0 then {total: $total, remaining: $remaining} else {} end)
|
||||
)
|
||||
}
|
||||
else {} end)
|
||||
+ (if $expire != null and $expire > 0 then {expire: $expire} else {} end)
|
||||
+ (if $refillDate != null and $refillDate > 0 then {refillDate: $refillDate} else {} end)
|
||||
+ (if $webPageUrl != "" then {webPageUrl: $webPageUrl} else {} end)
|
||||
+ (if $supportUrl != "" then {supportUrl: $supportUrl} else {} end)
|
||||
+ (if $announce != "" then {announce: $announce} else {} end)
|
||||
+ (if $announceUrl != "" then {announceUrl: $announceUrl} else {} end)
|
||||
+ (if $fileName != "" then {fileName: $fileName} else {} end)
|
||||
| if (keys | length) > 1 then . else empty end
|
||||
'
|
||||
}
|
||||
|
||||
subscription_extract_ui_metadata() {
|
||||
local headers_file="$1"
|
||||
local body_file="$2"
|
||||
local output="$3"
|
||||
local headers_json body_json raw_json metadata_json
|
||||
|
||||
headers_json="$(subscription_metadata_headers_json "$headers_file")" || headers_json="{}"
|
||||
body_json="$(subscription_metadata_body_json "$body_file")" || body_json="{}"
|
||||
|
||||
raw_json="$(jq -cn --argjson body "$body_json" --argjson headers "$headers_json" '$body + $headers')" || {
|
||||
rm -f "$output"
|
||||
return 1
|
||||
}
|
||||
|
||||
metadata_json="$(subscription_normalize_ui_metadata_json "$raw_json")" || metadata_json=""
|
||||
if [ -n "$metadata_json" ] && printf '%s\n' "$metadata_json" | jq -e 'type == "object" and (keys | length > 1)' >/dev/null 2>&1; then
|
||||
printf '%s\n' "$metadata_json" > "$output"
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f "$output"
|
||||
return 1
|
||||
}
|
||||
|
||||
subscription_strip_metadata_preamble_file() {
|
||||
local input="$1"
|
||||
local tmpfile
|
||||
|
||||
[ -s "$input" ] || return 0
|
||||
|
||||
tmpfile="$(mktemp)" || return 0
|
||||
awk '
|
||||
function allowed(k) {
|
||||
return k == "profile-title" ||
|
||||
k == "subscription-userinfo" ||
|
||||
k == "profile-web-page-url" ||
|
||||
k == "support-url" ||
|
||||
k == "announce" ||
|
||||
k == "announce-url" ||
|
||||
k == "subscription-refill-date" ||
|
||||
k == "content-disposition"
|
||||
}
|
||||
NR <= 20 {
|
||||
line = $0
|
||||
sub(/\r$/, "", line)
|
||||
gsub(/^[ \t]+|[ \t]+$/, "", line)
|
||||
if (line ~ /^(#|\/\/)[ \t]*[A-Za-z0-9][A-Za-z0-9_-]*[ \t]*:/) {
|
||||
key = line
|
||||
sub(/^(#|\/\/)[ \t]*/, "", key)
|
||||
sub(/[ \t]*:.*/, "", key)
|
||||
key = tolower(key)
|
||||
if (!allowed(key)) {
|
||||
print
|
||||
next
|
||||
}
|
||||
next
|
||||
}
|
||||
}
|
||||
{
|
||||
print
|
||||
}
|
||||
' "$input" > "$tmpfile" && mv "$tmpfile" "$input" || rm -f "$tmpfile"
|
||||
}
|
||||
|
||||
subscription_validate_normalized_file() {
|
||||
local output="$1"
|
||||
|
||||
@@ -1475,6 +1907,7 @@ subscription_normalize_content_file() {
|
||||
local decoded_tmp
|
||||
|
||||
convert_crlf_to_lf "$input"
|
||||
subscription_strip_metadata_preamble_file "$input"
|
||||
|
||||
if jq -e . "$input" >/dev/null 2>&1; then
|
||||
subscription_normalize_sing_box_json_file "$input" "$output" || return 1
|
||||
|
||||
Reference in New Issue
Block a user