Compare commits
231 commits
Author | SHA1 | Date | |
---|---|---|---|
|
8e3dd94c12 | ||
|
b1a3456646 | ||
|
d269ae9be9 | ||
|
20298e5c0e | ||
|
49880d63da | ||
|
0c2106993d | ||
|
3ac3a51ad1 | ||
|
9c05dd5d3d | ||
|
ae08ff847a | ||
|
8cc089357f | ||
|
4e92c06791 | ||
|
5d1639dddf | ||
|
88fdfff054 | ||
|
893c64edda | ||
|
1bf3050513 | ||
|
ffbac084fb | ||
|
861af6d3f0 | ||
|
30514e9507 | ||
|
7a397e068f | ||
|
d97b3d86d8 | ||
|
bef85c5c3e | ||
|
b68ff38028 | ||
|
a571fed456 | ||
|
f9be984d17 | ||
|
9adba49de1 | ||
|
6d93907df0 | ||
|
c5d6d87b8c | ||
|
8475941d47 | ||
|
71def367e8 | ||
|
d1c9634331 | ||
|
49aef0ccf3 | ||
|
ec15d44811 | ||
|
4494e15540 | ||
|
6a6e2a5ea1 | ||
|
3260672412 | ||
|
004cb16fce | ||
|
ec1c1ea87c | ||
|
69e5bd0a78 | ||
|
06a91b1c2d | ||
|
41f5286d48 | ||
|
29a09c2596 | ||
|
f2a52dd514 | ||
|
1e3e2d7581 | ||
|
eda8342a3d | ||
|
e9ed0812b9 | ||
|
4bd5d55676 | ||
|
c571a49093 | ||
|
b5477a4059 | ||
|
d33c06c892 | ||
|
04884a137f | ||
|
263f72dc7a | ||
|
c46343b46c | ||
|
6202321a42 | ||
|
3c72b5a92f | ||
|
085c6a2d2a | ||
|
1eb0f35bde | ||
|
49fcb99160 | ||
|
bd943675d8 | ||
|
030d37ddc0 | ||
|
d014ade9ca | ||
|
b8181f421d | ||
|
463ea5d416 | ||
|
da5d53898a | ||
|
2b842e267f | ||
|
08294f564b | ||
|
46a7f50115 | ||
|
7bb3aaf7b1 | ||
|
dd30454b5a | ||
|
d99848c6fc | ||
|
c973d5d06f | ||
|
d919441966 | ||
|
86c15d55fe | ||
|
1faaf660c4 | ||
|
8250d80d4b | ||
|
d9d09c4e8f | ||
|
20704eba75 | ||
|
3b329301c1 | ||
|
86c5429dce | ||
|
7f860faf66 | ||
|
de5ff6ff98 | ||
|
1b0d3d2eae | ||
|
28ac964ba3 | ||
|
44f7edfaa3 | ||
|
46879ffacd | ||
|
8945a53896 | ||
|
60d842bac7 | ||
|
bca9f5e150 | ||
|
519a5c3671 | ||
|
13031c323b | ||
|
2cfb060750 | ||
|
047fbf06b6 | ||
|
c01d797983 | ||
|
bb39ebccc9 | ||
|
8e0ce90093 | ||
|
d50fb677b4 | ||
|
003377e9ec | ||
|
2eca89e672 | ||
|
eb0f2b4b1d | ||
|
90d34bd05b | ||
|
541393eb0c | ||
|
2df57606e3 | ||
|
8e6992ab27 | ||
|
2d75ae45be | ||
|
e123108928 | ||
|
1a4efa76fa | ||
|
e5ffc4b9a2 | ||
|
e10e23ffec | ||
|
99bec9b4b4 | ||
|
5ea64e81d7 | ||
|
18da36a34a | ||
|
cc8bddd551 | ||
|
741684329a | ||
|
557a531e5b | ||
|
3f6818fde5 | ||
|
f6b8ab4413 | ||
|
05e147a03c | ||
|
f224edbc49 | ||
|
bdbcdd62e9 | ||
|
0522e5935c | ||
|
63bf02046a | ||
|
1831ffa7c8 | ||
|
334b452fcc | ||
|
ffa8683dbe | ||
|
9491f6c848 | ||
|
63ac2030a9 | ||
|
95ec4725e1 | ||
|
baa2be3ca3 | ||
|
c8467bf5b7 | ||
|
48c41d1c7c | ||
|
73ad8618c2 | ||
|
0e1f4aeafd | ||
|
b9d5ffa6fb | ||
|
9ee64ab7d5 | ||
|
3871a03127 | ||
|
4eb5134b68 | ||
|
2d635039b2 | ||
|
bc0b12d362 | ||
|
c11d6d2c48 | ||
|
2a29e378f2 | ||
|
b6837ee8e0 | ||
|
65c1e02d8b | ||
|
7f12c76413 | ||
|
e142c7a3db | ||
|
4b91be54a6 | ||
|
baf187ee46 | ||
|
f1412cdffa | ||
|
f53ff45057 | ||
|
9a3d16f86c | ||
|
58dc01224d | ||
|
32aa5e6938 | ||
|
79f139e663 | ||
|
f4d43443b9 | ||
|
6b5f0d3321 | ||
|
7fc1a5fb6a | ||
|
7485d884bf | ||
|
d171f46dfe | ||
|
bc4b25d3be | ||
|
967e0b29f0 | ||
|
a73da45f16 | ||
|
2a08dd5567 | ||
|
d499c1bbb4 | ||
|
a5c0dc77de | ||
|
8c9c92ecc5 | ||
|
01514f651b | ||
|
1f62f47dc8 | ||
|
935b56e4d2 | ||
|
f6536114ae | ||
|
bc19c66be3 | ||
|
a7d2b966a9 | ||
|
bff23cc770 | ||
|
604678b29a | ||
|
a99bfc5923 | ||
|
397326ce13 | ||
|
7a9b4910b4 | ||
|
515f5d940e | ||
|
427a75f2dd | ||
|
0d8ab40ba9 | ||
|
1680a35369 | ||
|
6e48ef1253 | ||
|
4acff224a8 | ||
|
ec4cf55696 | ||
|
27861a802d | ||
|
25a7db05fa | ||
|
566ec96144 | ||
|
548f7c02ba | ||
|
f3fc5aeb6a | ||
|
1f132bdfbd | ||
|
aa7622e953 | ||
|
b34931e1b6 | ||
|
6acfec8141 | ||
|
b658c00a09 | ||
|
bb31465fca | ||
|
31d0fd5f23 | ||
|
8df441dfee | ||
|
adb7f5075d | ||
|
3b9dd171c7 | ||
|
64a4ebeb16 | ||
|
50ab62fff7 | ||
|
6926600f45 | ||
|
6e895b3cc1 | ||
|
0f4d032399 | ||
|
b6cd50450a | ||
|
bb6eba5fdb | ||
|
caba1c041a | ||
|
40d1ddbab8 | ||
|
688e0a4c7e | ||
|
5d50c2337c | ||
|
36a05aac75 | ||
|
c96a6b69dd | ||
|
2fbfbede86 | ||
|
bfdd80a364 | ||
|
7f8f635677 | ||
|
297387b2b4 | ||
|
3b43957cee | ||
|
195f607817 | ||
|
92619ab3f7 | ||
|
141dac44ac | ||
|
9e9f25ff80 | ||
|
64b0e4d441 | ||
|
aafb71fc4b | ||
|
120d57b0e0 | ||
|
c334d8d124 | ||
|
ddfe2039b1 | ||
|
565e28a70d | ||
|
f2e45d1803 | ||
|
7a53e5d9ca | ||
|
baadfb4e1a | ||
|
d025ce59ae | ||
|
2c2fc6748d | ||
|
3e0ade9d34 | ||
|
89763ee762 |
35 changed files with 2923 additions and 1022 deletions
|
@ -20,6 +20,8 @@
|
||||||
"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",
|
||||||
|
|
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
|
@ -13,12 +13,16 @@ on:
|
||||||
ref:
|
ref:
|
||||||
description: 'The branch, tag or SHA to release from'
|
description: 'The branch, tag or SHA to release from'
|
||||||
required: false
|
required: false
|
||||||
|
chrome_ext_url:
|
||||||
|
description: 'URL to the Chrome Extension crx'
|
||||||
|
required: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ github.event.inputs.version }}
|
VERSION: ${{ github.event.inputs.version }}
|
||||||
NPM_TAG: ${{ github.event.inputs.npm_tag }}
|
NPM_TAG: ${{ github.event.inputs.npm_tag }}
|
||||||
REF: ${{ github.event.inputs.ref || github.sha }}
|
REF: ${{ github.event.inputs.ref || github.sha }}
|
||||||
ARTIFACTS_DIR: ./.artifacts
|
ARTIFACTS_DIR: ./.artifacts
|
||||||
|
CHROME_EXT_URL: ${{ github.event.inputs.chrome_ext_url }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
|
@ -49,6 +53,15 @@ 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
|
||||||
|
@ -64,6 +77,8 @@ jobs:
|
||||||
mkdir -p $ARTIFACTS_DIR
|
mkdir -p $ARTIFACTS_DIR
|
||||||
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 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
|
||||||
|
@ -83,6 +98,7 @@ 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=()
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -67,3 +67,4 @@ typings/
|
||||||
|
|
||||||
dist/cache
|
dist/cache
|
||||||
dist/ext*
|
dist/ext*
|
||||||
|
dist/cli.js
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2019-2020 Xmader
|
Copyright (c) 2019-2021 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
|
||||||
|
|
137
README.md
137
README.md
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
# musescore-downloader
|
# musescore-downloader
|
||||||
|
|
||||||
**English** | [简体中文](#musescore-downloader-1) | [Español](#musescore-downloader-2)
|
**English** | [简体中文](#musescore-downloader-1) | [Español](#musescore-downloader-2) | [Italian](#musescore-downloader-3)
|
||||||
|
|
||||||
> 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
|
||||||
|
|
||||||
|
@ -38,6 +38,18 @@ There is an article on their website: [Score download becomes a part of the Pro
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
### CLI Usage
|
||||||
|
|
||||||
|
(recommended, more bulletproof)
|
||||||
|
|
||||||
|
1. Install Node.js LTS (https://nodejs.org/)
|
||||||
|
2. Open a command line terminal or command prompt
|
||||||
|
3. Type `npx msdl`, enter
|
||||||
|
(`npx msdl` will always run the latest version)
|
||||||
|
4. Follow the instructions
|
||||||
|
|
||||||
|
[source code](/src/cli.ts)
|
||||||
|
|
||||||
### Install as Userscript
|
### Install as Userscript
|
||||||
|
|
||||||
This script is available as a [Userscript](https://en.wikipedia.org/wiki/Userscript). To use this Userscript, you need to first install a [user script manager](https://greasyfork.org/en/help/installing-user-scripts), like Tampermonkey.
|
This script is available as a [Userscript](https://en.wikipedia.org/wiki/Userscript). To use this Userscript, you need to first install a [user script manager](https://greasyfork.org/en/help/installing-user-scripts), like Tampermonkey.
|
||||||
|
@ -51,7 +63,7 @@ Install this script from <https://msdl.librescore.org/install.user.js>
|
||||||
|
|
||||||
The alternative method is to install this script as a Chrome or Firefox extension.
|
The alternative method is to install this script as a Chrome or Firefox extension.
|
||||||
|
|
||||||
You may install the browser extension directly from [addons.mozilla.org (for Firefox)](https://addons.mozilla.org/en-US/firefox/addon/musescore-downloader/) or [chrome web store (for Chrome and Chromium based browsers)](https://chrome.google.com/webstore/detail/fmmnkcdlphpgbdcdfnjkldfljedbbokp).
|
You may install the browser extension directly from [addons.mozilla.org (for Firefox)](https://addons.mozilla.org/en-US/firefox/addon/musescore-downloader/) or [chrome web store (for Chrome and Chromium based browsers)](https://chrome.google.com/webstore/detail/mhdlcdhakmmikknpefblmnhdhjloanjc).
|
||||||
|
|
||||||
The up-to-date version can be found on the [Github Releases](https://github.com/Xmader/musescore-downloader/releases) page.
|
The up-to-date version can be found on the [Github Releases](https://github.com/Xmader/musescore-downloader/releases) page.
|
||||||
|
|
||||||
|
@ -231,6 +243,7 @@ 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/.
|
||||||
|
@ -239,3 +252,123 @@ 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
1739
dist/main.js
vendored
1739
dist/main.js
vendored
File diff suppressed because it is too large
Load diff
650
package-lock.json
generated
650
package-lock.json
generated
File diff suppressed because it is too large
Load diff
28
package.json
28
package.json
|
@ -1,8 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "musescore-downloader",
|
"name": "musescore-downloader",
|
||||||
"version": "0.15.17",
|
"version": "0.24.1",
|
||||||
"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",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/Xmader/musescore-downloader.git"
|
"url": "git+https://github.com/Xmader/musescore-downloader.git"
|
||||||
|
@ -24,7 +25,7 @@
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": [
|
"matches": [
|
||||||
"*://musescore.com/*/*"
|
"*://*.musescore.com/*/*"
|
||||||
],
|
],
|
||||||
"js": [
|
"js": [
|
||||||
"src/web-ext.js"
|
"src/web-ext.js"
|
||||||
|
@ -36,23 +37,30 @@
|
||||||
"dist/main.js"
|
"dist/main.js"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pdfkit": "git+https://github.com/Xmader/pdfkit.git",
|
"@librescore/fonts": "^0.4.0",
|
||||||
"svg-to-pdfkit": "^0.1.8",
|
"@librescore/sf3": "^0.3.0",
|
||||||
"webmscore": "^0.10.4"
|
"detect-node": "^2.0.4",
|
||||||
|
"inquirer": "^7.3.3",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
|
"ora": "^5.1.0",
|
||||||
|
"webmscore": "^0.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-json": "^4.0.0",
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
"@types/pdfkit": "^0.10.4",
|
"@types/inquirer": "^7.3.1",
|
||||||
"rollup": "^1.26.3",
|
"@types/pdfkit": "^0.10.6",
|
||||||
|
"pdfkit": "git+https://github.com/Xmader/pdfkit.git",
|
||||||
|
"rollup": "^1.32.1",
|
||||||
"rollup-plugin-commonjs": "^10.1.0",
|
"rollup-plugin-commonjs": "^10.1.0",
|
||||||
"rollup-plugin-node-builtins": "^2.1.2",
|
"rollup-plugin-node-builtins": "^2.1.2",
|
||||||
"rollup-plugin-node-globals": "^1.4.0",
|
"rollup-plugin-node-globals": "^1.4.0",
|
||||||
"rollup-plugin-node-resolve": "^5.2.0",
|
"rollup-plugin-node-resolve": "^5.2.0",
|
||||||
"rollup-plugin-string": "^3.0.0",
|
"rollup-plugin-string": "^3.0.0",
|
||||||
"rollup-plugin-typescript": "^1.0.1",
|
"rollup-plugin-typescript": "^1.0.1",
|
||||||
"tslib": "^1.10.0",
|
"svg-to-pdfkit": "^0.1.8",
|
||||||
"typescript": "^4.1.1-rc"
|
"tslib": "^1.14.1",
|
||||||
|
"typescript": "^4.1.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c",
|
"build": "rollup -c",
|
||||||
|
|
|
@ -15,7 +15,12 @@ const getBannerText = () => {
|
||||||
return bannerText
|
return bannerText
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugins = [
|
const getWrapper = (startL, endL) => {
|
||||||
|
const js = fs.readFileSync("./src/wrapper.js", "utf-8")
|
||||||
|
return js.split(/\n/g).slice(startL, endL).join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
const basePlugins = [
|
||||||
typescript({
|
typescript({
|
||||||
target: "ES6",
|
target: "ES6",
|
||||||
sourceMap: false,
|
sourceMap: false,
|
||||||
|
@ -37,12 +42,6 @@ const plugins = [
|
||||||
string({
|
string({
|
||||||
include: "**/*.css",
|
include: "**/*.css",
|
||||||
}),
|
}),
|
||||||
builtins(),
|
|
||||||
nodeGlobals({
|
|
||||||
dirname: false,
|
|
||||||
filename: false,
|
|
||||||
baseDir: false,
|
|
||||||
}),
|
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* remove tslib license comments
|
* remove tslib license comments
|
||||||
|
@ -60,6 +59,16 @@ const plugins = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const plugins = [
|
||||||
|
...basePlugins,
|
||||||
|
builtins(),
|
||||||
|
nodeGlobals({
|
||||||
|
dirname: false,
|
||||||
|
filename: false,
|
||||||
|
baseDir: false,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
input: "src/worker.ts",
|
input: "src/worker.ts",
|
||||||
|
@ -79,9 +88,19 @@ export default [
|
||||||
format: "iife",
|
format: "iife",
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
banner: getBannerText,
|
banner: getBannerText,
|
||||||
intro: "// fix for Greasemonkey\nwindow.eval('(' + function () {",
|
intro: () => getWrapper(0, -1),
|
||||||
outro: "}.toString() + ')()')"
|
outro: () => getWrapper(-1)
|
||||||
},
|
},
|
||||||
plugins,
|
plugins,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: "src/cli.ts",
|
||||||
|
output: {
|
||||||
|
file: "dist/cli.js",
|
||||||
|
format: "cjs",
|
||||||
|
banner: "#!/usr/bin/env node",
|
||||||
|
sourcemap: false,
|
||||||
|
},
|
||||||
|
plugins: basePlugins,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
54
src/btn.css
54
src/btn.css
|
@ -1,31 +1,46 @@
|
||||||
div {
|
div {
|
||||||
flex-wrap: wrap;
|
width: 422px;
|
||||||
display: flex;
|
right: 0;
|
||||||
|
margin: 0 18px 18px 0;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: 'Open Sans', 'Roboto', 'Helvetica neue', Helvetica, sans-serif;
|
font-family: 'Inter', 'Helvetica neue', Helvetica, sans-serif;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 999;
|
z-index: 9999;
|
||||||
background: #f6f6f6;
|
background: #f6f6f6;
|
||||||
|
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: 205px !important;
|
width: 178px !important;
|
||||||
height: 38px;
|
min-width: 178px;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: #1f74bd;
|
background: #2e68c0;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
margin-bottom: 4px;
|
margin-bottom: 8px;
|
||||||
margin-right: 4px;
|
margin-right: 8px;
|
||||||
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: 2px;
|
border-radius: 6px;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
@ -34,6 +49,25 @@ 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;
|
||||||
|
|
179
src/btn.ts
179
src/btn.ts
|
@ -1,42 +1,76 @@
|
||||||
|
|
||||||
|
import { ScoreInfo } from './scoreinfo'
|
||||||
import { loadMscore, WebMscore } from './mscore'
|
import { loadMscore, WebMscore } from './mscore'
|
||||||
import { useTimeout, windowOpen, console } from './utils'
|
import { useTimeout, windowOpenAsync, console, attachShadow, DISCORD_URL } from './utils'
|
||||||
|
import { isGmAvailable, _GM } from './gm'
|
||||||
import i18n from './i18n'
|
import i18n from './i18n'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import btnListCss from './btn.css'
|
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 container = document.querySelectorAll('aside>section>section')[0]
|
const els = [...document.querySelectorAll('span')]
|
||||||
const btnParent = [...container.children].find((div) => {
|
const el = els.find(b => {
|
||||||
const b = div.querySelector('button, .button')
|
const text = b?.textContent?.replace(/\s/g, '') || ''
|
||||||
const text = b ? b.outerHTML.replace(/\s/g, '') : ''
|
|
||||||
return text.includes('Download') || text.includes('Print')
|
return text.includes('Download') || text.includes('Print')
|
||||||
}) as HTMLDivElement | null
|
}) as HTMLDivElement | null
|
||||||
if (!btnParent) throw new Error('btn parent not found')
|
const btnParent = el?.parentElement?.parentElement as HTMLDivElement | undefined
|
||||||
|
if (!btnParent || !(btnParent instanceof HTMLDivElement)) throw new Error('btn parent not found')
|
||||||
return btnParent
|
return btnParent
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildDownloadBtn = () => {
|
const buildDownloadBtn = (icon: ICON, lightTheme = false) => {
|
||||||
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', 'M9.6 2.4h4.8V12h2.784l-5.18 5.18L6.823 12H9.6V2.4zM19.2 19.2H4.8v2.4h14.4v-2.4z')
|
svgPath.setAttribute('d', icon)
|
||||||
svgPath.setAttribute('fill', '#fff')
|
svgPath.setAttribute('fill', lightTheme ? '#2e68c0' : '#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)
|
||||||
|
|
||||||
return {
|
return btn
|
||||||
btn,
|
|
||||||
textNode,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cloneBtn = (btn: HTMLButtonElement) => {
|
||||||
|
const n = btn.cloneNode(true) as HTMLButtonElement
|
||||||
|
n.onclick = btn.onclick
|
||||||
|
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 {
|
||||||
|
@ -44,6 +78,8 @@ interface BtnOptions {
|
||||||
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 {
|
||||||
|
@ -57,38 +93,55 @@ export class BtnList {
|
||||||
constructor (private getBtnParent: () => HTMLDivElement = getBtnContainer) { }
|
constructor (private getBtnParent: () => HTMLDivElement = getBtnContainer) { }
|
||||||
|
|
||||||
add (options: BtnOptions): BtnElement {
|
add (options: BtnOptions): BtnElement {
|
||||||
const { btn, textNode } = buildDownloadBtn()
|
const btnTpl = buildDownloadBtn(options.icon ?? ICON.DOWNLOAD, options.lightTheme)
|
||||||
const setText = (str: string): void => {
|
const setText = (btn: BtnElement) => {
|
||||||
textNode.textContent = str
|
const textNode = btn.querySelector('span')
|
||||||
|
return (str: string): void => {
|
||||||
|
if (textNode) textNode.textContent = str
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setText(options.name)
|
setText(btnTpl)(options.name)
|
||||||
|
|
||||||
btn.onclick = (): void => {
|
btnTpl.onclick = function () {
|
||||||
options.action(options.name, btn, setText)
|
const btn = this as BtnElement
|
||||||
|
options.action(options.name, btn, setText(btn))
|
||||||
}
|
}
|
||||||
|
|
||||||
this.list.push(btn)
|
this.list.push(btnTpl)
|
||||||
|
|
||||||
if (options.disabled) {
|
if (options.disabled) {
|
||||||
btn.disabled = options.disabled
|
btnTpl.disabled = options.disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.tooltip) {
|
if (options.tooltip) {
|
||||||
btn.title = options.tooltip
|
btnTpl.title = options.tooltip
|
||||||
}
|
}
|
||||||
|
|
||||||
return btn
|
// add buttons to the userscript manager menu
|
||||||
|
if (isGmAvailable('registerMenuCommand')) {
|
||||||
|
// eslint-disable-next-line no-void
|
||||||
|
void _GM.registerMenuCommand(options.name, () => {
|
||||||
|
options.action(options.name, btnTpl, () => undefined)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 () {
|
||||||
let btnParent: HTMLDivElement = document.createElement('div')
|
const btnParent = document.querySelector('div') as HTMLDivElement
|
||||||
try {
|
const shadow = attachShadow(btnParent)
|
||||||
btnParent = this.getBtnParent()
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
const shadow = btnParent.attachShadow({ mode: 'closed' })
|
|
||||||
|
|
||||||
// style the shadow DOM
|
// style the shadow DOM
|
||||||
const style = document.createElement('style')
|
const style = document.createElement('style')
|
||||||
|
@ -96,39 +149,47 @@ export class BtnList {
|
||||||
shadow.append(style)
|
shadow.append(style)
|
||||||
|
|
||||||
// hide buttons using the shadow DOM
|
// hide buttons using the shadow DOM
|
||||||
const newParent = btnParent.cloneNode(false) as HTMLDivElement
|
|
||||||
newParent.append(...this.list.map(e => e.cloneNode(true)))
|
|
||||||
shadow.append(newParent)
|
|
||||||
const slot = document.createElement('slot')
|
const slot = document.createElement('slot')
|
||||||
shadow.append(slot)
|
shadow.append(slot)
|
||||||
|
|
||||||
|
const newParent = document.createElement('div')
|
||||||
|
newParent.append(...this.list.map(e => cloneBtn(e)))
|
||||||
|
shadow.append(newParent)
|
||||||
|
|
||||||
|
// default position
|
||||||
|
newParent.style.top = `${window.innerHeight - newParent.getBoundingClientRect().height}px`
|
||||||
|
|
||||||
|
void onPageRendered(this.getBtnParent).then((anchorDiv: HTMLDivElement) => {
|
||||||
|
const pos = () => this._positionBtns(anchorDiv, newParent)
|
||||||
|
pos()
|
||||||
|
|
||||||
|
// reposition btns when window resizes
|
||||||
|
window.addEventListener('resize', pos, { passive: true })
|
||||||
|
|
||||||
|
// reposition btns when scrolling
|
||||||
|
const scroll = getScrollParent(anchorDiv)
|
||||||
|
scroll.addEventListener('scroll', pos, { passive: true })
|
||||||
|
})
|
||||||
|
|
||||||
return btnParent
|
return btnParent
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* replace the template button with the list of new buttons
|
* replace the template button with the list of new buttons
|
||||||
*/
|
*/
|
||||||
commit (mode: BtnListMode = BtnListMode.InPage): void {
|
async commit (mode: BtnListMode = BtnListMode.InPage): Promise<void> {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case BtnListMode.InPage: {
|
case BtnListMode.InPage: {
|
||||||
// fallback to BtnListMode.ExtWindow
|
let el: Element
|
||||||
try {
|
try {
|
||||||
this.getBtnParent()
|
el = this._commit()
|
||||||
} catch {
|
} catch {
|
||||||
|
// fallback to BtnListMode.ExtWindow
|
||||||
return this.commit(BtnListMode.ExtWindow)
|
return this.commit(BtnListMode.ExtWindow)
|
||||||
}
|
}
|
||||||
|
|
||||||
let el: Element = this._commit()
|
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
// check if the buttons are still in document when dom updates
|
// check if the buttons are still in document when dom updates
|
||||||
if (!document.contains(el)) {
|
if (!document.contains(el)) {
|
||||||
try {
|
|
||||||
this.getBtnParent()
|
|
||||||
} catch {
|
|
||||||
observer.disconnect()
|
|
||||||
this.commit(BtnListMode.ExtWindow)
|
|
||||||
}
|
|
||||||
|
|
||||||
// re-commit
|
// re-commit
|
||||||
// performance issue?
|
// performance issue?
|
||||||
el = this._commit()
|
el = this._commit()
|
||||||
|
@ -140,7 +201,7 @@ export class BtnList {
|
||||||
|
|
||||||
case BtnListMode.ExtWindow: {
|
case BtnListMode.ExtWindow: {
|
||||||
const div = this._commit()
|
const div = this._commit()
|
||||||
const w = windowOpen('', undefined, 'resizable,width=230,height=270')
|
const w = await windowOpenAsync(undefined, '', undefined, 'resizable,width=230,height=270')
|
||||||
// eslint-disable-next-line no-unused-expressions
|
// eslint-disable-next-line no-unused-expressions
|
||||||
w?.document.body.append(div)
|
w?.document.body.append(div)
|
||||||
window.addEventListener('unload', () => w?.close())
|
window.addEventListener('unload', () => w?.close())
|
||||||
|
@ -166,28 +227,25 @@ export namespace BtnAction {
|
||||||
else return url
|
else return url
|
||||||
}
|
}
|
||||||
|
|
||||||
export const openUrl = (url: UrlInput): BtnAction => {
|
export const download = (url: UrlInput, fallback?: () => Promisable<void>, timeout?: number, target?: '_blank'): BtnAction => {
|
||||||
return process(async (): Promise<any> => {
|
|
||||||
windowOpen(await normalizeUrlInput(url))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 mscoreWindow = (fn: (w: Window, score: WebMscore, processingTextEl: ChildNode) => any): BtnAction => {
|
export const openUrl = download
|
||||||
|
|
||||||
|
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
|
||||||
btn.onclick = null
|
btn.onclick = null
|
||||||
setText(i18n('PROCESSING')())
|
setText(i18n('PROCESSING')())
|
||||||
|
|
||||||
const w = windowOpen('') as Window
|
const w = await windowOpenAsync(btn, '') as Window
|
||||||
const txt = document.createTextNode(i18n('PROCESSING')())
|
const txt = document.createTextNode(i18n('PROCESSING')())
|
||||||
w.document.body.append(txt)
|
w.document.body.append(txt)
|
||||||
|
|
||||||
|
@ -206,7 +264,7 @@ export namespace BtnAction {
|
||||||
btn.onclick = _onclick
|
btn.onclick = _onclick
|
||||||
})
|
})
|
||||||
|
|
||||||
score = await loadMscore(w)
|
score = await loadMscore(scoreinfo, w)
|
||||||
|
|
||||||
fn(w, score, txt)
|
fn(w, score, txt)
|
||||||
}
|
}
|
||||||
|
@ -230,6 +288,17 @@ 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'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
206
src/cli.ts
Normal file
206
src/cli.ts
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
/* eslint-disable no-void */
|
||||||
|
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import os from 'os'
|
||||||
|
import { fetchMscz, setMscz, MSCZ_URL_SYM } from './mscz'
|
||||||
|
import { loadMscore, INDV_DOWNLOADS, WebMscore } from './mscore'
|
||||||
|
import { ScoreInfo, ScoreInfoHtml, ScoreInfoObj, getActualId } from './scoreinfo'
|
||||||
|
import { getLibreScoreLink } from './librescore-link'
|
||||||
|
import { escapeFilename, DISCORD_URL } from './utils'
|
||||||
|
import { isNpx, getVerInfo, getSelfVer } from './npm-data'
|
||||||
|
import i18n from './i18n'
|
||||||
|
|
||||||
|
const inquirer: typeof import('inquirer') = require('inquirer')
|
||||||
|
const ora: typeof import('ora') = require('ora')
|
||||||
|
const chalk: typeof import('chalk') = require('chalk')
|
||||||
|
|
||||||
|
const SCORE_URL_PREFIX = 'https://(s.)musescore.com/'
|
||||||
|
const SCORE_URL_REG = /https:\/\/(s\.)?musescore\.com\//
|
||||||
|
const EXT = '.mscz'
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
fileInit: string;
|
||||||
|
confirmed: boolean;
|
||||||
|
part: number;
|
||||||
|
types: number[];
|
||||||
|
dest: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 librescoreLink: Promise<string> | undefined
|
||||||
|
// ask for the page url or path to local file
|
||||||
|
const { fileInit } = await inquirer.prompt<Params>({
|
||||||
|
type: 'input',
|
||||||
|
name: 'fileInit',
|
||||||
|
message: 'Score URL or path to local MSCZ file:',
|
||||||
|
suffix: '\n ' +
|
||||||
|
`(starts with "${SCORE_URL_PREFIX}" or local filepath ends with "${EXT}") ` +
|
||||||
|
`${chalk.bgGray(pasteMessage)}\n `,
|
||||||
|
validate (input: string) {
|
||||||
|
return input &&
|
||||||
|
(
|
||||||
|
!!input.match(SCORE_URL_REG) ||
|
||||||
|
(input.endsWith(EXT) && fs.statSync(input).isFile())
|
||||||
|
)
|
||||||
|
},
|
||||||
|
default: arg,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLocalFile = fileInit.endsWith(EXT)
|
||||||
|
if (!isLocalFile) {
|
||||||
|
// request scoreinfo
|
||||||
|
scoreinfo = await ScoreInfoHtml.request(fileInit)
|
||||||
|
try {
|
||||||
|
await getActualId(scoreinfo as any)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// confirmation
|
||||||
|
const { confirmed } = await inquirer.prompt<Params>({
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'confirmed',
|
||||||
|
message: 'Continue?',
|
||||||
|
prefix: `${chalk.yellow('!')} ` +
|
||||||
|
`ID: ${scoreinfo.id}\n ` +
|
||||||
|
`Title: ${scoreinfo.title}\n `,
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
// initiate LibreScore link request
|
||||||
|
librescoreLink = getLibreScoreLink(scoreinfo)
|
||||||
|
librescoreLink.catch(() => '') // silence this unhandled Promise rejection
|
||||||
|
|
||||||
|
// print a blank line to the terminal
|
||||||
|
console.log()
|
||||||
|
} else {
|
||||||
|
scoreinfo = new ScoreInfoObj(0, path.basename(fileInit, EXT))
|
||||||
|
}
|
||||||
|
|
||||||
|
const spinner = ora({
|
||||||
|
text: i18n('PROCESSING')(),
|
||||||
|
color: 'blue',
|
||||||
|
spinner: 'bounce',
|
||||||
|
indent: 0,
|
||||||
|
}).start()
|
||||||
|
|
||||||
|
let score: WebMscore
|
||||||
|
let metadata: import('webmscore/schemas').ScoreMetadata
|
||||||
|
try {
|
||||||
|
if (!isLocalFile) {
|
||||||
|
// fetch mscz file from the dataset, and cache it for side effect
|
||||||
|
await fetchMscz(scoreinfo)
|
||||||
|
} else {
|
||||||
|
// load local file
|
||||||
|
const data = await fs.promises.readFile(fileInit)
|
||||||
|
await setMscz(scoreinfo, data.buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.info('MSCZ file loaded')
|
||||||
|
if (!isLocalFile) {
|
||||||
|
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()
|
||||||
|
|
||||||
|
// load score using webmscore
|
||||||
|
score = await loadMscore(scoreinfo)
|
||||||
|
metadata = await score.metadata()
|
||||||
|
|
||||||
|
spinner.info('Score loaded by webmscore')
|
||||||
|
} catch (err) {
|
||||||
|
spinner.fail(err.message)
|
||||||
|
spinner.info(
|
||||||
|
'Send your URL to the #dataset-bugs channel in the LibreScore Community Discord server:\n ' +
|
||||||
|
DISCORD_URL,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spinner.succeed('OK\n')
|
||||||
|
|
||||||
|
// build part choices
|
||||||
|
const partChoices = metadata.excerpts.map(p => ({ name: p.title, value: p.id }))
|
||||||
|
// add the "full score" option as a "part"
|
||||||
|
partChoices.unshift({ value: -1, name: i18n('FULL_SCORE')() })
|
||||||
|
// build filetype choices
|
||||||
|
const typeChoices = INDV_DOWNLOADS.map((d, i) => ({ name: d.name, value: i }))
|
||||||
|
|
||||||
|
// part selection
|
||||||
|
const { part } = await inquirer.prompt<Params>({
|
||||||
|
type: 'list',
|
||||||
|
name: 'part',
|
||||||
|
message: 'Part Selection',
|
||||||
|
choices: partChoices,
|
||||||
|
})
|
||||||
|
const partName = partChoices[part + 1].name
|
||||||
|
await score.setExcerptId(part)
|
||||||
|
|
||||||
|
// filetype selection
|
||||||
|
const { types } = await inquirer.prompt<Params>({
|
||||||
|
type: 'checkbox',
|
||||||
|
name: 'types',
|
||||||
|
message: 'Filetype Selection',
|
||||||
|
choices: typeChoices,
|
||||||
|
validate (input: number[]) {
|
||||||
|
return input.length >= 1
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const filetypes = types.map(i => INDV_DOWNLOADS[i])
|
||||||
|
|
||||||
|
// destination directory
|
||||||
|
const { dest } = await inquirer.prompt<Params>({
|
||||||
|
type: 'input',
|
||||||
|
name: 'dest',
|
||||||
|
message: 'Destination Directory:',
|
||||||
|
validate (input: string) {
|
||||||
|
return input && fs.statSync(input).isDirectory()
|
||||||
|
},
|
||||||
|
default: process.cwd(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// export files
|
||||||
|
const fileName = scoreinfo.fileName || await score.titleFilenameSafe()
|
||||||
|
spinner.start()
|
||||||
|
await Promise.all(
|
||||||
|
filetypes.map(async (d) => {
|
||||||
|
const data = await d.action(score)
|
||||||
|
const n = `${fileName} - ${escapeFilename(partName)}.${d.fileExt}`
|
||||||
|
const f = path.join(dest, n)
|
||||||
|
await fs.promises.writeFile(f, data)
|
||||||
|
spinner.info(`Saved ${chalk.underline(f)}`)
|
||||||
|
spinner.start()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
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.`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
49
src/file.ts
49
src/file.ts
|
@ -1,13 +1,12 @@
|
||||||
/* eslint-disable no-extend-native */
|
/* eslint-disable no-extend-native */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
|
|
||||||
import scoreinfo from './scoreinfo'
|
import { hookNative } from './anti-detection'
|
||||||
import { ALL, webpackGlobalOverride } from './webpack-hook'
|
|
||||||
import { console } from './utils'
|
import { console } from './utils'
|
||||||
|
|
||||||
type FileType = 'img' | 'mp3' | 'midi'
|
type FileType = 'img' | 'mp3' | 'midi'
|
||||||
|
|
||||||
const TYPE_REG = /id=(\d+)&type=(img|mp3|midi)/
|
const TYPE_REG = /type=(img|mp3|midi)/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* I know this is super hacky.
|
* I know this is super hacky.
|
||||||
|
@ -15,28 +14,42 @@ const TYPE_REG = /id=(\d+)&type=(img|mp3|midi)/
|
||||||
const magicHookConstr = (() => {
|
const magicHookConstr = (() => {
|
||||||
const l = {}
|
const l = {}
|
||||||
|
|
||||||
webpackGlobalOverride(ALL, (n, r, t) => { // override
|
try {
|
||||||
const e = n.exports
|
const p = Object.getPrototypeOf(document.body)
|
||||||
if (typeof e === 'object' && e.fetch) {
|
Object.setPrototypeOf(document.body, null)
|
||||||
const fn = e.fetch
|
|
||||||
t.d(e, 'fetch', () => {
|
hookNative(document.body, 'append', () => {
|
||||||
return function (...args) {
|
return function (...nodes: Node[]) {
|
||||||
const [url, init] = args
|
p.append.call(this, ...nodes)
|
||||||
|
|
||||||
|
if (nodes[0].nodeName === 'IFRAME') {
|
||||||
|
const iframe = nodes[0] as HTMLIFrameElement
|
||||||
|
const w = iframe.contentWindow as Window
|
||||||
|
|
||||||
|
hookNative(w, 'fetch', () => {
|
||||||
|
return function (url, init) {
|
||||||
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[2]
|
const type = m[1]
|
||||||
// eslint-disable-next-line no-unused-expressions
|
// eslint-disable-next-line no-unused-expressions
|
||||||
l[type]?.(token)
|
l[type]?.(token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fn(...args)
|
return fetch(url, init)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Object.setPrototypeOf(document.body, p)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
|
||||||
return async (type: FileType) => {
|
return async (type: FileType) => {
|
||||||
return new Promise<string>((resolve) => {
|
return new Promise<string>((resolve) => {
|
||||||
l[type] = (token) => {
|
l[type] = (token) => {
|
||||||
|
@ -53,8 +66,8 @@ const magics: Record<FileType, Promise<string>> = {
|
||||||
mp3: magicHookConstr('mp3'),
|
mp3: magicHookConstr('mp3'),
|
||||||
}
|
}
|
||||||
|
|
||||||
const getApiUrl = (type: FileType, index: number): string => {
|
const getApiUrl = (id: number, type: FileType, index: number): string => {
|
||||||
return `/api/jmuse?id=${scoreinfo.id}&type=${type}&index=${index}&v2=1`
|
return `/api/jmuse?id=${id}&type=${type}&index=${index}&v2=1`
|
||||||
}
|
}
|
||||||
|
|
||||||
const getApiAuth = async (type: FileType, index: number): Promise<string> => {
|
const getApiAuth = async (type: FileType, index: number): Promise<string> => {
|
||||||
|
@ -66,12 +79,12 @@ 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.querySelectorAll('.SD7H- > button')[3] as HTMLButtonElement
|
const el = document.querySelector('button[hasaccess]') as HTMLButtonElement
|
||||||
el.click()
|
el.click()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'mp3': {
|
case 'mp3': {
|
||||||
const el = document.querySelector('#playerBtnExprt') as HTMLButtonElement
|
const el = document.querySelector('button[title="Toggle Play"]') as HTMLButtonElement
|
||||||
el.click()
|
el.click()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -87,8 +100,8 @@ const getApiAuth = async (type: FileType, index: number): Promise<string> => {
|
||||||
return magic
|
return magic
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFileUrl = async (type: FileType, index = 0): Promise<string> => {
|
export const getFileUrl = async (id: number, type: FileType, index = 0): Promise<string> => {
|
||||||
const url = getApiUrl(type, index)
|
const url = getApiUrl(id, type, index)
|
||||||
const auth = await getApiAuth(type, index)
|
const auth = await getApiAuth(type, index)
|
||||||
|
|
||||||
const r = await fetch(url, {
|
const r = await fetch(url, {
|
||||||
|
|
22
src/gm.ts
Normal file
22
src/gm.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserScript APIs
|
||||||
|
*/
|
||||||
|
declare const GM: {
|
||||||
|
/** https://www.tampermonkey.net/documentation.php#GM_info */
|
||||||
|
info: Record<string, any>;
|
||||||
|
|
||||||
|
/** https://www.tampermonkey.net/documentation.php#GM_registerMenuCommand */
|
||||||
|
registerMenuCommand (name: string, fn: () => any, accessKey?: string): Promise<number>;
|
||||||
|
|
||||||
|
/** https://github.com/Tampermonkey/tampermonkey/issues/881#issuecomment-639705679 */
|
||||||
|
addElement<K extends keyof HTMLElementTagNameMap> (tagName: K, properties: Record<string, any>): Promise<HTMLElementTagNameMap[K]>;
|
||||||
|
}
|
||||||
|
export const _GM = (typeof GM === 'object' ? GM : undefined) as GM
|
||||||
|
|
||||||
|
type GM = typeof GM
|
||||||
|
|
||||||
|
export const isGmAvailable = (requiredMethod: keyof GM = 'info'): boolean => {
|
||||||
|
return typeof GM !== 'undefined' &&
|
||||||
|
typeof GM[requiredMethod] !== 'undefined'
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
import { createLocale } from './'
|
import { createLocale } from './utils'
|
||||||
|
|
||||||
export default createLocale({
|
export default createLocale({
|
||||||
'PROCESSING' () {
|
'PROCESSING' () {
|
||||||
|
@ -27,6 +27,10 @@ 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
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
import { createLocale } from './'
|
import { createLocale } from './utils'
|
||||||
|
|
||||||
export default createLocale({
|
export default createLocale({
|
||||||
'PROCESSING' () {
|
'PROCESSING' () {
|
||||||
|
@ -10,7 +10,7 @@ export default createLocale({
|
||||||
},
|
},
|
||||||
|
|
||||||
'DEPRECATION_NOTICE' (btnName: string) {
|
'DEPRECATION_NOTICE' (btnName: string) {
|
||||||
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
|
return `¡OBSOLETO!\nUtilizar \`${btnName}\` dentro de \`Partes Indivduales\` en su lugar.\n(Esto todavía puede funcionar. Pulsa \`Aceptar\` para continuar.)` as const
|
||||||
},
|
},
|
||||||
|
|
||||||
'DOWNLOAD' <T extends string> (fileType: T) {
|
'DOWNLOAD' <T extends string> (fileType: T) {
|
||||||
|
@ -27,6 +27,10 @@ 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
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
|
||||||
|
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;
|
||||||
|
@ -14,25 +18,31 @@ export interface LOCALE {
|
||||||
'IND_PARTS' (): string;
|
'IND_PARTS' (): string;
|
||||||
'IND_PARTS_TOOLTIP' (): string;
|
'IND_PARTS_TOOLTIP' (): string;
|
||||||
|
|
||||||
'FULL_SCORE' (): string;
|
'VIEW_IN_LIBRESCORE' (): string;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
'FULL_SCORE' (): string;
|
||||||
* type checking only so no missing keys
|
|
||||||
*/
|
|
||||||
export function createLocale<OBJ extends LOCALE> (obj: OBJ): OBJ {
|
|
||||||
return Object.freeze(obj)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
const lang = (() => {
|
const lang = (() => {
|
||||||
|
let userLangs: readonly string[]
|
||||||
|
if (!isNodeJs) {
|
||||||
|
userLangs = navigator.languages
|
||||||
|
} else {
|
||||||
|
const env = process.env
|
||||||
|
const l = env.LC_ALL || env.LC_MESSAGES || env.LANG || env.LANGUAGE || ''
|
||||||
|
userLangs = [l.slice(0, 2)]
|
||||||
|
}
|
||||||
|
|
||||||
const names = Object.keys(locales)
|
const names = Object.keys(locales)
|
||||||
const _lang = navigator.languages.find(l => {
|
const _lang = userLangs.find(l => {
|
||||||
// find the first occurrence of valid languages
|
// find the first occurrence of valid languages
|
||||||
return names.includes(l)
|
return names.includes(l)
|
||||||
})
|
})
|
||||||
|
|
37
src/i18n/it.ts
Normal file
37
src/i18n/it.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
|
||||||
|
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
|
||||||
|
},
|
||||||
|
})
|
9
src/i18n/utils.ts
Normal file
9
src/i18n/utils.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
import type { LOCALE } from './'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* type checking only so no missing keys
|
||||||
|
*/
|
||||||
|
export function createLocale<OBJ extends LOCALE> (obj: OBJ): OBJ {
|
||||||
|
return Object.freeze(obj)
|
||||||
|
}
|
37
src/i18n/zh.ts
Normal file
37
src/i18n/zh.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
|
||||||
|
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
|
||||||
|
},
|
||||||
|
})
|
26
src/librescore-link.ts
Normal file
26
src/librescore-link.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
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)
|
||||||
|
}
|
86
src/main.ts
86
src/main.ts
|
@ -1,17 +1,25 @@
|
||||||
import './meta'
|
import './meta'
|
||||||
|
|
||||||
import { waitForDocumentLoaded, saveAs, console } from './utils'
|
import FileSaver from 'file-saver'
|
||||||
|
import { waitForSheetLoaded, 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 { WebMscore, loadSoundFont } from './mscore'
|
import { INDV_DOWNLOADS } from './mscore'
|
||||||
import { BtnList, BtnAction, BtnListMode } from './btn'
|
import { getLibreScoreLink } from './librescore-link'
|
||||||
import scoreinfo from './scoreinfo'
|
import { BtnList, BtnAction, BtnListMode, ICON } from './btn'
|
||||||
|
import { ScoreInfoInPage, SheetInfoInPage, getActualId } from './scoreinfo'
|
||||||
import i18n from './i18n'
|
import i18n from './i18n'
|
||||||
|
|
||||||
|
const { saveAs } = FileSaver
|
||||||
|
|
||||||
const main = (): void => {
|
const main = (): void => {
|
||||||
const btnList = new BtnList()
|
const btnList = new BtnList()
|
||||||
const filename = scoreinfo.fileName
|
const scoreinfo = new ScoreInfoInPage(document)
|
||||||
|
const { fileName } = scoreinfo
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-void
|
||||||
|
void getActualId(scoreinfo)
|
||||||
|
|
||||||
let indvPartBtn: HTMLButtonElement | null = null
|
let indvPartBtn: HTMLButtonElement | null = null
|
||||||
const fallback = () => {
|
const fallback = () => {
|
||||||
|
@ -21,38 +29,38 @@ const main = (): void => {
|
||||||
|
|
||||||
btnList.add({
|
btnList.add({
|
||||||
name: i18n('DOWNLOAD')('MSCZ'),
|
name: i18n('DOWNLOAD')('MSCZ'),
|
||||||
action: BtnAction.process(downloadMscz),
|
action: BtnAction.process(() => downloadMscz(scoreinfo, saveAs)),
|
||||||
})
|
})
|
||||||
|
|
||||||
btnList.add({
|
btnList.add({
|
||||||
name: i18n('DOWNLOAD')('PDF'),
|
name: i18n('DOWNLOAD')('PDF'),
|
||||||
action: BtnAction.process(downloadPDF, 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')('MusicXML'),
|
name: i18n('DOWNLOAD')('MXL'),
|
||||||
action: BtnAction.mscoreWindow(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])
|
||||||
saveAs(data, `${filename}.mxl`)
|
saveAs(data, `${fileName}.mxl`)
|
||||||
w.close()
|
w.close()
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
btnList.add({
|
btnList.add({
|
||||||
name: i18n('DOWNLOAD')('MIDI'),
|
name: i18n('DOWNLOAD')('MIDI'),
|
||||||
action: BtnAction.download(() => getFileUrl('midi'), fallback, 30 * 1000 /* 30s */),
|
action: BtnAction.download(() => getFileUrl(scoreinfo.id, 'midi'), fallback, 30 * 1000 /* 30s */),
|
||||||
})
|
})
|
||||||
|
|
||||||
btnList.add({
|
btnList.add({
|
||||||
name: i18n('DOWNLOAD')('MP3'),
|
name: i18n('DOWNLOAD')('MP3'),
|
||||||
action: BtnAction.download(() => getFileUrl('mp3'), fallback, 30 * 1000 /* 30s */),
|
action: BtnAction.download(() => getFileUrl(scoreinfo.id, 'mp3'), fallback, 30 * 1000 /* 30s */),
|
||||||
})
|
})
|
||||||
|
|
||||||
indvPartBtn = btnList.add({
|
indvPartBtn = btnList.add({
|
||||||
name: i18n('IND_PARTS')(),
|
name: i18n('IND_PARTS')(),
|
||||||
tooltip: i18n('IND_PARTS_TOOLTIP')(),
|
tooltip: i18n('IND_PARTS_TOOLTIP')(),
|
||||||
action: BtnAction.mscoreWindow(async (w, score, txt) => {
|
action: BtnAction.mscoreWindow(scoreinfo, async (w, score, txt) => {
|
||||||
const metadata = await score.metadata()
|
const metadata = await score.metadata()
|
||||||
console.log('score metadata loaded by webmscore', metadata)
|
console.log('score metadata loaded by webmscore', metadata)
|
||||||
|
|
||||||
|
@ -64,44 +72,7 @@ const main = (): void => {
|
||||||
const fieldset = w.document.createElement('fieldset')
|
const fieldset = w.document.createElement('fieldset')
|
||||||
w.document.body.append(fieldset)
|
w.document.body.append(fieldset)
|
||||||
|
|
||||||
interface IndividualDownload {
|
const downloads = INDV_DOWNLOADS
|
||||||
name: string;
|
|
||||||
fileExt: string;
|
|
||||||
action (score: WebMscore): Promise<Uint8Array>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloads: IndividualDownload[] = [
|
|
||||||
{
|
|
||||||
name: i18n('DOWNLOAD')('PDF'),
|
|
||||||
fileExt: 'pdf',
|
|
||||||
action: (score) => score.savePdf(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n('DOWNLOAD')('MSCZ'),
|
|
||||||
fileExt: 'mscz',
|
|
||||||
action: (score) => score.saveMsc('mscz'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n('DOWNLOAD')('MusicXML'),
|
|
||||||
fileExt: 'mxl',
|
|
||||||
action: (score) => score.saveMxl(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n('DOWNLOAD')('MIDI'),
|
|
||||||
fileExt: 'mid',
|
|
||||||
action: (score) => score.saveMidi(true, true),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n('DOWNLOAD_AUDIO')('FLAC'),
|
|
||||||
fileExt: 'flac',
|
|
||||||
action: (score) => loadSoundFont(score).then(() => score.saveAudio('flac')),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n('DOWNLOAD_AUDIO')('OGG'),
|
|
||||||
fileExt: 'ogg',
|
|
||||||
action: (score) => loadSoundFont(score).then(() => score.saveAudio('ogg')),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// part selection
|
// part selection
|
||||||
const DEFAULT_PART = -1 // initially select "full score"
|
const DEFAULT_PART = -1 // initially select "full score"
|
||||||
|
@ -150,7 +121,7 @@ const main = (): void => {
|
||||||
const partName = checked.alt
|
const partName = checked.alt
|
||||||
|
|
||||||
const data = new Blob([await d.action(score)])
|
const data = new Blob([await d.action(score)])
|
||||||
saveAs(data, `${filename} - ${partName}.${d.fileExt}`)
|
saveAs(data, `${fileName} - ${partName}.${d.fileExt}`)
|
||||||
|
|
||||||
// unlock button
|
// unlock button
|
||||||
initBtn()
|
initBtn()
|
||||||
|
@ -161,8 +132,17 @@ 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
|
||||||
waitForDocumentLoaded().then(main)
|
waitForSheetLoaded().then(main)
|
||||||
|
|
|
@ -8,9 +8,14 @@
|
||||||
// @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/*/*
|
||||||
// @license MIT
|
// @license MIT
|
||||||
// @copyright Copyright (c) 2019-2020 Xmader
|
// @copyright Copyright (c) 2019-2021 Xmader
|
||||||
// @grant none
|
// @grant unsafeWindow
|
||||||
|
// @grant GM.registerMenuCommand
|
||||||
|
// @grant GM.addElement
|
||||||
|
// @grant GM.openInTab
|
||||||
// @run-at document-start
|
// @run-at document-start
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
|
|
@ -1,19 +1,27 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
import { fetchMscz } from './mscz'
|
import { fetchMscz } from './mscz'
|
||||||
import { fetchData } from './utils'
|
import { fetchData } from './utils'
|
||||||
|
import { ScoreInfo } from './scoreinfo'
|
||||||
|
import isNodeJs from 'detect-node'
|
||||||
|
import i18n from './i18n'
|
||||||
|
import { dependencies as depVers } from '../package.json'
|
||||||
|
|
||||||
const WEBMSCORE_URL = 'https://cdn.jsdelivr.net/npm/webmscore@0.10/webmscore.js'
|
const WEBMSCORE_URL = `https://cdn.jsdelivr.net/npm/webmscore@${depVers.webmscore}/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/SourceHanSans${l}-Regular.woff2`)
|
const FONT_URLS = ['CN', 'KR'].map(l => `https://cdn.jsdelivr.net/npm/@librescore/fonts@${depVers['@librescore/fonts']}/SourceHanSans${l}.min.woff2`)
|
||||||
|
|
||||||
const SF3_URL = 'https://cdn.jsdelivr.net/npm/@librescore/sf3/FluidR3Mono_GM.sf3'
|
const SF3_URL = `https://cdn.jsdelivr.net/npm/@librescore/sf3@${depVers['@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
|
||||||
|
export type WebMscoreConstr = typeof import('webmscore').default
|
||||||
|
|
||||||
const initMscore = async (w: Window) => {
|
const initMscore = async (w: Window): Promise<WebMscoreConstr> => {
|
||||||
|
if (!isNodeJs) { // attached to a page
|
||||||
if (!w['WebMscore']) {
|
if (!w['WebMscore']) {
|
||||||
// init webmscore (https://github.com/LibreScore/webmscore)
|
// init webmscore (https://github.com/LibreScore/webmscore)
|
||||||
const script = w.document.createElement('script')
|
const script = w.document.createElement('script')
|
||||||
|
@ -21,6 +29,10 @@ const initMscore = async (w: Window) => {
|
||||||
w.document.body.append(script)
|
w.document.body.append(script)
|
||||||
await new Promise(resolve => { script.onload = resolve })
|
await new Promise(resolve => { script.onload = resolve })
|
||||||
}
|
}
|
||||||
|
return w['WebMscore'] as WebMscoreConstr
|
||||||
|
} else { // nodejs
|
||||||
|
return require('webmscore').default as WebMscoreConstr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let fonts: Promise<Uint8Array[]> | undefined
|
let fonts: Promise<Uint8Array[]> | undefined
|
||||||
|
@ -28,17 +40,37 @@ const initFonts = () => {
|
||||||
// load CJK fonts
|
// load CJK fonts
|
||||||
// CJK (East Asian) characters will be rendered as "tofu" if there is no font
|
// CJK (East Asian) characters will be rendered as "tofu" if there is no font
|
||||||
if (!fonts) {
|
if (!fonts) {
|
||||||
|
if (isNodeJs) {
|
||||||
|
// module.exports.CN = ..., module.exports.KR = ...
|
||||||
|
const FONTS = Object.values(require('@librescore/fonts'))
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
fonts = Promise.all(
|
||||||
|
FONTS.map((path: string) => fs.promises.readFile(path) as Promise<Buffer>),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
fonts = Promise.all(
|
fonts = Promise.all(
|
||||||
FONT_URLS.map(url => fetchData(url)),
|
FONT_URLS.map(url => fetchData(url)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const loadSoundFont = (score: WebMscore): Promise<void> => {
|
export const loadSoundFont = (score: WebMscore): Promise<void> => {
|
||||||
if (!score[SOUND_FONT_LOADED]) {
|
if (!score[SOUND_FONT_LOADED]) {
|
||||||
const loadPromise = (async () => {
|
const loadPromise = (async () => {
|
||||||
|
let data: Uint8Array
|
||||||
|
if (isNodeJs) {
|
||||||
|
// module.exports.FluidR3Mono = ...
|
||||||
|
const SF3 = Object.values(require('@librescore/sf3'))[0]
|
||||||
|
const fs = require('fs')
|
||||||
|
data = await fs.promises.readFile(SF3)
|
||||||
|
} else {
|
||||||
|
data = await fetchData(SF3_URL)
|
||||||
|
}
|
||||||
|
|
||||||
await score.setSoundFont(
|
await score.setSoundFont(
|
||||||
await fetchData(SF3_URL),
|
data,
|
||||||
)
|
)
|
||||||
})()
|
})()
|
||||||
score[SOUND_FONT_LOADED] = loadPromise
|
score[SOUND_FONT_LOADED] = loadPromise
|
||||||
|
@ -46,17 +78,60 @@ export const loadSoundFont = (score: WebMscore): Promise<void> => {
|
||||||
return score[SOUND_FONT_LOADED] as Promise<void>
|
return score[SOUND_FONT_LOADED] as Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadMscore = async (w: Window): Promise<WebMscore> => {
|
export const loadMscore = async (scoreinfo: ScoreInfo, w?: Window): Promise<WebMscore> => {
|
||||||
initFonts()
|
initFonts()
|
||||||
await initMscore(w)
|
const WebMscore = await initMscore(w!)
|
||||||
|
|
||||||
const WebMscore: typeof import('webmscore').default = w['WebMscore']
|
|
||||||
// parse mscz data
|
// parse mscz data
|
||||||
const data = new Uint8Array(
|
const data = new Uint8Array(
|
||||||
new Uint8Array(await fetchMscz()), // copy its ArrayBuffer
|
new Uint8Array(await fetchMscz(scoreinfo)), // copy its ArrayBuffer
|
||||||
)
|
)
|
||||||
const score = await WebMscore.load('mscz', data, await fonts)
|
const score = await WebMscore.load('mscz', data, await fonts)
|
||||||
await score.generateExcerpts()
|
await score.generateExcerpts()
|
||||||
|
|
||||||
return score
|
return score
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IndividualDownload {
|
||||||
|
name: string;
|
||||||
|
fileExt: string;
|
||||||
|
action (score: WebMscore): Promise<Uint8Array>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const INDV_DOWNLOADS: IndividualDownload[] = [
|
||||||
|
{
|
||||||
|
name: i18n('DOWNLOAD')('PDF'),
|
||||||
|
fileExt: 'pdf',
|
||||||
|
action: (score) => score.savePdf(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n('DOWNLOAD')('MSCZ'),
|
||||||
|
fileExt: 'mscz',
|
||||||
|
action: (score) => score.saveMsc('mscz'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n('DOWNLOAD')('MusicXML'),
|
||||||
|
fileExt: 'mxl',
|
||||||
|
action: (score) => score.saveMxl(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n('DOWNLOAD')('MIDI'),
|
||||||
|
fileExt: 'mid',
|
||||||
|
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'),
|
||||||
|
fileExt: 'flac',
|
||||||
|
action: (score) => loadSoundFont(score).then(() => score.saveAudio('flac')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n('DOWNLOAD_AUDIO')('OGG'),
|
||||||
|
fileExt: 'ogg',
|
||||||
|
action: (score) => loadSoundFont(score).then(() => score.saveAudio('ogg')),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
84
src/mscz.ts
84
src/mscz.ts
|
@ -1,28 +1,88 @@
|
||||||
|
|
||||||
import { saveAs, assertRes } from './utils'
|
import { assertRes, getFetch } from './utils'
|
||||||
import scoreinfo from './scoreinfo'
|
import { ScoreInfo } from './scoreinfo'
|
||||||
|
|
||||||
let msczBufferP: Promise<ArrayBuffer> | undefined
|
export const MSCZ_BUF_SYM = Symbol('msczBufferP')
|
||||||
|
export const MSCZ_URL_SYM = Symbol('msczUrl')
|
||||||
|
export const MAIN_CID_SYM = Symbol('mainCid')
|
||||||
|
|
||||||
export const fetchMscz = async (): Promise<ArrayBuffer> => {
|
const IPNS_KEY = 'QmSdXtvzC8v8iTTZuj5cVmiugnzbR1QATYRcGix4bBsioP'
|
||||||
if (!msczBufferP) {
|
const IPNS_RS_URL = `https://ipfs.io/api/v0/dag/resolve?arg=/ipns/${IPNS_KEY}`
|
||||||
const url = scoreinfo.msczCidUrl
|
|
||||||
msczBufferP = (async (): Promise<ArrayBuffer> => {
|
export const getMainCid = async (scoreinfo: ScoreInfo, _fetch = getFetch()): Promise<string> => {
|
||||||
const r0 = await fetch(url)
|
// look for the persisted msczUrl inside scoreinfo
|
||||||
|
let result = scoreinfo.store.get(MAIN_CID_SYM) as string
|
||||||
|
if (result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = await _fetch(IPNS_RS_URL)
|
||||||
|
assertRes(r)
|
||||||
|
const json = await r.json()
|
||||||
|
result = json.Cid['/']
|
||||||
|
|
||||||
|
scoreinfo.store.set(MAIN_CID_SYM, result) // persist to scoreinfo
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadMsczUrl = async (scoreinfo: ScoreInfo, _fetch = getFetch()): Promise<string> => {
|
||||||
|
// look for the persisted msczUrl inside scoreinfo
|
||||||
|
let result = scoreinfo.store.get(MSCZ_URL_SYM) as string
|
||||||
|
if (result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainCid = await getMainCid(scoreinfo, _fetch)
|
||||||
|
|
||||||
|
const url = scoreinfo.getMsczCidUrl(mainCid)
|
||||||
|
const r0 = await _fetch(url)
|
||||||
|
// ipfs-http-gateway specific error
|
||||||
|
// may read further error msg as json
|
||||||
|
if (r0.status !== 500) {
|
||||||
assertRes(r0)
|
assertRes(r0)
|
||||||
const { Key } = await r0.json()
|
}
|
||||||
const r = await fetch(`https://ipfs.infura.io/ipfs/${Key as string}`)
|
const cidRes: { Key: string; Message: string } = await r0.json()
|
||||||
|
|
||||||
|
const cid = cidRes.Key
|
||||||
|
if (!cid) {
|
||||||
|
// read further error msg
|
||||||
|
const err = cidRes.Message
|
||||||
|
if (err.includes('no link named')) { // file not found
|
||||||
|
throw new Error('Score not in dataset')
|
||||||
|
} else {
|
||||||
|
throw new Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = `https://ipfs.infura.io/ipfs/${cid}`
|
||||||
|
|
||||||
|
scoreinfo.store.set(MSCZ_URL_SYM, result) // persist to scoreinfo
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchMscz = async (scoreinfo: ScoreInfo, _fetch = getFetch()): Promise<ArrayBuffer> => {
|
||||||
|
let msczBufferP = scoreinfo.store.get(MSCZ_BUF_SYM) as Promise<ArrayBuffer> | undefined
|
||||||
|
|
||||||
|
if (!msczBufferP) {
|
||||||
|
msczBufferP = (async (): Promise<ArrayBuffer> => {
|
||||||
|
const url = await loadMsczUrl(scoreinfo, _fetch)
|
||||||
|
const r = await _fetch(url)
|
||||||
assertRes(r)
|
assertRes(r)
|
||||||
const data = await r.arrayBuffer()
|
const data = await r.arrayBuffer()
|
||||||
return data
|
return data
|
||||||
})()
|
})()
|
||||||
|
scoreinfo.store.set(MSCZ_BUF_SYM, msczBufferP)
|
||||||
}
|
}
|
||||||
|
|
||||||
return msczBufferP
|
return msczBufferP
|
||||||
}
|
}
|
||||||
|
|
||||||
export const downloadMscz = async (): Promise<void> => {
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
const data = new Blob([await fetchMscz()])
|
export const setMscz = async (scoreinfo: ScoreInfo, buffer: ArrayBuffer): Promise<void> => {
|
||||||
|
scoreinfo.store.set(MSCZ_BUF_SYM, Promise.resolve(buffer))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const downloadMscz = async (scoreinfo: ScoreInfo, saveAs: typeof import('file-saver').saveAs): Promise<void> => {
|
||||||
|
const data = new Blob([await fetchMscz(scoreinfo)])
|
||||||
const filename = scoreinfo.fileName
|
const filename = scoreinfo.fileName
|
||||||
saveAs(data, `${filename}.mscz`)
|
saveAs(data, `${filename}.mscz`)
|
||||||
}
|
}
|
||||||
|
|
2
src/msdl/cli.js
Normal file
2
src/msdl/cli.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
require("musescore-downloader/dist/cli.js")
|
19
src/msdl/package.json
Normal file
19
src/msdl/package.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"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%"
|
||||||
|
}
|
||||||
|
}
|
34
src/npm-data.ts
Normal file
34
src/npm-data.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/* 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,
|
||||||
|
}
|
||||||
|
}
|
18
src/pdf.ts
18
src/pdf.ts
|
@ -1,14 +1,14 @@
|
||||||
|
|
||||||
import { PDFWorkerHelper } from './worker-helper'
|
import { PDFWorkerHelper } from './worker-helper'
|
||||||
import { getFileUrl } from './file'
|
import { getFileUrl } from './file'
|
||||||
import { saveAs } from './utils'
|
import FileSaver from 'file-saver'
|
||||||
import scoreinfo from './scoreinfo'
|
import { ScoreInfo, SheetInfo } from './scoreinfo'
|
||||||
|
|
||||||
let pdfBlob: Blob
|
let pdfBlob: Blob
|
||||||
|
|
||||||
const _downloadPDF = async (imgURLs: string[], imgType: 'svg' | 'png', name = ''): Promise<void> => {
|
const _downloadPDF = async (imgURLs: string[], imgType: 'svg' | 'png', name = ''): Promise<void> => {
|
||||||
if (pdfBlob) {
|
if (pdfBlob) {
|
||||||
return saveAs(pdfBlob, `${name}.pdf`)
|
return FileSaver.saveAs(pdfBlob, `${name}.pdf`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedImg = document.querySelector('img[src*=score_]') as HTMLImageElement
|
const cachedImg = document.querySelector('img[src*=score_]') as HTMLImageElement
|
||||||
|
@ -20,18 +20,18 @@ const _downloadPDF = async (imgURLs: string[], imgType: 'svg' | 'png', name = ''
|
||||||
|
|
||||||
pdfBlob = new Blob([pdfArrayBuffer])
|
pdfBlob = new Blob([pdfArrayBuffer])
|
||||||
|
|
||||||
saveAs(pdfBlob, `${name}.pdf`)
|
FileSaver.saveAs(pdfBlob, `${name}.pdf`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const downloadPDF = async (): Promise<void> => {
|
export const downloadPDF = async (scoreinfo: ScoreInfo, sheet: SheetInfo): Promise<void> => {
|
||||||
const imgType = scoreinfo.sheetImgType
|
const imgType = sheet.imgType
|
||||||
const pageCount = scoreinfo.pageCount
|
const pageCount = sheet.pageCount
|
||||||
|
|
||||||
const rs = Array.from({ length: pageCount }).map((_, i) => {
|
const rs = Array.from({ length: pageCount }).map((_, i) => {
|
||||||
if (i === 0) { // The url to the first page is static. We don't need to use API to obtain it.
|
if (i === 0) { // The url to the first page is static. We don't need to use API to obtain it.
|
||||||
return scoreinfo.baseUrl + `score_${i}.${imgType}`
|
return sheet.thumbnailUrl
|
||||||
} else { // obtain image urls using the API
|
} else { // obtain image urls using the API
|
||||||
return getFileUrl('img', i)
|
return getFileUrl(scoreinfo.id, 'img', i)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const sheetImgURLs = await Promise.all(rs)
|
const sheetImgURLs = await Promise.all(rs)
|
||||||
|
|
205
src/scoreinfo.ts
205
src/scoreinfo.ts
|
@ -1,99 +1,154 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
||||||
|
|
||||||
import { console } from './utils'
|
import { getFetch, escapeFilename, assertRes } from './utils'
|
||||||
|
import { getMainCid } from './mscz'
|
||||||
|
|
||||||
// run at document-start
|
export abstract class ScoreInfo {
|
||||||
export const ugappJsStore: Record<string, any> | null = (() => {
|
private readonly RADIX = 20;
|
||||||
try {
|
private readonly INDEX_RADIX = 32;
|
||||||
const l = document.body.children as HTMLCollectionOf<HTMLElement>
|
|
||||||
const el = [...l].find(e => Object.keys(e.dataset).length > 0) as HTMLDivElement
|
abstract id: number;
|
||||||
const json = Object.values(el.dataset)[0] as string
|
abstract title: string;
|
||||||
return JSON.parse(json)
|
|
||||||
} catch (err) {
|
public store = new Map<symbol, any>();
|
||||||
console.error(err)
|
|
||||||
return null
|
get idLastDigit (): number {
|
||||||
|
return (+this.id) % this.RADIX
|
||||||
}
|
}
|
||||||
})()
|
|
||||||
|
|
||||||
const IPNS_KEY = 'QmSdXtvzC8v8iTTZuj5cVmiugnzbR1QATYRcGix4bBsioP'
|
get fileName (): string {
|
||||||
const RADIX = 20
|
return escapeFilename(this.title)
|
||||||
|
}
|
||||||
|
|
||||||
export const scoreinfo = {
|
public getMsczIpfsRef (mainCid: string): string {
|
||||||
|
return `/ipfs/${mainCid}/${this.idLastDigit}/${this.id}.mscz`
|
||||||
|
}
|
||||||
|
|
||||||
get playerdata (): any {
|
public getMsczCidUrl (mainCid: string): string {
|
||||||
// @ts-ignore
|
return `https://ipfs.infura.io:5001/api/v0/block/stat?arg=${this.getMsczIpfsRef(mainCid)}`
|
||||||
return ugappJsStore.store.page.data.score
|
}
|
||||||
},
|
|
||||||
|
|
||||||
get id (this: typeof scoreinfo): number {
|
public getScorepackRef (mainCid: string): string {
|
||||||
try {
|
return `/ipfs/${mainCid}/index/${(+this.id) % this.INDEX_RADIX}/${this.id}`
|
||||||
return this.playerdata.id
|
}
|
||||||
} catch {
|
}
|
||||||
const el = document.querySelector("meta[property='al:ios:url']") as HTMLMetaElement
|
|
||||||
|
export class ScoreInfoObj extends ScoreInfo {
|
||||||
|
constructor (public id: number = 0, public title: string = '') { super() }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScoreInfoInPage extends ScoreInfo {
|
||||||
|
constructor (private document: Document) { super() }
|
||||||
|
|
||||||
|
get id (): number {
|
||||||
|
const el = this.document.querySelector("meta[property='al:ios:url']") as HTMLMetaElement
|
||||||
const m = el.content.match(/(\d+)$/) as RegExpMatchArray
|
const m = el.content.match(/(\d+)$/) as RegExpMatchArray
|
||||||
return +m[1]
|
return +m[1]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
get idLastDigit (this: typeof scoreinfo): number {
|
get title (): string {
|
||||||
return (+this.id) % RADIX
|
const el = this.document.querySelector("meta[property='og:title']") as HTMLMetaElement
|
||||||
},
|
|
||||||
|
|
||||||
get title (this: typeof scoreinfo): string {
|
|
||||||
try {
|
|
||||||
return this.playerdata.title
|
|
||||||
} catch {
|
|
||||||
const el = document.querySelector("meta[property='og:title']") as HTMLMetaElement
|
|
||||||
return el.content
|
return el.content
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
get fileName (this: typeof scoreinfo): string {
|
get baseUrl (): string {
|
||||||
return this.title.replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, '_')
|
const el = this.document.querySelector("meta[property='og:image']") as HTMLMetaElement
|
||||||
},
|
const m = el.content.match(/^(.+\/)score_/) as RegExpMatchArray
|
||||||
|
return m[1]
|
||||||
get pageCount (this: typeof scoreinfo): number {
|
|
||||||
try {
|
|
||||||
return this.playerdata.pages_count
|
|
||||||
} catch {
|
|
||||||
return document.querySelectorAll('.gXB83').length
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
get baseUrl (this: typeof scoreinfo): string {
|
|
||||||
let thumbnailUrl: string
|
|
||||||
try {
|
|
||||||
thumbnailUrl = this.playerdata.thumbnails.original
|
|
||||||
} catch {
|
|
||||||
const el = document.querySelector("meta[property='og:image']") as HTMLMetaElement
|
|
||||||
thumbnailUrl = el.content
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { origin, pathname } = new URL(thumbnailUrl)
|
export class ScoreInfoHtml extends ScoreInfo {
|
||||||
// remove the last part
|
private readonly ID_REG = /<meta property="al:ios:url" content="musescore:\/\/score\/(\d+)">/
|
||||||
return origin + pathname.split('/').slice(0, -1).join('/') + '/'
|
private readonly TITLE_REG = /<meta property="og:title" content="(.*)">/
|
||||||
},
|
private readonly BASEURL_REG = /<meta property="og:image" content="(.+\/)score_.*">/
|
||||||
|
|
||||||
get msczIpfsRef (this: typeof scoreinfo): string {
|
constructor (private html: string) { super() }
|
||||||
return `/ipns/${IPNS_KEY}/${this.idLastDigit}/${this.id}.mscz`
|
|
||||||
},
|
|
||||||
|
|
||||||
get msczCidUrl (this: typeof scoreinfo): string {
|
get id (): number {
|
||||||
return `https://ipfs.infura.io:5001/api/v0/block/stat?arg=${this.msczIpfsRef}`
|
const m = this.html.match(this.ID_REG)
|
||||||
},
|
if (!m) return 0
|
||||||
|
return +m[1]
|
||||||
|
}
|
||||||
|
|
||||||
get sheetImgType (): 'svg' | 'png' {
|
get title (): string {
|
||||||
try {
|
const m = this.html.match(this.TITLE_REG)
|
||||||
const imgE = document.querySelector('img[src*=score_]') as HTMLImageElement
|
if (!m) return ''
|
||||||
const { pathname } = new URL(imgE.src)
|
return m[1]
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
}
|
||||||
const imgtype = pathname.match(/\.(\w+)$/)![1]
|
|
||||||
|
get baseUrl (): string {
|
||||||
|
const m = this.html.match(this.BASEURL_REG)
|
||||||
|
if (!m) return ''
|
||||||
|
return m[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
static async request (url: string, _fetch = getFetch()): Promise<ScoreInfoHtml> {
|
||||||
|
const r = await _fetch(url)
|
||||||
|
if (!r.ok) return new ScoreInfoHtml('')
|
||||||
|
|
||||||
|
const html = await r.text()
|
||||||
|
return new ScoreInfoHtml(html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class SheetInfo {
|
||||||
|
abstract pageCount: number;
|
||||||
|
abstract thumbnailUrl: string;
|
||||||
|
|
||||||
|
get imgType (): 'svg' | 'png' {
|
||||||
|
const thumbnail = this.thumbnailUrl
|
||||||
|
const imgtype = thumbnail.match(/score_0\.(\w+)/)![1]
|
||||||
return imgtype as 'svg' | 'png'
|
return imgtype as 'svg' | 'png'
|
||||||
} catch (_) {
|
|
||||||
// return null
|
|
||||||
return 'svg'
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default scoreinfo
|
export class SheetInfoInPage extends SheetInfo {
|
||||||
|
constructor (private document: Document) { super() }
|
||||||
|
|
||||||
|
private get sheet0Img (): HTMLImageElement | null {
|
||||||
|
return this.document.querySelector('img[src*=score_]')
|
||||||
|
}
|
||||||
|
|
||||||
|
get pageCount (): number {
|
||||||
|
const sheet0Div = this.sheet0Img?.parentElement
|
||||||
|
if (!sheet0Div) {
|
||||||
|
throw new Error('no sheet images found')
|
||||||
|
}
|
||||||
|
return this.document.getElementsByClassName(sheet0Div.className).length
|
||||||
|
}
|
||||||
|
|
||||||
|
get thumbnailUrl (): string {
|
||||||
|
// url to the image of the first page
|
||||||
|
const el = this.document.querySelector<HTMLLinkElement>('link[as=image]')
|
||||||
|
const url = (el?.href || this.sheet0Img?.src) as string
|
||||||
|
return url.split('@')[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getActualId = async (scoreinfo: ScoreInfoInPage | ScoreInfoHtml, _fetch = getFetch()): Promise<number> => {
|
||||||
|
if (scoreinfo.id <= 1000000000000) {
|
||||||
|
// actual id already
|
||||||
|
return scoreinfo.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainCid = await getMainCid(scoreinfo, _fetch)
|
||||||
|
const ref = `${mainCid}/sid2id/${scoreinfo.id}`
|
||||||
|
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 } | number = await 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', {
|
||||||
|
get () { return res },
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
111
src/utils.ts
111
src/utils.ts
|
@ -1,7 +1,12 @@
|
||||||
|
|
||||||
import FileSaver from 'file-saver/dist/FileSaver.js'
|
import isNodeJs from 'detect-node'
|
||||||
|
import { isGmAvailable, _GM } from './gm'
|
||||||
|
|
||||||
export const saveAs: typeof import('file-saver').saveAs = FileSaver.saveAs
|
export const DISCORD_URL = 'https://discord.gg/gSsTUvJmD8'
|
||||||
|
|
||||||
|
export const escapeFilename = (s: string): string => {
|
||||||
|
return s.replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, '_')
|
||||||
|
}
|
||||||
|
|
||||||
export const getIndexPath = (id: number): string => {
|
export const getIndexPath = (id: number): string => {
|
||||||
const idStr = String(id)
|
const idStr = String(id)
|
||||||
|
@ -13,6 +18,25 @@ export const getIndexPath = (id: number): string => {
|
||||||
return indexN.join('/')
|
return indexN.join('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NODE_FETCH_HEADERS = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.8',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFetch = (): typeof fetch => {
|
||||||
|
if (!isNodeJs) {
|
||||||
|
return fetch
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const nodeFetch = require('node-fetch')
|
||||||
|
return (input: RequestInfo, init?: RequestInit) => {
|
||||||
|
init = Object.assign({ headers: NODE_FETCH_HEADERS }, init)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return nodeFetch(input, init)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchData = async (url: string, init?: RequestInit): Promise<Uint8Array> => {
|
export const fetchData = async (url: string, init?: RequestInit): Promise<Uint8Array> => {
|
||||||
const r = await fetch(url, init)
|
const r = await fetch(url, init)
|
||||||
const data = await r.arrayBuffer()
|
const data = await r.arrayBuffer()
|
||||||
|
@ -36,19 +60,68 @@ export const useTimeout = async <T> (promise: T | Promise<T>, ms: number): Promi
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSandboxWindow = (): Window => {
|
export const getSandboxWindowAsync = async (targetEl: Element | undefined = undefined): Promise<Window> => {
|
||||||
const iframe = document.createElement('iframe')
|
if (typeof document === 'undefined') return {} as any as Window
|
||||||
|
|
||||||
|
if (isGmAvailable('addElement')) {
|
||||||
|
// create iframe using GM_addElement API
|
||||||
|
const iframe = await _GM.addElement('iframe', {})
|
||||||
iframe.style.display = 'none'
|
iframe.style.display = 'none'
|
||||||
document.body.append(iframe)
|
return iframe.contentWindow as Window
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetEl) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// You need ads in your pages, right?
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
for (let i = 0; i < window.frames.length; i++) {
|
||||||
|
// find iframe windows created by ads
|
||||||
|
const frame = frames[i]
|
||||||
|
try {
|
||||||
|
const href = frame.location.href
|
||||||
|
if (href === location.href || href === 'about:blank') {
|
||||||
|
resolve(frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
observer.observe(document.body, { subtree: true, childList: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const eventName = 'onmousemove'
|
||||||
|
const id = Math.random().toString()
|
||||||
|
|
||||||
|
targetEl[id] = (iframe: HTMLIFrameElement) => {
|
||||||
|
delete targetEl[id]
|
||||||
|
targetEl.removeAttribute(eventName)
|
||||||
|
|
||||||
|
iframe.style.display = 'none'
|
||||||
|
targetEl.append(iframe)
|
||||||
const w = iframe.contentWindow
|
const w = iframe.contentWindow
|
||||||
return w as Window
|
resolve(w as Window)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const windowOpen: Window['open'] = (...args): Window | null => {
|
targetEl.setAttribute(eventName, `this['${id}'](document.createElement('iframe'))`)
|
||||||
return getSandboxWindow().open(...args)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const console: Console = getSandboxWindow()['console']
|
export const getUnsafeWindow = (): Window => {
|
||||||
|
// eslint-disable-next-line no-eval
|
||||||
|
return window.eval('window') as Window
|
||||||
|
}
|
||||||
|
|
||||||
|
export const console: Console = (window || global).console // Object.is(window.console, unsafeWindow.console) == false
|
||||||
|
|
||||||
|
export const windowOpenAsync = (targetEl: Element | undefined, ...args: Parameters<Window['open']>): Promise<Window | null> => {
|
||||||
|
return getSandboxWindowAsync(targetEl).then(w => w.open(...args))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const attachShadow = (el: Element): ShadowRoot => {
|
||||||
|
return Element.prototype.attachShadow.call(el, { mode: 'closed' }) as ShadowRoot
|
||||||
|
}
|
||||||
|
|
||||||
export const waitForDocumentLoaded = (): Promise<void> => {
|
export const waitForDocumentLoaded = (): Promise<void> => {
|
||||||
if (document.readyState !== 'complete') {
|
if (document.readyState !== 'complete') {
|
||||||
|
@ -65,3 +138,23 @@ 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
import { hookNative } from './anti-detection'
|
import { hookNative } from './anti-detection'
|
||||||
import { console } from './utils'
|
import { console, getUnsafeWindow } from './utils'
|
||||||
|
|
||||||
const CHUNK_PUSH_FN = /^function [^r]\(\w\){/
|
const CHUNK_PUSH_FN = /^function [^r]\(\w\){/
|
||||||
|
|
||||||
|
@ -86,9 +86,10 @@ export const [webpackGlobalOverride, onPackLoad] = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// hook `webpackJsonpmusescore.push` as soon as `webpackJsonpmusescore` is available
|
// hook `webpackJsonpmusescore.push` as soon as `webpackJsonpmusescore` is available
|
||||||
let jsonp = window['webpackJsonpmusescore']
|
const _w = getUnsafeWindow()
|
||||||
|
let jsonp = _w['webpackJsonpmusescore']
|
||||||
let hooked = false
|
let hooked = false
|
||||||
Object.defineProperty(window, 'webpackJsonpmusescore', {
|
Object.defineProperty(_w, 'webpackJsonpmusescore', {
|
||||||
get () { return jsonp },
|
get () { return jsonp },
|
||||||
set (v: WebpackJson) {
|
set (v: WebpackJson) {
|
||||||
jsonp = v
|
jsonp = v
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { PDFWorker } from '../dist/cache/worker'
|
||||||
|
|
||||||
const scriptUrlFromFunction = (fn: () => any): string => {
|
const scriptUrlFromFunction = (fn: () => any): string => {
|
||||||
const blob = new Blob(['(' + fn.toString() + ')()'], { type: 'application/javascript' })
|
const blob = new Blob(['(' + fn.toString() + ')()'], { type: 'application/javascript' })
|
||||||
return URL.createObjectURL(blob)
|
return window.URL.createObjectURL(blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PDFWorkerHelper extends Worker {
|
export class PDFWorkerHelper extends Worker {
|
||||||
|
|
|
@ -6,7 +6,9 @@ import SVGtoPDF from 'svg-to-pdfkit'
|
||||||
|
|
||||||
type ImgType = 'svg' | 'png'
|
type ImgType = 'svg' | 'png'
|
||||||
|
|
||||||
const getDataURL = (blob: Blob): Promise<string> => {
|
type DataResultType = 'dataUrl' | 'text'
|
||||||
|
|
||||||
|
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 => {
|
||||||
|
@ -14,22 +16,22 @@ const getDataURL = (blob: Blob): 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 fetchDataURL = async (imgUrl: string): Promise<string> => {
|
const fetchBlob = async (imgUrl: string): Promise<Blob> => {
|
||||||
const r = await fetch(imgUrl)
|
const r = await fetch(imgUrl, {
|
||||||
const blob = await r.blob()
|
cache: 'no-cache',
|
||||||
return getDataURL(blob)
|
})
|
||||||
|
return r.blob()
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchText = async (imgUrl: string): Promise<string> => {
|
const generatePDF = async (imgBlobs: Blob[], imgType: ImgType, width: number, height: number): Promise<ArrayBuffer> => {
|
||||||
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,
|
||||||
|
@ -40,7 +42,7 @@ const generatePDF = async (imgURLs: string[], imgType: ImgType, width: number, h
|
||||||
})
|
})
|
||||||
|
|
||||||
if (imgType === 'png') {
|
if (imgType === 'png') {
|
||||||
const imgDataUrlList: string[] = await Promise.all(imgURLs.map(fetchDataURL))
|
const imgDataUrlList: string[] = await Promise.all(imgBlobs.map(b => readData(b, 'dataUrl')))
|
||||||
|
|
||||||
imgDataUrlList.forEach((data) => {
|
imgDataUrlList.forEach((data) => {
|
||||||
pdf.addPage()
|
pdf.addPage()
|
||||||
|
@ -50,7 +52,7 @@ const generatePDF = async (imgURLs: string[], imgType: ImgType, width: number, h
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
} else { // imgType == "svg"
|
} else { // imgType == "svg"
|
||||||
const svgList = await Promise.all(imgURLs.map(fetchText))
|
const svgList = await Promise.all(imgBlobs.map(b => readData(b, 'text')))
|
||||||
|
|
||||||
svgList.forEach((svg) => {
|
svgList.forEach((svg) => {
|
||||||
pdf.addPage()
|
pdf.addPage()
|
||||||
|
@ -70,14 +72,16 @@ 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(
|
||||||
imgURLs,
|
imgBlobs,
|
||||||
imgType,
|
imgType,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
|
52
src/wrapper.js
Normal file
52
src/wrapper.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
const w = typeof unsafeWindow == 'object' ? unsafeWindow : window;
|
||||||
|
|
||||||
|
// GM APIs glue
|
||||||
|
const _GM = typeof GM == 'object' ? GM : undefined;
|
||||||
|
const gmId = '' + Math.random();
|
||||||
|
w[gmId] = _GM;
|
||||||
|
|
||||||
|
if (_GM && _GM.registerMenuCommand && _GM.openInTab) {
|
||||||
|
// 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(
|
||||||
|
'** Source Code **',
|
||||||
|
() => _GM.openInTab(_GM.info.script.homepage, { active: true })
|
||||||
|
)
|
||||||
|
|
||||||
|
_GM.registerMenuCommand(
|
||||||
|
'** Discord **',
|
||||||
|
() => _GM.openInTab("https://discord.gg/DKu7cUZ4XQ", { active: true })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandL () {
|
||||||
|
return String.fromCharCode(97 + Math.floor(Math.random() * 26))
|
||||||
|
}
|
||||||
|
|
||||||
|
// script loader
|
||||||
|
new Promise(resolve => {
|
||||||
|
const id = '' + Math.random();
|
||||||
|
w[id] = resolve;
|
||||||
|
|
||||||
|
const stackN = 9
|
||||||
|
let loaderIntro = ''
|
||||||
|
for (let i = 0; i < stackN; i++) {
|
||||||
|
loaderIntro += `(function ${getRandL()}(){`
|
||||||
|
}
|
||||||
|
const loaderOutro = '})()'.repeat(stackN)
|
||||||
|
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}`)()
|
||||||
|
}).then(d => {
|
||||||
|
d.style.display = 'none';
|
||||||
|
d.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
||||||
|
d.once = false;
|
||||||
|
d.setAttribute('onload', `if(this.once)return;this.once=true;this.remove();const GM=window['${gmId}'];delete window['${gmId}'];(` + function a () {
|
||||||
|
/** script code here */
|
||||||
|
|
||||||
|
}.toString() + ')()')})
|
Loading…
Reference in a new issue