Musescore Free PDF Downloader

An automatic and free score downloader for musescore

// ==UserScript==
// @name         Musescore Free PDF Downloader
// @namespace    http://tampermonkey.net/
// @version      1.2
// @author       malatia
// @match        https://musescore.com/*/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=musescore.com
// @grant        GM_addStyle
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.16.0/pdf-lib.min.js
// @description  An automatic and free score downloader for musescore
// @license MIT
// ==/UserScript==

// Idea from MuseScore Download By flancast90

let DEBUG = true


function get_lazy_imgs() {
    console.log("Début get_lazy_images");
    return new Promise((resolve, reject) => {
        // Fonction récursive pour récupérer les images
        function fetchImages() {

            scrollContainer = document.getElementById('jmuse-scroller-component');
            scrollHeight = scrollContainer.scrollHeight;

            if (scrolled < scrollHeight) {
                // Faire défiler la fenêtre
                scrollContainer.scrollTop = scrolled;
                scrolled += height;

                // Attendre un peu avant de récupérer les images
                setTimeout(() => {
                    // Récupérer les images
                    const images = document.getElementsByClassName('KfFlO');
                    if (images.length > 0) {
                        // Ajouter les URLs des images au tableau
                        urls.push(images[images.length - 1].src);
                    }

                    // Rappel récursif jusqu'à ce que le scroll atteigne la fin
                    fetchImages();
                }, 1000);
            } else {
                // Résoudre la promesse avec les URLs des images
                resolve(urls);
                console.log("Fin get_lazy_images");
            }
        }

        // Début de la récupération des images
        fetchImages();
    });
}

async function download_pdf() {
    console.log("Avant get_lazy_images");
    await get_lazy_imgs(); // Attendre la récupération de toutes les URLs d'images
    console.log("Après get_lazy_images");
    console.log("Avant fetchAndAssemble");
    await fetchAndAssemblePDF(urls); // Utiliser les URLs pour générer le PDF
    console.log("Après fetchAndAssemble");
    scrolled = 0; // Réinitialiser le défilement
}

function createFloatingIframeButton() {
  if (document.getElementById('tm-fab-frame')) return;

  const iframe = document.createElement('iframe');
  iframe.id = 'tm-fab-frame';
  Object.assign(iframe.style, {
    position: 'fixed',
    bottom: '20px',
    left: '20px',
    width: '160px',
    height: '48px',
    border: '0',
    padding: '0',
    margin: '0',
    zIndex: '2147483647',
    background: 'transparent',
    pointerEvents: 'auto'
  });

  // Contenu interne de l'iframe (même origine → on peut y accéder)
  iframe.srcdoc = `
<!doctype html><html><head><meta charset="utf-8">
<style>
html,body{margin:0;padding:0;background:transparent}
#btn{
  all:unset; display:inline-block; padding:10px 14px; border-radius:9999px;
  box-shadow:0 8px 30px rgba(0,0,0,.25);
  background:#0dbc79; color:#fff; font:14px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
  cursor:pointer; user-select:none;
}
#btn:hover{filter:brightness(1.06)}
#btn:active{transform:translateY(1px)}
</style></head><body>
<button id="btn" title="Télécharger le PDF">Télécharger PDF</button>
</body></html>`.trim();

  document.documentElement.appendChild(iframe);

  // Quand l'iframe est prête, on accroche NOTRE handler (dans le contexte userscript)
  iframe.addEventListener('load', () => {
    const doc = iframe.contentDocument;
    const btn = doc && doc.getElementById('btn');
    if (!btn) return;
    btn.addEventListener('click', download_pdf_wrapper); // ici, le site ne peut pas bloquer
    console.log('[TM] Bouton flottant (iframe) prêt.');
  });
}

async function download_pdf_wrapper(e) {
  if (e) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); }
  try {
    // On repart de zéro à chaque run
    scrolled = 0;
    urls = [];

    // Si la page n'a pas encore initialisé height/scrollContainer, on tente ici
    if (!height) {
      const firstImg = document.getElementsByClassName('KfFlO')[0];
      if (firstImg) height = parseInt(firstImg.height);
    }
    if (!scrollContainer) {
      scrollContainer = document.getElementById('jmuse-scroller-component');
    }

    await download_pdf();
  } catch (err) {
    console.error('[TM] download_pdf_wrapper error:', err);
    alert('Échec du téléchargement PDF. Regarde la console pour les détails.');
  }
}


async function img_to_canvas_to_bytes(img) {
    return new Promise((resolve, reject) => {
        // Création d'un canvas pour dessiner l'image
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');

        // Chargement de l'image dans le canvas
        img.crossOrigin = 'Anonymous'; // Permet d'accéder à l'image cross-origin
        img.onload = () => {
            canvas.width = img.width;
            canvas.height = img.height;
            ctx.drawImage(img, 0, 0);

            // Conversion du contenu du canvas en données PNG
            const pngBytes = canvas.toDataURL('image/png').split(',')[1];
            resolve(pngBytes);
        };
        img.onerror = () => {
            reject(new Error('Échec du chargement de l\'image.'));
        };
        img.src = img.src; // Déclenche le chargement de l'image
    });
}



async function fetchAndAssemblePDF(urls) {
    // Création d'un nouveau document PDF
    const pdfDoc = await PDFLib.PDFDocument.create();

    // Tableau pour stocker les documents PNG générés
    const pngDocs = [];

    // Parcours de chaque URL de fichier img
    console.log("Avant boucle urls")

    for (const url of urls) {

        console.log("url = " + url)
        // Récupération du fichier img à partir de l'URL
        const response = await fetch(url);
        console.log(response)
        const imgText = await response.text();

        if (url.includes(".svg")) {
            const img = new Image();
            const imgLoaded = new Promise((resolve, reject) => {
                img.onload = resolve;
                img.onerror = reject;
            });
            // Création d'un blob à partir du texte SVG
            const svgBlob = new Blob([imgText], { type: 'image/svg+xml' });

            // Création d'une image à partir du blob SVG
            img.src = URL.createObjectURL(svgBlob);

            // Attente du chargement complet de l'image
            try {
                // Attendre le chargement complet de l'image
                await imgLoaded;
                console.log("Image SVG chargée avec succès");
            } catch (error) {
                console.error("Erreur lors du chargement de l'image SVG:", error);
                continue; // Passe à l'itération suivante dans la boucle
            }

            // Création d'un canvas pour dessiner l'image SVG
            const canvas = document.createElement('canvas');
            canvas.width = img.width;
            canvas.height = img.height;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0);

            // Conversion du canvas en format PNG
            const pngBytes = canvas.toDataURL('image/png').split(',')[1];

            // Incorporation du PNG dans le document PDF
            const pngDoc = await pdfDoc.embedPng(pngBytes);
            pngDocs.push(pngDoc); // Ajout du document PNG au tableau
        }

        else if (url.includes(".png")) {
            console.log("Dans la partie PNG");
            const img = new Image();
            const imgLoaded = new Promise((resolve, reject) => {
                img.onload = resolve;
                img.onerror = reject;
            });

            // Chargement de l'image à partir de l'URL
            img.src = url;
            // Promesse basée sur le chargement complet de l'image
            try {
                // Attendre le chargement complet de l'image
                await imgLoaded;
                console.log("Image PNG chargée avec succès");
            } catch (error) {
                console.error("Erreur lors du chargement de l'image PNG:", error);
                continue; // Passe à l'itération suivante dans la boucle
            }

            const pngBytes = await img_to_canvas_to_bytes(img)

            // Incorporation de l'image PNG dans le document PDF
            const pngDoc = await pdfDoc.embedPng(pngBytes);
            pngDocs.push(pngDoc);
        }
    }

    // Parcours des documents PNG pour les ajouter au document PDF
    for (const pngDoc of pngDocs) {
        const page = pdfDoc.addPage([pngDoc.width, pngDoc.height]);
        page.drawImage(pngDoc, {
            x: 0,
            y: 0,
            width: pngDoc.width,
            height: pngDoc.height,
        });
    }

    // Enregistrement du document PDF
    const pdfBytes = await pdfDoc.save();

    // Téléchargement du PDF
    const blob = new Blob([pdfBytes], { type: 'application/pdf' });
    const link = document.createElement('a');
    link.href = window.URL.createObjectURL(blob);
    let title = document.title.replace(" ", "_") + ".pdf"
    link.download = title;
    link.click();
}

let scrolled = 0;
let urls = [];
let height;
let scrollHeight;
let scrollContainer



$(document).ready(async function () {
    console.log("SCRIPT LOADED");
    setTimeout(() => {
        console.log("Set timeout");
        // ICI ON PARLE DU CONTENANT AVEC SCROLL
        scrollContainer = parseInt(document.getElementById('jmuse-scroller-component').scrollHeight);
        // ICI ON PARLE DES IMAGES
        height = parseInt(document.getElementsByClassName('KfFlO')[0].height);
        console.log("height = " + height);
        scrollHeight = (scrollContainer - (scrollContainer % height));
        console.log("scrollHeight = " + scrollHeight);

        // installe le bouton iframe (à la fin de ton $(document).ready(...))
        createFloatingIframeButton();

        // (optionnel) réinstalle si le site détruit le nœud
        const mo = new MutationObserver(() => {
            if (!document.getElementById('tm-fab-frame')) createFloatingIframeButton();
        });
        mo.observe(document.documentElement, { childList: true, subtree: true });

        // (optionnel) menu Tampermonkey en secours
        if (typeof GM_registerMenuCommand === 'function') {
            GM_registerMenuCommand('Télécharger PDF', () => download_pdf_wrapper());
        }

        // (optionnel) raccourci clavier Ctrl+Alt+D
        window.addEventListener('keydown', (e) => {
            if ((e.ctrlKey || e.metaKey) && e.altKey && e.key.toLowerCase() === 'd') {
                e.preventDefault();
                download_pdf_wrapper();
            }
        }, true);


    }, 2000)




});