Code: Select all
// ==UserScript==
// @name Save DL Sentences
// @namespace http://tampermonkey.net/
// @version 0.0.1
// @description Save Duolingo sentences for a lesson
// @author credit goes to https://forum.duome.eu
// @match https://www.duolingo.com/
// @match https://www.duolingo.com/*
// @run-at document-start
// @icon https://www.google.com/s2/favicons?sz=64&domain=duolingo.com
// @grant none
// ==/UserScript==
;(() => {
const debug = false;
const console = (/** @param {Console} c */ (c) => {
for (const k in window.console) {
c[k] = window.console[k]
}
return c
})({})
let session = null;
let link = document.createElement("a");
const ankiHeader = `#separator:tab
#html:false
#deck column:1
#tags column:5
`
/**
* @param {(response: AjaxResponse) => void} listener
*
* modified from https://stackoverflow.com/questions/24555370/how-can-i-catch-and-process-the-data-from-the-xhr-responses-using-casperjs/58168312#58168312
* and https://blog.logrocket.com/intercepting-javascript-fetch-api-requests-responses/
*/
const addAjaxListener = (listener) => {
// fetch
const originalFetch = window.fetch
window.fetch = async (...args) => {
const res = await originalFetch(...args)
const { url, status, ok, headers } = res
const clone = res.clone()
const getBody = (type = 'json') => clone[type]()
const getHeaders = () => Promise.resolve(headers)
listener({ url, status, ok, headers, getBody, getHeaders })
return res
}
/** @this {XMLHttpRequest} */
const onLoadHandler = function () {
const xhr = this
if (xhr.readyState === 4) {
const { responseURL: url, status } = xhr
const getBody = () => Promise.resolve(xhr.response)
const getHeaders = () => Promise.resolve(
new Headers(xhr.getAllResponseHeaders()
.trim()
.split(/[\r\n]+/)
.map(line => line.split(/: /))
.map(([k, v]) => [k.trim(), v.trim()]))
)
listener({ url, ok: status < 400, status, getBody, getHeaders })
}
}
const { open, send } = XMLHttpRequest.prototype
XMLHttpRequest.prototype.open = function (...args) {
this.requestUrl = args[1]
open.apply(this, args)
}
XMLHttpRequest.prototype.send = function (...args) {
const xhr = this
if (xhr.addEventListener) {
xhr.removeEventListener('readystatechange', onLoadHandler)
xhr.addEventListener('readystatechange', onLoadHandler, false)
} else {
let handler
const readyStateChange = (...args) => {
if (handler) {
if (handler.handleEvent) {
handler.handleEvent.apply(xhr, args)
} else {
handler.apply(xhr, args)
}
}
onLoadHandler.apply(xhr, args)
setReadyStateChange()
}
const setReadyStateChange = () => {
setTimeout(() => {
if (xhr.onreadystatechange !== readyStateChange) {
handler = xhr.onreadystatechange
xhr.onreadystatechange = readyStateChange
}
}, 1)
}
setReadyStateChange()
}
send.apply(xhr, args)
}
}
/* === main logic === */
const Selector = {
NextButton: '[data-test="player-next"]',
Blame: '[data-test*="blame"]',
ChallengeControls: `#${CSS.escape('session/PlayerFooter')}`,
}
const sessionsUrl = 'https://www.duolingo.com/2017-06-30/sessions'
const sessionsUrlMatcher =
new RegExp(`^${sessionsUrl.replace(/\d/g, '\\d')}\/?$`)
const observer = new MutationObserver((mutations) => {
if (!session) return
const {
challenges,
adaptiveChallenges,
adaptiveInterleavedChallenges: a,
} = session
})
;(async () => {
let tries = 0
while (++tries <= 100) {
const root = document.body
if (root) {
observer.observe(root, { childList: true, subtree: true })
break
} else {
await new Promise(res => setTimeout(res, 10))
}
}
if (tries > 1) {
console.warn(`tried to find root element ${tries} times`)
}
})()
addAjaxListener(async (res) => {
if (sessionsUrlMatcher.test(res.url)) {
session = window.session = await res.getBody()
}
})
const savePhrases = async () => {
if (session == null || session.challenges == null) {
return;
}
const phraseSet = [];
phraseSet.push(ankiHeader);
let chunks = document.URL.split("/");
let unit_nr = chunks[chunks.length - 3];
let unit_nr_string = "xxx";
if (!isNaN(unit_nr)) {
unit_nr_string = new String(unit_nr).padStart(3, '0');
} else {
unit_nr_string = chunks[chunks.length-2];
}
let anki_tag = "DL_" + session.learningLanguage + "_" + session.fromLanguage + "_" + unit_nr_string;
let deck_name = "DL_" + session.learningLanguage + "_from_" + session.fromLanguage;
for (let i = 0; i < session.challenges.length; i++) {
let challenge = session.challenges[i];
let tts = challenge.solutionTts ? challenge.solutionTts : challenge.tts;
let chType = challenge.challengeGeneratorIdentifier.specificType;
let t_source = challenge.metadata.text;
let t_target = challenge.metadata.translation;
let add_phrase = true;
switch (chType) {
case "dialogue":
case "definition":
case "select_transcription":
case "character_select":
case "character_match":
case "gap_fill":
case "match":
case "listen_match":
case "assist":
case "read_comprehension":
case "name_example":
add_phrase = false;
break;
// done to here
case "reverse_tap":
case "tap_gap":
case "listen_isolation":
break;
// done from here
case "name":
t_target = challenge.challengeResponseTrackingProperties.best_solution;
t_source = challenge.metadata.hint;
tts = "no_audio";
break
case "listen":
case "listen_complete":
t_target = challenge.metadata.text;
t_source = challenge.metadata.solution_translation;
break;
case "tap":
case "speak":
t_target = challenge.metadata.text;
t_source = challenge.metadata.translation;
break;
case "listen_tap":
t_target = challenge.metadata.text;
t_source = challenge.metadata.solution_translation;
break;
case "tap_complete":
t_target = challenge.metadata.text;
t_source = challenge.metadata.best_translation;
tts = "no_audio";
break;
case "complete_reverse_translation":
t_target = challenge.challengeResponseTrackingProperties.best_solution;
tts = "no_audio";
break;
case "reverse_translate":
default:
break;
}
if (add_phrase) {
//phraseSet.push([t_source, t_target, tts, chType].join("\t") + "\n");
phraseSet.push([deck_name + "::" + unit_nr_string, t_source, t_target, /* tts, chType, */ anki_tag].join('\t') + '\n');
}
}
if (phraseSet.length == 0) {
return
}
const file = new Blob(phraseSet, { type: 'text/plain' });
link.href = URL.createObjectURL(file);
link.download = "DL_" + session.learningLanguage + "_from_" + session.fromLanguage + "_" + unit_nr_string + "_phrases.csv";
link.click();
URL.revokeObjectURL(link.href);
}
function renderBtnForAudio () {
var saveSent = document.getElementById("save_sent");
if (saveSent != null) {
return;
}
const btn_next = document.querySelectorAll('[data-test="player-next"]')[0];
if (btn_next != undefined) {
const buttonStyle = `
min-width: 150px;
font-size: 17px;
border:none;
border-bottom: 4px solid #58a700;
border-radius: 18px;
padding: 13px 16px;
transform: translateZ(0);
transition: filter .2s;
font-weight: 700;
letter-spacing: .8px;
background: #55CD2E;
color:#fff;
margin-left:20px;
cursor:pointer;
`;
//alert("adding button");
try {
saveSent = document.createElement('button');
const wrapper = btn_next.parentNode;
wrapper.style.display = "flex";
//Object.assign(saveSent, btn_next);
saveSent.innerHTML = "Save Sentences";
saveSent.id = "save_sent";
saveSent.disabled = false;
saveSent.style.cssText = buttonStyle;
saveSent.className = btn_next.ClassName;
saveSent.addEventListener("mousemove", () => {
saveSent.style.filter = "brightness(1.1)";
});
saveSent.addEventListener("mouseleave", () => {
saveSent.style.filter = "none";
});
wrapper.appendChild(saveSent);
saveSent.addEventListener('click', savePhrases);
} catch (e) {
alert("creating button failed");
}
} else {
console.log("next button not found!");
}
}
window.originalConsole = console
window.session = session
setInterval(renderBtnForAudio, 3000);
})()