Is YouTube Subtitle Downloader safe?
YouTube Subtitle Downloader generates a persistent browser fingerprint and sends video IDs and user actions to the developer's server.
On first use, the extension generates a persistent fingerprint ID using the FingerprintJS library, drawing on canvas, WebGL, audio, and platform signals. This ID is stored locally and attached as a query parameter to every request sent to antonkhoteev.com. Each subtitle load, translation, copy, or download action — along with the associated YouTube video ID — is reported to the developer's API endpoint, linking usage history to the persistent identifier.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
YouTube subtitle actions tied to persistent IDs
The extension sends YouTube subtitle activity to antonkhoteev.com.
Loading subtitles can send the video ID, language, service name, and persistent user ID/UUID; translate, copy, and download also hit that endpoint with the same identifiers.
You use subtitle features on a YouTube video.
Opening the side panel loads subtitle languages, and actions such as load, translate, copy, and download trigger additional code paths.
The extension sends the video and action context to antonkhoteev.com with persistent identifiers.
The same actionLog function appends userId and userUuid before making a GET request.
| Field | Value | Why it matters | |
|---|---|---|---|
Persistent user ID | 8f4b0e0c2a9f4b5e8f8d4b6a4c9e7d12 (illustrative) | Lets the server recognize the same extension user across multiple subtitle actions. | |
Persistent UUID | 123e4567-e89b-12d3-a456-426614174000 (illustrative) | Links extension install events and later action events to the same browser profile. | |
YouTube video ID | dQw4w9WgXcQ (illustrative) | Identifies the specific YouTube video where you used the subtitle feature. | |
Subtitle action | subs-loading (illustrative) | Shows which feature you used on that video, such as loading subtitles, translating, copying, or downloading. | |
Language or service | lang=en, service=backend (illustrative) | Adds detail about the subtitle language or backend service involved in the action. |
Source paths that create identifiers, add video context, and send action logs
const useUserStore = /* @__PURE__ */ defineStore("ysd-user", () => {
const userId = ref(null);
const userUuid = ref(null);
const init = async () => {
const storageData = await chrome.storage.local.get(null);
if (storageData.userId) {
userId.value = storageData.userId;
}
if (storageData.userUuid) {
userUuid.value = storageData.userUuid;
}
if (!userId.value) {
try {
userId.value = await generateUserId();
await chrome.storage.local.set({ userId: userId.value });
} catch (e) {}
}
};
const generateUserId = async () => {
try {
const fp = await load();
const result = await fp.get();
if (result.visitorId) {
return result.visitorId;
}
} catch (e) {}
return null;
};
return {
userId,
userUuid,
init,
};
});async function actionLog(action, params = {}) {
const userStore = useUserStore();
const url = new URL(
`https://antonkhoteev.com/khoteev-api/ysd/actions/${action}`,
);
url.searchParams.set("userId", userStore.userId || "");
url.searchParams.set("userUuid", userStore.userUuid || "");
Object.entries(params).forEach(([key, val]) => {
if (val) url.searchParams.set(key, val);
});
try {
await fetch(url, { method: "GET" });
} catch (e) {}
}class LanguageBackendService {
async fetch(videoId) {
try {
const result = await fetchWithTimeout(
`https://antonkhoteev.com/khoteev-api/ysd/video/${videoId}/languages`,
{ headers: { Accept: "application/json" } },
);
if (!result.ok) {
return null;
}
const data = await result.json();
const original = Array.isArray(
data == null ? void 0 : data.original,
)
? data.original
: [];
const translate = Array.isArray(
data == null ? void 0 : data.translate,
)
? data.translate
: [];
if (!original.length && !translate.length) return null;
return {
original,
translate,
};
} catch (e) {}
return null;
}
}
class SubtitlesExternalLinkService {
constructor() {
__privateAdd(this, _SubtitlesExternalLinkService_instances);
__publicField(this, "id", "external");
__privateAdd(this, _cache3, /* @__PURE__ */ new Map());
// key = `${videoId}::${lang}` -> baseUrl
__privateAdd(this, _inflight2, /* @__PURE__ */ new Map());
}
async fetch(videoId, olang = null, tlang = null) {
const langForKey = olang ?? tlang ?? "";
const baseUrl = await this.getLink(videoId, langForKey);
if (!baseUrl) {
return null;
}
let url = baseUrl;
if (tlang) {
const u = new URL(baseUrl);
u.searchParams.set("tlang", tlang);
url = u.toString();
}
const res = await fetchWithTimeout(url);
if (!res.ok) {
__privateGet(this, _cache3).delete(
__privateMethod(
this,
_SubtitlesExternalLinkService_instances,
key_fn2,
).call(this, videoId, langForKey),
);
return null;
}
const xml = await res.text();
if (!(xml == null ? void 0 : xml.length)) {
return null;
}
const subs = await parseYouTubeSubtitles(xml);
return subs.length ? subs : null;
}
async getLink(videoId, lang = "") {
const key = __privateMethod(
this,
_SubtitlesExternalLinkService_instances,
key_fn2,
).call(this, videoId, lang);
const cached = __privateGet(this, _cache3).get(key);
if (cached) {
return cached;
}
const pending = __privateGet(this, _inflight2).get(key);
if (pending) {
return pending;
}
const inflightPromise = (async () => {
var _a;
try {
const response = await fetch(
`https://antonkhoteev.com/khoteev-api/ysd/video/${videoId}/url`,
{ method: "GET", headers: { Accept: "application/json" } },
);
if (!response.ok) {
return null;
}
const data = await response.json();
const urls = Array.isArray(data == null ? void 0 : data.urls)
? data.urls
: [];
if (urls.length === 0) {
return null;
}
let picked = null;
if (lang) {
const exact = urls.find(
(u) => (u == null ? void 0 : u.code) === lang,
);
picked = (exact == null ? void 0 : exact.url) ?? null;
}
if (!picked) {
picked = ((_a = urls[0]) == null ? void 0 : _a.url) ?? null;
}
if (picked) {
__privateGet(this, _cache3).set(key, picked);
}
return picked;
} finally {
__privateGet(this, _inflight2).delete(key);
}
})();
__privateGet(this, _inflight2).set(key, inflightPromise);
return inflightPromise;
}
invalidate(videoId, lang = "") {
__privateGet(this, _cache3).delete(
__privateMethod(
this,
_SubtitlesExternalLinkService_instances,
key_fn2,
).call(this, videoId, lang),
);
}
clear() {
__privateGet(this, _cache3).clear();
__privateGet(this, _inflight2).clear();
}
}
class SubtitlesBackendService {
constructor() {
__publicField(this, "id", "backend");
}
async fetch(videoId, olang = null, tlang = null) {
try {
const lang = olang ?? tlang;
if (!lang) {
return [];
}
const params = new URLSearchParams();
if (olang) params.append("olang", olang);
if (tlang) params.append("tlang", tlang);
const result = await fetch(
`https://antonkhoteev.com/khoteev-api/ysd/video/${videoId}/subtitles?${params}`,
{ headers: { Accept: "application/json" } },
);
if (!result.ok) {
return [];
}
return await result.json();
} catch (e) {}
return [];
}
}const ACTIONS = Object.freeze({
SUBS_LOADING: "subs-loading",
TRANSLATE: "translate",
DOWNLOAD: "download",
COPY: "copy",
});
class SubtitlesProvider {
constructor() {
this.services = [
new SubtitlesInternalLinkService(),
new SubtitlesExternalLinkService(),
new SubtitlesBackendService(),
];
}
async getSubtitles(videoId, olang, tlang) {
for (const service of this.services) {
try {
const subs = await service.fetch(videoId, olang, tlang);
if (await this.isValidSubs(subs)) {
const serviceName = service.id ?? "unknown";
Promise.resolve(
actionLog(ACTIONS.SUBS_LOADING, {
video: videoId,
lang: olang ?? tlang,
service: serviceName,
}),
).catch(() => {});
return subs;
}
} catch (e) {}
}
await actionLog("error", {
message: "There is not subtitles",
video: videoId,
});
return [];
}
isValidSubs(subs) {
return Array.isArray(subs) && subs.length && this.isValidByLength(subs);
}
isValidByLength(subs) {
var _a;
for (let i = 0; i < subs.length; i++) {
const len = String(
((_a = subs[i]) == null ? void 0 : _a.text) ?? "",
).trim().length;
if (len > 1e3) {
return false;
}
}
return true;
}
} const getSubs = async (lang, isTranslate = false) => {
if (configStore.isActionLimit(ACTIONS.SUBS_LOADING)) {
await configStore.showPaywall(false);
}
const cachedSubs = subtitles.value.find(
(item) => item.languageCode === lang,
);
if (cachedSubs) {
return cachedSubs.subs;
}
const loadedSubs = isTranslate
? await subtitlesProvider.getSubtitles(
communicationStore.videoId,
null,
lang,
)
: await subtitlesProvider.getSubtitles(
communicationStore.videoId,
lang,
null,
);
const decodedSubs = decodeSubtitles(loadedSubs);
if (decodedSubs.length > 0) {
subtitles.value.push({
languageCode: lang,
subs: decodedSubs,
});
}
if (isTranslate) {
await configStore.decrementUsage(ACTIONS.TRANSLATE);
await actionLog(ACTIONS.TRANSLATE);
}
return decodedSubs;
};
const copySubtitles = async () => {
if (configStore.isActionLimit(ACTIONS.COPY)) {
await configStore.showPaywall();
return;
}
const merged = mergedSubtitles.value;
const { displayMode, isShowTime } = panelStore;
const textToCopy = merged
.map((item) => {
var _a;
const lines = [];
if (isShowTime) {
lines.push(item.startInFormat);
}
const original = item.text.trim();
const translate =
((_a = item.translate) == null ? void 0 : _a.trim()) ?? "";
if (displayMode === "original-translate") {
lines.push(original, translate);
} else if (displayMode === "translate-original") {
lines.push(translate, original);
} else if (displayMode === "original-only") {
lines.push(original);
} else if (displayMode === "translate-only") {
lines.push(translate);
}
return lines.join("\n");
})
.join("\n\n");
try {
await navigator.clipboard.writeText(textToCopy);
isCopied.value = true;
setTimeout(() => {
isCopied.value = false;
}, 800);
} catch (e) {}
await actionLog(ACTIONS.COPY);
await configStore.decrementUsage(ACTIONS.COPY);
};
const downloadSubtitles = async () => {
if (configStore.isActionLimit(ACTIONS.DOWNLOAD)) {
await configStore.showPaywall();
return;
}
isDownloadingFile.value = true;
const merged = mergedSubtitles.value;
const { displayMode, format } = panelStore;
const buildTextBlock = (original, translate) => {
if (displayMode === "original-translate") {
return [original, translate].filter(Boolean).join("\n");
} else if (displayMode === "translate-original") {
return [translate, original].filter(Boolean).join("\n");
} else if (displayMode === "original-only") {
return original;
} else if (displayMode === "translate-only") {
return translate;
}
return "";
};
let content = "";
if (format === "SRT") {
content = merged
.map((item, i) => {
var _a;
const index = i + 1;
const start = formatTimeForSRT(item.start);
const end = formatTimeForSRT(item.end);
const original = item.text.trim();
const translated =
((_a = item.translate) == null ? void 0 : _a.trim()) ??
"";
const textBlock = buildTextBlock(original, translated);
return `${index}
${start} --> ${end}
${textBlock}
`;
})
.join("\n");
} else if (format === "VTT") {
content =
"WEBVTT\n\n" +
merged
.map((item) => {
var _a;
const start = formatTimeForVTT(item.start);
const end = formatTimeForVTT(item.end);
const original = item.text.trim();
const translated =
((_a = item.translate) == null
? void 0
: _a.trim()) ?? "";
const textBlock = buildTextBlock(original, translated);
return `${start} --> ${end}
${textBlock}
`;
})
.join("\n");
} else if (format === "TXT") {
content = merged
.map((item) => {
var _a;
const original = item.text.trim();
const translated =
((_a = item.translate) == null ? void 0 : _a.trim()) ??
"";
const textBlock = buildTextBlock(original, translated);
if (panelStore.isShowTime) {
const time = formatTimeForTXT(item.start);
return `${time}
${textBlock}`;
} else {
return `${textBlock}`;
}
})
.join("\n\n");
}
const rawTitle = videoTitle.value ?? "";
const sanitizedTitle = sanitizeFileName(rawTitle);
const fallbackName = `subtitles-${/* @__PURE__ */ new Date().toISOString().split("T")[0]}`;
const baseFileName = sanitizedTitle || fallbackName;
const fileName = `${baseFileName}.${format.toLowerCase()}`;
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
isDownloadingFile.value = false;
await actionLog(ACTIONS.DOWNLOAD, { format });
await configStore.decrementUsage(ACTIONS.DOWNLOAD);
};- antonkhoteev.com
Developer API host that receives action logs and video-specific subtitle API requests for YouTube Subtitle Downloader.