Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions chrome/player/modules/SubtitleTranslator.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* SubtitleTranslator.mjs
* "Interactive" Version: Opens the specific API URL to force the CAPTCHA check.
*/
export class SubtitleTranslator {
constructor() {
this.apis = [
{
name: "gtx",
url: "https://translate.googleapis.com/translate_a/single",
params: { client: "gtx", dt: "t" },
batchSize: 8,
delay: 3000
},
{
name: "clients5",
url: "https://clients5.google.com/translate_a/t",
params: { client: "dict-chrome-ex" },
batchSize: 12,
delay: 3000
}
];
this.currentApiIndex = 0;
}

async translateStream(cues, targetLang, onProgress) {
if (!cues || cues.length === 0) throw new Error("No cues found.");

let api = this.apis[this.currentApiIndex];

// FIX: Use double newline. This forces Google to treat cues as separate paragraphs.
// Single newlines cause Japanese/Chinese lines to merge during translation.
let delimiter = "\n\n";

for (let i = 0; i < cues.length; i += api.batchSize) {
api = this.apis[this.currentApiIndex];

const batch = cues.slice(i, i + api.batchSize);
const texts = batch.map(c => c.text.replace(/\r?\n|\r/g, " ").trim());

try {
const translatedTexts = await this.fetchWithRetry(texts, targetLang, delimiter, api);

const segmentCues = [];
batch.forEach((cue, index) => {
// Safety check: ensure we have a translation for this index
if (translatedTexts[index]) {
segmentCues.push({
startTime: cue.startTime,
endTime: cue.endTime,
text: translatedTexts[index]
});
}
});

if (onProgress) onProgress(segmentCues, null);
await this.delay(api.delay);

} catch (e) {
console.warn(`[FastStream] API ${api.name} error:`, e);

// CHECK FOR BAN
if (e.message.includes("429") || e.message.includes("Rate Limit")) {
if (onProgress) onProgress([], "⚠️ Google Ban Detected. Opening Unblocker...");

// CONSTRUCT THE EXACT URL THAT IS BLOCKED
const testUrl = `https://translate.googleapis.com/translate_a/single?client=gtx&dt=t&sl=auto&tl=${targetLang}&q=Hello%20World`;

// 1. Open the specific API URL
window.open(testUrl, "_blank");

// 2. Pause and ask user
const resolved = confirm(
"Google has temporarily blocked the translation signal.\n\n" +
"1. A new tab has opened with a weird code page.\n" +
"2. IF you see a CAPTCHA ('I am not a robot'), solve it.\n" +
"3. IF you just see text code (JSON), close the tab.\n\n" +
"Click OK when you are done to resume."
);

if (resolved) {
if (onProgress) onProgress([], "Resuming...");
// Wait 5 seconds for the unblock to register
await this.delay(5000);
i -= api.batchSize; // Retry batch
continue;
} else {
throw new Error("Translation stopped by user.");
}
}

// Normal rotation for other errors
this.currentApiIndex = (this.currentApiIndex + 1) % this.apis.length;
api = this.apis[this.currentApiIndex];
i -= api.batchSize;
await this.delay(2000);
}
}
}

async fetchWithRetry(texts, targetLang, delimiter, api) {
const combinedText = texts.join(delimiter);
const params = new URLSearchParams({
...api.params,
sl: "auto",
tl: targetLang,
q: combinedText
});

const response = await fetch(`${api.url}?${params.toString()}`);

// If 429 or redirected to the "Sorry" page
if (response.status === 429 || response.url.includes("google.com/sorry")) {
throw new Error("429 Rate Limit");
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

const data = await response.json();
let fullText = "";

if (api.name === "clients5") {
if (Array.isArray(data)) {
if (typeof data[0] === 'string') fullText = data[0];
else data.forEach(item => { if (item && item[0]) fullText += item[0]; });
}
} else {
if (data && data[0]) {
data[0].forEach(segment => {
if (segment[0]) fullText += segment[0];
});
}
}

// Split by the double newline to get original segments back
return fullText.split(delimiter).map(s => s.trim());
}

delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
15 changes: 11 additions & 4 deletions chrome/player/options/OptionsStore.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@ export class OptionsStore {
/**
* Initialize the store by loading options and wiring external updates.
*/
static async init() {
if (this.#initialized) return this.#options;
this.#initialized = true;
this.#options = await Utils.getOptionsFromStorage();
static async init() {
// 1. If we have options, return them immediately
if (this.#initialized && this.#options) return this.#options;

// 2. Load the options first
const options = await Utils.getOptionsFromStorage();
this.#options = options;
this.#wireExternalUpdates();

// 3. ONLY NOW mark as initialized
this.#initialized = true;

return this.#options;
}

Expand Down
23 changes: 23 additions & 0 deletions chrome/player/options/defaults/DefaultLanguages.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const DefaultLanguages = {
"af": "Afrikaans", "sq": "Albanian", "am": "Amharic", "ar": "Arabic", "hy": "Armenian",
"az": "Azerbaijani", "eu": "Basque", "be": "Belarusian", "bn": "Bengali", "bs": "Bosnian",
"bg": "Bulgarian", "ca": "Catalan", "ceb": "Cebuano", "ny": "Chichewa", "zh-CN": "Chinese (Simplified)",
"zh-TW": "Chinese (Traditional)", "co": "Corsican", "hr": "Croatian", "cs": "Czech", "da": "Danish",
"nl": "Dutch", "en": "English", "eo": "Esperanto", "et": "Estonian", "tl": "Filipino",
"fi": "Finnish", "fr": "French", "fy": "Frisian", "gl": "Galician", "ka": "Georgian",
"de": "German", "el": "Greek", "gu": "Gujarati", "ht": "Haitian Creole", "ha": "Hausa",
"haw": "Hawaiian", "iw": "Hebrew", "hi": "Hindi", "hmn": "Hmong", "hu": "Hungarian",
"is": "Icelandic", "ig": "Igbo", "id": "Indonesian", "ga": "Irish", "it": "Italian",
"ja": "Japanese", "jw": "Javanese", "kn": "Kannada", "kk": "Kazakh", "km": "Khmer",
"ko": "Korean", "ku": "Kurdish (Kurmanji)", "ky": "Kyrgyz", "lo": "Lao", "la": "Latin",
"lv": "Latvian", "lt": "Lithuanian", "lb": "Luxembourgish", "mk": "Macedonian", "mg": "Malagasy",
"ms": "Malay", "ml": "Malayalam", "mt": "Maltese", "mi": "Maori", "mr": "Marathi",
"mn": "Mongolian", "my": "Myanmar (Burmese)", "ne": "Nepali", "no": "Norwegian", "ps": "Pashto",
"fa": "Persian", "pl": "Polish", "pt": "Portuguese", "pa": "Punjabi", "ro": "Romanian",
"ru": "Russian", "sm": "Samoan", "gd": "Scots Gaelic", "sr": "Serbian", "st": "Sesotho",
"sn": "Shona", "sd": "Sindhi", "si": "Sinhala", "sk": "Slovak", "sl": "Slovenian",
"so": "Somali", "es": "Spanish", "su": "Sundanese", "sw": "Swahili", "sv": "Swedish",
"tg": "Tajik", "ta": "Tamil", "te": "Telugu", "th": "Thai", "tr": "Turkish",
"uk": "Ukrainian", "ur": "Urdu", "uz": "Uzbek", "vi": "Vietnamese", "cy": "Welsh",
"xh": "Xhosa", "yi": "Yiddish", "yo": "Yoruba", "zu": "Zulu"
};
2 changes: 2 additions & 0 deletions chrome/player/options/defaults/DefaultOptions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,6 @@ export const DefaultOptions = {
videoDelay: 0,
maximumDownloaders: 6,
youtubePlayerID: '',
autoTranslate: false, // Default to off for Store compliance
defaultTranslateLanguage: 'en', // Default language code
};
16 changes: 16 additions & 0 deletions chrome/player/options/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,23 @@ <h1 data-i18n="options_general_header"></h1>
<div class="label search-target-text" data-i18n="options_general_autosub"></div>
</div>
</div>

<div class="search-target-remove">
<br>
<div class="option grid1">
<input type="checkbox" id="autotranslate">
<div class="label search-target-text">Enable Auto-Translate</div>
</div>
</div>

<div class="search-target-remove">
<br>
<div class="option grid2">
<div class="label search-target-text">Default Translation Language</div>
<div class="select" id="defaulttranslatelang"></div>
</div>
</div>

<div class="search-target-remove">
<br>
<div class="option grid1">
Expand Down
47 changes: 47 additions & 0 deletions chrome/player/options/options.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {DaltonizerTypes} from './defaults/DaltonizerTypes.mjs';
import {DefaultToolSettings} from './defaults/ToolSettings.mjs';
import {DefaultQualities} from './defaults/DefaultQualities.mjs';
import {ColorThemes} from './defaults/ColorThemes.mjs';
import {DefaultLanguages} from './defaults/DefaultLanguages.mjs';

let Options = {};
const analyzeVideos = document.getElementById('analyzevideos');
Expand Down Expand Up @@ -53,6 +54,8 @@ const optionsSearchBar = document.getElementById('searchbar');
const optionsResetButton = document.getElementById('resetsearch');
// const ytclient = document.getElementById('ytclient');
const maxdownloaders = document.getElementById('maxdownloaders');
const autoTranslate = document.getElementById('autotranslate');
const defaultTranslateLang = document.getElementById('defaulttranslatelang');
autoEnableURLSInput.setAttribute('autocapitalize', 'off');
autoEnableURLSInput.setAttribute('autocomplete', 'off');
autoEnableURLSInput.setAttribute('autocorrect', 'off');
Expand Down Expand Up @@ -159,6 +162,14 @@ async function loadOptions(newOptions) {
document.getElementById('dev').style.display = '';
}
initsearch();

autoTranslate.checked = !!Options.autoTranslate;
// Re-run builder to ensure correct selection is shown
buildLanguageMenu();

// Disable if feature is off
const langSelect = defaultTranslateLang.querySelector('select');
if (langSelect) langSelect.disabled = !Options.autoTranslate;
}

function createSelectMenu(container, options, selected, localPrefix, callback) {
Expand Down Expand Up @@ -240,6 +251,34 @@ createSelectMenu(qualityMenu, Object.values(DefaultQualities), Options.defaultQu
// optionChanged();
// });

// Custom builder for Language Dropdown
function buildLanguageMenu() {
defaultTranslateLang.replaceChildren(); // Clear existing
const select = document.createElement('select');

// Sort languages alphabetically by Name, not Code
const sortedLangs = Object.entries(DefaultLanguages).sort((a, b) => a[1].localeCompare(b[1]));

for (const [code, name] of sortedLangs) {
const option = document.createElement('option');
option.value = code;
option.textContent = name;
if (code === Options.defaultTranslateLanguage) {
option.selected = true;
}
select.appendChild(option);
}

select.addEventListener('change', (e) => {
Options.defaultTranslateLanguage = e.target.value;
optionChanged();
});

defaultTranslateLang.appendChild(select);
}
// Run it immediately
buildLanguageMenu();

document.querySelectorAll('.option').forEach((option) => {
option.addEventListener('click', (e) => {
if (e.target.tagName !== 'INPUT') {
Expand Down Expand Up @@ -675,3 +714,11 @@ if (EnvUtils.isExtension()) {
// SPLICER:NO_UPDATE_CHECKER:REMOVE_END
}

autoTranslate.addEventListener('change', () => {
Options.autoTranslate = autoTranslate.checked;

const select = defaultTranslateLang.querySelector('select');
if (select) select.disabled = !Options.autoTranslate;

optionChanged();
});
Loading