527 lines
20 KiB
JavaScript
527 lines
20 KiB
JavaScript
(function (root, factory) {
|
|
"use strict";
|
|
|
|
const api = Object.freeze(factory());
|
|
if (typeof module === "object" && module.exports) module.exports = api;
|
|
if (root) root.MbnOverseasWorkflow = api;
|
|
})(typeof globalThis === "object" ? globalThis : this, function () {
|
|
"use strict";
|
|
|
|
const DEFAULT_FADE_DURATION = 6;
|
|
const DEFAULT_MAXIMUM_RESULTS = 100;
|
|
const MAXIMUM_RESULTS = 500;
|
|
const MAXIMUM_QUERY_LENGTH = 64;
|
|
const SCHEMA_VERSION = 1;
|
|
const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/;
|
|
// F_SYMB is provider identity data, not executable syntax. Current Oracle
|
|
// contains Hangul-letter symbols with internal ASCII spaces; keep letters and
|
|
// numbers Unicode-aware and allow only that observed space plus a closed
|
|
// punctuation set. Canonical trim and this pattern still exclude edge space,
|
|
// `|`, `^`, controls and arbitrary punctuation from the dataCode identity.
|
|
const symbolPattern = /^[\p{L}\p{N}@._$:\x20-]{1,64}$/u;
|
|
const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u;
|
|
|
|
const bridgeContracts = Object.freeze({
|
|
industry: Object.freeze({
|
|
requestType: "search-overseas-industries",
|
|
resultType: "overseas-industry-results",
|
|
errorType: "overseas-industry-error",
|
|
defaultMaximumResults: DEFAULT_MAXIMUM_RESULTS,
|
|
maximumResults: MAXIMUM_RESULTS,
|
|
maximumQueryLength: MAXIMUM_QUERY_LENGTH
|
|
}),
|
|
stock: Object.freeze({
|
|
requestType: "search-world-stocks",
|
|
resultType: "world-stock-search-results",
|
|
errorType: "world-stock-search-error",
|
|
defaultMaximumResults: DEFAULT_MAXIMUM_RESULTS,
|
|
maximumResults: MAXIMUM_RESULTS,
|
|
maximumQueryLength: MAXIMUM_QUERY_LENGTH
|
|
})
|
|
});
|
|
|
|
const sourceContract = Object.freeze({
|
|
originalControl: "Control/UC5.cs",
|
|
industry: Object.freeze({
|
|
fdtc: "0",
|
|
nationCodes: Object.freeze(["US"]),
|
|
lookupField: "F_KNAM",
|
|
currentCutCode: "5001"
|
|
}),
|
|
stock: Object.freeze({
|
|
fdtc: "1",
|
|
nationCodes: Object.freeze(["US", "TW"]),
|
|
lookupField: "F_INPUT_NAME",
|
|
currentCutCode: "5001",
|
|
candleCutCodes: Object.freeze(["8061", "8040", "8046", "8051", "8056"])
|
|
})
|
|
});
|
|
|
|
function action(id, label, builderKey, code, kind, periodDays = null) {
|
|
return Object.freeze({
|
|
id,
|
|
label,
|
|
builderKey,
|
|
code,
|
|
aliases: Object.freeze([code]),
|
|
kind,
|
|
periodDays
|
|
});
|
|
}
|
|
|
|
const industryActions = Object.freeze([
|
|
action("industry-current", "\uBBF8\uAD6D \uD574\uC678\uC5C5\uC885 \uD604\uC7AC\uAC00 1\uC5F4\uD310", "s5001", "5001", "current")
|
|
]);
|
|
const stockActions = Object.freeze([
|
|
action("stock-current", "\uD574\uC678\uC885\uBAA9 \uD604\uC7AC\uAC00 1\uC5F4\uD310", "s5001", "5001", "current"),
|
|
action("stock-candle-5d", "\uD574\uC678\uC885\uBAA9 \uCE94\uB4E4 5\uC77C", "s8010", "8061", "candle", 5),
|
|
action("stock-candle-20d", "\uD574\uC678\uC885\uBAA9 \uCE94\uB4E4 20\uC77C", "s8010", "8040", "candle", 20),
|
|
action("stock-candle-60d", "\uD574\uC678\uC885\uBAA9 \uCE94\uB4E4 60\uC77C", "s8010", "8046", "candle", 60),
|
|
action("stock-candle-120d", "\uD574\uC678\uC885\uBAA9 \uCE94\uB4E4 120\uC77C", "s8010", "8051", "candle", 120),
|
|
action("stock-candle-240d", "\uD574\uC678\uC885\uBAA9 \uCE94\uB4E4 240\uC77C", "s8010", "8056", "candle", 240)
|
|
]);
|
|
const actionsById = new Map(
|
|
[...industryActions, ...stockActions].map(value => [value.id, value]));
|
|
|
|
function hasExactKeys(value, expected) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
const actual = Object.keys(value).sort();
|
|
const required = [...expected].sort();
|
|
return actual.length === required.length && actual.every((key, index) => key === required[index]);
|
|
}
|
|
|
|
function isSafeCanonicalText(value, maximumLength, allowEmpty = false) {
|
|
return typeof value === "string" &&
|
|
value.length <= maximumLength &&
|
|
(allowEmpty || value.length > 0) &&
|
|
value === value.trim() &&
|
|
!unsafeTextPattern.test(value);
|
|
}
|
|
|
|
function normalizeQuery(value) {
|
|
if (typeof value !== "string" || unsafeTextPattern.test(value)) return null;
|
|
const query = value.trim();
|
|
return query.length <= MAXIMUM_QUERY_LENGTH && !unsafeTextPattern.test(query) ? query : null;
|
|
}
|
|
|
|
function normalizeSymbol(value) {
|
|
return isSafeCanonicalText(value, 64) && symbolPattern.test(value) ? value : null;
|
|
}
|
|
|
|
function normalizeIndustry(value) {
|
|
if (!hasExactKeys(value, [
|
|
"koreanName", "inputName", "symbol", "nationCode", "fdtc"
|
|
])) return null;
|
|
if (!isSafeCanonicalText(value.koreanName, 200) || value.koreanName.includes("|") ||
|
|
!isSafeCanonicalText(value.inputName, 200) || value.inputName.includes("|") ||
|
|
!normalizeSymbol(value.symbol) || value.nationCode !== "US" || value.fdtc !== "0") {
|
|
return null;
|
|
}
|
|
return Object.freeze({
|
|
koreanName: value.koreanName,
|
|
inputName: value.inputName,
|
|
symbol: value.symbol,
|
|
nationCode: "US",
|
|
fdtc: "0"
|
|
});
|
|
}
|
|
|
|
function normalizeStock(value) {
|
|
if (!hasExactKeys(value, ["inputName", "symbol", "nationCode", "fdtc"])) return null;
|
|
if (!isSafeCanonicalText(value.inputName, 200) || value.inputName.includes("|") ||
|
|
!normalizeSymbol(value.symbol) || !["US", "TW"].includes(value.nationCode) ||
|
|
value.fdtc !== "1") {
|
|
return null;
|
|
}
|
|
return Object.freeze({
|
|
inputName: value.inputName,
|
|
symbol: value.symbol,
|
|
nationCode: value.nationCode,
|
|
fdtc: "1"
|
|
});
|
|
}
|
|
|
|
function createSearchRequest(requestId, query, maximumResults) {
|
|
if (!isSafeCanonicalText(requestId, 128) || !identifierPattern.test(requestId)) {
|
|
throw new TypeError("A safe overseas search request id is required.");
|
|
}
|
|
const normalizedQuery = normalizeQuery(query ?? "");
|
|
if (normalizedQuery === null) {
|
|
throw new TypeError(`An overseas search query must contain at most ${MAXIMUM_QUERY_LENGTH} safe characters.`);
|
|
}
|
|
if (maximumResults !== undefined &&
|
|
(!Number.isInteger(maximumResults) || maximumResults < 1 || maximumResults > MAXIMUM_RESULTS)) {
|
|
throw new RangeError(`Overseas results must be limited from 1 through ${MAXIMUM_RESULTS}.`);
|
|
}
|
|
const result = { requestId, query: normalizedQuery };
|
|
if (maximumResults !== undefined) result.maximumResults = maximumResults;
|
|
return Object.freeze(result);
|
|
}
|
|
|
|
function createIndustrySearchRequest(requestId, query = "", maximumResults) {
|
|
return createSearchRequest(requestId, query, maximumResults);
|
|
}
|
|
|
|
function createStockSearchRequest(requestId, query = "", maximumResults) {
|
|
return createSearchRequest(requestId, query, maximumResults);
|
|
}
|
|
|
|
function compareUpper(left, right) {
|
|
const upperLeft = left.toLocaleUpperCase("en-US");
|
|
const upperRight = right.toLocaleUpperCase("en-US");
|
|
if (upperLeft < upperRight) return -1;
|
|
if (upperLeft > upperRight) return 1;
|
|
return 0;
|
|
}
|
|
|
|
function compareOrdinal(left, right) {
|
|
if (left < right) return -1;
|
|
if (left > right) return 1;
|
|
return 0;
|
|
}
|
|
|
|
function compareIndustries(left, right) {
|
|
return compareUpper(left.koreanName, right.koreanName) ||
|
|
compareOrdinal(left.inputName, right.inputName) ||
|
|
compareOrdinal(left.symbol, right.symbol);
|
|
}
|
|
|
|
function compareStocks(left, right) {
|
|
return compareUpper(left.inputName, right.inputName) ||
|
|
compareOrdinal(left.symbol, right.symbol) ||
|
|
compareOrdinal(left.nationCode, right.nationCode);
|
|
}
|
|
|
|
function normalizeSearchResponse(value, expectedRequest, normalizeItem, compare, duplicateKeys) {
|
|
if (!hasExactKeys(value, [
|
|
"requestId", "query", "retrievedAt", "totalRowCount", "truncated", "results"
|
|
]) || !isSafeCanonicalText(value.requestId, 128) || !identifierPattern.test(value.requestId) ||
|
|
normalizeQuery(value.query) !== value.query || !isSafeCanonicalText(value.retrievedAt, 64) ||
|
|
!Number.isFinite(Date.parse(value.retrievedAt)) || !Number.isInteger(value.totalRowCount) ||
|
|
value.totalRowCount < 0 || value.totalRowCount > MAXIMUM_RESULTS ||
|
|
typeof value.truncated !== "boolean" || !Array.isArray(value.results) ||
|
|
value.results.length !== value.totalRowCount) {
|
|
return null;
|
|
}
|
|
if (expectedRequest) {
|
|
let expected;
|
|
try {
|
|
expected = createSearchRequest(
|
|
expectedRequest.requestId,
|
|
expectedRequest.query,
|
|
expectedRequest.maximumResults);
|
|
} catch {
|
|
return null;
|
|
}
|
|
const expectedLimit = expected.maximumResults ?? DEFAULT_MAXIMUM_RESULTS;
|
|
if (value.requestId !== expected.requestId || value.query !== expected.query ||
|
|
value.results.length > expectedLimit) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const results = [];
|
|
const keySets = duplicateKeys.map(() => new Set());
|
|
for (const raw of value.results) {
|
|
const item = normalizeItem(raw);
|
|
if (!item) return null;
|
|
for (let index = 0; index < duplicateKeys.length; index += 1) {
|
|
const key = duplicateKeys[index](item);
|
|
if (keySets[index].has(key)) return null;
|
|
keySets[index].add(key);
|
|
}
|
|
if (results.length && compare(results[results.length - 1], item) > 0) return null;
|
|
results.push(item);
|
|
}
|
|
return Object.freeze({
|
|
requestId: value.requestId,
|
|
query: value.query,
|
|
retrievedAt: value.retrievedAt,
|
|
totalRowCount: value.totalRowCount,
|
|
truncated: value.truncated,
|
|
results: Object.freeze(results)
|
|
});
|
|
}
|
|
|
|
function normalizeIndustrySearchResponse(value, expectedRequest) {
|
|
return normalizeSearchResponse(
|
|
value,
|
|
expectedRequest,
|
|
normalizeIndustry,
|
|
compareIndustries,
|
|
[item => item.koreanName, item => item.inputName, item => item.symbol]);
|
|
}
|
|
|
|
function normalizeStockSearchResponse(value, expectedRequest) {
|
|
return normalizeSearchResponse(
|
|
value,
|
|
expectedRequest,
|
|
normalizeStock,
|
|
compareStocks,
|
|
[item => item.inputName, item => `${item.inputName}\u0000${item.symbol}\u0000${item.nationCode}`]);
|
|
}
|
|
|
|
function normalizeSearchError(value, expectedRequest) {
|
|
if (!hasExactKeys(value, ["requestId", "query", "message"]) ||
|
|
!isSafeCanonicalText(value.requestId, 128) || !identifierPattern.test(value.requestId) ||
|
|
normalizeQuery(value.query) !== value.query || !isSafeCanonicalText(value.message, 512)) {
|
|
return null;
|
|
}
|
|
if (expectedRequest &&
|
|
(value.requestId !== expectedRequest.requestId ||
|
|
value.query !== normalizeQuery(expectedRequest.query))) return null;
|
|
return Object.freeze({ requestId: value.requestId, query: value.query, message: value.message });
|
|
}
|
|
|
|
function normalizeIndustrySearchError(value, expectedRequest) {
|
|
return normalizeSearchError(value, expectedRequest);
|
|
}
|
|
|
|
function normalizeStockSearchError(value, expectedRequest) {
|
|
return normalizeSearchError(value, expectedRequest);
|
|
}
|
|
|
|
function requireOptions(options) {
|
|
if (!options || typeof options !== "object" || Array.isArray(options) ||
|
|
!isSafeCanonicalText(options.id, 128) || !identifierPattern.test(options.id)) {
|
|
throw new TypeError("Safe overseas playlist options are required.");
|
|
}
|
|
const fadeDuration = options.fadeDuration === undefined
|
|
? DEFAULT_FADE_DURATION
|
|
: options.fadeDuration;
|
|
const ma5 = options.ma5 === undefined ? false : options.ma5;
|
|
const ma20 = options.ma20 === undefined ? false : options.ma20;
|
|
if (!Number.isInteger(fadeDuration) || fadeDuration < 0 || fadeDuration > 60) {
|
|
throw new RangeError("Fade duration must be an integer from 0 through 60.");
|
|
}
|
|
if (typeof ma5 !== "boolean" || typeof ma20 !== "boolean") {
|
|
throw new TypeError("Moving-average flags must be boolean values.");
|
|
}
|
|
return Object.freeze({ id: options.id, fadeDuration, ma5, ma20 });
|
|
}
|
|
|
|
function encodeIndustryIdentity(value) {
|
|
return `${value.inputName}|${value.symbol}|${value.nationCode}|${value.fdtc}`;
|
|
}
|
|
|
|
function encodeStockIdentity(value) {
|
|
return `${value.symbol}|${value.nationCode}|${value.fdtc}`;
|
|
}
|
|
|
|
function movingAverageFlags(actionValue, options) {
|
|
const enabled = actionValue.kind === "candle";
|
|
const movingAverages = Object.freeze({
|
|
ma5: enabled && options.ma5,
|
|
ma20: enabled && options.ma20
|
|
});
|
|
const flags = [];
|
|
if (movingAverages.ma5) flags.push("MA5");
|
|
if (movingAverages.ma20) flags.push("MA20");
|
|
return Object.freeze({ movingAverages, graphicType: flags.join(",") });
|
|
}
|
|
|
|
function buildOverseasIndustrySelection(industryValue) {
|
|
const industry = normalizeIndustry(industryValue);
|
|
if (!industry) throw new TypeError("A valid US overseas-industry identity is required.");
|
|
return Object.freeze({
|
|
builderKey: "s5001",
|
|
code: "5001",
|
|
aliases: Object.freeze(["5001"]),
|
|
selection: Object.freeze({
|
|
groupCode: "FOREIGN_INDUSTRY",
|
|
subject: industry.koreanName,
|
|
graphicType: "\u0031\uC5F4\uD310\uAE30\uBCF8",
|
|
subtype: "CURRENT",
|
|
dataCode: encodeIndustryIdentity(industry)
|
|
}),
|
|
movingAverages: Object.freeze({ ma5: false, ma20: false })
|
|
});
|
|
}
|
|
|
|
function buildOverseasStockSelection(stockValue, actionId, options = {}) {
|
|
const stock = normalizeStock(stockValue);
|
|
const selectedAction = actionsById.get(actionId);
|
|
if (!stock) throw new TypeError("A valid US/TW overseas-stock identity is required.");
|
|
if (!selectedAction || !stockActions.includes(selectedAction)) {
|
|
throw new RangeError("The overseas-stock action is unknown.");
|
|
}
|
|
const ma5 = options.ma5 === undefined ? false : options.ma5;
|
|
const ma20 = options.ma20 === undefined ? false : options.ma20;
|
|
if (typeof ma5 !== "boolean" || typeof ma20 !== "boolean") {
|
|
throw new TypeError("Moving-average flags must be boolean values.");
|
|
}
|
|
const flags = movingAverageFlags(selectedAction, { ma5, ma20 });
|
|
return Object.freeze({
|
|
builderKey: selectedAction.builderKey,
|
|
code: selectedAction.code,
|
|
aliases: selectedAction.aliases,
|
|
selection: Object.freeze({
|
|
groupCode: "FOREIGN_STOCK",
|
|
subject: stock.inputName,
|
|
graphicType: selectedAction.kind === "current"
|
|
? "\u0031\uC5F4\uD310\uAE30\uBCF8"
|
|
: flags.graphicType,
|
|
subtype: selectedAction.kind === "current" ? "CURRENT" : "PRICE",
|
|
dataCode: encodeStockIdentity(stock)
|
|
}),
|
|
movingAverages: flags.movingAverages
|
|
});
|
|
}
|
|
|
|
function createOverseasIndustryPlaylistEntry(industryValue, options) {
|
|
const industry = normalizeIndustry(industryValue);
|
|
if (!industry) throw new TypeError("A valid US overseas-industry identity is required.");
|
|
const normalizedOptions = requireOptions(options);
|
|
const mapping = buildOverseasIndustrySelection(industry);
|
|
const actionValue = industryActions[0];
|
|
return {
|
|
id: normalizedOptions.id,
|
|
builderKey: mapping.builderKey,
|
|
code: mapping.code,
|
|
aliases: [...mapping.aliases],
|
|
title: industry.koreanName,
|
|
detail: actionValue.label,
|
|
category: "plate",
|
|
market: "overseas",
|
|
symbol: industry.symbol,
|
|
nationCode: industry.nationCode,
|
|
fdtc: industry.fdtc,
|
|
selection: mapping.selection,
|
|
enabled: true,
|
|
fadeDuration: normalizedOptions.fadeDuration,
|
|
graphicType: mapping.selection.graphicType,
|
|
subtype: mapping.selection.subtype,
|
|
operator: Object.freeze({
|
|
source: "legacy-overseas-workflow",
|
|
schemaVersion: SCHEMA_VERSION,
|
|
kind: "industry",
|
|
actionId: actionValue.id,
|
|
identity: industry,
|
|
movingAverages: mapping.movingAverages
|
|
})
|
|
};
|
|
}
|
|
|
|
function createOverseasStockPlaylistEntry(stockValue, actionId, options) {
|
|
const stock = normalizeStock(stockValue);
|
|
if (!stock) throw new TypeError("A valid US/TW overseas-stock identity is required.");
|
|
const selectedAction = actionsById.get(actionId);
|
|
if (!selectedAction || !stockActions.includes(selectedAction)) {
|
|
throw new RangeError("The overseas-stock action is unknown.");
|
|
}
|
|
const normalizedOptions = requireOptions(options);
|
|
const mapping = buildOverseasStockSelection(stock, actionId, normalizedOptions);
|
|
return {
|
|
id: normalizedOptions.id,
|
|
builderKey: mapping.builderKey,
|
|
code: mapping.code,
|
|
aliases: [...mapping.aliases],
|
|
title: stock.inputName,
|
|
detail: selectedAction.label,
|
|
category: selectedAction.kind === "candle" ? "chart" : "stock",
|
|
market: "overseas",
|
|
symbol: stock.symbol,
|
|
nationCode: stock.nationCode,
|
|
fdtc: stock.fdtc,
|
|
selection: mapping.selection,
|
|
enabled: true,
|
|
fadeDuration: normalizedOptions.fadeDuration,
|
|
graphicType: mapping.selection.graphicType,
|
|
subtype: mapping.selection.subtype,
|
|
operator: Object.freeze({
|
|
source: "legacy-overseas-workflow",
|
|
schemaVersion: SCHEMA_VERSION,
|
|
kind: "stock",
|
|
actionId,
|
|
identity: stock,
|
|
movingAverages: mapping.movingAverages
|
|
})
|
|
};
|
|
}
|
|
|
|
function sameSelection(left, right) {
|
|
return hasExactKeys(left, ["groupCode", "subject", "graphicType", "subtype", "dataCode"]) &&
|
|
hasExactKeys(right, ["groupCode", "subject", "graphicType", "subtype", "dataCode"]) &&
|
|
["groupCode", "subject", "graphicType", "subtype", "dataCode"]
|
|
.every(key => left[key] === right[key]);
|
|
}
|
|
|
|
function sameAliases(value, recreated) {
|
|
return Array.isArray(value.aliases) && value.aliases.length === 1 &&
|
|
value.aliases[0] === recreated.code;
|
|
}
|
|
|
|
function restoreOverseasPlaylistEntry(value) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value) ||
|
|
typeof value.enabled !== "boolean") return null;
|
|
const operator = value.operator;
|
|
if (!hasExactKeys(operator, [
|
|
"source", "schemaVersion", "kind", "actionId", "identity", "movingAverages"
|
|
]) || operator.source !== "legacy-overseas-workflow" ||
|
|
operator.schemaVersion !== SCHEMA_VERSION ||
|
|
!hasExactKeys(operator.movingAverages, ["ma5", "ma20"]) ||
|
|
typeof operator.movingAverages.ma5 !== "boolean" ||
|
|
typeof operator.movingAverages.ma20 !== "boolean") return null;
|
|
|
|
let recreated;
|
|
try {
|
|
recreated = operator.kind === "industry"
|
|
? createOverseasIndustryPlaylistEntry(operator.identity, {
|
|
id: value.id,
|
|
fadeDuration: value.fadeDuration,
|
|
ma5: operator.movingAverages.ma5,
|
|
ma20: operator.movingAverages.ma20
|
|
})
|
|
: operator.kind === "stock"
|
|
? createOverseasStockPlaylistEntry(operator.identity, operator.actionId, {
|
|
id: value.id,
|
|
fadeDuration: value.fadeDuration,
|
|
ma5: operator.movingAverages.ma5,
|
|
ma20: operator.movingAverages.ma20
|
|
})
|
|
: null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
if (!recreated || operator.actionId !== recreated.operator.actionId) return null;
|
|
const scalars = [
|
|
"id", "builderKey", "code", "title", "detail", "category", "market",
|
|
"symbol", "nationCode", "fdtc", "fadeDuration", "graphicType", "subtype"
|
|
];
|
|
if (!scalars.every(key => value[key] === recreated[key]) ||
|
|
!sameAliases(value, recreated) || !sameSelection(value.selection, recreated.selection) ||
|
|
JSON.stringify(operator) !== JSON.stringify(recreated.operator)) return null;
|
|
return { ...recreated, enabled: value.enabled };
|
|
}
|
|
|
|
function isActionAllowed(kind, actionId, identity) {
|
|
if (kind === "industry") {
|
|
return actionId === "industry-current" && Boolean(normalizeIndustry(identity));
|
|
}
|
|
return kind === "stock" && Boolean(normalizeStock(identity)) &&
|
|
stockActions.some(value => value.id === actionId);
|
|
}
|
|
|
|
return {
|
|
bridgeContracts,
|
|
sourceContract,
|
|
industryActions,
|
|
stockActions,
|
|
normalizeIndustry,
|
|
normalizeStock,
|
|
createIndustrySearchRequest,
|
|
createStockSearchRequest,
|
|
normalizeIndustrySearchResponse,
|
|
normalizeStockSearchResponse,
|
|
normalizeIndustrySearchError,
|
|
normalizeStockSearchError,
|
|
isActionAllowed,
|
|
buildOverseasIndustrySelection,
|
|
buildOverseasStockSelection,
|
|
createOverseasIndustryPlaylistEntry,
|
|
createOverseasStockPlaylistEntry,
|
|
restoreOverseasPlaylistEntry,
|
|
refreshOverseasPlaylistEntry: restoreOverseasPlaylistEntry
|
|
};
|
|
});
|