Closed MarlonPassos-git closed 3 years ago
No problem!
If what you would like is to have a button (for example) to read an entire story at once, you can:
XMLHttpRequest.open()
method (example),https://stories.duolingo.com/api2/stories/
),load
listener to the request,This is a bit different from what the extension does in that the extension applies to sounds that are loaded by Duolingo itself, but I think that using your own sounds is the way to go here.
It seemed like an interesting exercise, so I've taken a stab at it (who knows, this might end up in an extension!).
Here is a userscript that reads the entire story when its icon is clicked (it lacks some controls such as a button to stop the playback - you can fill in the gaps if necessary):
// ==UserScript==
// @name Duo Story Playback
// @version 1.0.0
// @description Reads an entire story when its icon is clicked.
// @author blmage
// @match https://*.duolingo.com/stories/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const isArray = Array.isArray;
const isObject = x => ('object' === typeof x) && !!x && !isArray(x);
const isString = x => ('string' === typeof x);
let storyHowls = [];
let playbackQueue = null;
let isPlaybackReady = false;
let isPlaybackRunning = false;
let currentStoryIcon = null;
setInterval(() => {
const storyIcon = document.querySelector('._3kxdZ');
if (storyIcon !== currentStoryIcon) {
currentStoryIcon = storyIcon;
if (storyIcon) {
storyIcon.style.cursor = 'pointer';
storyIcon.addEventListener('click', playStory);
}
}
}, 100);
const playStory = () => {
if (isPlaybackReady && !isPlaybackRunning) {
isPlaybackRunning = true;
playbackQueue = storyHowls.slice();
const playNextSound = () => {
const nextSound = playbackQueue.shift();
if (nextSound) {
nextSound.once('end', playNextSound);
nextSound.play();
} else {
isPlaybackRunning = false;
}
};
playNextSound();
}
};
const preparePlaybackButton = sounds => {
isPlaybackReady = false;
storyHowls = sounds.map(url => new Howl({ src: [ url ] }));
const loadPromises = storyHowls.map(howl => (
new Promise((resolve, reject) => {
howl.once('load', resolve);
howl.once('loaderror', reject);
})
));
Promise.all(loadPromises)
.then(() => {
isPlaybackReady = true;
}).catch(error => {
alert(`Could not load all the sounds: ${error}.`);
});
};
const originalXhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
if (url.startsWith('https://stories.duolingo.com/api2/stories/')) {
this.addEventListener('load', () => {
try {
const data = isObject(this.response) ? this.response : JSON.parse(this.responseText);
if (isObject(data) && isArray(data.elements)) {
const storySounds = data.elements.map(element => {
if ('HEADER' === element.type) {
return element?.learningLanguageTitleContent?.audio?.url;
} else if ('LINE' === element.type) {
return element?.line?.content?.audio?.url;
}
}).filter(isString);
preparePlaybackButton(storySounds);
}
} catch (error) {
logError(`Could not parse the story: ${error}.`);
}
});
}
return originalXhrOpen.call(this, method, url, async, user, password);
};
})();
Hi, sorry for my ignorance, but I'm trying to create a script for duolingo to automatically play stories, but for this to work I should somehow have access to audio that by default is not shown in HTML. I saw that you managed to solve this problem but when I tried to read your code I didn't understand how you did it (I'm still a beginner), could you not explain more directly to me what I should research to be able to solve this problem. Sorry if this isn't the best place to ask questions, but it's the only one I found