Compare commits

..

No commits in common. "master" and "v0.21.3" have entirely different histories.

28 changed files with 158 additions and 886 deletions

View File

@ -20,8 +20,6 @@
"no-useless-constructor": "off", "no-useless-constructor": "off",
"@typescript-eslint/no-useless-constructor": "error", "@typescript-eslint/no-useless-constructor": "error",
"no-dupe-class-members": "off", "no-dupe-class-members": "off",
"no-void": "off",
"no-use-before-define": "off",
"@typescript-eslint/no-dupe-class-members": "error", "@typescript-eslint/no-dupe-class-members": "error",
"@typescript-eslint/no-floating-promises": "warn", "@typescript-eslint/no-floating-promises": "warn",
"@typescript-eslint/member-delimiter-style": "warn", "@typescript-eslint/member-delimiter-style": "warn",

View File

@ -53,15 +53,6 @@ jobs:
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # 0301... NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # 0301...
- name: NPM Publish msdl
run: |
cd ./src/msdl
sed -i "s/%VERSION%/$VERSION/" package.json
npm publish --tag $NPM_TAG
cd -
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish Firefox Extension - name: Publish Firefox Extension
id: web-ext-build id: web-ext-build
uses: kewisch/action-web-ext@v1 uses: kewisch/action-web-ext@v1
@ -78,7 +69,6 @@ jobs:
cp dist/main.js $ARTIFACTS_DIR/musescore-downloader.user.js cp dist/main.js $ARTIFACTS_DIR/musescore-downloader.user.js
cp dist/ext.zip $ARTIFACTS_DIR/musescore-downloader.webextension.zip cp dist/ext.zip $ARTIFACTS_DIR/musescore-downloader.webextension.zip
wget -q $CHROME_EXT_URL -P $ARTIFACTS_DIR/ wget -q $CHROME_EXT_URL -P $ARTIFACTS_DIR/
wget -q https://github.com/Xmader/musescore-downloader/archive/$REF.tar.gz -O $ARTIFACTS_DIR/source.tar.gz
- run: bash ./.github/workflows/get-signed-ext.sh - run: bash ./.github/workflows/get-signed-ext.sh
env: env:
EXT_ID: musescore-downloader EXT_ID: musescore-downloader
@ -98,7 +88,6 @@ jobs:
IPFS_HASH: ${{ steps.ipfs.outputs.hash }} IPFS_HASH: ${{ steps.ipfs.outputs.hash }}
run: | run: |
cd $ARTIFACTS_DIR cd $ARTIFACTS_DIR
rm *.tar.gz
files=$(ls .) files=$(ls .)
assets=() assets=()

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019-2021 Xmader Copyright (c) 2019-2020 Xmader
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

123
README.md
View File

@ -1,7 +1,7 @@
# musescore-downloader # musescore-downloader
**English** | [简体中文](#musescore-downloader-1) | [Español](#musescore-downloader-2) | [Italian](#musescore-downloader-3) **English** | [简体中文](#musescore-downloader-1) | [Español](#musescore-downloader-2)
> download sheet music from musescore.com for free, no login or Musescore Pro required > download sheet music from musescore.com for free, no login or Musescore Pro required
@ -243,7 +243,6 @@ En tercer lugar, la propiedad de los derechos de autor de los contenidos de muse
Si no podemos ver pruebas de que musescore.com realmente paga la tarifa de licencia a los propietarios de los derechos de autor, podemos pensar que es solo una excusa para obtener ganancias robando. Si no podemos ver pruebas de que musescore.com realmente paga la tarifa de licencia a los propietarios de los derechos de autor, podemos pensar que es solo una excusa para obtener ganancias robando.
> utilizo ilegalmente nuestra API privada con contenido de música licenciada. > utilizo ilegalmente nuestra API privada con contenido de música licenciada.
No, el documento de la API está en https://developers.musescore.com/. No, el documento de la API está en https://developers.musescore.com/.
@ -252,123 +251,3 @@ No, el documento de la API está en https://developers.musescore.com/.
**Lanzaré una alternativa de código abierto (GPLv3), sin servidor, offline, y totalmente gratuita a musescore.com, [LibreScore](https://github.com/LibreScore). ETodos son bienvenidos a unirse al desarrollo del proyecto abriendo un problema o [enviándome un correo electrónico.](mailto:i@xmader.com).** **Lanzaré una alternativa de código abierto (GPLv3), sin servidor, offline, y totalmente gratuita a musescore.com, [LibreScore](https://github.com/LibreScore). ETodos son bienvenidos a unirse al desarrollo del proyecto abriendo un problema o [enviándome un correo electrónico.](mailto:i@xmader.com).**
**Además, estoy desarrollando musescore.js. Podría convertir un archivo mscz en cualquier formato que admita el software Musescore, y en el navegador.** Dado que el software Musescore es de código abierto bajo [GPL](https://github.com/musescore/MuseScore/blob/master/LICENSE.GPL), Podría traducir el código fuente a js o compilarlo en asm.js/WASM. **Además, estoy desarrollando musescore.js. Podría convertir un archivo mscz en cualquier formato que admita el software Musescore, y en el navegador.** Dado que el software Musescore es de código abierto bajo [GPL](https://github.com/musescore/MuseScore/blob/master/LICENSE.GPL), Podría traducir el código fuente a js o compilarlo en asm.js/WASM.
---
# musescore-downloader
[English](#musescore-downloader) | [简体中文](#musescore-downloader-1) | [Español](#musescore-downloader-2) | **Italian**
> Scarica spartiti da musescore.com gratuitamente, non è richisto login o Musescore Pro.
**Avvia questo progetto su [Github](https://github.com/Xmader/musescore-downloader) e [Gitlab](https://gitlab.com/Xmader/musescore-downloader)** (Mirror)
[![Discord](https://img.shields.io/discord/774491656643674122?color=7289da&label=Discord&logo=discord)](https://discord.gg/DKu7cUZ4XQ)
Hai bisogno di datset di musescore.com per l'analisi/machine learning? Prova [musescore-dataset](https://github.com/Xmader/musescore-dataset).
![](https://cdn.statically.io/gh/Xmader/musescore-downloader/master/screenshot.png?env=dev)
## Utilizzo leale
Solo per scopi di ricerca e studio
## In breve
Di recente è necessario un account Musescore Pro ($6,99/mese) per scaricare spartiti da musescore.com.
(Tuttavia, alcuni mesi fa, era possibile scaricare gratuitamente.)
La società Musescore ha affermato che a causa di copyright e licenze, devono pagare i proprietari del copyright.
Molte musiche su musescore.com sono già di **Pubblico Dominio**, il che significa che o l'autore le ha pubblicate in pubblico dominio o l'autore è morto da oltre 70 anni.
Devono pagare anche i compositori che sono morti centinaia di anni fa?
*Aggiornamento: gli spartiti di Dominio Pubblico, ora possono essere scaricati senza Musescore Pro, ma hai ancora bisogno di un account per poter scaricare.*
Inoltre, ci sono molti autori di spartiti su musescore.com che hanno creato le proprie canzoni e le hanno pubblicate sotto licenza [CC-BY-**NC** (Creative Commons Attribution-**NonCommercial**)](https://creativecommons.org/licenses/by-nc/4.0/).
È illegale venderli **a scopo di lucro**?
*Nota: Mettere annunci (per vendere Musescore Pro) sul sito web significa anche che lo usano per generare entrate.*
Questo è assolutamente inaccettabile e l'unico scopo è trarre profitto dal furto.
C'è un articolo sul loro sito web: [Il download degli spartiti diventa parte dell'abbonamento Pro](https://musescore.com/groups/improving-musescore-com/discuss/5044610)
## Installazione
### Utilizzo della CLI
(consigliato)
1. Installa Node.js LTS (https://nodejs.org/)
2. Apri il terminale CMD o Powershell
3. Digita `npx msdl`, e premi invio
(`npx msdl` eseguirà sempre l'ultima versione)
4. Seguire le istruzioni
[codice sorgente](/src/cli.ts)
### Installa come Userscript
Questo script è disponibile come [Userscript](https://en.wikipedia.org/wiki/Userscript). Per utilizzare questo Userscript, è necessario prima installare un [gestore di script utente].(https://greasyfork.org/en/help/installing-user-scripts), come Tampermonkey.
1. Installa [Tampermonkey](https://www.tampermonkey.net/)
2. ~~Installa da [Greasy Fork](https://greasyfork.org/scripts/391931).~~ [#42](https://github.com/Xmader/musescore-downloader/issues/42)
Installa lo script da <https://msdl.librescore.org/install.user.js>
### Installa come estensione web
Il metodo alternativo consiste nell'installare questo script come estensione per Chrome o Firefox.
Puoi installare l'estensione del browser direttamente da [addons.mozilla.org (per Firefox)](https://addons.mozilla.org/en-US/firefox/addon/musescore-downloader/) o dal [web store di Chrome (per browser basati su Chrome e Chromium)](https://chrome.google.com/webstore/detail/mhdlcdhakmmikknpefblmnhdhjloanjc).
La versione aggiornata può essere trovata nella pagina [Github Releases](https://github.com/Xmader/musescore-downloader/releases).
## Istruzioni per il building
Assicurati di avere installato [Node.js](https://nodejs.org/en/).
```bash
npm install
npm run build # build come script utente
npm run pack:ext # pack Web Extension
```
## Mirrors
* Visualizza questo progetto su [Github](https://github.com/Xmader/musescore-downloader) (Repo principale) | [Gitlab](https://gitlab.com/Xmader/musescore-downloader) (Mirror)
* Questo repo è disponibile anche su IPFS per evitare la rimozione DMCA: [ipns://msdl.librescore.org](https://ipfs.io/ipns/msdl.librescore.org/)
## Feedback
[Problemi con GitHub](https://github.com/Xmader/musescore-downloader/issues)
## Licenza
MIT
## Informazioni sulla richiesta di rimozione
Ho ricevuto un'e-mail di [richiesta di rimozione](https://github.com/Xmader/musescore-downloader/issues/5)da uno degli sviluppatori di Musescore, ma ho qualcosa da ridire.
> Non tutti i contenuti di pubblico dominio su musescore.com sono concessi in licenza dai principali editori musicali (Alfred, EMI, Sony, ecc.). State distribuendo gratuitamente contenuti musicali con licenza da Musescore.com violando i loro diritti.
In primo luogo, se violi i diritti dei principali editori musicali, la richiesta di rimozione dovrebbe essere inviata da loro invece che dagli sviluppatori di Musescore.
In secondo luogo, musescore.com non è un semplice sito di condivisione di musica. Gli autori di spartiti devono trascrivere e riorganizzare le canzoni originali in spartiti, non solo copiare file da qualche altra parte su musescore.com. Di conseguenza, la licenza dovrebbe concentrarsi sui diritti di trascrizione / riorganizzazione degli autori di spartiti, invece che sui diritti di condivisione della musica su alcuni siti web.
In terzo luogo, la proprietà del copyright dei contenuti su musescore.com non è chiara. Non tutte le canzoni di pubblico dominio su musescore.com sono di proprietà dei principali editori musicali. Ci sono molti piccoli editori musicali e cantautori indipendenti. Le canzoni potrebbero essere concesse in licenza con licenze gratuite come Creative Commons. Inoltre, ci sono molti autori che hanno creato le proprie canzoni e pubblicato gli spartiti su musescore.com; Musescore.com paga questi autori?
Se ci sono prove che musescore.com paga davvero la tassa di licenza ai proprietari del copyright, potremmo pensare che sia solo una scusa per ottenere profitto dal furto.
> utilizzi illegalmente la nostra API privata con contenuti musicali con licenza.
No, il documento API è su https://developers.musescore.com/.
**Avvierò un'alternativa open source (GPLv3), serverless, offline-first, frontend-first e totalmente gratuita a musescore.com, [LibreScore](https://github.com/LibreScore). Tutti sono invitati a partecipare allo sviluppo del progetto aprendo una issue o [inviandomi un'e-mail](mailto:i@xmader.com).**
**Inoltre, sto sviluppando musescore.js. Potrebbe convertire un file mscz in qualsiasi formato supportato dal software Musescore e nel browser.** Poiché il software Musescore è open source sotto [GPL](https://github.com/musescore/MuseScore/blob/master/LICENSE.GPL), potrei tradurre il codice sorgente in js o compilarlo in asm.js/WASM.
---

329
dist/main.js vendored
View File

@ -5,13 +5,13 @@
// @supportURL https://github.com/Xmader/musescore-downloader/issues // @supportURL https://github.com/Xmader/musescore-downloader/issues
// @updateURL https://msdl.librescore.org/install.user.js // @updateURL https://msdl.librescore.org/install.user.js
// @downloadURL https://msdl.librescore.org/install.user.js // @downloadURL https://msdl.librescore.org/install.user.js
// @version 0.24.1 // @version 0.21.3
// @description download sheet music from musescore.com for free, no login or Musescore Pro required | 免登录、免 Musescore Pro免费下载 musescore.com 上的曲谱 // @description download sheet music from musescore.com for free, no login or Musescore Pro required | 免登录、免 Musescore Pro免费下载 musescore.com 上的曲谱
// @author Xmader // @author Xmader
// @match https://musescore.com/*/* // @match https://musescore.com/*/*
// @match https://s.musescore.com/*/* // @match https://s.musescore.com/*/*
// @license MIT // @license MIT
// @copyright Copyright (c) 2019-2021 Xmader // @copyright Copyright (c) 2019-2020 Xmader
// @grant unsafeWindow // @grant unsafeWindow
// @grant GM.registerMenuCommand // @grant GM.registerMenuCommand
// @grant GM.addElement // @grant GM.addElement
@ -32,11 +32,6 @@
if (_GM && _GM.registerMenuCommand && _GM.openInTab) { if (_GM && _GM.registerMenuCommand && _GM.openInTab) {
// add buttons to the userscript manager menu // add buttons to the userscript manager menu
_GM.registerMenuCommand(
`** Version: ${_GM.info.script.version} **`,
() => _GM.openInTab("https://github.com/Xmader/musescore-downloader/releases", { active: true })
)
_GM.registerMenuCommand( _GM.registerMenuCommand(
'** Source Code **', '** Source Code **',
() => _GM.openInTab(_GM.info.script.homepage, { active: true }) () => _GM.openInTab(_GM.info.script.homepage, { active: true })
@ -48,24 +43,17 @@
) )
} }
function getRandL () {
return String.fromCharCode(97 + Math.floor(Math.random() * 26))
}
// script loader // script loader
new Promise(resolve => { new Promise(resolve => {
const id = '' + Math.random(); const id = '' + Math.random();
w[id] = resolve; w[id] = resolve;
const stackN = 9 const stackN = 9
let loaderIntro = '' const loaderIntro = '(function a(){'.repeat(stackN)
for (let i = 0; i < stackN; i++) {
loaderIntro += `(function ${getRandL()}(){`
}
const loaderOutro = '})()'.repeat(stackN) const loaderOutro = '})()'.repeat(stackN)
const mockUrl = "https://c.amazon-adsystem.com/aax2/apstag.js" const mockUrl = "https://c.amazon-adsystem.com/aax2/apstag.js"
Function(`${loaderIntro}const d=new Image();window['${id}'](d);delete window['${id}'];document.body.prepend(d)${loaderOutro}//# sourceURL=${mockUrl}`)() setTimeout(`${loaderIntro}const d=new Image();window['${id}'](d);delete window['${id}'];document.body.prepend(d)${loaderOutro}//# sourceURL=${mockUrl}`)
}).then(d => { }).then(d => {
d.style.display = 'none'; d.style.display = 'none';
d.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; d.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
@ -331,7 +319,6 @@
typeof GM[requiredMethod] !== 'undefined'; typeof GM[requiredMethod] !== 'undefined';
}; };
const DISCORD_URL = 'https://discord.gg/gSsTUvJmD8';
const escapeFilename = (s) => { const escapeFilename = (s) => {
return s.replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, '_'); return s.replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, '_');
}; };
@ -423,20 +410,16 @@
const attachShadow = (el) => { const attachShadow = (el) => {
return Element.prototype.attachShadow.call(el, { mode: 'closed' }); return Element.prototype.attachShadow.call(el, { mode: 'closed' });
}; };
/** const waitForDocumentLoaded = () => {
* Run script before the page is fully loaded
*/
const waitForSheetLoaded = () => {
if (document.readyState !== 'complete') { if (document.readyState !== 'complete') {
return new Promise(resolve => { return new Promise(resolve => {
const observer = new MutationObserver(() => { const cb = () => {
const img = document.querySelector('img'); if (document.readyState === 'complete') {
if (img) {
resolve(); resolve();
observer.disconnect(); document.removeEventListener('readystatechange', cb);
} }
}); };
observer.observe(document, { childList: true, subtree: true }); document.addEventListener('readystatechange', cb);
}); });
} }
else { else {
@ -26440,7 +26423,7 @@ Please pipe the document into a Node stream.\
}); });
/// <reference lib="webworker" /> /// <reference lib="webworker" />
const readData = (blob, type) => { const getDataURL = (blob) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
@ -26448,21 +26431,19 @@ Please pipe the document into a Node stream.\
resolve(result); resolve(result);
}; };
reader.onerror = reject; reader.onerror = reject;
if (type === 'dataUrl') {
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
}
else {
reader.readAsText(blob);
}
}); });
}; };
const fetchBlob = (imgUrl) => __awaiter(void 0, void 0, void 0, function* () { const fetchDataURL = (imgUrl) => __awaiter(void 0, void 0, void 0, function* () {
const r = yield fetch(imgUrl, { const r = yield fetch(imgUrl);
cache: 'no-cache', const blob = yield r.blob();
return getDataURL(blob);
}); });
return r.blob(); const fetchText = (imgUrl) => __awaiter(void 0, void 0, void 0, function* () {
const r = yield fetch(imgUrl);
return r.text();
}); });
const generatePDF = (imgBlobs, imgType, width, height) => __awaiter(void 0, void 0, void 0, function* () { const generatePDF = (imgURLs, imgType, width, height) => __awaiter(void 0, void 0, void 0, function* () {
// @ts-ignore // @ts-ignore
const pdf = new PDFDocument({ const pdf = new PDFDocument({
// compress: true, // compress: true,
@ -26472,7 +26453,7 @@ Please pipe the document into a Node stream.\
layout: 'portrait', layout: 'portrait',
}); });
if (imgType === 'png') { if (imgType === 'png') {
const imgDataUrlList = yield Promise.all(imgBlobs.map(b => readData(b, 'dataUrl'))); const imgDataUrlList = yield Promise.all(imgURLs.map(fetchDataURL));
imgDataUrlList.forEach((data) => { imgDataUrlList.forEach((data) => {
pdf.addPage(); pdf.addPage();
pdf.image(data, { pdf.image(data, {
@ -26482,7 +26463,7 @@ Please pipe the document into a Node stream.\
}); });
} }
else { // imgType == "svg" else { // imgType == "svg"
const svgList = yield Promise.all(imgBlobs.map(b => readData(b, 'text'))); const svgList = yield Promise.all(imgURLs.map(fetchText));
svgList.forEach((svg) => { svgList.forEach((svg) => {
pdf.addPage(); pdf.addPage();
source(pdf, svg, 0, 0, { source(pdf, svg, 0, 0, {
@ -26495,9 +26476,8 @@ Please pipe the document into a Node stream.\
return buf.buffer; return buf.buffer;
}); });
onmessage = (e) => __awaiter(void 0, void 0, void 0, function* () { onmessage = (e) => __awaiter(void 0, void 0, void 0, function* () {
const [imgUrls, imgType, width, height,] = e.data; const [imgURLs, imgType, width, height,] = e.data;
const imgBlobs = yield Promise.all(imgUrls.map(url => fetchBlob(url))); const pdfBuf = yield generatePDF(imgURLs, imgType, width, height);
const pdfBuf = yield generatePDF(imgBlobs, imgType, width, height);
postMessage(pdfBuf, [pdfBuf]); postMessage(pdfBuf, [pdfBuf]);
}); });
@ -26575,7 +26555,7 @@ Please pipe the document into a Node stream.\
} }
/* eslint-disable no-extend-native */ /* eslint-disable no-extend-native */
const TYPE_REG = /type=(img|mp3|midi)/; const TYPE_REG = /id=(\d+)&type=(img|mp3|midi)/;
/** /**
* I know this is super hacky. * I know this is super hacky.
*/ */
@ -26596,9 +26576,8 @@ Please pipe the document into a Node stream.\
const token = (_a = init === null || init === void 0 ? void 0 : init.headers) === null || _a === void 0 ? void 0 : _a.Authorization; const token = (_a = init === null || init === void 0 ? void 0 : init.headers) === null || _a === void 0 ? void 0 : _a.Authorization;
if (typeof url === 'string' && token) { if (typeof url === 'string' && token) {
const m = url.match(TYPE_REG); const m = url.match(TYPE_REG);
console$1.debug(url, token, m);
if (m) { if (m) {
const type = m[1]; const type = m[2];
// eslint-disable-next-line no-unused-expressions // eslint-disable-next-line no-unused-expressions
(_b = l[type]) === null || _b === void 0 ? void 0 : _b.call(l, token); (_b = l[type]) === null || _b === void 0 ? void 0 : _b.call(l, token);
} }
@ -26638,7 +26617,7 @@ Please pipe the document into a Node stream.\
// force to retrieve the MAGIC // force to retrieve the MAGIC
switch (type) { switch (type) {
case 'midi': { case 'midi': {
const el = document.querySelector('button[hasaccess]'); const el = document.querySelectorAll('.SD7H- > button')[3];
el.click(); el.click();
break; break;
} }
@ -26736,7 +26715,7 @@ Please pipe the document into a Node stream.\
// read further error msg // read further error msg
const err = cidRes.Message; const err = cidRes.Message;
if (err.includes('no link named')) { // file not found if (err.includes('no link named')) { // file not found
throw new Error('Score not in dataset'); throw new Error('score not in dataset');
} }
else { else {
throw new Error(err); throw new Error(err);
@ -26795,9 +26774,6 @@ Please pipe the document into a Node stream.\
'IND_PARTS_TOOLTIP'() { 'IND_PARTS_TOOLTIP'() {
return 'Download individual parts (BETA)'; return 'Download individual parts (BETA)';
}, },
'VIEW_IN_LIBRESCORE'() {
return 'View in LibreScore';
},
'FULL_SCORE'() { 'FULL_SCORE'() {
return 'Full score'; return 'Full score';
}, },
@ -26811,7 +26787,7 @@ Please pipe the document into a Node stream.\
return '❌¡Descarga Fallida!'; return '❌¡Descarga Fallida!';
}, },
'DEPRECATION_NOTICE'(btnName) { 'DEPRECATION_NOTICE'(btnName) {
return `¡OBSOLETO!\nUtilizar \`${btnName}\` dentro de \`Partes Indivduales\` en su lugar.\n(Esto todavía puede funcionar. Pulsa \`Aceptar\` para continuar.)`; return `¡OBSOLETO!\nParecer ser que \`${btnName}\` no funciona correctamente, use \`Partes Indivduales\` en su lugar.\n(Esto todavía puede funcionar. Haga click en \`Aceptar\` para continuar.)`;
}, },
'DOWNLOAD'(fileType) { 'DOWNLOAD'(fileType) {
return `Descargar ${fileType}`; return `Descargar ${fileType}`;
@ -26825,79 +26801,14 @@ Please pipe the document into a Node stream.\
'IND_PARTS_TOOLTIP'() { 'IND_PARTS_TOOLTIP'() {
return 'Descargar partes individuales (BETA)'; return 'Descargar partes individuales (BETA)';
}, },
'VIEW_IN_LIBRESCORE'() {
return 'Visualizar en LibreScore';
},
'FULL_SCORE'() { 'FULL_SCORE'() {
return 'Partitura Completa'; return 'Partitura Completa';
}, },
}); });
var it = createLocale({
'PROCESSING'() {
return 'Caricamento…';
},
'BTN_ERROR'() {
return '❌Download Fallito!';
},
'DEPRECATION_NOTICE'(btnName) {
return `¡DEPRECATO!\nUtilizzare \`${btnName}\` all'interno di \`Parti Indivduali\`.\n(Qusto potrebbe funzionare. Cliccare \`Ok\` per continuare.)`;
},
'DOWNLOAD'(fileType) {
return `Scaricare ${fileType}`;
},
'DOWNLOAD_AUDIO'(fileType) {
return `Scaricare ${fileType} Audio`;
},
'IND_PARTS'() {
return 'Parti Singole';
},
'IND_PARTS_TOOLTIP'() {
return 'Scaricare Parti Singole (BETA)';
},
'VIEW_IN_LIBRESCORE'() {
return 'Visualizzare in LibreScore';
},
'FULL_SCORE'() {
return 'Spartito Completo';
},
});
var zh = createLocale({
'PROCESSING'() {
return '处理中…';
},
'BTN_ERROR'() {
return '❌下载失败!';
},
'DEPRECATION_NOTICE'(btnName) {
return `不建议使用\n请使用 \`单独分谱\` 里的 \`${btnName}\` 按钮代替\n(这也许仍会起作用。单击\`确定\`以继续。)`;
},
'DOWNLOAD'(fileType) {
return `下载 ${fileType}`;
},
'DOWNLOAD_AUDIO'(fileType) {
return `下载 ${fileType} 音频`;
},
'IND_PARTS'() {
return '单独分谱';
},
'IND_PARTS_TOOLTIP'() {
return '下载单独分谱 (BETA)';
},
'VIEW_IN_LIBRESCORE'() {
return '在 LibreScore 中查看';
},
'FULL_SCORE'() {
return '完整乐谱';
},
});
const locales = ((l) => Object.freeze(l))({ const locales = ((l) => Object.freeze(l))({
en, en,
es, es,
it,
zh,
}); });
// detect browser language // detect browser language
const lang = (() => { const lang = (() => {
@ -26923,22 +26834,12 @@ Please pipe the document into a Node stream.\
return locale[key]; return locale[key];
} }
var dependencies = {
"@librescore/fonts": "^0.4.0",
"@librescore/sf3": "^0.3.0",
"detect-node": "^2.0.4",
inquirer: "^7.3.3",
"node-fetch": "^2.6.1",
ora: "^5.1.0",
webmscore: "^0.18.0"
};
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
const WEBMSCORE_URL = `https://cdn.jsdelivr.net/npm/webmscore@${dependencies.webmscore}/webmscore.js`; const WEBMSCORE_URL = 'https://cdn.jsdelivr.net/npm/webmscore@0.10/webmscore.js';
// fonts for Chinese characters (CN) and Korean hangul (KR) // fonts for Chinese characters (CN) and Korean hangul (KR)
// JP characters are included in the CN font // JP characters are included in the CN font
const FONT_URLS = ['CN', 'KR'].map(l => `https://cdn.jsdelivr.net/npm/@librescore/fonts@${dependencies['@librescore/fonts']}/SourceHanSans${l}.min.woff2`); const FONT_URLS = ['CN', 'KR'].map(l => `https://cdn.jsdelivr.net/npm/@librescore/fonts/SourceHanSans${l}-Regular.woff2`);
const SF3_URL = `https://cdn.jsdelivr.net/npm/@librescore/sf3@${dependencies['@librescore/sf3']}/FluidR3Mono_GM.sf3`; const SF3_URL = 'https://cdn.jsdelivr.net/npm/@librescore/sf3/FluidR3Mono_GM.sf3';
const SOUND_FONT_LOADED = Symbol('SoundFont loaded'); const SOUND_FONT_LOADED = Symbol('SoundFont loaded');
const initMscore = (w) => __awaiter(void 0, void 0, void 0, function* () { const initMscore = (w) => __awaiter(void 0, void 0, void 0, function* () {
if (!detectNode) { // attached to a page if (!detectNode) { // attached to a page
@ -27020,11 +26921,6 @@ Please pipe the document into a Node stream.\
fileExt: 'mid', fileExt: 'mid',
action: (score) => score.saveMidi(true, true), action: (score) => score.saveMidi(true, true),
}, },
{
name: i18n('DOWNLOAD_AUDIO')('MP3'),
fileExt: 'mp3',
action: (score) => loadSoundFont(score).then(() => score.saveAudio('mp3')),
},
{ {
name: i18n('DOWNLOAD_AUDIO')('FLAC'), name: i18n('DOWNLOAD_AUDIO')('FLAC'),
fileExt: 'flac', fileExt: 'flac',
@ -27037,57 +26933,30 @@ Please pipe the document into a Node stream.\
}, },
]; ];
const _getLink = (indexingInfo) => { var btnListCss = "div {\n flex-wrap: wrap;\n display: flex;\n align-items: center;\n font-family: 'Open Sans', 'Roboto', 'Helvetica neue', Helvetica, sans-serif;\n position: absolute;\n z-index: 9999;\n background: #f6f6f6;\n min-width: 230px;\n}\n\nbutton {\n width: 205px !important;\n height: 38px;\n\n color: #fff;\n background: #1f74bd;\n\n cursor: pointer;\n\n margin-bottom: 4px;\n margin-right: 4px;\n padding: 4px 12px;\n\n justify-content: start;\n align-self: center;\n\n font-size: 16px;\n border-radius: 2px;\n border: 0;\n\n display: inline-flex;\n position: relative;\n\n font-family: inherit;\n}\n\nsvg {\n display: inline-block;\n margin-right: 5px;\n width: 20px;\n height: 20px;\n margin-top: auto;\n margin-bottom: auto;\n}\n\nspan {\n margin-top: auto;\n margin-bottom: auto;\n}";
const { scorepack } = JSON.parse(indexingInfo);
return `https://librescore.org/score/${scorepack}`;
};
const getLibreScoreLink = (scoreinfo, _fetch = getFetch()) => __awaiter(void 0, void 0, void 0, function* () {
const mainCid = yield getMainCid(scoreinfo, _fetch);
const ref = scoreinfo.getScorepackRef(mainCid);
const url = `https://ipfs.infura.io:5001/api/v0/dag/get?arg=${ref}`;
const r0 = yield _fetch(url);
if (r0.status !== 500) {
assertRes(r0);
}
const res = yield r0.json();
if (typeof res !== 'string') {
// read further error msg
throw new Error(res.Message);
}
return _getLink(res);
});
var btnListCss = "div {\n width: 422px;\n right: 0;\n margin: 0 18px 18px 0;\n\n text-align: center;\n align-items: center;\n font-family: 'Inter', 'Helvetica neue', Helvetica, sans-serif;\n position: absolute;\n z-index: 9999;\n background: #f6f6f6;\n min-width: 230px;\n\n /* pass the scroll event through the btns background */\n pointer-events: none;\n}\n\n@media screen and (max-width: 950px) {\n div {\n width: auto !important;\n }\n}\n\nbutton {\n width: 178px !important;\n min-width: 178px;\n height: 40px;\n\n color: #fff;\n background: #2e68c0;\n\n cursor: pointer;\n pointer-events: auto;\n\n margin-bottom: 8px;\n margin-right: 8px;\n padding: 4px 12px;\n\n justify-content: start;\n align-self: center;\n\n font-size: 16px;\n border-radius: 6px;\n border: 0;\n\n display: inline-flex;\n position: relative;\n\n font-family: inherit;\n}\n\n/* fix `View in LibreScore` button text overflow */\nbutton:last-of-type {\n width: unset !important;\n}\n\nbutton:hover {\n background: #1a4f9f;\n}\n\n/* light theme btn */\nbutton.light {\n color: #2e68c0;\n background: #e1effe;\n}\n\nbutton.light:hover {\n background: #c3ddfd;\n}\n\nsvg {\n display: inline-block;\n margin-right: 5px;\n width: 20px;\n height: 20px;\n margin-top: auto;\n margin-bottom: auto;\n}\n\nspan {\n margin-top: auto;\n margin-bottom: auto;\n}";
var ICON;
(function (ICON) {
ICON["DOWNLOAD"] = "M9.6 2.4h4.8V12h2.784l-5.18 5.18L6.823 12H9.6V2.4zM19.2 19.2H4.8v2.4h14.4v-2.4z";
ICON["LIBRESCORE"] = "m5.4837 4.4735v10.405c-1.25-0.89936-3.0285-0.40896-4.1658 0.45816-1.0052 0.76659-1.7881 2.3316-0.98365 3.4943 1 1.1346 2.7702 0.70402 3.8817-0.02809 1.0896-0.66323 1.9667-1.8569 1.8125-3.1814v-5.4822h8.3278v9.3865h9.6438v-2.6282h-6.4567v-12.417c-4.0064-0.015181-8.0424-0.0027-12.06-0.00676zm0.54477 2.2697h8.3278v1.1258h-8.3278v-1.1258z";
})(ICON || (ICON = {}));
const getBtnContainer = () => { const getBtnContainer = () => {
var _a; var _a;
const els = [...document.querySelectorAll('span')]; const els = [...document.querySelectorAll('*')].reverse();
const el = els.find(b => { const el = els.find(b => {
var _a; var _a;
const text = ((_a = b === null || b === void 0 ? void 0 : b.textContent) === null || _a === void 0 ? void 0 : _a.replace(/\s/g, '')) || ''; const text = ((_a = b === null || b === void 0 ? void 0 : b.textContent) === null || _a === void 0 ? void 0 : _a.replace(/\s/g, '')) || '';
return text.includes('Download') || text.includes('Print'); return text.includes('Download') || text.includes('Print');
}); });
const btnParent = (_a = el === null || el === void 0 ? void 0 : el.parentElement) === null || _a === void 0 ? void 0 : _a.parentElement; const btnParent = (_a = el === null || el === void 0 ? void 0 : el.parentElement) === null || _a === void 0 ? void 0 : _a.parentElement;
if (!btnParent || !(btnParent instanceof HTMLDivElement)) if (!btnParent)
throw new Error('btn parent not found'); throw new Error('btn parent not found');
return btnParent; return btnParent;
}; };
const buildDownloadBtn = (icon, lightTheme = false) => { const buildDownloadBtn = () => {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.type = 'button'; btn.type = 'button';
if (lightTheme)
btn.className = 'light';
// build icon svg element // build icon svg element
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('viewBox', '0 0 24 24');
const svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); const svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
svgPath.setAttribute('d', icon); svgPath.setAttribute('d', 'M9.6 2.4h4.8V12h2.784l-5.18 5.18L6.823 12H9.6V2.4zM19.2 19.2H4.8v2.4h14.4v-2.4z');
svgPath.setAttribute('fill', lightTheme ? '#2e68c0' : '#fff'); svgPath.setAttribute('fill', '#fff');
svg.append(svgPath); svg.append(svgPath);
const textNode = document.createElement('span'); const textNode = document.createElement('span');
btn.append(svg, textNode); btn.append(svg, textNode);
@ -27098,30 +26967,6 @@ Please pipe the document into a Node stream.\
n.onclick = btn.onclick; n.onclick = btn.onclick;
return n; return n;
}; };
function getScrollParent(node) {
if (node.scrollHeight > node.clientHeight) {
return node;
}
else {
return getScrollParent(node.parentNode);
}
}
function onPageRendered(getEl) {
return new Promise((resolve) => {
var _a;
const observer = new MutationObserver(() => {
try {
const el = getEl();
if (el) {
observer.disconnect();
resolve(el);
}
}
catch (_a) { }
});
observer.observe((_a = document.querySelector('div > section')) !== null && _a !== void 0 ? _a : document.body, { childList: true, subtree: true });
});
}
var BtnListMode; var BtnListMode;
(function (BtnListMode) { (function (BtnListMode) {
BtnListMode[BtnListMode["InPage"] = 0] = "InPage"; BtnListMode[BtnListMode["InPage"] = 0] = "InPage";
@ -27133,8 +26978,7 @@ Please pipe the document into a Node stream.\
this.list = []; this.list = [];
} }
add(options) { add(options) {
var _a; const btnTpl = buildDownloadBtn();
const btnTpl = buildDownloadBtn((_a = options.icon) !== null && _a !== void 0 ? _a : ICON.DOWNLOAD, options.lightTheme);
const setText = (btn) => { const setText = (btn) => {
const textNode = btn.querySelector('span'); const textNode = btn.querySelector('span');
return (str) => { return (str) => {
@ -27163,16 +27007,6 @@ Please pipe the document into a Node stream.\
} }
return btnTpl; return btnTpl;
} }
_positionBtns(anchorDiv, newParent) {
let { top } = anchorDiv.getBoundingClientRect();
top += window.scrollY; // relative to the entire document instead of viewport
if (top > 0) {
newParent.style.top = `${top}px`;
}
else {
newParent.style.top = '0px';
}
}
_commit() { _commit() {
const btnParent = document.querySelector('div'); const btnParent = document.querySelector('div');
const shadow = attachShadow(btnParent); const shadow = attachShadow(btnParent);
@ -27186,17 +27020,16 @@ Please pipe the document into a Node stream.\
const newParent = document.createElement('div'); const newParent = document.createElement('div');
newParent.append(...this.list.map(e => cloneBtn(e))); newParent.append(...this.list.map(e => cloneBtn(e)));
shadow.append(newParent); shadow.append(newParent);
// default position try {
newParent.style.top = `${window.innerHeight - newParent.getBoundingClientRect().height}px`; const anchorDiv = this.getBtnParent();
void onPageRendered(this.getBtnParent).then((anchorDiv) => { const { width, top, left } = anchorDiv.getBoundingClientRect();
const pos = () => this._positionBtns(anchorDiv, newParent); newParent.style.width = `${width}px`;
pos(); newParent.style.top = `${top}px`;
// reposition btns when window resizes newParent.style.left = `${left}px`;
window.addEventListener('resize', pos, { passive: true }); }
// reposition btns when scrolling catch (err) {
const scroll = getScrollParent(anchorDiv); console$1.error(err);
scroll.addEventListener('scroll', pos, { passive: true }); }
});
return btnParent; return btnParent;
} }
/** /**
@ -27248,17 +27081,14 @@ Please pipe the document into a Node stream.\
else else
return url; return url;
}; };
BtnAction.download = (url, fallback, timeout, target) => { BtnAction.download = (url, fallback, timeout) => {
return BtnAction.process(() => __awaiter(this, void 0, void 0, function* () { return BtnAction.process(() => __awaiter(this, void 0, void 0, function* () {
const _url = yield normalizeUrlInput(url); const _url = yield normalizeUrlInput(url);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = _url; a.href = _url;
if (target)
a.target = target;
a.dispatchEvent(new MouseEvent('click')); a.dispatchEvent(new MouseEvent('click'));
}), fallback, timeout); }), fallback, timeout);
}; };
BtnAction.openUrl = BtnAction.download;
BtnAction.mscoreWindow = (scoreinfo, fn) => { BtnAction.mscoreWindow = (scoreinfo, fn) => {
return (btnName, btn, setText) => __awaiter(this, void 0, void 0, function* () { return (btnName, btn, setText) => __awaiter(this, void 0, void 0, function* () {
const _onclick = btn.onclick; const _onclick = btn.onclick;
@ -27303,15 +27133,6 @@ Please pipe the document into a Node stream.\
} }
else { else {
setText(i18n('BTN_ERROR')()); setText(i18n('BTN_ERROR')());
// ask user to send Discord message
alert('❌Download Failed!\n\n' +
'Send your URL to the #dataset-bugs channel ' +
'in the LibreScore Community Discord server:\n' + DISCORD_URL);
// open Discord on 'OK'
const a = document.createElement('a');
a.href = DISCORD_URL;
a.target = '_blank';
a.dispatchEvent(new MouseEvent('click'));
} }
} }
btn.onclick = _onclick; btn.onclick = _onclick;
@ -27329,7 +27150,6 @@ Please pipe the document into a Node stream.\
class ScoreInfo { class ScoreInfo {
constructor() { constructor() {
this.RADIX = 20; this.RADIX = 20;
this.INDEX_RADIX = 32;
this.store = new Map(); this.store = new Map();
} }
get idLastDigit() { get idLastDigit() {
@ -27344,9 +27164,6 @@ Please pipe the document into a Node stream.\
getMsczCidUrl(mainCid) { getMsczCidUrl(mainCid) {
return `https://ipfs.infura.io:5001/api/v0/block/stat?arg=${this.getMsczIpfsRef(mainCid)}`; return `https://ipfs.infura.io:5001/api/v0/block/stat?arg=${this.getMsczIpfsRef(mainCid)}`;
} }
getScorepackRef(mainCid) {
return `/ipfs/${mainCid}/index/${(+this.id) % this.INDEX_RADIX}/${this.id}`;
}
} }
class ScoreInfoInPage extends ScoreInfo { class ScoreInfoInPage extends ScoreInfo {
constructor(document) { constructor(document) {
@ -27380,22 +27197,13 @@ Please pipe the document into a Node stream.\
super(); super();
this.document = document; this.document = document;
} }
get sheet0Img() {
return this.document.querySelector('img[src*=score_]');
}
get pageCount() { get pageCount() {
var _a; return this.document.querySelectorAll('.gXB83').length;
const sheet0Div = (_a = this.sheet0Img) === null || _a === void 0 ? void 0 : _a.parentElement;
if (!sheet0Div) {
throw new Error('no sheet images found');
}
return this.document.getElementsByClassName(sheet0Div.className).length;
} }
get thumbnailUrl() { get thumbnailUrl() {
var _a;
// url to the image of the first page // url to the image of the first page
const el = this.document.querySelector('link[as=image]'); const el = this.document.querySelector('link[as=image]');
const url = ((el === null || el === void 0 ? void 0 : el.href) || ((_a = this.sheet0Img) === null || _a === void 0 ? void 0 : _a.src)); const url = el.href;
return url.split('@')[0]; return url.split('@')[0];
} }
} }
@ -27404,23 +27212,16 @@ Please pipe the document into a Node stream.\
// actual id already // actual id already
return scoreinfo.id; return scoreinfo.id;
} }
const mainCid = yield getMainCid(scoreinfo, _fetch); const jsonPUrl = new URL(`${scoreinfo.baseUrl}space.jsonp`);
const ref = `${mainCid}/sid2id/${scoreinfo.id}`; jsonPUrl.hostname = 's.musescore.com';
const url = `https://ipfs.infura.io:5001/api/v0/dag/get?arg=${ref}`; const r = yield _fetch(jsonPUrl.href);
const r0 = yield _fetch(url); const text = yield r.text();
if (r0.status !== 500) { const m = text.match(/^jsonp(\d+)/);
assertRes(r0); const id = +m[1];
}
const res = yield r0.json();
if (typeof res !== 'number') {
// read further error msg
throw new Error(res.Message);
}
// assign the actual id back to scoreinfo
Object.defineProperty(scoreinfo, 'id', { Object.defineProperty(scoreinfo, 'id', {
get() { return res; }, get() { return id; },
}); });
return res; return id;
}); });
const { saveAs } = FileSaver_min; const { saveAs } = FileSaver_min;
@ -27444,7 +27245,7 @@ Please pipe the document into a Node stream.\
action: BtnAction.process(() => downloadPDF(scoreinfo, new SheetInfoInPage(document)), fallback, 3 * 60 * 1000 /* 3min */), action: BtnAction.process(() => downloadPDF(scoreinfo, new SheetInfoInPage(document)), fallback, 3 * 60 * 1000 /* 3min */),
}); });
btnList.add({ btnList.add({
name: i18n('DOWNLOAD')('MXL'), name: i18n('DOWNLOAD')('MusicXML'),
action: BtnAction.mscoreWindow(scoreinfo, (w, score) => __awaiter(void 0, void 0, void 0, function* () { action: BtnAction.mscoreWindow(scoreinfo, (w, score) => __awaiter(void 0, void 0, void 0, function* () {
const mxl = yield score.saveMxl(); const mxl = yield score.saveMxl();
const data = new Blob([mxl]); const data = new Blob([mxl]);
@ -27519,18 +27320,10 @@ Please pipe the document into a Node stream.\
} }
})), })),
}); });
btnList.add({
name: i18n('VIEW_IN_LIBRESCORE')(),
action: BtnAction.openUrl(() => getLibreScoreLink(scoreinfo)),
tooltip: 'BETA',
icon: ICON.LIBRESCORE,
lightTheme: true,
});
// eslint-disable-next-line @typescript-eslint/no-floating-promises
btnList.commit(BtnListMode.InPage); btnList.commit(BtnListMode.InPage);
}; };
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
waitForSheetLoaded().then(main); waitForDocumentLoaded().then(main);
}.toString() + ')()')}) }.toString() + ')()')})

40
package-lock.json generated
View File

@ -1,13 +1,13 @@
{ {
"name": "musescore-downloader", "name": "musescore-downloader",
"version": "0.24.1", "version": "0.21.3",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@librescore/fonts": { "@librescore/fonts": {
"version": "0.4.0", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/@librescore/fonts/-/fonts-0.4.0.tgz", "resolved": "https://registry.npmjs.org/@librescore/fonts/-/fonts-0.2.1.tgz",
"integrity": "sha512-T286OfxcQAYc/Bll9AtSP2ElggqTpoa08uY9Kgx6z1TcDVn7i7uMkKVO7sw/8aELWFNRmQE2vGQuEkmJNfWmBA==" "integrity": "sha512-lzEk82wZWZVA4CvE2S6Wwc6EAvFZ0G6L2ExNjpJLebxAh0k/eNpHeO9a2LFwfMVUfacVWwXhDkAbmJpvUGcqzA=="
}, },
"@librescore/sf3": { "@librescore/sf3": {
"version": "0.3.0", "version": "0.3.0",
@ -709,26 +709,18 @@
} }
}, },
"elliptic": { "elliptic": {
"version": "6.5.4", "version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"dev": true, "dev": true,
"requires": { "requires": {
"bn.js": "^4.11.9", "bn.js": "^4.4.0",
"brorand": "^1.1.0", "brorand": "^1.0.1",
"hash.js": "^1.0.0", "hash.js": "^1.0.0",
"hmac-drbg": "^1.0.1", "hmac-drbg": "^1.0.0",
"inherits": "^2.0.4", "inherits": "^2.0.1",
"minimalistic-assert": "^1.0.1", "minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.1" "minimalistic-crypto-utils": "^1.0.0"
},
"dependencies": {
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"dev": true
}
} }
}, },
"emoji-regex": { "emoji-regex": {
@ -2439,9 +2431,9 @@
} }
}, },
"webmscore": { "webmscore": {
"version": "0.18.0", "version": "0.10.4",
"resolved": "https://registry.npmjs.org/webmscore/-/webmscore-0.18.0.tgz", "resolved": "https://registry.npmjs.org/webmscore/-/webmscore-0.10.4.tgz",
"integrity": "sha512-/J/2/KKWKST0A+Qix/SBSVtZY0C/33GQoYI3V84XEu/V3nij2ZFIcsyGQPYVr6y0HVasj6dQtvY+y7MrmYcsTw==" "integrity": "sha512-aKFXfK5QpRfJ0xBn+zRV4/HVS4VI6tr+pLkLIHI0n0rMtSBWlkcUeP8eCfP1c1f5LRlrTIaAo4yKZ6Hxg5O7kw=="
}, },
"word-wrap": { "word-wrap": {
"version": "1.2.3", "version": "1.2.3",

View File

@ -1,6 +1,6 @@
{ {
"name": "musescore-downloader", "name": "musescore-downloader",
"version": "0.24.1", "version": "0.21.3",
"description": "download sheet music from musescore.com for free, no login or Musescore Pro required | 免登录、免 Musescore Pro免费下载 musescore.com 上的曲谱", "description": "download sheet music from musescore.com for free, no login or Musescore Pro required | 免登录、免 Musescore Pro免费下载 musescore.com 上的曲谱",
"main": "dist/main.js", "main": "dist/main.js",
"bin": "dist/cli.js", "bin": "dist/cli.js",
@ -37,13 +37,13 @@
"dist/main.js" "dist/main.js"
], ],
"dependencies": { "dependencies": {
"@librescore/fonts": "^0.4.0", "@librescore/fonts": "^0.2.1",
"@librescore/sf3": "^0.3.0", "@librescore/sf3": "^0.3.0",
"detect-node": "^2.0.4", "detect-node": "^2.0.4",
"inquirer": "^7.3.3", "inquirer": "^7.3.3",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"ora": "^5.1.0", "ora": "^5.1.0",
"webmscore": "^0.18.0" "webmscore": "^0.10.4"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",

View File

@ -1,46 +1,32 @@
div { div {
width: 422px; flex-wrap: wrap;
right: 0; display: flex;
margin: 0 18px 18px 0;
text-align: center;
align-items: center; align-items: center;
font-family: 'Inter', 'Helvetica neue', Helvetica, sans-serif; font-family: 'Open Sans', 'Roboto', 'Helvetica neue', Helvetica, sans-serif;
position: absolute; position: absolute;
z-index: 9999; z-index: 9999;
background: #f6f6f6; background: #f6f6f6;
min-width: 230px; min-width: 230px;
/* pass the scroll event through the btns background */
pointer-events: none;
}
@media screen and (max-width: 950px) {
div {
width: auto !important;
}
} }
button { button {
width: 178px !important; width: 205px !important;
min-width: 178px; height: 38px;
height: 40px;
color: #fff; color: #fff;
background: #2e68c0; background: #1f74bd;
cursor: pointer; cursor: pointer;
pointer-events: auto;
margin-bottom: 8px; margin-bottom: 4px;
margin-right: 8px; margin-right: 4px;
padding: 4px 12px; padding: 4px 12px;
justify-content: start; justify-content: start;
align-self: center; align-self: center;
font-size: 16px; font-size: 16px;
border-radius: 6px; border-radius: 2px;
border: 0; border: 0;
display: inline-flex; display: inline-flex;
@ -49,25 +35,6 @@ button {
font-family: inherit; font-family: inherit;
} }
/* fix `View in LibreScore` button text overflow */
button:last-of-type {
width: unset !important;
}
button:hover {
background: #1a4f9f;
}
/* light theme btn */
button.light {
color: #2e68c0;
background: #e1effe;
}
button.light:hover {
background: #c3ddfd;
}
svg { svg {
display: inline-block; display: inline-block;
margin-right: 5px; margin-right: 5px;

View File

@ -1,7 +1,7 @@
import { ScoreInfo } from './scoreinfo' import { ScoreInfo } from './scoreinfo'
import { loadMscore, WebMscore } from './mscore' import { loadMscore, WebMscore } from './mscore'
import { useTimeout, windowOpenAsync, console, attachShadow, DISCORD_URL } from './utils' import { useTimeout, windowOpenAsync, console, attachShadow } from './utils'
import { isGmAvailable, _GM } from './gm' import { isGmAvailable, _GM } from './gm'
import i18n from './i18n' import i18n from './i18n'
// @ts-ignore // @ts-ignore
@ -9,33 +9,27 @@ import btnListCss from './btn.css'
type BtnElement = HTMLButtonElement type BtnElement = HTMLButtonElement
export enum ICON {
DOWNLOAD = 'M9.6 2.4h4.8V12h2.784l-5.18 5.18L6.823 12H9.6V2.4zM19.2 19.2H4.8v2.4h14.4v-2.4z',
LIBRESCORE = 'm5.4837 4.4735v10.405c-1.25-0.89936-3.0285-0.40896-4.1658 0.45816-1.0052 0.76659-1.7881 2.3316-0.98365 3.4943 1 1.1346 2.7702 0.70402 3.8817-0.02809 1.0896-0.66323 1.9667-1.8569 1.8125-3.1814v-5.4822h8.3278v9.3865h9.6438v-2.6282h-6.4567v-12.417c-4.0064-0.015181-8.0424-0.0027-12.06-0.00676zm0.54477 2.2697h8.3278v1.1258h-8.3278v-1.1258z',
}
const getBtnContainer = (): HTMLDivElement => { const getBtnContainer = (): HTMLDivElement => {
const els = [...document.querySelectorAll('span')] const els = [...document.querySelectorAll('*')].reverse()
const el = els.find(b => { const el = els.find(b => {
const text = b?.textContent?.replace(/\s/g, '') || '' const text = b?.textContent?.replace(/\s/g, '') || ''
return text.includes('Download') || text.includes('Print') return text.includes('Download') || text.includes('Print')
}) as HTMLDivElement | null }) as HTMLDivElement | null
const btnParent = el?.parentElement?.parentElement as HTMLDivElement | undefined const btnParent = el?.parentElement?.parentElement as HTMLDivElement | undefined
if (!btnParent || !(btnParent instanceof HTMLDivElement)) throw new Error('btn parent not found') if (!btnParent) throw new Error('btn parent not found')
return btnParent return btnParent
} }
const buildDownloadBtn = (icon: ICON, lightTheme = false) => { const buildDownloadBtn = () => {
const btn = document.createElement('button') const btn = document.createElement('button')
btn.type = 'button' btn.type = 'button'
if (lightTheme) btn.className = 'light'
// build icon svg element // build icon svg element
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svg.setAttribute('viewBox', '0 0 24 24') svg.setAttribute('viewBox', '0 0 24 24')
const svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path') const svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path')
svgPath.setAttribute('d', icon) svgPath.setAttribute('d', 'M9.6 2.4h4.8V12h2.784l-5.18 5.18L6.823 12H9.6V2.4zM19.2 19.2H4.8v2.4h14.4v-2.4z')
svgPath.setAttribute('fill', lightTheme ? '#2e68c0' : '#fff') svgPath.setAttribute('fill', '#fff')
svg.append(svgPath) svg.append(svgPath)
const textNode = document.createElement('span') const textNode = document.createElement('span')
@ -50,36 +44,11 @@ const cloneBtn = (btn: HTMLButtonElement) => {
return n return n
} }
function getScrollParent (node: HTMLElement): HTMLElement {
if (node.scrollHeight > node.clientHeight) {
return node
} else {
return getScrollParent(node.parentNode as HTMLElement)
}
}
function onPageRendered (getEl: () => HTMLElement) {
return new Promise<HTMLElement>((resolve) => {
const observer = new MutationObserver(() => {
try {
const el = getEl()
if (el) {
observer.disconnect()
resolve(el)
}
} catch { }
})
observer.observe(document.querySelector('div > section') ?? document.body, { childList: true, subtree: true })
})
}
interface BtnOptions { interface BtnOptions {
readonly name: string; readonly name: string;
readonly action: BtnAction; readonly action: BtnAction;
readonly disabled?: boolean; readonly disabled?: boolean;
readonly tooltip?: string; readonly tooltip?: string;
readonly icon?: ICON;
readonly lightTheme?: boolean;
} }
export enum BtnListMode { export enum BtnListMode {
@ -93,7 +62,7 @@ export class BtnList {
constructor (private getBtnParent: () => HTMLDivElement = getBtnContainer) { } constructor (private getBtnParent: () => HTMLDivElement = getBtnContainer) { }
add (options: BtnOptions): BtnElement { add (options: BtnOptions): BtnElement {
const btnTpl = buildDownloadBtn(options.icon ?? ICON.DOWNLOAD, options.lightTheme) const btnTpl = buildDownloadBtn()
const setText = (btn: BtnElement) => { const setText = (btn: BtnElement) => {
const textNode = btn.querySelector('span') const textNode = btn.querySelector('span')
return (str: string): void => { return (str: string): void => {
@ -129,16 +98,6 @@ export class BtnList {
return btnTpl return btnTpl
} }
private _positionBtns (anchorDiv: HTMLDivElement, newParent: HTMLDivElement) {
let { top } = anchorDiv.getBoundingClientRect()
top += window.scrollY // relative to the entire document instead of viewport
if (top > 0) {
newParent.style.top = `${top}px`
} else {
newParent.style.top = '0px'
}
}
private _commit () { private _commit () {
const btnParent = document.querySelector('div') as HTMLDivElement const btnParent = document.querySelector('div') as HTMLDivElement
const shadow = attachShadow(btnParent) const shadow = attachShadow(btnParent)
@ -156,20 +115,15 @@ export class BtnList {
newParent.append(...this.list.map(e => cloneBtn(e))) newParent.append(...this.list.map(e => cloneBtn(e)))
shadow.append(newParent) shadow.append(newParent)
// default position try {
newParent.style.top = `${window.innerHeight - newParent.getBoundingClientRect().height}px` const anchorDiv = this.getBtnParent()
const { width, top, left } = anchorDiv.getBoundingClientRect()
void onPageRendered(this.getBtnParent).then((anchorDiv: HTMLDivElement) => { newParent.style.width = `${width}px`
const pos = () => this._positionBtns(anchorDiv, newParent) newParent.style.top = `${top}px`
pos() newParent.style.left = `${left}px`
} catch (err) {
// reposition btns when window resizes console.error(err)
window.addEventListener('resize', pos, { passive: true }) }
// reposition btns when scrolling
const scroll = getScrollParent(anchorDiv)
scroll.addEventListener('scroll', pos, { passive: true })
})
return btnParent return btnParent
} }
@ -227,18 +181,15 @@ export namespace BtnAction {
else return url else return url
} }
export const download = (url: UrlInput, fallback?: () => Promisable<void>, timeout?: number, target?: '_blank'): BtnAction => { export const download = (url: UrlInput, fallback?: () => Promisable<void>, timeout?: number): BtnAction => {
return process(async (): Promise<void> => { return process(async (): Promise<void> => {
const _url = await normalizeUrlInput(url) const _url = await normalizeUrlInput(url)
const a = document.createElement('a') const a = document.createElement('a')
a.href = _url a.href = _url
if (target) a.target = target
a.dispatchEvent(new MouseEvent('click')) a.dispatchEvent(new MouseEvent('click'))
}, fallback, timeout) }, fallback, timeout)
} }
export const openUrl = download
export const mscoreWindow = (scoreinfo: ScoreInfo, fn: (w: Window, score: WebMscore, processingTextEl: ChildNode) => any): BtnAction => { export const mscoreWindow = (scoreinfo: ScoreInfo, fn: (w: Window, score: WebMscore, processingTextEl: ChildNode) => any): BtnAction => {
return async (btnName, btn, setText) => { return async (btnName, btn, setText) => {
const _onclick = btn.onclick const _onclick = btn.onclick
@ -288,17 +239,6 @@ export namespace BtnAction {
setText(name) setText(name)
} else { } else {
setText(i18n('BTN_ERROR')()) setText(i18n('BTN_ERROR')())
// ask user to send Discord message
alert(
'❌Download Failed!\n\n' +
'Send your URL to the #dataset-bugs channel ' +
'in the LibreScore Community Discord server:\n' + DISCORD_URL,
)
// open Discord on 'OK'
const a = document.createElement('a')
a.href = DISCORD_URL
a.target = '_blank'
a.dispatchEvent(new MouseEvent('click'))
} }
} }

View File

@ -4,13 +4,10 @@
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import os from 'os'
import { fetchMscz, setMscz, MSCZ_URL_SYM } from './mscz' import { fetchMscz, setMscz, MSCZ_URL_SYM } from './mscz'
import { loadMscore, INDV_DOWNLOADS, WebMscore } from './mscore' import { loadMscore, INDV_DOWNLOADS, WebMscore } from './mscore'
import { ScoreInfo, ScoreInfoHtml, ScoreInfoObj, getActualId } from './scoreinfo' import { ScoreInfo, ScoreInfoHtml, ScoreInfoObj, getActualId } from './scoreinfo'
import { getLibreScoreLink } from './librescore-link' import { escapeFilename } from './utils'
import { escapeFilename, DISCORD_URL } from './utils'
import { isNpx, getVerInfo, getSelfVer } from './npm-data'
import i18n from './i18n' import i18n from './i18n'
const inquirer: typeof import('inquirer') = require('inquirer') const inquirer: typeof import('inquirer') = require('inquirer')
@ -30,23 +27,7 @@ interface Params {
} }
void (async () => { void (async () => {
const arg = process.argv[2]
if (['-v', '--version'].includes(arg)) { // ran with flag -v or --version, `msdl -v`
console.log(getSelfVer()) // print musescore-downloader version
return // exit process
}
// Determine platform and paste message
const platform = os.platform()
let pasteMessage = ''
if (platform === 'win32') {
pasteMessage = 'right-click to paste'
} else if (platform === 'linux') {
pasteMessage = 'usually Ctrl+Shift+V to paste'
} // For MacOS, no hint is needed because the paste shortcut is universal.
let scoreinfo: ScoreInfo let scoreinfo: ScoreInfo
let librescoreLink: Promise<string> | undefined
// ask for the page url or path to local file // ask for the page url or path to local file
const { fileInit } = await inquirer.prompt<Params>({ const { fileInit } = await inquirer.prompt<Params>({
type: 'input', type: 'input',
@ -54,7 +35,7 @@ void (async () => {
message: 'Score URL or path to local MSCZ file:', message: 'Score URL or path to local MSCZ file:',
suffix: '\n ' + suffix: '\n ' +
`(starts with "${SCORE_URL_PREFIX}" or local filepath ends with "${EXT}") ` + `(starts with "${SCORE_URL_PREFIX}" or local filepath ends with "${EXT}") ` +
`${chalk.bgGray(pasteMessage)}\n `, `${chalk.bgGray`right-click to paste`}\n `,
validate (input: string) { validate (input: string) {
return input && return input &&
( (
@ -62,7 +43,7 @@ void (async () => {
(input.endsWith(EXT) && fs.statSync(input).isFile()) (input.endsWith(EXT) && fs.statSync(input).isFile())
) )
}, },
default: arg, default: process.argv[2],
}) })
const isLocalFile = fileInit.endsWith(EXT) const isLocalFile = fileInit.endsWith(EXT)
@ -86,13 +67,7 @@ void (async () => {
default: true, default: true,
}) })
if (!confirmed) return if (!confirmed) return
console.log() // print a blank line to the terminal
// initiate LibreScore link request
librescoreLink = getLibreScoreLink(scoreinfo)
librescoreLink.catch(() => '') // silence this unhandled Promise rejection
// print a blank line to the terminal
console.log()
} else { } else {
scoreinfo = new ScoreInfoObj(0, path.basename(fileInit, EXT)) scoreinfo = new ScoreInfoObj(0, path.basename(fileInit, EXT))
} }
@ -120,11 +95,6 @@ void (async () => {
if (!isLocalFile) { if (!isLocalFile) {
spinner.info(`File URL: ${scoreinfo.store.get(MSCZ_URL_SYM) as string}`) spinner.info(`File URL: ${scoreinfo.store.get(MSCZ_URL_SYM) as string}`)
} }
if (librescoreLink) {
try {
spinner.info(`${i18n('VIEW_IN_LIBRESCORE')()}: ${await librescoreLink}`)
} catch { } // it doesn't affect the main feature
}
spinner.start() spinner.start()
// load score using webmscore // load score using webmscore
@ -134,10 +104,6 @@ void (async () => {
spinner.info('Score loaded by webmscore') spinner.info('Score loaded by webmscore')
} catch (err) { } catch (err) {
spinner.fail(err.message) spinner.fail(err.message)
spinner.info(
'Send your URL to the #dataset-bugs channel in the LibreScore Community Discord server:\n ' +
DISCORD_URL,
)
return return
} }
spinner.succeed('OK\n') spinner.succeed('OK\n')
@ -196,11 +162,4 @@ void (async () => {
}), }),
) )
spinner.succeed('OK') spinner.succeed('OK')
if (!isNpx()) {
const { installed, latest, isLatest } = await getVerInfo()
if (!isLatest) {
console.log(chalk.yellowBright(`\nYour installed version (${installed}) of the musescore-downloader CLI is not the latest one (${latest})!\nRun npm i -g musescore-downloader@${latest} to update.`))
}
}
})() })()

View File

@ -6,7 +6,7 @@ import { console } from './utils'
type FileType = 'img' | 'mp3' | 'midi' type FileType = 'img' | 'mp3' | 'midi'
const TYPE_REG = /type=(img|mp3|midi)/ const TYPE_REG = /id=(\d+)&type=(img|mp3|midi)/
/** /**
* I know this is super hacky. * I know this is super hacky.
@ -31,9 +31,8 @@ const magicHookConstr = (() => {
const token = init?.headers?.Authorization const token = init?.headers?.Authorization
if (typeof url === 'string' && token) { if (typeof url === 'string' && token) {
const m = url.match(TYPE_REG) const m = url.match(TYPE_REG)
console.debug(url, token, m)
if (m) { if (m) {
const type = m[1] const type = m[2]
// eslint-disable-next-line no-unused-expressions // eslint-disable-next-line no-unused-expressions
l[type]?.(token) l[type]?.(token)
} }
@ -79,7 +78,7 @@ const getApiAuth = async (type: FileType, index: number): Promise<string> => {
// force to retrieve the MAGIC // force to retrieve the MAGIC
switch (type) { switch (type) {
case 'midi': { case 'midi': {
const el = document.querySelector('button[hasaccess]') as HTMLButtonElement const el = document.querySelectorAll('.SD7H- > button')[3] as HTMLButtonElement
el.click() el.click()
break break
} }

View File

@ -27,10 +27,6 @@ export default createLocale({
return 'Download individual parts (BETA)' as const return 'Download individual parts (BETA)' as const
}, },
'VIEW_IN_LIBRESCORE' () {
return 'View in LibreScore' as const
},
'FULL_SCORE' () { 'FULL_SCORE' () {
return 'Full score' as const return 'Full score' as const
}, },

View File

@ -10,7 +10,7 @@ export default createLocale({
}, },
'DEPRECATION_NOTICE' (btnName: string) { 'DEPRECATION_NOTICE' (btnName: string) {
return `¡OBSOLETO!\nUtilizar \`${btnName}\` dentro de \`Partes Indivduales\` en su lugar.\n(Esto todavía puede funcionar. Pulsa \`Aceptar\` para continuar.)` as const return `¡OBSOLETO!\nParecer ser que \`${btnName}\` no funciona correctamente, use \`Partes Indivduales\` en su lugar.\n(Esto todavía puede funcionar. Haga click en \`Aceptar\` para continuar.)` as const
}, },
'DOWNLOAD' <T extends string> (fileType: T) { 'DOWNLOAD' <T extends string> (fileType: T) {
@ -27,10 +27,6 @@ export default createLocale({
return 'Descargar partes individuales (BETA)' as const return 'Descargar partes individuales (BETA)' as const
}, },
'VIEW_IN_LIBRESCORE' () {
return 'Visualizar en LibreScore' as const
},
'FULL_SCORE' () { 'FULL_SCORE' () {
return 'Partitura Completa' as const return 'Partitura Completa' as const
}, },

View File

@ -3,8 +3,6 @@ import isNodeJs from 'detect-node'
import en from './en' import en from './en'
import es from './es' import es from './es'
import it from './it'
import zh from './zh'
export interface LOCALE { export interface LOCALE {
'PROCESSING' (): string; 'PROCESSING' (): string;
@ -18,16 +16,12 @@ export interface LOCALE {
'IND_PARTS' (): string; 'IND_PARTS' (): string;
'IND_PARTS_TOOLTIP' (): string; 'IND_PARTS_TOOLTIP' (): string;
'VIEW_IN_LIBRESCORE' (): string;
'FULL_SCORE' (): string; 'FULL_SCORE' (): string;
} }
const locales = (<L extends { [n: string]: LOCALE } /** type checking */> (l: L) => Object.freeze(l))({ const locales = (<L extends { [n: string]: LOCALE } /** type checking */> (l: L) => Object.freeze(l))({
en, en,
es, es,
it,
zh,
}) })
// detect browser language // detect browser language

View File

@ -1,37 +0,0 @@
import { createLocale } from './utils'
export default createLocale({
'PROCESSING' () {
return 'Caricamento…' as const
},
'BTN_ERROR' () {
return '❌Download Fallito!' as const
},
'DEPRECATION_NOTICE' (btnName: string) {
return `¡DEPRECATO!\nUtilizzare \`${btnName}\` all'interno di \`Parti Indivduali\`.\n(Qusto potrebbe funzionare. Cliccare \`Ok\` per continuare.)` as const
},
'DOWNLOAD' <T extends string> (fileType: T) {
return `Scaricare ${fileType}` as const
},
'DOWNLOAD_AUDIO' <T extends string> (fileType: T) {
return `Scaricare ${fileType} Audio` as const
},
'IND_PARTS' () {
return 'Parti Singole' as const
},
'IND_PARTS_TOOLTIP' () {
return 'Scaricare Parti Singole (BETA)' as const
},
'VIEW_IN_LIBRESCORE' () {
return 'Visualizzare in LibreScore' as const
},
'FULL_SCORE' () {
return 'Spartito Completo' as const
},
})

View File

@ -1,37 +0,0 @@
import { createLocale } from './utils'
export default createLocale({
'PROCESSING' () {
return '处理中…' as const
},
'BTN_ERROR' () {
return '❌下载失败!' as const
},
'DEPRECATION_NOTICE' (btnName: string) {
return `不建议使用\n请使用 \`单独分谱\` 里的 \`${btnName}\` 按钮代替\n这也许仍会起作用。单击\`确定\`以继续。)` as const
},
'DOWNLOAD' <T extends string> (fileType: T) {
return `下载 ${fileType}` as const
},
'DOWNLOAD_AUDIO' <T extends string> (fileType: T) {
return `下载 ${fileType} 音频` as const
},
'IND_PARTS' () {
return '单独分谱' as const
},
'IND_PARTS_TOOLTIP' () {
return '下载单独分谱 (BETA)' as const
},
'VIEW_IN_LIBRESCORE' () {
return '在 LibreScore 中查看' as const
},
'FULL_SCORE' () {
return '完整乐谱' as const
},
})

View File

@ -1,26 +0,0 @@
import { assertRes, getFetch } from './utils'
import { getMainCid } from './mscz'
import { ScoreInfo } from './scoreinfo'
const _getLink = (indexingInfo: string) => {
const { scorepack } = JSON.parse(indexingInfo)
return `https://librescore.org/score/${scorepack as string}`
}
export const getLibreScoreLink = async (scoreinfo: ScoreInfo, _fetch = getFetch()): Promise<string> => {
const mainCid = await getMainCid(scoreinfo, _fetch)
const ref = scoreinfo.getScorepackRef(mainCid)
const url = `https://ipfs.infura.io:5001/api/v0/dag/get?arg=${ref}`
const r0 = await _fetch(url)
if (r0.status !== 500) {
assertRes(r0)
}
const res: { Message: string } | string = await r0.json()
if (typeof res !== 'string') {
// read further error msg
throw new Error(res.Message)
}
return _getLink(res)
}

View File

@ -1,13 +1,12 @@
import './meta' import './meta'
import FileSaver from 'file-saver' import FileSaver from 'file-saver'
import { waitForSheetLoaded, console } from './utils' import { waitForDocumentLoaded, console } from './utils'
import { downloadPDF } from './pdf' import { downloadPDF } from './pdf'
import { downloadMscz } from './mscz' import { downloadMscz } from './mscz'
import { getFileUrl } from './file' import { getFileUrl } from './file'
import { INDV_DOWNLOADS } from './mscore' import { INDV_DOWNLOADS } from './mscore'
import { getLibreScoreLink } from './librescore-link' import { BtnList, BtnAction, BtnListMode } from './btn'
import { BtnList, BtnAction, BtnListMode, ICON } from './btn'
import { ScoreInfoInPage, SheetInfoInPage, getActualId } from './scoreinfo' import { ScoreInfoInPage, SheetInfoInPage, getActualId } from './scoreinfo'
import i18n from './i18n' import i18n from './i18n'
@ -38,7 +37,7 @@ const main = (): void => {
}) })
btnList.add({ btnList.add({
name: i18n('DOWNLOAD')('MXL'), name: i18n('DOWNLOAD')('MusicXML'),
action: BtnAction.mscoreWindow(scoreinfo, async (w, score) => { action: BtnAction.mscoreWindow(scoreinfo, async (w, score) => {
const mxl = await score.saveMxl() const mxl = await score.saveMxl()
const data = new Blob([mxl]) const data = new Blob([mxl])
@ -132,17 +131,8 @@ const main = (): void => {
}), }),
}) })
btnList.add({
name: i18n('VIEW_IN_LIBRESCORE')(),
action: BtnAction.openUrl(() => getLibreScoreLink(scoreinfo)),
tooltip: 'BETA',
icon: ICON.LIBRESCORE,
lightTheme: true,
})
// eslint-disable-next-line @typescript-eslint/no-floating-promises
btnList.commit(BtnListMode.InPage) btnList.commit(BtnListMode.InPage)
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
waitForSheetLoaded().then(main) waitForDocumentLoaded().then(main)

View File

@ -8,11 +8,10 @@
// @version %VERSION% // @version %VERSION%
// @description download sheet music from musescore.com for free, no login or Musescore Pro required | 免登录、免 Musescore Pro免费下载 musescore.com 上的曲谱 // @description download sheet music from musescore.com for free, no login or Musescore Pro required | 免登录、免 Musescore Pro免费下载 musescore.com 上的曲谱
// @author Xmader // @author Xmader
// @icon https://librescore.org/img/icons/logo.svg
// @match https://musescore.com/*/* // @match https://musescore.com/*/*
// @match https://s.musescore.com/*/* // @match https://s.musescore.com/*/*
// @license MIT // @license MIT
// @copyright Copyright (c) 2019-2021 Xmader // @copyright Copyright (c) 2019-2020 Xmader
// @grant unsafeWindow // @grant unsafeWindow
// @grant GM.registerMenuCommand // @grant GM.registerMenuCommand
// @grant GM.addElement // @grant GM.addElement

View File

@ -6,15 +6,14 @@ import { fetchData } from './utils'
import { ScoreInfo } from './scoreinfo' import { ScoreInfo } from './scoreinfo'
import isNodeJs from 'detect-node' import isNodeJs from 'detect-node'
import i18n from './i18n' import i18n from './i18n'
import { dependencies as depVers } from '../package.json'
const WEBMSCORE_URL = `https://cdn.jsdelivr.net/npm/webmscore@${depVers.webmscore}/webmscore.js` const WEBMSCORE_URL = 'https://cdn.jsdelivr.net/npm/webmscore@0.10/webmscore.js'
// fonts for Chinese characters (CN) and Korean hangul (KR) // fonts for Chinese characters (CN) and Korean hangul (KR)
// JP characters are included in the CN font // JP characters are included in the CN font
const FONT_URLS = ['CN', 'KR'].map(l => `https://cdn.jsdelivr.net/npm/@librescore/fonts@${depVers['@librescore/fonts']}/SourceHanSans${l}.min.woff2`) const FONT_URLS = ['CN', 'KR'].map(l => `https://cdn.jsdelivr.net/npm/@librescore/fonts/SourceHanSans${l}-Regular.woff2`)
const SF3_URL = `https://cdn.jsdelivr.net/npm/@librescore/sf3@${depVers['@librescore/sf3']}/FluidR3Mono_GM.sf3` const SF3_URL = 'https://cdn.jsdelivr.net/npm/@librescore/sf3/FluidR3Mono_GM.sf3'
const SOUND_FONT_LOADED = Symbol('SoundFont loaded') const SOUND_FONT_LOADED = Symbol('SoundFont loaded')
export type WebMscore = import('webmscore').default export type WebMscore = import('webmscore').default
@ -119,11 +118,6 @@ export const INDV_DOWNLOADS: IndividualDownload[] = [
fileExt: 'mid', fileExt: 'mid',
action: (score) => score.saveMidi(true, true), action: (score) => score.saveMidi(true, true),
}, },
{
name: i18n('DOWNLOAD_AUDIO')('MP3'),
fileExt: 'mp3',
action: (score) => loadSoundFont(score).then(() => score.saveAudio('mp3')),
},
{ {
name: i18n('DOWNLOAD_AUDIO')('FLAC'), name: i18n('DOWNLOAD_AUDIO')('FLAC'),
fileExt: 'flac', fileExt: 'flac',

View File

@ -48,7 +48,7 @@ export const loadMsczUrl = async (scoreinfo: ScoreInfo, _fetch = getFetch()): Pr
// read further error msg // read further error msg
const err = cidRes.Message const err = cidRes.Message
if (err.includes('no link named')) { // file not found if (err.includes('no link named')) { // file not found
throw new Error('Score not in dataset') throw new Error('score not in dataset')
} else { } else {
throw new Error(err) throw new Error(err)
} }

View File

@ -1,2 +0,0 @@
#!/usr/bin/env node
require("musescore-downloader/dist/cli.js")

View File

@ -1,19 +0,0 @@
{
"name": "msdl",
"version": "%VERSION%",
"author": "Xmader",
"bin": "cli.js",
"description": "Alias for musescore-downloader",
"repository": {
"type": "git",
"url": "git+https://github.com/Xmader/musescore-downloader.git"
},
"bugs": {
"url": "https://github.com/Xmader/musescore-downloader/issues"
},
"homepage": "https://github.com/Xmader/musescore-downloader#readme",
"license": "MIT",
"dependencies": {
"musescore-downloader": "%VERSION%"
}
}

View File

@ -1,34 +0,0 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { name as pkgName, version as pkgVer } from '../package.json'
import { getFetch } from './utils'
const IS_NPX_REG = /_npx(\/|\\)\d+\1/
const NPM_REGISTRY = 'https://registry.npmjs.org'
export function isNpx (): boolean {
// file is in a npx cache dir
// TODO: installed locally?
return __dirname.match(IS_NPX_REG) !== null
}
export function getSelfVer (): string {
return pkgVer
}
export async function getLatestVer (_fetch = getFetch()): Promise<string> {
// fetch pkg info from the npm registry
const r = await _fetch(`${NPM_REGISTRY}/${pkgName}`)
const json = await r.json()
return json['dist-tags'].latest as string
}
export async function getVerInfo () {
const installed = getSelfVer()
const latest = await getLatestVer()
return {
installed,
latest,
isLatest: installed === latest,
}
}

View File

@ -1,10 +1,8 @@
import { getFetch, escapeFilename, assertRes } from './utils' import { getFetch, escapeFilename } from './utils'
import { getMainCid } from './mscz'
export abstract class ScoreInfo { export abstract class ScoreInfo {
private readonly RADIX = 20; private readonly RADIX = 20;
private readonly INDEX_RADIX = 32;
abstract id: number; abstract id: number;
abstract title: string; abstract title: string;
@ -26,10 +24,6 @@ export abstract class ScoreInfo {
public getMsczCidUrl (mainCid: string): string { public getMsczCidUrl (mainCid: string): string {
return `https://ipfs.infura.io:5001/api/v0/block/stat?arg=${this.getMsczIpfsRef(mainCid)}` return `https://ipfs.infura.io:5001/api/v0/block/stat?arg=${this.getMsczIpfsRef(mainCid)}`
} }
public getScorepackRef (mainCid: string): string {
return `/ipfs/${mainCid}/index/${(+this.id) % this.INDEX_RADIX}/${this.id}`
}
} }
export class ScoreInfoObj extends ScoreInfo { export class ScoreInfoObj extends ScoreInfo {
@ -105,22 +99,14 @@ export abstract class SheetInfo {
export class SheetInfoInPage extends SheetInfo { export class SheetInfoInPage extends SheetInfo {
constructor (private document: Document) { super() } constructor (private document: Document) { super() }
private get sheet0Img (): HTMLImageElement | null {
return this.document.querySelector('img[src*=score_]')
}
get pageCount (): number { get pageCount (): number {
const sheet0Div = this.sheet0Img?.parentElement return this.document.querySelectorAll('.gXB83').length
if (!sheet0Div) {
throw new Error('no sheet images found')
}
return this.document.getElementsByClassName(sheet0Div.className).length
} }
get thumbnailUrl (): string { get thumbnailUrl (): string {
// url to the image of the first page // url to the image of the first page
const el = this.document.querySelector<HTMLLinkElement>('link[as=image]') const el = this.document.querySelector('link[as=image]') as HTMLLinkElement
const url = (el?.href || this.sheet0Img?.src) as string const url = el.href
return url.split('@')[0] return url.split('@')[0]
} }
} }
@ -131,24 +117,18 @@ export const getActualId = async (scoreinfo: ScoreInfoInPage | ScoreInfoHtml, _f
return scoreinfo.id return scoreinfo.id
} }
const mainCid = await getMainCid(scoreinfo, _fetch) const jsonPUrl = new URL(`${scoreinfo.baseUrl}space.jsonp`)
const ref = `${mainCid}/sid2id/${scoreinfo.id}` jsonPUrl.hostname = 's.musescore.com'
const url = `https://ipfs.infura.io:5001/api/v0/dag/get?arg=${ref}`
const r0 = await _fetch(url) const r = await _fetch(jsonPUrl.href)
if (r0.status !== 500) { const text = await r.text()
assertRes(r0)
} const m = text.match(/^jsonp(\d+)/) as RegExpMatchArray
const res: { Message: string } | number = await r0.json() const id = +m[1]
if (typeof res !== 'number') {
// read further error msg
throw new Error(res.Message)
}
// assign the actual id back to scoreinfo
Object.defineProperty(scoreinfo, 'id', { Object.defineProperty(scoreinfo, 'id', {
get () { return res }, get () { return id },
}) })
return res return id
} }

View File

@ -2,8 +2,6 @@
import isNodeJs from 'detect-node' import isNodeJs from 'detect-node'
import { isGmAvailable, _GM } from './gm' import { isGmAvailable, _GM } from './gm'
export const DISCORD_URL = 'https://discord.gg/gSsTUvJmD8'
export const escapeFilename = (s: string): string => { export const escapeFilename = (s: string): string => {
return s.replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, '_') return s.replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, '_')
} }
@ -138,23 +136,3 @@ export const waitForDocumentLoaded = (): Promise<void> => {
return Promise.resolve() return Promise.resolve()
} }
} }
/**
* Run script before the page is fully loaded
*/
export const waitForSheetLoaded = (): Promise<void> => {
if (document.readyState !== 'complete') {
return new Promise(resolve => {
const observer = new MutationObserver(() => {
const img = document.querySelector('img')
if (img) {
resolve()
observer.disconnect()
}
})
observer.observe(document, { childList: true, subtree: true })
})
} else {
return Promise.resolve()
}
}

View File

@ -6,9 +6,7 @@ import SVGtoPDF from 'svg-to-pdfkit'
type ImgType = 'svg' | 'png' type ImgType = 'svg' | 'png'
type DataResultType = 'dataUrl' | 'text' const getDataURL = (blob: Blob): Promise<string> => {
const readData = (blob: Blob, type: DataResultType): Promise<string> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader() const reader = new FileReader()
reader.onload = (): void => { reader.onload = (): void => {
@ -16,22 +14,22 @@ const readData = (blob: Blob, type: DataResultType): Promise<string> => {
resolve(result as string) resolve(result as string)
} }
reader.onerror = reject reader.onerror = reject
if (type === 'dataUrl') {
reader.readAsDataURL(blob) reader.readAsDataURL(blob)
} else {
reader.readAsText(blob)
}
}) })
} }
const fetchBlob = async (imgUrl: string): Promise<Blob> => { const fetchDataURL = async (imgUrl: string): Promise<string> => {
const r = await fetch(imgUrl, { const r = await fetch(imgUrl)
cache: 'no-cache', const blob = await r.blob()
}) return getDataURL(blob)
return r.blob()
} }
const generatePDF = async (imgBlobs: Blob[], imgType: ImgType, width: number, height: number): Promise<ArrayBuffer> => { const fetchText = async (imgUrl: string): Promise<string> => {
const r = await fetch(imgUrl)
return r.text()
}
const generatePDF = async (imgURLs: string[], imgType: ImgType, width: number, height: number): Promise<ArrayBuffer> => {
// @ts-ignore // @ts-ignore
const pdf = new (PDFDocument as typeof import('pdfkit'))({ const pdf = new (PDFDocument as typeof import('pdfkit'))({
// compress: true, // compress: true,
@ -42,7 +40,7 @@ const generatePDF = async (imgBlobs: Blob[], imgType: ImgType, width: number, he
}) })
if (imgType === 'png') { if (imgType === 'png') {
const imgDataUrlList: string[] = await Promise.all(imgBlobs.map(b => readData(b, 'dataUrl'))) const imgDataUrlList: string[] = await Promise.all(imgURLs.map(fetchDataURL))
imgDataUrlList.forEach((data) => { imgDataUrlList.forEach((data) => {
pdf.addPage() pdf.addPage()
@ -52,7 +50,7 @@ const generatePDF = async (imgBlobs: Blob[], imgType: ImgType, width: number, he
}) })
}) })
} else { // imgType == "svg" } else { // imgType == "svg"
const svgList = await Promise.all(imgBlobs.map(b => readData(b, 'text'))) const svgList = await Promise.all(imgURLs.map(fetchText))
svgList.forEach((svg) => { svgList.forEach((svg) => {
pdf.addPage() pdf.addPage()
@ -72,16 +70,14 @@ export type PDFWorkerMessage = [string[], ImgType, number, number];
onmessage = async (e): Promise<void> => { onmessage = async (e): Promise<void> => {
const [ const [
imgUrls, imgURLs,
imgType, imgType,
width, width,
height, height,
] = e.data as PDFWorkerMessage ] = e.data as PDFWorkerMessage
const imgBlobs = await Promise.all(imgUrls.map(url => fetchBlob(url)))
const pdfBuf = await generatePDF( const pdfBuf = await generatePDF(
imgBlobs, imgURLs,
imgType, imgType,
width, width,
height, height,

View File

@ -8,11 +8,6 @@ w[gmId] = _GM;
if (_GM && _GM.registerMenuCommand && _GM.openInTab) { if (_GM && _GM.registerMenuCommand && _GM.openInTab) {
// add buttons to the userscript manager menu // add buttons to the userscript manager menu
_GM.registerMenuCommand(
`** Version: ${_GM.info.script.version} **`,
() => _GM.openInTab("https://github.com/Xmader/musescore-downloader/releases", { active: true })
)
_GM.registerMenuCommand( _GM.registerMenuCommand(
'** Source Code **', '** Source Code **',
() => _GM.openInTab(_GM.info.script.homepage, { active: true }) () => _GM.openInTab(_GM.info.script.homepage, { active: true })
@ -24,24 +19,17 @@ if (_GM && _GM.registerMenuCommand && _GM.openInTab) {
) )
} }
function getRandL () {
return String.fromCharCode(97 + Math.floor(Math.random() * 26))
}
// script loader // script loader
new Promise(resolve => { new Promise(resolve => {
const id = '' + Math.random(); const id = '' + Math.random();
w[id] = resolve; w[id] = resolve;
const stackN = 9 const stackN = 9
let loaderIntro = '' const loaderIntro = '(function a(){'.repeat(stackN)
for (let i = 0; i < stackN; i++) {
loaderIntro += `(function ${getRandL()}(){`
}
const loaderOutro = '})()'.repeat(stackN) const loaderOutro = '})()'.repeat(stackN)
const mockUrl = "https://c.amazon-adsystem.com/aax2/apstag.js" const mockUrl = "https://c.amazon-adsystem.com/aax2/apstag.js"
Function(`${loaderIntro}const d=new Image();window['${id}'](d);delete window['${id}'];document.body.prepend(d)${loaderOutro}//# sourceURL=${mockUrl}`)() setTimeout(`${loaderIntro}const d=new Image();window['${id}'](d);delete window['${id}'];document.body.prepend(d)${loaderOutro}//# sourceURL=${mockUrl}`)
}).then(d => { }).then(d => {
d.style.display = 'none'; d.style.display = 'none';
d.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; d.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';