// ==UserScript==
// @name TTSFree Long Text Reader
// @namespace http://tampermonkey.net/
// @version 1.9
// @description Divide un testo lungo in chunk, intercetta il pulsante di download su ttsfree.com e riproduce gli audio in sequenza con un lettore personalizzato
// @author Flejta & Grok (con aiuto di xAI)
// @match https://ttsfree.com/*
// @grant none
// @license mit
// ==/UserScript==
(function() {
'use strict';
// Aggiunge l’interfaccia personalizzata
function addCustomControls() {
const container = document.createElement('div');
container.id = 'custom-tts-controls';
container.style = 'position: fixed; top: 10px; right: 10px; background: white; padding: 15px; border: 2px solid #007bff; border-radius: 8px; z-index: 1000; box-shadow: 0 2px 5px rgba(0,0,0,0.2); width: 400px;';
container.innerHTML = `
<h3 style="margin: 0 0 10px; font-size: 16px; color: #333;">TTS Long Text Reader</h3>
<textarea id="custom-text" placeholder="Inserisci il testo lungo" style="width: 100%; height: 150px; border: 1px solid #ccc; border-radius: 5px; padding: 5px; font-size: 14px; box-sizing: border-box;"></textarea>
<div style="margin-top: 10px;">
<label style="font-size: 14px;">Limite caratteri: </label>
<select id="chunk-size" style="margin-left: 5px; padding: 2px; width: 150px;">
<option value="500">500 (senza login)</option>
<option value="2000" selected>2000 (con login)</option>
</select>
</div>
<button id="custom-start" style="margin-top: 10px; padding: 8px 15px; background: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer;">Avvia lettura</button>
<div id="status" style="margin-top: 10px; font-size: 14px; color: #333;">Stato: In attesa</div>
`;
document.body.appendChild(container);
}
// Divide il testo in chunk
function splitText(text, maxLength) {
console.log(`Divisione testo: lunghezza=${text.length}, maxLength=${maxLength}`);
const chunks = [];
let start = 0;
while (start < text.length) {
let end = Math.min(start + maxLength, text.length);
if (end < text.length) {
const lastPeriod = text.lastIndexOf('.', end);
const lastSpace = text.lastIndexOf(' ', end);
end = Math.max(lastPeriod, lastSpace) > start ? Math.max(lastPeriod, lastSpace) + 1 : end;
}
chunks.push(text.slice(start, end));
start = end;
}
console.log(`Creati ${chunks.length} chunk`);
return chunks;
}
// Simula un'azione di incolla usando document.execCommand
function simulatePaste(textarea, text) {
console.log('Inizio simulazione incolla');
// Imposta il focus sulla textarea
textarea.focus();
console.log('Focus impostato sulla textarea');
// Seleziona tutto il testo esistente (per sovrascriverlo)
textarea.select();
// Inserisce il testo usando document.execCommand
try {
const success = document.execCommand('insertText', false, text);
if (success) {
console.log('Testo inserito con document.execCommand:', text.substring(0, 50) + '...');
// Invia eventi input e paste per aggiornare il framework
const inputEvent = new Event('input', { bubbles: true });
const pasteEvent = new Event('paste', { bubbles: true });
textarea.dispatchEvent(inputEvent);
textarea.dispatchEvent(pasteEvent);
console.log('Eventi input e paste inviati');
} else {
console.error('Errore: document.execCommand non ha funzionato');
throw new Error('document.execCommand non ha funzionato');
}
} catch (error) {
console.error('Errore durante l\'inserimento del testo:', error);
throw error;
}
}
// Simula l’inserimento del testo e il click su "Convert Now"
function insertTextAndConvert(text) {
return new Promise((resolve, reject) => {
console.log('Tentativo di inserimento testo:', text.substring(0, 50) + '...');
const textarea = document.querySelector('#input_text');
if (!textarea) {
console.error('Textarea non trovata');
reject(new Error('Textarea non trovata'));
return;
}
console.log('Textarea trovata');
try {
simulatePaste(textarea, text);
// Verifica se il testo è stato inserito
setTimeout(() => {
if (textarea.value !== text) {
console.error('Errore: il testo non è stato inserito correttamente. Valore attuale:', textarea.value);
reject(new Error('Testo non inserito nella textarea'));
return;
}
console.log('Testo inserito correttamente:', textarea.value.substring(0, 50) + '...');
const convertButton = document.querySelector('.convert-now');
if (!convertButton) {
console.error('Pulsante "Convert Now" non trovato');
reject(new Error('Pulsante "Convert Now" non trovato'));
return;
}
console.log('Pulsante "Convert Now" trovato, simulazione click');
convertButton.click();
resolve();
}, 100); // Breve ritardo per consentire al framework di aggiornare
} catch (error) {
reject(error);
}
});
}
// Monitora l’aggiunta o la modifica del pulsante di download
function waitForDownloadButton(chunkIndex) {
return new Promise((resolve, reject) => {
console.log(`Inizio monitoraggio pulsante di download per chunk ${chunkIndex}`);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
// Controlla i nodi aggiunti
mutation.addedNodes.forEach((node) => {
if (node.id === 'savevoice' || (node.querySelector && node.querySelector('#savevoice'))) {
const downloadButton = node.id === 'savevoice' ? node : node.querySelector('#savevoice');
if (downloadButton && downloadButton.href) {
console.log(`Pulsante di download trovato per chunk ${chunkIndex}, URL: ${downloadButton.href}`);
observer.disconnect();
resolve(downloadButton.href);
}
}
});
// Controlla le modifiche agli attributi di savevoice
if (mutation.type === 'attributes' && mutation.target.id === 'savevoice' && mutation.target.href) {
console.log(`Modifica attributo href trovata per chunk ${chunkIndex}, URL: ${mutation.target.href}`);
observer.disconnect();
resolve(mutation.target.href);
}
});
});
// Monitora sia l'aggiunta di nodi che le modifiche agli attributi
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['href']
});
// Timeout per evitare attese infinite
setTimeout(() => {
console.error(`Timeout attesa pulsante di download per chunk ${chunkIndex}`);
observer.disconnect();
reject(new Error('Timeout attesa pulsante di download'));
}, 30000); // 30 secondi
});
}
// Gestore della coda audio
class AudioPlayer {
constructor() {
this.audio = new Audio();
this.queue = [];
this.isPlaying = false;
}
addToQueue(audioUrl, chunkIndex) {
console.log(`Aggiunto audio alla coda per chunk ${chunkIndex}: ${audioUrl}`);
this.queue.push({ url: audioUrl, index: chunkIndex });
console.log(`Stato coda: ${JSON.stringify(this.queue.map(item => ({ index: item.index, url: item.url })))}`);
if (!this.isPlaying) {
this.playNext();
}
}
playNext() {
if (this.queue.length === 0) {
console.log('Coda audio vuota, riproduzione terminata');
this.isPlaying = false;
return;
}
this.isPlaying = true;
const { url, index } = this.queue.shift();
console.log(`Riproduzione audio per chunk ${index}: ${url}`);
this.audio.src = url;
this.audio.play().catch((error) => {
console.error(`Errore riproduzione per chunk ${index}:`, error);
});
this.audio.onended = () => {
console.log(`Audio terminato per chunk ${index}, passaggio al successivo`);
this.playNext();
};
}
}
// Processo principale
async function processLongText(maxLength) {
const customText = document.querySelector('#custom-text').value;
const status = document.querySelector('#status');
const player = new AudioPlayer();
const chunks = splitText(customText, maxLength);
if (chunks.length === 0 || !customText) {
console.error('Testo non valido o vuoto');
status.textContent = 'Stato: Inserisci un testo valido';
return;
}
console.log(`Inizio elaborazione, ${chunks.length} chunk`);
status.textContent = `Stato: Elaborazione 1/${chunks.length}`;
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
try {
console.log(`Elaborazione chunk ${i + 1}/${chunks.length}`);
await insertTextAndConvert(chunk);
const audioUrl = await waitForDownloadButton(i + 1);
player.addToQueue(audioUrl, i + 1);
status.textContent = `Stato: Elaborazione ${i + 2}/${chunks.length}`;
await new Promise(resolve => setTimeout(resolve, 20000)); // Ritardo di 20 secondi
} catch (error) {
console.error(`Errore con il blocco ${i + 1}:`, error);
status.textContent = `Stato: Errore al blocco ${i + 1} - ${error.message}`;
break;
}
}
console.log('Elaborazione completata');
status.textContent = `Stato: Completato`;
}
// Inizializza
addCustomControls();
document.querySelector('#custom-start').addEventListener('click', () => {
console.log('Avvio processo');
const chunkSize = parseInt(document.querySelector('#chunk-size').value, 10);
processLongText(chunkSize);
});
})();