Show subscription metadata in dashboard

This commit is contained in:
ushan0v
2026-05-14 14:36:06 +03:00
parent e59a6355fd
commit 8cbe407e9b
16 changed files with 1362 additions and 133 deletions
+49 -27
View File
@@ -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"
]
},
{
+42 -29
View File
@@ -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 ""
+11 -2
View File
@@ -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 "Интервал обновления подписки"
+1
View File
@@ -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;
+24
View File
@@ -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;
+11 -2
View File
@@ -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
View File
@@ -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
;;
+70 -16
View File
@@ -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)" \
+433
View File
@@ -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