mirror of
https://git.mirrors.martin98.com/https://github.com/sub-store-org/Sub-Store.git
synced 2025-09-12 16:13:14 +08:00
refactor: Download API
Now the download APIs are moved into a new file
This commit is contained in:
parent
9653b09844
commit
bb87a6c41e
6
backend/dist/cron-sync-artifacts.min.js
vendored
6
backend/dist/cron-sync-artifacts.min.js
vendored
File diff suppressed because one or more lines are too long
6
backend/dist/sub-store-parser.loon.min.js
vendored
6
backend/dist/sub-store-parser.loon.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.2.2",
|
||||
"version": "2.2.3",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
|
@ -29,6 +29,14 @@ export default function register($app) {
|
||||
$app.get('/api/cron/sync-artifacts', cronSyncArtifacts);
|
||||
}
|
||||
|
||||
function getAllArtifacts(req, res) {
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
res.json({
|
||||
status: 'success',
|
||||
data: allArtifacts,
|
||||
});
|
||||
}
|
||||
|
||||
async function getArtifact(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
@ -152,6 +160,37 @@ function updateArtifact(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteArtifact(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
$.info(`正在删除远程配置:${name}`);
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
try {
|
||||
const artifact = allArtifacts[name];
|
||||
if (!artifact) throw new Error(`远程配置:${name}不存在!`);
|
||||
if (artifact.updated) {
|
||||
// delete gist
|
||||
const files = {};
|
||||
files[encodeURIComponent(artifact.name)] = {
|
||||
content: '',
|
||||
};
|
||||
await syncArtifact(files);
|
||||
}
|
||||
// delete local cache
|
||||
delete allArtifacts[name];
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
res.json({
|
||||
status: 'success',
|
||||
});
|
||||
} catch (err) {
|
||||
$.error(`无法删除远程配置:${name},原因:${err}`);
|
||||
res.status(500).json({
|
||||
status: 'failed',
|
||||
message: `无法删除远程配置:${name}, 原因:${err}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function cronSyncArtifacts(_, res) {
|
||||
$.info('开始同步所有远程配置...');
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
@ -210,45 +249,6 @@ async function cronSyncArtifacts(_, res) {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteArtifact(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
$.info(`正在删除远程配置:${name}`);
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
try {
|
||||
const artifact = allArtifacts[name];
|
||||
if (!artifact) throw new Error(`远程配置:${name}不存在!`);
|
||||
if (artifact.updated) {
|
||||
// delete gist
|
||||
const files = {};
|
||||
files[encodeURIComponent(artifact.name)] = {
|
||||
content: '',
|
||||
};
|
||||
await syncArtifact(files);
|
||||
}
|
||||
// delete local cache
|
||||
delete allArtifacts[name];
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
res.json({
|
||||
status: 'success',
|
||||
});
|
||||
} catch (err) {
|
||||
$.error(`无法删除远程配置:${name},原因:${err}`);
|
||||
res.status(500).json({
|
||||
status: 'failed',
|
||||
message: `无法删除远程配置:${name}, 原因:${err}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getAllArtifacts(req, res) {
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
res.json({
|
||||
status: 'success',
|
||||
data: allArtifacts,
|
||||
});
|
||||
}
|
||||
|
||||
async function syncArtifact(files) {
|
||||
const { gistToken } = $.read(SETTINGS_KEY);
|
||||
if (!gistToken) {
|
||||
@ -276,6 +276,7 @@ async function produceArtifact({ type, item, platform, noProcessor }) {
|
||||
// parse proxies
|
||||
let proxies = ProxyUtils.parse(raw);
|
||||
if (!noProcessor) {
|
||||
console.log('Processing proxy...');
|
||||
// apply processors
|
||||
proxies = await ProxyUtils.process(
|
||||
proxies,
|
||||
|
@ -1,13 +1,9 @@
|
||||
import { getPlatformFromHeaders, getFlowHeaders } from './subscriptions';
|
||||
import { SUBS_KEY, COLLECTIONS_KEY } from './constants';
|
||||
import { produceArtifact } from './artifacts';
|
||||
import { COLLECTIONS_KEY } from './constants';
|
||||
import $ from '@/core/app';
|
||||
|
||||
export default function register($app) {
|
||||
if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
|
||||
|
||||
$app.get('/download/collection/:name', downloadCollection);
|
||||
|
||||
$app.route('/api/collection/:name')
|
||||
.get(getCollection)
|
||||
.patch(updateCollection)
|
||||
@ -19,69 +15,6 @@ export default function register($app) {
|
||||
}
|
||||
|
||||
// collection API
|
||||
async function downloadCollection(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
|
||||
const { raw } = req.query || 'false';
|
||||
const platform =
|
||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||
|
||||
const allCollections = $.read(COLLECTIONS_KEY);
|
||||
const collection = allCollections[name];
|
||||
|
||||
$.info(`正在下载组合订阅:${name}`);
|
||||
|
||||
// forward flow header from the first subscription in this collection
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const subs = collection['subscriptions'];
|
||||
if (subs.length > 0) {
|
||||
const sub = allSubs[subs[0]];
|
||||
if (sub.source !== 'local') {
|
||||
const flowInfo = await getFlowHeaders(sub.url);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
try {
|
||||
const output = await produceArtifact({
|
||||
type: 'collection',
|
||||
item: collection,
|
||||
platform,
|
||||
noProcessor: raw,
|
||||
});
|
||||
if (platform === 'JSON') {
|
||||
res.set('Content-Type', 'application/json;charset=utf-8').send(
|
||||
output,
|
||||
);
|
||||
} else {
|
||||
res.send(output);
|
||||
}
|
||||
} catch (err) {
|
||||
$.notify(
|
||||
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
|
||||
`❌ 下载组合订阅错误:${name}!`,
|
||||
`🤔 原因:${err}`,
|
||||
);
|
||||
res.status(500).json({
|
||||
status: 'failed',
|
||||
message: err,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$.notify(
|
||||
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
|
||||
`❌ 未找到组合订阅:${name}!`,
|
||||
);
|
||||
res.status(404).json({
|
||||
status: 'failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createCollection(req, res) {
|
||||
const collection = req.body;
|
||||
$.info(`正在创建组合订阅:${collection.name}`);
|
||||
|
0
backend/src/restful/compare.js
Normal file
0
backend/src/restful/compare.js
Normal file
129
backend/src/restful/download.js
Normal file
129
backend/src/restful/download.js
Normal file
@ -0,0 +1,129 @@
|
||||
import { getPlatformFromHeaders } from '@/utils/platform';
|
||||
import { COLLECTIONS_KEY, SUBS_KEY } from './constants';
|
||||
import { getFlowHeaders } from '@/utils/flow';
|
||||
import { produceArtifact } from './artifacts';
|
||||
import $ from '@/core/app';
|
||||
|
||||
export default function register($app) {
|
||||
$app.get('/download/collection/:name', downloadCollection);
|
||||
$app.get('/download/:name', downloadSubscription);
|
||||
}
|
||||
|
||||
async function downloadSubscription(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
|
||||
const raw = req.query.raw || false;
|
||||
const platform =
|
||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||
|
||||
$.info(`正在下载订阅:${name}`);
|
||||
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const sub = allSubs[name];
|
||||
if (sub) {
|
||||
try {
|
||||
const output = await produceArtifact({
|
||||
type: 'subscription',
|
||||
item: sub,
|
||||
platform,
|
||||
noProcessor: raw,
|
||||
});
|
||||
|
||||
if (sub.source !== 'local') {
|
||||
// forward flow headers
|
||||
const flowInfo = await getFlowHeaders(sub.url);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === 'JSON') {
|
||||
res.set('Content-Type', 'application/json;charset=utf-8').send(
|
||||
output,
|
||||
);
|
||||
} else {
|
||||
res.send(output);
|
||||
}
|
||||
} catch (err) {
|
||||
$.notify(
|
||||
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`,
|
||||
`❌ 无法下载订阅:${name}!`,
|
||||
`🤔 原因:${JSON.stringify(err)}`,
|
||||
);
|
||||
$.error(JSON.stringify(err));
|
||||
res.status(500).json({
|
||||
status: 'failed',
|
||||
message: err,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`, `❌ 未找到订阅:${name}!`);
|
||||
res.status(404).json({
|
||||
status: 'failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadCollection(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
|
||||
const { raw } = req.query || 'false';
|
||||
const platform =
|
||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||
|
||||
const allCollections = $.read(COLLECTIONS_KEY);
|
||||
const collection = allCollections[name];
|
||||
|
||||
$.info(`正在下载组合订阅:${name}`);
|
||||
|
||||
// forward flow header from the first subscription in this collection
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const subs = collection['subscriptions'];
|
||||
if (subs.length > 0) {
|
||||
const sub = allSubs[subs[0]];
|
||||
if (sub.source !== 'local') {
|
||||
const flowInfo = await getFlowHeaders(sub.url);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
try {
|
||||
const output = await produceArtifact({
|
||||
type: 'collection',
|
||||
item: collection,
|
||||
platform,
|
||||
noProcessor: raw,
|
||||
});
|
||||
if (platform === 'JSON') {
|
||||
res.set('Content-Type', 'application/json;charset=utf-8').send(
|
||||
output,
|
||||
);
|
||||
} else {
|
||||
res.send(output);
|
||||
}
|
||||
} catch (err) {
|
||||
$.notify(
|
||||
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
|
||||
`❌ 下载组合订阅错误:${name}!`,
|
||||
`🤔 原因:${err}`,
|
||||
);
|
||||
res.status(500).json({
|
||||
status: 'failed',
|
||||
message: err,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$.notify(
|
||||
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
|
||||
`❌ 未找到组合订阅:${name}!`,
|
||||
);
|
||||
res.status(404).json({
|
||||
status: 'failed',
|
||||
});
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ import $ from '@/core/app';
|
||||
import registerSubscriptionRoutes from './subscriptions';
|
||||
import registerCollectionRoutes from './collections';
|
||||
import registerArtifactRoutes from './artifacts';
|
||||
import registerDownloadRoutes from './download';
|
||||
import registerSettingRoutes from './settings';
|
||||
|
||||
export default function serve() {
|
||||
@ -20,6 +21,7 @@ export default function serve() {
|
||||
// register routes
|
||||
registerCollectionRoutes($app);
|
||||
registerSubscriptionRoutes($app);
|
||||
registerDownloadRoutes($app);
|
||||
registerSettingRoutes($app);
|
||||
registerArtifactRoutes($app);
|
||||
|
||||
|
0
backend/src/restful/preview.js
Normal file
0
backend/src/restful/preview.js
Normal file
@ -4,15 +4,12 @@ import {
|
||||
ResourceNotFoundError,
|
||||
} from './errors';
|
||||
import { SUBS_KEY, COLLECTIONS_KEY } from './constants';
|
||||
import { produceArtifact } from './artifacts';
|
||||
import { getFlowHeaders } from '@/utils/flow';
|
||||
import { success, failed } from './response';
|
||||
import $ from '@/core/app';
|
||||
|
||||
export default function register($app) {
|
||||
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
|
||||
|
||||
$app.get('/download/:name', downloadSubscription);
|
||||
|
||||
$app.get('/api/sub/flow/:name', getFlowInfo);
|
||||
|
||||
$app.route('/api/sub/:name')
|
||||
@ -24,62 +21,6 @@ export default function register($app) {
|
||||
}
|
||||
|
||||
// subscriptions API
|
||||
async function downloadSubscription(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
|
||||
const { raw } = req.query || 'false';
|
||||
const platform =
|
||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||
|
||||
$.info(`正在下载订阅:${name}`);
|
||||
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const sub = allSubs[name];
|
||||
if (sub) {
|
||||
try {
|
||||
const output = await produceArtifact({
|
||||
type: 'subscription',
|
||||
item: sub,
|
||||
platform,
|
||||
noProcessor: raw,
|
||||
});
|
||||
|
||||
if (sub.source !== 'local') {
|
||||
// forward flow headers
|
||||
const flowInfo = await getFlowHeaders(sub.url);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === 'JSON') {
|
||||
res.set('Content-Type', 'application/json;charset=utf-8').send(
|
||||
output,
|
||||
);
|
||||
} else {
|
||||
res.send(output);
|
||||
}
|
||||
} catch (err) {
|
||||
$.notify(
|
||||
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`,
|
||||
`❌ 无法下载订阅:${name}!`,
|
||||
`🤔 原因:${JSON.stringify(err)}`,
|
||||
);
|
||||
$.error(JSON.stringify(err));
|
||||
res.status(500).json({
|
||||
status: 'failed',
|
||||
message: err,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`, `❌ 未找到订阅:${name}!`);
|
||||
res.status(404).json({
|
||||
status: 'failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function getFlowInfo(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
@ -231,40 +172,3 @@ function getAllSubscriptions(req, res) {
|
||||
data: allSubs,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getFlowHeaders(url) {
|
||||
const { headers } = await $.http.get({
|
||||
url,
|
||||
headers: {
|
||||
'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)',
|
||||
},
|
||||
});
|
||||
const subkey = Object.keys(headers).filter((k) =>
|
||||
/SUBSCRIPTION-USERINFO/i.test(k),
|
||||
)[0];
|
||||
return headers[subkey];
|
||||
}
|
||||
|
||||
export function getPlatformFromHeaders(headers) {
|
||||
const keys = Object.keys(headers);
|
||||
let UA = '';
|
||||
for (let k of keys) {
|
||||
if (/USER-AGENT/i.test(k)) {
|
||||
UA = headers[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (UA.indexOf('Quantumult%20X') !== -1) {
|
||||
return 'QX';
|
||||
} else if (UA.indexOf('Surge') !== -1) {
|
||||
return 'Surge';
|
||||
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
|
||||
return 'Loon';
|
||||
} else if (UA.indexOf('Shadowrocket') !== -1) {
|
||||
return 'Clash';
|
||||
} else if (UA.indexOf('Stash') !== -1) {
|
||||
return 'Stash';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -16,12 +16,16 @@ export default async function download(url, ua) {
|
||||
});
|
||||
|
||||
const result = new Promise((resolve, reject) => {
|
||||
http.get(url).then((resp) => {
|
||||
const body = resp.body;
|
||||
if (body.replace(/\s/g, '').length === 0)
|
||||
reject(new Error('订阅内容为空!'));
|
||||
else resolve(body);
|
||||
});
|
||||
http.get(url)
|
||||
.then((resp) => {
|
||||
const body = resp.body;
|
||||
if (body.replace(/\s/g, '').length === 0)
|
||||
reject(new Error('订阅内容为空!'));
|
||||
else resolve(body);
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error(`无法下载 URL:${url}`));
|
||||
});
|
||||
});
|
||||
|
||||
cache.set(id, result);
|
||||
|
15
backend/src/utils/flow.js
Normal file
15
backend/src/utils/flow.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { HTTP } from '@/vendor/open-api';
|
||||
|
||||
export async function getFlowHeaders(url) {
|
||||
const http = HTTP();
|
||||
const { headers } = await http.get({
|
||||
url,
|
||||
headers: {
|
||||
'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)',
|
||||
},
|
||||
});
|
||||
const subkey = Object.keys(headers).filter((k) =>
|
||||
/SUBSCRIPTION-USERINFO/i.test(k),
|
||||
)[0];
|
||||
return headers[subkey];
|
||||
}
|
23
backend/src/utils/platform.js
Normal file
23
backend/src/utils/platform.js
Normal file
@ -0,0 +1,23 @@
|
||||
export function getPlatformFromHeaders(headers) {
|
||||
const keys = Object.keys(headers);
|
||||
let UA = '';
|
||||
for (let k of keys) {
|
||||
if (/USER-AGENT/i.test(k)) {
|
||||
UA = headers[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (UA.indexOf('Quantumult%20X') !== -1) {
|
||||
return 'QX';
|
||||
} else if (UA.indexOf('Surge') !== -1) {
|
||||
return 'Surge';
|
||||
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
|
||||
return 'Loon';
|
||||
} else if (UA.indexOf('Shadowrocket') !== -1) {
|
||||
return 'Clash';
|
||||
} else if (UA.indexOf('Stash') !== -1) {
|
||||
return 'Stash';
|
||||
} else {
|
||||
return 'JSON';
|
||||
}
|
||||
}
|
6
backend/sub-store.min.js
vendored
6
backend/sub-store.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user