mirror of
https://github.com/LibreScore/dl-librescore
synced 2025-01-22 01:50:52 +01:00
feat: add links to app and remove dataset
This commit is contained in:
parent
87a2efdddd
commit
ece517e587
15
.eslintrc
15
.eslintrc
@ -6,9 +6,7 @@
|
||||
"node": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": [
|
||||
"standard",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
@ -38,14 +36,9 @@
|
||||
"ignoreComments": true
|
||||
}
|
||||
],
|
||||
"comma-dangle": [
|
||||
"warn",
|
||||
"always-multiline"
|
||||
]
|
||||
"comma-dangle": ["warn", "always-multiline"]
|
||||
},
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"./tsconfig.json"
|
||||
]
|
||||
"project": ["./tsconfig.json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
194
.github/workflows/release.yml
vendored
194
.github/workflows/release.yml
vendored
@ -1,127 +1,101 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'The version number. default: the version number in package.json'
|
||||
required: false
|
||||
npm_tag:
|
||||
description: 'The tag to register the published NPM package with'
|
||||
required: false
|
||||
default: 'latest'
|
||||
ref:
|
||||
description: 'The branch, tag or SHA to release from'
|
||||
required: false
|
||||
chrome_ext_url:
|
||||
description: 'URL to the Chrome Extension crx'
|
||||
required: true
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "The version number. default: the version number in package.json"
|
||||
required: false
|
||||
npm_tag:
|
||||
description: "The tag to register the published NPM package with"
|
||||
required: false
|
||||
default: "latest"
|
||||
ref:
|
||||
description: "The branch, tag or SHA to release from"
|
||||
required: false
|
||||
|
||||
env:
|
||||
VERSION: ${{ github.event.inputs.version }}
|
||||
NPM_TAG: ${{ github.event.inputs.npm_tag }}
|
||||
REF: ${{ github.event.inputs.ref || github.sha }}
|
||||
ARTIFACTS_DIR: ./.artifacts
|
||||
CHROME_EXT_URL: ${{ github.event.inputs.chrome_ext_url }}
|
||||
VERSION: ${{ github.event.inputs.version }}
|
||||
NPM_TAG: ${{ github.event.inputs.npm_tag }}
|
||||
REF: ${{ github.event.inputs.ref || github.sha }}
|
||||
ARTIFACTS_DIR: ./.artifacts
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
ref: ${{ env.REF }}
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
registry-url: https://registry.npmjs.org/
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ env.REF }}
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
- name: Set Env
|
||||
run: |
|
||||
VER=$(node -p "require('./package.json').version")
|
||||
echo "VERSION=$VER" >> $GITHUB_ENV
|
||||
if: ${{ !env.VERSION }}
|
||||
- name: Set Env
|
||||
run: |
|
||||
VER=$(node -p "require('./package.json').version")
|
||||
echo "VERSION=$VER" >> $GITHUB_ENV
|
||||
if: ${{ !env.VERSION }}
|
||||
|
||||
- run: npm install
|
||||
- name: Bump Version
|
||||
run: npm version --allow-same-version --no-git-tag $VERSION
|
||||
- run: npm run build
|
||||
- run: npm run pack:ext
|
||||
- run: npm install
|
||||
- name: Bump Version
|
||||
run: npm version --allow-same-version --no-git-tag $VERSION
|
||||
- run: npm run build
|
||||
- run: npm run pack:ext
|
||||
|
||||
- name: NPM Publish
|
||||
run: npm publish --tag $NPM_TAG
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # 0301...
|
||||
- name: NPM Publish
|
||||
run: npm publish --tag $NPM_TAG
|
||||
env:
|
||||
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: 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
|
||||
id: web-ext-build
|
||||
uses: kewisch/action-web-ext@v1
|
||||
continue-on-error: true
|
||||
with:
|
||||
cmd: sign
|
||||
source: dist/ext.zip
|
||||
channel: listed
|
||||
apiKey: ${{ secrets.AMO_SIGN_KEY }}
|
||||
apiSecret: ${{ secrets.AMO_SIGN_SECRET }}
|
||||
- name: Upload to IPFS
|
||||
uses: aquiladev/ipfs-action@v0.1.5
|
||||
id: ipfs
|
||||
with:
|
||||
path: ${{ env.ARTIFACTS_DIR }}
|
||||
service: infura
|
||||
verbose: true
|
||||
|
||||
- run: |
|
||||
mkdir -p $ARTIFACTS_DIR
|
||||
cp dist/main.js $ARTIFACTS_DIR/musescore-downloader.user.js
|
||||
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
|
||||
env:
|
||||
EXT_ID: musescore-downloader
|
||||
OUT_DIR: ${{ env.ARTIFACTS_DIR }}
|
||||
- name: Github Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
IPFS_HASH: ${{ steps.ipfs.outputs.hash }}
|
||||
run: |
|
||||
cd $ARTIFACTS_DIR
|
||||
rm *.tar.gz
|
||||
|
||||
- name: Upload to IPFS
|
||||
uses: aquiladev/ipfs-action@v0.1.5
|
||||
id: ipfs
|
||||
with:
|
||||
path: ${{ env.ARTIFACTS_DIR }}
|
||||
service: infura
|
||||
verbose: true
|
||||
files=$(ls .)
|
||||
assets=()
|
||||
for f in $files; do [ -f "$f" ] && assets+=(-a "$f"); done
|
||||
|
||||
- name: Github Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
IPFS_HASH: ${{ steps.ipfs.outputs.hash }}
|
||||
run: |
|
||||
cd $ARTIFACTS_DIR
|
||||
rm *.tar.gz
|
||||
SHORT_SHA=$(echo $REF | cut -c 1-7)
|
||||
|
||||
files=$(ls .)
|
||||
assets=()
|
||||
for f in $files; do [ -f "$f" ] && assets+=(-a "$f"); done
|
||||
hub release create \
|
||||
"${assets[@]}" \
|
||||
-m v$VERSION \
|
||||
-m "Mirrors:<br><https://github.com/musescore/MuseScore/tree/$SHORT_SHA><br><https://github.com/github/dmca/tree/$SHORT_SHA>" \
|
||||
"[$IPFS_HASH](https://ipfs.io/ipfs/$IPFS_HASH)" \
|
||||
-t $REF \
|
||||
v$VERSION
|
||||
|
||||
SHORT_SHA=$(echo $REF | cut -c 1-7)
|
||||
|
||||
hub release create \
|
||||
"${assets[@]}" \
|
||||
-m v$VERSION \
|
||||
-m "IPFS Hash: [$IPFS_HASH](https://ipfs.io/ipfs/$IPFS_HASH)" \
|
||||
-m "Guess what? Mirrors!<br><https://github.com/musescore/MuseScore/tree/$SHORT_SHA><br><https://github.com/github/dmca/tree/$SHORT_SHA>" \
|
||||
-t $REF \
|
||||
v$VERSION
|
||||
|
||||
- name: Archive to archive.org
|
||||
continue-on-error: true
|
||||
env:
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
URL="https://github.com/$REPO/releases/"
|
||||
curl "https://web.archive.org/save/" \
|
||||
--compressed -s \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-raw "url=$URL&capture_all=on" \
|
||||
| grep github
|
||||
- name: Archive to archive.org
|
||||
continue-on-error: true
|
||||
env:
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
URL="https://github.com/$REPO/releases/"
|
||||
curl "https://web.archive.org/save/" \
|
||||
--compressed -s \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-raw "url=$URL&capture_all=on" \
|
||||
| grep github
|
||||
|
2
CNAME
2
CNAME
@ -1 +1 @@
|
||||
msdl.librescore.org
|
||||
msdl.librescore.org
|
||||
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-2021 Xmader
|
||||
Copyright (c) 2021 LibreScore
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
385
README.md
385
README.md
@ -1,375 +1,48 @@
|
||||
|
||||
# musescore-downloader
|
||||
|
||||
**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
|
||||
[](https://discord.gg/DKu7cUZ4XQ) [](https://github.com/LibreScore/musescore-downloader/releases/latest)
|
||||
|
||||
**Star this project on [Github](https://github.com/Xmader/musescore-downloader) and [Gitlab](https://gitlab.com/Xmader/musescore-downloader)** (Mirror)
|
||||
Download sheet music from Musescore
|
||||
|
||||
[](https://discord.gg/DKu7cUZ4XQ)
|
||||
|
||||
Need dataset of musescore.com for analysis / machine learning? try [musescore-dataset](https://github.com/Xmader/musescore-dataset).
|
||||
|
||||

|
||||
|
||||
## Fair Use
|
||||
|
||||
For the purposes of research and study only
|
||||
|
||||
## About
|
||||
|
||||
Musescore Pro ($6.99/mo) is required to download sheet music from musescore.com recently.
|
||||
(However, a few months ago, it was free to download.)
|
||||
|
||||
The Musescore company said that this is about copyright and licensing, and they must pay the copyright owners.
|
||||
|
||||
Many kinds of music on musescore.com are already in the **Public Domain**, which means either the author posted them in Public Domain, or the author has been dead for over 70 years.
|
||||
Do they need to pay those composers who died hundreds of years ago?
|
||||
*Update: sheets in Public Domain are able to be downloaded without Musescore Pro now, but we still need an account to access them.*
|
||||
|
||||
Also, there are many sheet music authors on musescore.com who created their own songs and posted them under [CC-BY-**NC** (Creative Commons Attribution-**NonCommercial**) License](https://creativecommons.org/licenses/by-nc/4.0/).
|
||||
Is it illegal that they sell them for **profit**?
|
||||
*Note: Putting ads (to sell Musescore Pro) on the website also means that they use it to generate revenue.*
|
||||
|
||||
This is absolutely not acceptable, and the only purpose is to profit from stealing.
|
||||
|
||||
There is an article on their website: [Score download becomes a part of the Pro subscription](https://musescore.com/groups/improving-musescore-com/discuss/5044610)
|
||||
> DISCLAIMER: This is not an officially supported Musescore product
|
||||
|
||||
## Installation
|
||||
|
||||
### CLI Usage
|
||||
There are 3 different installable programs:
|
||||
|
||||
(recommended, more bulletproof)
|
||||
| Program | `MSCZ` | MIDI | PDF | MP3 | Conversion | \| | Windows | macOS | Linux | Android | iOS |
|
||||
| ----------------- | ------ | ---- | --- | --- | ---------- | --- | ------- | ----- | ----- | ------- | --- |
|
||||
| App | ✔️ | ✔️ | ❌ | ✔️ | ✔️ | \| | ✔️ | ✔️ | WIP | ✔️ | ❌ |
|
||||
| Browser extension | ❌ | ✔️ | ✔️ | ✔️ | ❌ | \| | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
|
||||
| Command-line tool | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | \| | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
|
||||
|
||||
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
|
||||
> Note: `Conversion` refers to the ability to convert MSCZ files into any other file type, including those not downloadable in the program.
|
||||
|
||||
[source code](/src/cli.ts)
|
||||
### App
|
||||
|
||||
### Install as Userscript
|
||||
1. Go to the [Releases](https://github.com/LibreScore/librescore-app/releases/latest) page of the librescore-app repository
|
||||
2. Download the latest version for your device
|
||||
3. Open the file to install it
|
||||
|
||||
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.
|
||||
### Browser extension
|
||||
|
||||
1. Install [Tampermonkey](https://www.tampermonkey.net/)
|
||||
1. Install [Tampermonkey](https://www.tampermonkey.net)
|
||||
2. Go to <https://msdl.librescore.org/install.user.js>
|
||||
3. Press the Install button
|
||||
|
||||
2. ~~Install from [Greasy Fork](https://greasyfork.org/scripts/391931).~~ [#42](https://github.com/Xmader/musescore-downloader/issues/42)
|
||||
Install this script from <https://msdl.librescore.org/install.user.js>
|
||||
### Command-line tool
|
||||
|
||||
### Install as Web Extension
|
||||
1. Install [Node.js LTS](https://nodejs.org)
|
||||
2. Open a terminal (do _not_ open the Node.js application)
|
||||
3. Type `npx msdl`, then press `Enter ↵`
|
||||
|
||||
The alternative method is to install this script as a Chrome or Firefox extension.
|
||||
## Building
|
||||
|
||||
You may install the Firefox extension directly from [addons.mozilla.org (for Firefox)](https://addons.mozilla.org/en-US/firefox/addon/musescore-downloader/).
|
||||
1. Install [Node.js LTS](https://nodejs.org)
|
||||
2. `npm install` to install packages
|
||||
3. `npm run build` to build
|
||||
|
||||
The up-to-date versions of the Chrome and Firefox extensions can be found on the [Github Releases](https://github.com/Xmader/musescore-downloader/releases) page.
|
||||
|
||||
## Building Instructions
|
||||
|
||||
Make sure you have [Node.js](https://nodejs.org/en/) environment installed.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build # build as User Script
|
||||
npm run pack:ext # pack Web Extension
|
||||
```
|
||||
|
||||
## Mirrors
|
||||
|
||||
* View this project on [Github](https://github.com/Xmader/musescore-downloader) (Main repo) | [Gitlab](https://gitlab.com/Xmader/musescore-downloader) (Mirror)
|
||||
|
||||
* This repo is also available on IPFS to avoid DMCA takedown: [ipns://msdl.librescore.org](https://ipfs.io/ipns/msdl.librescore.org/)
|
||||
|
||||
## Feedback
|
||||
|
||||
[Github Issues](https://github.com/Xmader/musescore-downloader/issues)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## About the Takedown Request
|
||||
|
||||
I received a [takedown request email](https://github.com/Xmader/musescore-downloader/issues/5) from one of the Musescore developers, but I would like to say something against it.
|
||||
|
||||
> All not Public domain content on musescore.com is licensed by major music publishers (Alfred, EMI, Sony, etc.). Distribute licensed music content from Musescore.com for free you violate their rights.
|
||||
|
||||
Firstly, if I violate the rights of major music publishers, the takedown request should be sent by them instead of the Musescore developers.
|
||||
|
||||
Secondly, musescore.com is not a simple music sharing website. The authors of sheet music must transcript and rearrange the original songs to sheets, not just copying files from somewhere else to musescore.com. As a result, the licensing should focus on the rights of transcription/rearranging to the sheet music authors, instead of the rights of sharing the music on some websites.
|
||||
|
||||
Thirdly, the copyright ownership of contents on musescore.com is not clear. Not every non-public-domain song on musescore.com is owned by major music publishers. There are many small music publishers and independent songwriters; Songs might be licensed under free licenses like Creative Commons. Also, there are many authors who created their own songs and posted the sheet music on musescore.com. Does musescore.com pay to those authors?
|
||||
|
||||
If we can't see proof that musescore.com really pays license fees to the copyright owners, we may think it is just an excuse to get profit from stealing.
|
||||
|
||||
> you illegaly [sic] use our private API with licensed music content. (https://github.com/Xmader/musescore-downloader/issues/5)
|
||||
|
||||
No, the API documentation is on https://developers.musescore.com/.
|
||||
|
||||
|
||||
**I will launch an open source (GPLv3), serverless, offline-first, frontend-first, and totally free alternative to musescore.com, [LibreScore](https://github.com/LibreScore). Everyone is welcome to join the project development by opening an issue or [emailing me](mailto:i@xmader.com).**
|
||||
|
||||
**Also, I'm developing [webmscore](https://github.com/LibreScore/webmscore). It could convert a mscz file into any format that the Musescore software supports, and in the browser.** Because the Musescore software is open source under [GPL](https://github.com/musescore/MuseScore/blob/master/LICENSE.GPL), I could translate the source code to js, or compile it into asm.js/WASM.
|
||||
|
||||
---
|
||||
|
||||
# musescore-downloader
|
||||
|
||||
[English](#musescore-downloader) | **简体中文** | [Español](#musescore-downloader-2) | [Italian](#musescore-downloader-3)
|
||||
|
||||
*中英文版本项目 README 分开撰写,中文版较不完整。如果有能力,请阅读英文版。*
|
||||
|
||||
> 免登录、免 Musescore Pro,下载 musescore.com 上的曲谱
|
||||
|
||||
**在 [Github](https://github.com/Xmader/musescore-downloader) 和 [Gitlab](https://gitlab.com/Xmader/musescore-downloader)** (镜像) **上给项目打星**
|
||||
|
||||
[](https://discord.gg/DKu7cUZ4XQ)
|
||||
|
||||

|
||||
|
||||
## 关于
|
||||
|
||||
在 musescore.com 上下载曲谱需要支付 Musescore Pro 了,
|
||||
这种吸引人气后再对原来免费的东西收费的盈利模式十分令人反感,
|
||||
并且也违反了曲谱作者发布时的许可证协议(通常是 [CC-BY-NC 署名-非商业使用](https://creativecommons.org/licenses/by-nc/4.0/),平台网站却将其作商业使用)
|
||||
|
||||
参见官网文章 [Score download becomes a part of the Pro subscription](https://musescore.com/groups/improving-musescore-com/discuss/5044610)
|
||||
|
||||
## 安装、更新、讨论
|
||||
|
||||
脚本以 [Userscript](https://en.wikipedia.org/wiki/Userscript) 的形式提供,需要事先安装一个 [用户脚本管理器](https://greasyfork.org/zh-CN/help/installing-user-scripts),例如 Tampermonkey
|
||||
|
||||
在 [Github](https://github.com/Xmader/musescore-downloader) 上查看、讨论、更新
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
---
|
||||
|
||||
# musescore-downloader
|
||||
|
||||
[English](#musescore-downloader) | [简体中文](#musescore-downloader-1) | **Español** | [Italian](#musescore-downloader-3)
|
||||
|
||||
> descarga partituras de musescore.com de forma gratuita, no se requiere iniciar sesión o Musescore Pro
|
||||
|
||||
**Dale una estrella a este proyecto en [Github](https://github.com/Xmader/musescore-downloader) y [Gitlab](https://gitlab.com/Xmader/musescore-downloader)** (Respaldo)
|
||||
|
||||
[](https://discord.gg/DKu7cUZ4XQ)
|
||||
|
||||
¿Necesita un conjunto de datos de musescore.com para análisis / Machine Learning? prueba [musescore-dataset](https://github.com/Xmader/musescore-dataset).
|
||||
|
||||

|
||||
|
||||
## Fair Use
|
||||
|
||||
Solo para fines de investigación y estudio
|
||||
|
||||
## Acerca de
|
||||
|
||||
Se requiere Musescore Pro ($ 6.99/mes) para descargar partituras de musescore.com desde hace poco.
|
||||
(Sin embargo, hace unos meses, se podían descargar gratis).
|
||||
|
||||
La compañía Musescore dijo que es debido a derechos de autor y licencias, y deben pagar a los propietarios de los derechos de autor.
|
||||
|
||||
Muchas canciones en musescore.com ya son de ** dominio público **, lo que significa que el autor las publicó en el dominio público o que el autor ha estado muerto durante más de 70 años.
|
||||
¿Necesitan pagarle a compositores que murieron hace cientos de años?
|
||||
*Actualización: Las partituras de dominio público se pueden descargar sin Musescore Pro ahora, pero aún se necesita de una cuenta para acceder a ellas.*
|
||||
|
||||
Además, hay muchos autores de partituras en musescore.com que crearon sus propias canciones y las publicaron bajo la licencia [CC-BY-**NC** (Creative Commons Attribution-**NonCommercial**)](https://creativecommons.org/licenses/by-nc/4.0/).
|
||||
¿No es ilegal que las vendan para generar **ganancias**?
|
||||
*Nota: Poner anuncios (para vender Musescore Pro) en el sitio web también significa que lo usan para generar ingresos. *
|
||||
|
||||
Esto es absolutamente inaceptable, y el único propósito es generar ganancias robando.
|
||||
|
||||
Hay un artículo en su sitio web: [La descarga de partituras se convierte en parte de la suscripción Pro](https://musescore.com/groups/improving-musescore-com/discuss/5044610)
|
||||
|
||||
## Instalación
|
||||
|
||||
### Instalar como Script de Usuario
|
||||
|
||||
Este script está disponible como un [Script de usuario](https://en.wikipedia.org/wiki/Userscript). Para utilizar este script, primero debe instalar un [administrador de script de usuario](https://greasyfork.org/en/help/installing-user-scripts), como Tampermonkey.
|
||||
|
||||
1. Instala [Tampermonkey](https://www.tampermonkey.net/)
|
||||
|
||||
2. ~~Instalar desde [Greasy Fork](https://greasyfork.org/scripts/391931).~~ [#42](https://github.com/Xmader/musescore-downloader/issues/42)
|
||||
Instale este script desde <https://msdl.librescore.org/install.user.js>
|
||||
|
||||
### Instalar como Extensión Web
|
||||
|
||||
El método alternativo es instalar este script como una extensión de Firefox.
|
||||
|
||||
Puedes instalar la extensión del navegador directamente desde [addons.mozilla.org (para Firefox)](https://addons.mozilla.org/en-US/firefox/addon/musescore-downloader/).
|
||||
|
||||
La versión más reciente se puede encontrar en la página de [Github Releases](https://github.com/Xmader/musescore-downloader/releases).
|
||||
|
||||
## Instrucciones de Compilación
|
||||
|
||||
Asegúrate de tener el entorno [Node.js](https://nodejs.org/en/) instalado.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build # build as User Script
|
||||
npm run pack:ext # pack Web Extension
|
||||
```
|
||||
|
||||
## Archivos
|
||||
|
||||
* Mira este proyecto en [Github](https://github.com/Xmader/musescore-downloader) (Principal) | [Gitlab](https://gitlab.com/Xmader/musescore-downloader) (Respaldo)
|
||||
|
||||
* Este repositorio también está disponible en IPFS para evitar la eliminación por parte de DMCA. [ipns://msdl.librescore.org](https://ipfs.io/ipns/msdl.librescore.org/)
|
||||
|
||||
## Feedback
|
||||
|
||||
[Github Issues](https://github.com/Xmader/musescore-downloader/issues)
|
||||
|
||||
## Licencia
|
||||
|
||||
MIT
|
||||
|
||||
## Acerca de la Solicitud de Eliminación
|
||||
|
||||
Recibí un [correo de solicitud de elimnacion l](https://github.com/Xmader/musescore-downloader/issues/5) de uno de los desarrolladores de Musescore, pero me gustaría decir algo en contra de esto.
|
||||
|
||||
> Todo el contenido que no es de dominio público en musescore.com tiene licencia de los principales editores de música (Alfred, EMI, Sony, etc.). Distribuir contenido de musical licenciado de Musescore.com de forma gratuita viola sus derechos.
|
||||
|
||||
En primer lugar, si violé los derechos de los principales editores de música, ellos deberían enviar la solicitud de eliminación en lugar de los desarrolladores de Musescore.
|
||||
|
||||
En segundo lugar, musescore.com no es un simple sitio web para compartir música. Los autores de partituras deben transcribir y recomponer las canciones originales en partituras, no solo copiar archivos de cualquier otro lugar a musescore.com. Como resultado, la licencia debe centrarse en los derechos de transcripción/recomposición de los autores de las partituras, en lugar de los derechos de compartir la música en algunos sitios web.
|
||||
|
||||
En tercer lugar, la propiedad de los derechos de autor de los contenidos de musescore.com no está clara. No todas las canciones que no son de dominio público en musescore.com son propiedad de las principales editoriales de música. Hay muchos pequeños editores de música y compositores independientes; Las canciones pueden tener licencias gratuitas como Creative Commons. Además, hay muchos autores que crearon sus propias canciones y publicaron la partitura en musescore.com. ¿Musescore.com le paga a esos autores?
|
||||
|
||||
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.
|
||||
|
||||
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).**
|
||||
|
||||
**Además, estoy desarrollando [webmscore](https://github.com/LibreScore/webmscore). 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)
|
||||
|
||||
[](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).
|
||||
|
||||

|
||||
|
||||
## 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/).
|
||||
|
||||
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 [webmscore](https://github.com/LibreScore/webmscore). 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.
|
||||
|
||||
---
|
||||
- Install `./dist/main.js` with Tampermonkey
|
||||
- `node ./dist/cli.js` to run command-line tool
|
||||
|
57222
dist/main.js
vendored
57222
dist/main.js
vendored
File diff suppressed because one or more lines are too long
BIN
images/logo.png
Normal file
BIN
images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 127 KiB |
@ -1 +1 @@
|
||||
dist/main.js
|
||||
dist / main.js;
|
||||
|
@ -1 +1 @@
|
||||
package.json
|
||||
package.json
|
||||
|
7541
package-lock.json
generated
7541
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
137
package.json
137
package.json
@ -1,74 +1,69 @@
|
||||
{
|
||||
"name": "musescore-downloader",
|
||||
"version": "0.26.0",
|
||||
"description": "download sheet music from musescore.com for free, no login or Musescore Pro required | 免登录、免 Musescore Pro,免费下载 musescore.com 上的曲谱",
|
||||
"main": "dist/main.js",
|
||||
"bin": "dist/cli.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Xmader/musescore-downloader.git"
|
||||
},
|
||||
"author": "Xmader",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Xmader/musescore-downloader/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Xmader/musescore-downloader#readme",
|
||||
"homepage_url": "https://github.com/Xmader/musescore-downloader#readme",
|
||||
"manifest_version": 2,
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "{69856097-6e10-42e9-acc7-0c063550c7b8}"
|
||||
"name": "musescore-downloader",
|
||||
"version": "0.27.0",
|
||||
"description": "Download sheet music from Musescore",
|
||||
"main": "dist/main.js",
|
||||
"bin": "dist/cli.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/LibreScore/musescore-downloader.git"
|
||||
},
|
||||
"author": "LibreScore",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/LibreScore/musescore-downloader/issues"
|
||||
},
|
||||
"homepage": "https://github.com/LibreScore/musescore-downloader#readme",
|
||||
"homepage_url": "https://github.com/LibreScore/musescore-downloader#readme",
|
||||
"manifest_version": 2,
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"*://*.musescore.com/*/*"
|
||||
],
|
||||
"js": [
|
||||
"src/web-ext.js"
|
||||
],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"dist/main.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@librescore/fonts": "^0.4.0",
|
||||
"@librescore/sf3": "^0.3.0",
|
||||
"detect-node": "^2.0.4",
|
||||
"inquirer": "^7.3.3",
|
||||
"node-fetch": "^2.6.1",
|
||||
"ora": "^5.1.0",
|
||||
"webmscore": "^0.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/inquirer": "^7.3.1",
|
||||
"@types/pdfkit": "^0.10.6",
|
||||
"pdfkit": "git+https://github.com/LibreScore/pdfkit.git",
|
||||
"rollup": "^1.32.1",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-node-builtins": "^2.1.2",
|
||||
"rollup-plugin-node-globals": "^1.4.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-string": "^3.0.0",
|
||||
"rollup-plugin-typescript": "^1.0.1",
|
||||
"svg-to-pdfkit": "^0.1.8",
|
||||
"tslib": "^1.14.1",
|
||||
"typescript": "^4.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"watch": "npm run build -- --watch",
|
||||
"start:ext": "web-ext run --url https://musescore.com/",
|
||||
"start:ext-chrome": "npm run start:ext -- -t chromium",
|
||||
"pack:ext": "zip -r dist/ext.zip manifest.json src/web-ext.js dist/main.js",
|
||||
"bump-version:patch": "npm version patch --no-git-tag && npm run build",
|
||||
"bump-version:minor": "npm version minor --no-git-tag && npm run build"
|
||||
}
|
||||
},
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"*://*.musescore.com/*/*"
|
||||
],
|
||||
"js": [
|
||||
"src/web-ext.js"
|
||||
],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"dist/main.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@librescore/fonts": "^0.4.0",
|
||||
"@librescore/sf3": "^0.3.0",
|
||||
"detect-node": "^2.0.4",
|
||||
"inquirer": "^7.3.3",
|
||||
"node-fetch": "^2.6.1",
|
||||
"ora": "^5.1.0",
|
||||
"webmscore": "^0.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/inquirer": "^7.3.1",
|
||||
"@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-node-builtins": "^2.1.2",
|
||||
"rollup-plugin-node-globals": "^1.4.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-string": "^3.0.0",
|
||||
"rollup-plugin-typescript": "^1.0.1",
|
||||
"svg-to-pdfkit": "^0.1.8",
|
||||
"tslib": "^1.14.1",
|
||||
"typescript": "^4.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"watch": "npm run build -- --watch",
|
||||
"start:ext": "web-ext run --url https://musescore.com/",
|
||||
"start:ext-chrome": "npm run start:ext -- -t chromium",
|
||||
"pack:ext": "zip -r dist/ext.zip manifest.json src/web-ext.js dist/main.js",
|
||||
"bump-version:patch": "npm version patch --no-git-tag && npm run build",
|
||||
"bump-version:minor": "npm version minor --no-git-tag && npm run build"
|
||||
}
|
||||
}
|
||||
|
@ -1,42 +1,39 @@
|
||||
import typescript from "rollup-plugin-typescript"
|
||||
import resolve from "rollup-plugin-node-resolve"
|
||||
import commonjs from "rollup-plugin-commonjs"
|
||||
import builtins from "rollup-plugin-node-builtins"
|
||||
import nodeGlobals from "rollup-plugin-node-globals"
|
||||
import json from "@rollup/plugin-json"
|
||||
import { string } from "rollup-plugin-string"
|
||||
import fs from "fs"
|
||||
import typescript from "rollup-plugin-typescript";
|
||||
import resolve from "rollup-plugin-node-resolve";
|
||||
import commonjs from "rollup-plugin-commonjs";
|
||||
import builtins from "rollup-plugin-node-builtins";
|
||||
import nodeGlobals from "rollup-plugin-node-globals";
|
||||
import json from "@rollup/plugin-json";
|
||||
import { string } from "rollup-plugin-string";
|
||||
import fs from "fs";
|
||||
|
||||
const getBannerText = () => {
|
||||
const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf-8"))
|
||||
const { version } = packageJson
|
||||
let bannerText = fs.readFileSync("./src/meta.js", "utf-8")
|
||||
bannerText = bannerText.replace("%VERSION%", version)
|
||||
return bannerText
|
||||
}
|
||||
const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf-8"));
|
||||
const { version } = packageJson;
|
||||
let bannerText = fs.readFileSync("./src/meta.js", "utf-8");
|
||||
bannerText = bannerText.replace("%VERSION%", version);
|
||||
return bannerText;
|
||||
};
|
||||
|
||||
const getWrapper = (startL, endL) => {
|
||||
const js = fs.readFileSync("./src/wrapper.js", "utf-8")
|
||||
return js.split(/\n/g).slice(startL, endL).join("\n")
|
||||
}
|
||||
const js = fs.readFileSync("./src/wrapper.js", "utf-8");
|
||||
return js.split(/\n/g).slice(startL, endL).join("\n");
|
||||
};
|
||||
|
||||
const basePlugins = [
|
||||
typescript({
|
||||
target: "ES6",
|
||||
sourceMap: false,
|
||||
allowJs: true,
|
||||
lib: [
|
||||
"ES6",
|
||||
"dom"
|
||||
],
|
||||
lib: ["ES6", "dom"],
|
||||
}),
|
||||
resolve({
|
||||
preferBuiltins: true,
|
||||
jsnext: true,
|
||||
extensions: [".js", ".ts"]
|
||||
extensions: [".js", ".ts"],
|
||||
}),
|
||||
commonjs({
|
||||
extensions: [".js", ".ts"]
|
||||
extensions: [".js", ".ts"],
|
||||
}),
|
||||
json(),
|
||||
string({
|
||||
@ -45,19 +42,19 @@ const basePlugins = [
|
||||
{
|
||||
/**
|
||||
* remove tslib license comments
|
||||
* @param {string} code
|
||||
* @param {string} id
|
||||
* @param {string} code
|
||||
* @param {string} id
|
||||
*/
|
||||
transform (code, id) {
|
||||
transform(code, id) {
|
||||
if (id.includes("tslib")) {
|
||||
code = code.split(/\r?\n/g).slice(15).join("\n")
|
||||
code = code.split(/\r?\n/g).slice(15).join("\n");
|
||||
}
|
||||
return {
|
||||
code
|
||||
}
|
||||
}
|
||||
code,
|
||||
};
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const plugins = [
|
||||
...basePlugins,
|
||||
@ -67,7 +64,7 @@ const plugins = [
|
||||
filename: false,
|
||||
baseDir: false,
|
||||
}),
|
||||
]
|
||||
];
|
||||
|
||||
export default [
|
||||
{
|
||||
@ -75,7 +72,7 @@ export default [
|
||||
output: {
|
||||
file: "dist/cache/worker.js",
|
||||
format: "iife",
|
||||
name: 'worker',
|
||||
name: "worker",
|
||||
banner: "export const PDFWorker = function () { ",
|
||||
footer: "return worker\n}\n",
|
||||
sourcemap: false,
|
||||
@ -90,7 +87,7 @@ export default [
|
||||
sourcemap: false,
|
||||
banner: getBannerText,
|
||||
intro: () => getWrapper(0, -1),
|
||||
outro: () => getWrapper(-1)
|
||||
outro: () => getWrapper(-1),
|
||||
},
|
||||
plugins,
|
||||
},
|
||||
@ -104,4 +101,4 @@ export default [
|
||||
},
|
||||
plugins: basePlugins,
|
||||
},
|
||||
]
|
||||
];
|
||||
|
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
Before Width: | Height: | Size: 264 KiB |
@ -5,49 +5,55 @@
|
||||
* make hooked methods "native"
|
||||
*/
|
||||
export const makeNative = (() => {
|
||||
const l = new Map<Function, Function>()
|
||||
const l = new Map<Function, Function>();
|
||||
|
||||
hookNative(Function.prototype, 'toString', (_toString) => {
|
||||
return function () {
|
||||
if (l.has(this)) {
|
||||
const _fn = l.get(this) || parseInt // "function () {\n [native code]\n}"
|
||||
if (l.has(_fn)) { // nested
|
||||
return _fn.toString()
|
||||
} else {
|
||||
return _toString.call(_fn) as string
|
||||
}
|
||||
}
|
||||
return _toString.call(this) as string
|
||||
}
|
||||
}, true)
|
||||
hookNative(
|
||||
Function.prototype,
|
||||
"toString",
|
||||
(_toString) => {
|
||||
return function () {
|
||||
if (l.has(this)) {
|
||||
const _fn = l.get(this) || parseInt; // "function () {\n [native code]\n}"
|
||||
if (l.has(_fn)) {
|
||||
// nested
|
||||
return _fn.toString();
|
||||
} else {
|
||||
return _toString.call(_fn) as string;
|
||||
}
|
||||
}
|
||||
return _toString.call(this) as string;
|
||||
};
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
return (fn: Function, original: Function) => {
|
||||
l.set(fn, original)
|
||||
}
|
||||
})()
|
||||
return (fn: Function, original: Function) => {
|
||||
l.set(fn, original);
|
||||
};
|
||||
})();
|
||||
|
||||
export function hookNative<T extends object, M extends (keyof T)> (
|
||||
target: T,
|
||||
method: M,
|
||||
hook: (originalFn: T[M], detach: () => void) => T[M],
|
||||
async = false,
|
||||
export function hookNative<T extends object, M extends keyof T>(
|
||||
target: T,
|
||||
method: M,
|
||||
hook: (originalFn: T[M], detach: () => void) => T[M],
|
||||
async = false
|
||||
): void {
|
||||
// reserve for future hook update
|
||||
const _fn = target[method]
|
||||
const detach = () => {
|
||||
target[method] = _fn // detach
|
||||
}
|
||||
// reserve for future hook update
|
||||
const _fn = target[method];
|
||||
const detach = () => {
|
||||
target[method] = _fn; // detach
|
||||
};
|
||||
|
||||
// This script can run before anything on the page,
|
||||
// so setting this function to be non-configurable and non-writable is no use.
|
||||
const hookedFn = hook(_fn, detach)
|
||||
target[method] = hookedFn
|
||||
// This script can run before anything on the page,
|
||||
// so setting this function to be non-configurable and non-writable is no use.
|
||||
const hookedFn = hook(_fn, detach);
|
||||
target[method] = hookedFn;
|
||||
|
||||
if (!async) {
|
||||
makeNative(hookedFn as any, _fn as any)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
makeNative(hookedFn as any, _fn as any)
|
||||
})
|
||||
}
|
||||
if (!async) {
|
||||
makeNative(hookedFn as any, _fn as any);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
makeNative(hookedFn as any, _fn as any);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
94
src/btn.css
94
src/btn.css
@ -1,83 +1,83 @@
|
||||
div {
|
||||
width: 422px;
|
||||
right: 0;
|
||||
margin: 0 18px 18px 0;
|
||||
width: 422px;
|
||||
right: 0;
|
||||
margin: 0 18px 18px 0;
|
||||
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
font-family: 'Inter', 'Helvetica neue', Helvetica, sans-serif;
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
background: #f6f6f6;
|
||||
min-width: 230px;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
font-family: "Inter", "Helvetica neue", Helvetica, sans-serif;
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
background: #f6f6f6;
|
||||
min-width: 230px;
|
||||
|
||||
/* pass the scroll event through the btns background */
|
||||
pointer-events: none;
|
||||
/* pass the scroll event through the btns background */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 950px) {
|
||||
div {
|
||||
width: auto !important;
|
||||
}
|
||||
div {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
width: 178px !important;
|
||||
min-width: 178px;
|
||||
height: 40px;
|
||||
width: 178px !important;
|
||||
min-width: 178px;
|
||||
height: 40px;
|
||||
|
||||
color: #fff;
|
||||
background: #2e68c0;
|
||||
color: #fff;
|
||||
background: #2e68c0;
|
||||
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
|
||||
margin-bottom: 8px;
|
||||
margin-right: 8px;
|
||||
padding: 4px 12px;
|
||||
margin-bottom: 8px;
|
||||
margin-right: 8px;
|
||||
padding: 4px 12px;
|
||||
|
||||
justify-content: start;
|
||||
align-self: center;
|
||||
justify-content: start;
|
||||
align-self: center;
|
||||
|
||||
font-size: 16px;
|
||||
border-radius: 6px;
|
||||
border: 0;
|
||||
font-size: 16px;
|
||||
border-radius: 6px;
|
||||
border: 0;
|
||||
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
|
||||
font-family: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* fix `View in LibreScore` button text overflow */
|
||||
button:last-of-type {
|
||||
width: unset !important;
|
||||
width: unset !important;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #1a4f9f;
|
||||
background: #1a4f9f;
|
||||
}
|
||||
|
||||
/* light theme btn */
|
||||
button.light {
|
||||
color: #2e68c0;
|
||||
background: #e1effe;
|
||||
color: #2e68c0;
|
||||
background: #e1effe;
|
||||
}
|
||||
|
||||
button.light:hover {
|
||||
background: #c3ddfd;
|
||||
background: #c3ddfd;
|
||||
}
|
||||
|
||||
svg {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
566
src/btn.ts
566
src/btn.ts
@ -1,336 +1,330 @@
|
||||
|
||||
import { ScoreInfo } from './scoreinfo'
|
||||
import { loadMscore, WebMscore } from './mscore'
|
||||
import { useTimeout, windowOpenAsync, console, attachShadow, DISCORD_URL } from './utils'
|
||||
import { isGmAvailable, _GM } from './gm'
|
||||
import i18n from './i18n'
|
||||
import {
|
||||
useTimeout,
|
||||
windowOpenAsync,
|
||||
console,
|
||||
attachShadow,
|
||||
APP_URL,
|
||||
} from "./utils";
|
||||
import { isGmAvailable, _GM } from "./gm";
|
||||
import i18n from "./i18n";
|
||||
// @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',
|
||||
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 els = [...document.querySelectorAll('span')]
|
||||
const el = els.find(b => {
|
||||
const text = b?.textContent?.replace(/\s/g, '') || ''
|
||||
return text.includes('Download') || text.includes('Print')
|
||||
}) as HTMLDivElement | null
|
||||
const btnParent = el?.parentElement?.parentElement as HTMLDivElement | undefined
|
||||
if (!btnParent || !(btnParent instanceof HTMLDivElement)) throw new Error('btn parent not found')
|
||||
return btnParent
|
||||
}
|
||||
const els = [...document.querySelectorAll("span")];
|
||||
const el = els.find((b) => {
|
||||
const text = b?.textContent?.replace(/\s/g, "") || "";
|
||||
return text.includes("Download") || text.includes("Print");
|
||||
}) as HTMLDivElement | null;
|
||||
const btnParent = el?.parentElement?.parentElement as
|
||||
| HTMLDivElement
|
||||
| undefined;
|
||||
if (!btnParent || !(btnParent instanceof HTMLDivElement))
|
||||
throw new Error("btn parent not found");
|
||||
return btnParent;
|
||||
};
|
||||
|
||||
const buildDownloadBtn = (icon: ICON, lightTheme = false) => {
|
||||
const btn = document.createElement('button')
|
||||
btn.type = 'button'
|
||||
if (lightTheme) btn.className = 'light'
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
if (lightTheme) btn.className = "light";
|
||||
|
||||
// build icon svg element
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
svg.setAttribute('viewBox', '0 0 24 24')
|
||||
const svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
svgPath.setAttribute('d', icon)
|
||||
svgPath.setAttribute('fill', lightTheme ? '#2e68c0' : '#fff')
|
||||
svg.append(svgPath)
|
||||
// build icon svg element
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("viewBox", "0 0 24 24");
|
||||
const svgPath = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"path"
|
||||
);
|
||||
svgPath.setAttribute("d", icon);
|
||||
svgPath.setAttribute("fill", lightTheme ? "#2e68c0" : "#fff");
|
||||
svg.append(svgPath);
|
||||
|
||||
const textNode = document.createElement('span')
|
||||
btn.append(svg, textNode)
|
||||
const textNode = document.createElement("span");
|
||||
btn.append(svg, textNode);
|
||||
|
||||
return btn
|
||||
}
|
||||
return btn;
|
||||
};
|
||||
|
||||
const cloneBtn = (btn: HTMLButtonElement) => {
|
||||
const n = btn.cloneNode(true) as HTMLButtonElement
|
||||
n.onclick = btn.onclick
|
||||
return n
|
||||
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 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 })
|
||||
})
|
||||
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 {
|
||||
readonly name: string;
|
||||
readonly action: BtnAction;
|
||||
readonly disabled?: boolean;
|
||||
readonly tooltip?: string;
|
||||
readonly icon?: ICON;
|
||||
readonly lightTheme?: boolean;
|
||||
readonly name: string;
|
||||
readonly action: BtnAction;
|
||||
readonly disabled?: boolean;
|
||||
readonly tooltip?: string;
|
||||
readonly icon?: ICON;
|
||||
readonly lightTheme?: boolean;
|
||||
}
|
||||
|
||||
export enum BtnListMode {
|
||||
InPage,
|
||||
ExtWindow,
|
||||
InPage,
|
||||
ExtWindow,
|
||||
}
|
||||
|
||||
export class BtnList {
|
||||
private readonly list: BtnElement[] = [];
|
||||
private readonly list: BtnElement[] = [];
|
||||
|
||||
constructor (private getBtnParent: () => HTMLDivElement = getBtnContainer) { }
|
||||
constructor(private getBtnParent: () => HTMLDivElement = getBtnContainer) {}
|
||||
|
||||
add (options: BtnOptions): BtnElement {
|
||||
const btnTpl = buildDownloadBtn(options.icon ?? ICON.DOWNLOAD, options.lightTheme)
|
||||
const setText = (btn: BtnElement) => {
|
||||
const textNode = btn.querySelector('span')
|
||||
return (str: string): void => {
|
||||
if (textNode) textNode.textContent = str
|
||||
}
|
||||
}
|
||||
add(options: BtnOptions): BtnElement {
|
||||
const btnTpl = buildDownloadBtn(
|
||||
options.icon ?? ICON.DOWNLOAD,
|
||||
options.lightTheme
|
||||
);
|
||||
const setText = (btn: BtnElement) => {
|
||||
const textNode = btn.querySelector("span");
|
||||
return (str: string): void => {
|
||||
if (textNode) textNode.textContent = str;
|
||||
};
|
||||
};
|
||||
|
||||
setText(btnTpl)(options.name)
|
||||
setText(btnTpl)(options.name);
|
||||
|
||||
btnTpl.onclick = function () {
|
||||
const btn = this as BtnElement
|
||||
options.action(options.name, btn, setText(btn))
|
||||
}
|
||||
btnTpl.onclick = function () {
|
||||
const btn = this as BtnElement;
|
||||
options.action(options.name, btn, setText(btn));
|
||||
};
|
||||
|
||||
this.list.push(btnTpl)
|
||||
this.list.push(btnTpl);
|
||||
|
||||
if (options.disabled) {
|
||||
btnTpl.disabled = options.disabled
|
||||
}
|
||||
|
||||
if (options.tooltip) {
|
||||
btnTpl.title = options.tooltip
|
||||
}
|
||||
|
||||
// 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 () {
|
||||
const btnParent = document.querySelector('div') as HTMLDivElement
|
||||
const shadow = attachShadow(btnParent)
|
||||
|
||||
// style the shadow DOM
|
||||
const style = document.createElement('style')
|
||||
style.innerText = btnListCss
|
||||
shadow.append(style)
|
||||
|
||||
// hide buttons using the shadow DOM
|
||||
const slot = document.createElement('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
|
||||
}
|
||||
|
||||
/**
|
||||
* replace the template button with the list of new buttons
|
||||
*/
|
||||
async commit (mode: BtnListMode = BtnListMode.InPage): Promise<void> {
|
||||
switch (mode) {
|
||||
case BtnListMode.InPage: {
|
||||
let el: Element
|
||||
try {
|
||||
el = this._commit()
|
||||
} catch {
|
||||
// fallback to BtnListMode.ExtWindow
|
||||
return this.commit(BtnListMode.ExtWindow)
|
||||
if (options.disabled) {
|
||||
btnTpl.disabled = options.disabled;
|
||||
}
|
||||
const observer = new MutationObserver(() => {
|
||||
// check if the buttons are still in document when dom updates
|
||||
if (!document.contains(el)) {
|
||||
// re-commit
|
||||
// performance issue?
|
||||
el = this._commit()
|
||||
}
|
||||
})
|
||||
observer.observe(document, { childList: true, subtree: true })
|
||||
break
|
||||
}
|
||||
|
||||
case BtnListMode.ExtWindow: {
|
||||
const div = this._commit()
|
||||
const w = await windowOpenAsync(undefined, '', undefined, 'resizable,width=230,height=270')
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
w?.document.body.append(div)
|
||||
window.addEventListener('unload', () => w?.close())
|
||||
break
|
||||
}
|
||||
if (options.tooltip) {
|
||||
btnTpl.title = options.tooltip;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error('unknown BtnListMode')
|
||||
// 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() {
|
||||
const btnParent = document.querySelector("div") as HTMLDivElement;
|
||||
const shadow = attachShadow(btnParent);
|
||||
|
||||
// style the shadow DOM
|
||||
const style = document.createElement("style");
|
||||
style.innerText = btnListCss;
|
||||
shadow.append(style);
|
||||
|
||||
// hide buttons using the shadow DOM
|
||||
const slot = document.createElement("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;
|
||||
}
|
||||
|
||||
/**
|
||||
* replace the template button with the list of new buttons
|
||||
*/
|
||||
async commit(mode: BtnListMode = BtnListMode.InPage): Promise<void> {
|
||||
switch (mode) {
|
||||
case BtnListMode.InPage: {
|
||||
let el: Element;
|
||||
try {
|
||||
el = this._commit();
|
||||
} catch {
|
||||
// fallback to BtnListMode.ExtWindow
|
||||
return this.commit(BtnListMode.ExtWindow);
|
||||
}
|
||||
const observer = new MutationObserver(() => {
|
||||
// check if the buttons are still in document when dom updates
|
||||
if (!document.contains(el)) {
|
||||
// re-commit
|
||||
// performance issue?
|
||||
el = this._commit();
|
||||
}
|
||||
});
|
||||
observer.observe(document, { childList: true, subtree: true });
|
||||
break;
|
||||
}
|
||||
|
||||
case BtnListMode.ExtWindow: {
|
||||
const div = this._commit();
|
||||
const w = await windowOpenAsync(
|
||||
undefined,
|
||||
"",
|
||||
undefined,
|
||||
"resizable,width=230,height=270"
|
||||
);
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
w?.document.body.append(div);
|
||||
window.addEventListener("unload", () => w?.close());
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error("unknown BtnListMode");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type BtnAction = (btnName: string, btnEl: BtnElement, setText: (str: string) => void) => any
|
||||
type BtnAction = (
|
||||
btnName: string,
|
||||
btnEl: BtnElement,
|
||||
setText: (str: string) => void
|
||||
) => any;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace BtnAction {
|
||||
type Promisable<T> = T | Promise<T>;
|
||||
type UrlInput = Promisable<string> | (() => Promisable<string>);
|
||||
|
||||
type Promisable<T> = T | Promise<T>
|
||||
type UrlInput = Promisable<string> | (() => Promisable<string>)
|
||||
const normalizeUrlInput = (url: UrlInput) => {
|
||||
if (typeof url === "function") return url();
|
||||
else return url;
|
||||
};
|
||||
|
||||
const normalizeUrlInput = (url: UrlInput) => {
|
||||
if (typeof url === 'function') return url()
|
||||
else return url
|
||||
}
|
||||
export const download = (
|
||||
url: UrlInput,
|
||||
fallback?: () => Promisable<void>,
|
||||
timeout?: number,
|
||||
target?: "_blank"
|
||||
): BtnAction => {
|
||||
return process(
|
||||
async (): Promise<void> => {
|
||||
const _url = await normalizeUrlInput(url);
|
||||
const a = document.createElement("a");
|
||||
a.href = _url;
|
||||
if (target) a.target = target;
|
||||
a.dispatchEvent(new MouseEvent("click"));
|
||||
},
|
||||
fallback,
|
||||
timeout
|
||||
);
|
||||
};
|
||||
|
||||
export const download = (url: UrlInput, fallback?: () => Promisable<void>, timeout?: number, target?: '_blank'): BtnAction => {
|
||||
return process(async (): Promise<void> => {
|
||||
const _url = await normalizeUrlInput(url)
|
||||
const a = document.createElement('a')
|
||||
a.href = _url
|
||||
if (target) a.target = target
|
||||
a.dispatchEvent(new MouseEvent('click'))
|
||||
}, fallback, timeout)
|
||||
}
|
||||
export const openUrl = download;
|
||||
|
||||
export const openUrl = download
|
||||
export const errorPopup = (): BtnAction => {
|
||||
return (btnName: unknown, btn: unknown, setText) => {
|
||||
setText(i18n("BTN_ERROR")());
|
||||
// ask user to use LibreScore app
|
||||
alert("Get the LibreScore app to download:\n" + APP_URL);
|
||||
// open LibreScore app releases page on 'OK'
|
||||
const a = document.createElement("a");
|
||||
a.href = APP_URL;
|
||||
a.target = "_blank";
|
||||
a.dispatchEvent(new MouseEvent("click"));
|
||||
};
|
||||
};
|
||||
|
||||
export const mscoreWindow = (scoreinfo: ScoreInfo, fn: (w: Window, score: WebMscore, processingTextEl: ChildNode) => any): BtnAction => {
|
||||
return async (btnName, btn, setText) => {
|
||||
// save btn event for later use
|
||||
const _onclick = btn.onclick
|
||||
// clear btn event
|
||||
btn.onclick = null
|
||||
// set btn text to "PROCESSING"
|
||||
setText(i18n('PROCESSING')())
|
||||
export const process = (
|
||||
fn: () => any,
|
||||
fallback?: () => Promisable<void>,
|
||||
timeout = 0 /* 10min */
|
||||
): BtnAction => {
|
||||
return async (name, btn, setText): Promise<void> => {
|
||||
const _onclick = btn.onclick;
|
||||
|
||||
// open a new tab
|
||||
const w = await windowOpenAsync(btn, '') as Window
|
||||
// add texts to the new tab
|
||||
const txt = document.createTextNode(i18n('PROCESSING')())
|
||||
w.document.body.append(txt)
|
||||
btn.onclick = null;
|
||||
setText(i18n("PROCESSING")());
|
||||
|
||||
// set page hooks that the new tab also closes as the og tab closes
|
||||
let score: WebMscore // eslint-disable-line prefer-const
|
||||
const destroy = (): void => {
|
||||
score && score.destroy()
|
||||
w.close()
|
||||
}
|
||||
window.addEventListener('unload', destroy)
|
||||
w.addEventListener('beforeunload', () => {
|
||||
score && score.destroy()
|
||||
window.removeEventListener('unload', destroy)
|
||||
// reset btn text
|
||||
setText(btnName)
|
||||
// reinstate btn event
|
||||
btn.onclick = _onclick
|
||||
})
|
||||
try {
|
||||
await useTimeout(fn(), timeout);
|
||||
setText(name);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (fallback) {
|
||||
// use fallback
|
||||
await fallback();
|
||||
setText(name);
|
||||
} else {
|
||||
BtnAction.errorPopup()(name, btn, setText);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// fetch mscz & process using mscore
|
||||
score = await loadMscore(scoreinfo, w)
|
||||
fn(w, score, txt)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
// close the new tab & show error popup
|
||||
w.close()
|
||||
BtnAction.errorPopup()(btnName, btn, setText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const errorPopup = (): BtnAction => {
|
||||
return (btnName: unknown, btn: unknown, setText) => {
|
||||
setText(i18n('BTN_ERROR')())
|
||||
// ask user to send Discord message
|
||||
alert(
|
||||
'❌Download Failed!\n\n' +
|
||||
'Send your URL to the #dataset-patcher 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'))
|
||||
}
|
||||
}
|
||||
|
||||
export const process = (fn: () => any, fallback?: () => Promisable<void>, timeout = 10 * 60 * 1000 /* 10min */): BtnAction => {
|
||||
return async (name, btn, setText): Promise<void> => {
|
||||
const _onclick = btn.onclick
|
||||
|
||||
btn.onclick = null
|
||||
setText(i18n('PROCESSING')())
|
||||
|
||||
try {
|
||||
await useTimeout(fn(), timeout)
|
||||
setText(name)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
if (fallback) {
|
||||
// use fallback
|
||||
await fallback()
|
||||
setText(name)
|
||||
} else {
|
||||
BtnAction.errorPopup()(name, btn, setText)
|
||||
}
|
||||
}
|
||||
|
||||
btn.onclick = _onclick
|
||||
}
|
||||
}
|
||||
|
||||
export const deprecate = (action: BtnAction): BtnAction => {
|
||||
return (name, btn, setText) => {
|
||||
alert(i18n('DEPRECATION_NOTICE')(name))
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return action(name, btn, setText)
|
||||
}
|
||||
}
|
||||
btn.onclick = _onclick;
|
||||
};
|
||||
};
|
||||
|
||||
export const deprecate = (action: BtnAction): BtnAction => {
|
||||
return (name, btn, setText) => {
|
||||
alert(i18n("DEPRECATION_NOTICE")(name));
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return action(name, btn, setText);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
491
src/cli.ts
491
src/cli.ts
@ -1,301 +1,272 @@
|
||||
|
||||
/* 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, fetchBuffer } from './utils'
|
||||
import { isNpx, getVerInfo, getSelfVer } from './npm-data'
|
||||
import { getFileUrl } from './file'
|
||||
import { exportPDF } from './pdf'
|
||||
import i18n from './i18n'
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { setMscz } from "./mscz";
|
||||
import { loadMscore, INDV_DOWNLOADS, WebMscore } from "./mscore";
|
||||
import { ScoreInfo, ScoreInfoHtml, ScoreInfoObj } from "./scoreinfo";
|
||||
import { escapeFilename, APP_URL, fetchBuffer } from "./utils";
|
||||
import { isNpx, getVerInfo, getSelfVer } from "./npm-data";
|
||||
import { getFileUrl } from "./file";
|
||||
import { exportPDF } from "./pdf";
|
||||
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 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'
|
||||
const SCORE_URL_PREFIX = "https://(s.)musescore.com/";
|
||||
const SCORE_URL_REG = /https:\/\/(s\.)?musescore\.com\//;
|
||||
const EXT = ".mscz";
|
||||
|
||||
type ExpDlType = 'midi' | 'mp3' | 'pdf'
|
||||
type ExpDlType = "midi" | "mp3" | "pdf";
|
||||
|
||||
interface Params {
|
||||
fileInit: string;
|
||||
confirmed: boolean;
|
||||
useExpDL: boolean;
|
||||
expDlTypes: ExpDlType[];
|
||||
part: number;
|
||||
types: number[];
|
||||
dest: string;
|
||||
fileInit: string;
|
||||
confirmed: boolean;
|
||||
useExpDL: boolean;
|
||||
expDlTypes: ExpDlType[];
|
||||
part: number;
|
||||
types: number[];
|
||||
dest: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for destination directory
|
||||
*/
|
||||
const promptDest = async () => {
|
||||
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(),
|
||||
})
|
||||
return dest
|
||||
}
|
||||
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(),
|
||||
});
|
||||
return dest;
|
||||
};
|
||||
|
||||
const createSpinner = () => {
|
||||
return ora({
|
||||
text: i18n('PROCESSING')(),
|
||||
color: 'blue',
|
||||
spinner: 'bounce',
|
||||
indent: 0,
|
||||
}).start()
|
||||
}
|
||||
return ora({
|
||||
text: i18n("PROCESSING")(),
|
||||
color: "blue",
|
||||
spinner: "bounce",
|
||||
indent: 0,
|
||||
}).start();
|
||||
};
|
||||
|
||||
const checkboxValidate = (input: number[]) => {
|
||||
return input.length >= 1
|
||||
}
|
||||
return input.length >= 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* MIDI/MP3/PDF express download using the file API (./file.ts)
|
||||
*/
|
||||
const expDL = async (scoreinfo: ScoreInfoHtml) => {
|
||||
// print a blank line
|
||||
console.log()
|
||||
// print a blank line
|
||||
console.log();
|
||||
|
||||
// filetype selection
|
||||
const { expDlTypes } = await inquirer.prompt<Params>({
|
||||
type: 'checkbox',
|
||||
name: 'expDlTypes',
|
||||
message: 'Filetype Selection',
|
||||
choices: [
|
||||
'midi', 'mp3', 'pdf', // ExpDlType
|
||||
new inquirer.Separator(),
|
||||
new inquirer.Separator(
|
||||
'Unavailable in express download\n' +
|
||||
' - MSCZ\n' +
|
||||
' - MusicXML\n' +
|
||||
' - FLAC/OGG Audio\n' +
|
||||
' - Individual Parts',
|
||||
),
|
||||
],
|
||||
validate: checkboxValidate,
|
||||
pageSize: Infinity,
|
||||
})
|
||||
// filetype selection
|
||||
const { expDlTypes } = await inquirer.prompt<Params>({
|
||||
type: "checkbox",
|
||||
name: "expDlTypes",
|
||||
message: "Filetype Selection",
|
||||
choices: [
|
||||
"midi",
|
||||
"mp3",
|
||||
"pdf", // ExpDlType
|
||||
new inquirer.Separator(),
|
||||
],
|
||||
validate: checkboxValidate,
|
||||
pageSize: Infinity,
|
||||
});
|
||||
|
||||
// destination directory selection
|
||||
const dest = await promptDest()
|
||||
const spinner = createSpinner()
|
||||
// destination directory selection
|
||||
const dest = await promptDest();
|
||||
const spinner = createSpinner();
|
||||
|
||||
await Promise.all(
|
||||
expDlTypes.map(async (type) => {
|
||||
// download/generate file data
|
||||
let fileData: Buffer
|
||||
switch (type) {
|
||||
case 'midi':
|
||||
case 'mp3': {
|
||||
const fileUrl = await getFileUrl(scoreinfo.id, type)
|
||||
fileData = await fetchBuffer(fileUrl)
|
||||
break
|
||||
}
|
||||
await Promise.all(
|
||||
expDlTypes.map(async (type) => {
|
||||
// download/generate file data
|
||||
let fileData: Buffer;
|
||||
switch (type) {
|
||||
case "midi":
|
||||
case "mp3": {
|
||||
const fileUrl = await getFileUrl(scoreinfo.id, type);
|
||||
fileData = await fetchBuffer(fileUrl);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'pdf': {
|
||||
fileData = Buffer.from(
|
||||
await exportPDF(scoreinfo, scoreinfo.sheet),
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
case "pdf": {
|
||||
fileData = Buffer.from(
|
||||
await exportPDF(scoreinfo, scoreinfo.sheet)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// save to filesystem
|
||||
const f = path.join(dest, `${scoreinfo.fileName}.${type}`)
|
||||
await fs.promises.writeFile(f, fileData)
|
||||
spinner.info(`Saved ${chalk.underline(f)}`)
|
||||
}),
|
||||
)
|
||||
// save to filesystem
|
||||
const f = path.join(dest, `${scoreinfo.fileName}.${type}`);
|
||||
await fs.promises.writeFile(f, fileData);
|
||||
spinner.info(`Saved ${chalk.underline(f)}`);
|
||||
})
|
||||
);
|
||||
|
||||
spinner.succeed('OK')
|
||||
}
|
||||
spinner.succeed("OK");
|
||||
};
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
|
||||
// print a blank line
|
||||
console.log()
|
||||
let scoreinfo: ScoreInfo;
|
||||
// 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,
|
||||
});
|
||||
|
||||
// ask for express download
|
||||
const { useExpDL } = await inquirer.prompt<Params>({
|
||||
type: 'confirm',
|
||||
name: 'useExpDL',
|
||||
prefix: `${chalk.blueBright('ℹ')} ` +
|
||||
'MIDI/MP3/PDF express download is now available.\n ',
|
||||
message: '🚀 Give it a try?',
|
||||
default: true,
|
||||
})
|
||||
if (useExpDL) return expDL(scoreinfo as ScoreInfoHtml)
|
||||
|
||||
// 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 = createSpinner()
|
||||
|
||||
let score: WebMscore
|
||||
let metadata: import('webmscore/schemas').ScoreMetadata
|
||||
try {
|
||||
const isLocalFile = fileInit.endsWith(EXT);
|
||||
if (!isLocalFile) {
|
||||
// fetch mscz file from the dataset, and cache it for side effect
|
||||
await fetchMscz(scoreinfo)
|
||||
// request scoreinfo
|
||||
scoreinfo = await ScoreInfoHtml.request(fileInit);
|
||||
|
||||
// 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;
|
||||
|
||||
// print a blank line
|
||||
console.log();
|
||||
|
||||
expDL(scoreinfo as ScoreInfoHtml);
|
||||
} else {
|
||||
// load local file
|
||||
const data = await fs.promises.readFile(fileInit)
|
||||
await setMscz(scoreinfo, data.buffer)
|
||||
scoreinfo = new ScoreInfoObj(0, path.basename(fileInit, EXT));
|
||||
}
|
||||
|
||||
spinner.info('MSCZ file loaded')
|
||||
if (!isLocalFile) {
|
||||
spinner.info(`File URL: ${scoreinfo.store.get(MSCZ_URL_SYM) as string}`)
|
||||
if (isLocalFile) {
|
||||
const spinner = createSpinner();
|
||||
|
||||
let score: WebMscore;
|
||||
let metadata: import("webmscore/schemas").ScoreMetadata;
|
||||
try {
|
||||
// load local file
|
||||
const data = await fs.promises.readFile(fileInit);
|
||||
await setMscz(scoreinfo, data.buffer);
|
||||
spinner.info("MSCZ file loaded");
|
||||
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("Try using the LibreScore app instead:" + APP_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: checkboxValidate,
|
||||
});
|
||||
const filetypes = types.map((i) => INDV_DOWNLOADS[i]);
|
||||
|
||||
// destination directory
|
||||
const dest = await promptDest();
|
||||
|
||||
// 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 (librescoreLink) {
|
||||
try {
|
||||
spinner.info(`${i18n('VIEW_IN_LIBRESCORE')()}: ${await librescoreLink}`)
|
||||
} catch { } // it doesn't affect the main feature
|
||||
|
||||
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.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
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-patcher 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: checkboxValidate,
|
||||
})
|
||||
const filetypes = types.map(i => INDV_DOWNLOADS[i])
|
||||
|
||||
// destination directory
|
||||
const dest = await promptDest()
|
||||
|
||||
// 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.`))
|
||||
}
|
||||
}
|
||||
})()
|
||||
})();
|
||||
|
@ -1,68 +1,68 @@
|
||||
import isNodeJs from "detect-node";
|
||||
import { hookNative } from "./anti-detection";
|
||||
import type { FileType } from "./file";
|
||||
|
||||
import isNodeJs from 'detect-node'
|
||||
import { hookNative } from './anti-detection'
|
||||
import type { FileType } from './file'
|
||||
|
||||
const TYPE_REG = /type=(img|mp3|midi)/
|
||||
const TYPE_REG = /type=(img|mp3|midi)/;
|
||||
|
||||
/**
|
||||
* I know this is super hacky.
|
||||
*/
|
||||
const magicHookConstr = (() => {
|
||||
const l = {}
|
||||
const l = {};
|
||||
|
||||
if (isNodeJs) { // noop in CLI
|
||||
return () => Promise.resolve('')
|
||||
}
|
||||
if (isNodeJs) {
|
||||
// noop in CLI
|
||||
return () => Promise.resolve("");
|
||||
}
|
||||
|
||||
try {
|
||||
const p = Object.getPrototypeOf(document.body)
|
||||
Object.setPrototypeOf(document.body, null)
|
||||
try {
|
||||
const p = Object.getPrototypeOf(document.body);
|
||||
Object.setPrototypeOf(document.body, null);
|
||||
|
||||
hookNative(document.body, 'append', () => {
|
||||
return function (...nodes: Node[]) {
|
||||
p.append.call(this, ...nodes)
|
||||
hookNative(document.body, "append", () => {
|
||||
return function (...nodes: Node[]) {
|
||||
p.append.call(this, ...nodes);
|
||||
|
||||
if (nodes[0].nodeName === 'IFRAME') {
|
||||
const iframe = nodes[0] as HTMLIFrameElement
|
||||
const w = iframe.contentWindow as Window
|
||||
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
|
||||
if (typeof url === 'string' && token) {
|
||||
const m = url.match(TYPE_REG)
|
||||
console.debug(url, token, m)
|
||||
if (m) {
|
||||
const type = m[1]
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
l[type]?.(token)
|
||||
hookNative(w, "fetch", () => {
|
||||
return function (url, init) {
|
||||
const token = init?.headers?.Authorization;
|
||||
if (typeof url === "string" && token) {
|
||||
const m = url.match(TYPE_REG);
|
||||
console.debug(url, token, m);
|
||||
if (m) {
|
||||
const type = m[1];
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
l[type]?.(token);
|
||||
}
|
||||
}
|
||||
return fetch(url, init);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
return fetch(url, init)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
Object.setPrototypeOf(document.body, p)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
Object.setPrototypeOf(document.body, p);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return async (type: FileType) => {
|
||||
return new Promise<string>((resolve) => {
|
||||
l[type] = (token) => {
|
||||
resolve(token)
|
||||
magics[type] = token
|
||||
}
|
||||
})
|
||||
}
|
||||
})()
|
||||
return async (type: FileType) => {
|
||||
return new Promise<string>((resolve) => {
|
||||
l[type] = (token) => {
|
||||
resolve(token);
|
||||
magics[type] = token;
|
||||
};
|
||||
});
|
||||
};
|
||||
})();
|
||||
|
||||
export const magics: Record<FileType, Promise<string>> = {
|
||||
img: magicHookConstr('img'),
|
||||
midi: magicHookConstr('midi'),
|
||||
mp3: magicHookConstr('mp3'),
|
||||
}
|
||||
img: magicHookConstr("img"),
|
||||
midi: magicHookConstr("midi"),
|
||||
mp3: magicHookConstr("mp3"),
|
||||
};
|
||||
|
145
src/file.ts
145
src/file.ts
@ -1,84 +1,99 @@
|
||||
/* eslint-disable no-extend-native */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
|
||||
import isNodeJs from 'detect-node'
|
||||
import { useTimeout, getFetch } from './utils'
|
||||
import { magics } from './file-magics'
|
||||
import isNodeJs from "detect-node";
|
||||
import { useTimeout, getFetch } from "./utils";
|
||||
import { magics } from "./file-magics";
|
||||
|
||||
export type FileType = 'img' | 'mp3' | 'midi'
|
||||
export type FileType = "img" | "mp3" | "midi";
|
||||
|
||||
const getApiUrl = (id: number, type: FileType, index: number): string => {
|
||||
return `/api/jmuse?id=${id}&type=${type}&index=${index}&v2=1`
|
||||
}
|
||||
return `/api/jmuse?id=${id}&type=${type}&index=${index}&v2=1`;
|
||||
};
|
||||
|
||||
/**
|
||||
* hard-coded auth tokens
|
||||
*/
|
||||
const useBuiltinAuth = (type: FileType): string => {
|
||||
switch (type) {
|
||||
case 'img': return '8c022bdef45341074ce876ae57a48f64b86cdcf5'
|
||||
case 'midi': return '38fb9efaae51b0c83b5bb5791a698b48292129e7'
|
||||
case 'mp3': return '63794e5461e4cfa046edfbdddfccc1ac16daffd2'
|
||||
}
|
||||
}
|
||||
switch (type) {
|
||||
case "img":
|
||||
return "8c022bdef45341074ce876ae57a48f64b86cdcf5";
|
||||
case "midi":
|
||||
return "38fb9efaae51b0c83b5bb5791a698b48292129e7";
|
||||
case "mp3":
|
||||
return "63794e5461e4cfa046edfbdddfccc1ac16daffd2";
|
||||
}
|
||||
};
|
||||
|
||||
const getApiAuth = async (type: FileType, index: number): Promise<string> => {
|
||||
// eslint-disable-next-line no-void
|
||||
void index // unused
|
||||
// eslint-disable-next-line no-void
|
||||
void index; // unused
|
||||
|
||||
if (isNodeJs) {
|
||||
// we cannot intercept API requests in Node.js (as no requests are sent), so go straightforward to the hard-coded tokens
|
||||
return useBuiltinAuth(type)
|
||||
}
|
||||
|
||||
const magic = magics[type]
|
||||
if (magic instanceof Promise) {
|
||||
// force to retrieve the MAGIC
|
||||
try {
|
||||
switch (type) {
|
||||
case 'midi': {
|
||||
const fsBtn = document.querySelector('button[title="Toggle Fullscreen"]') as HTMLButtonElement
|
||||
const el = fsBtn.parentElement?.parentElement?.querySelector('button') as HTMLButtonElement
|
||||
el.click()
|
||||
break
|
||||
}
|
||||
case 'mp3': {
|
||||
const el = document.querySelector('button[title="Toggle Play"]') as HTMLButtonElement
|
||||
el.click()
|
||||
break
|
||||
}
|
||||
case 'img': {
|
||||
const imgE = document.querySelector('img[src*=score_]')
|
||||
const nextE = imgE?.parentElement?.nextElementSibling
|
||||
if (nextE) nextE.scrollIntoView()
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return useBuiltinAuth(type)
|
||||
if (isNodeJs) {
|
||||
// we cannot intercept API requests in Node.js (as no requests are sent), so go straightforward to the hard-coded tokens
|
||||
return useBuiltinAuth(type);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await useTimeout(magic, 5 * 1000 /* 5s */)
|
||||
} catch {
|
||||
console.error(type, 'token timeout')
|
||||
// try hard-coded tokens
|
||||
return useBuiltinAuth(type)
|
||||
}
|
||||
}
|
||||
const magic = magics[type];
|
||||
if (magic instanceof Promise) {
|
||||
// force to retrieve the MAGIC
|
||||
try {
|
||||
switch (type) {
|
||||
case "midi": {
|
||||
const fsBtn = document.querySelector(
|
||||
'button[title="Toggle Fullscreen"]'
|
||||
) as HTMLButtonElement;
|
||||
const el =
|
||||
fsBtn.parentElement?.parentElement?.querySelector(
|
||||
"button"
|
||||
) as HTMLButtonElement;
|
||||
el.click();
|
||||
break;
|
||||
}
|
||||
case "mp3": {
|
||||
const el = document.querySelector(
|
||||
'button[title="Toggle Play"]'
|
||||
) as HTMLButtonElement;
|
||||
el.click();
|
||||
break;
|
||||
}
|
||||
case "img": {
|
||||
const imgE = document.querySelector("img[src*=score_]");
|
||||
const nextE = imgE?.parentElement?.nextElementSibling;
|
||||
if (nextE) nextE.scrollIntoView();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return useBuiltinAuth(type);
|
||||
}
|
||||
}
|
||||
|
||||
export const getFileUrl = async (id: number, type: FileType, index = 0, _fetch = getFetch()): Promise<string> => {
|
||||
const url = getApiUrl(id, type, index)
|
||||
const auth = await getApiAuth(type, index)
|
||||
try {
|
||||
return await useTimeout(magic, 5 * 1000 /* 5s */);
|
||||
} catch {
|
||||
console.error(type, "token timeout");
|
||||
// try hard-coded tokens
|
||||
return useBuiltinAuth(type);
|
||||
}
|
||||
};
|
||||
|
||||
const r = await _fetch(url, {
|
||||
headers: {
|
||||
Authorization: auth,
|
||||
},
|
||||
})
|
||||
export const getFileUrl = async (
|
||||
id: number,
|
||||
type: FileType,
|
||||
index = 0,
|
||||
_fetch = getFetch()
|
||||
): Promise<string> => {
|
||||
const url = getApiUrl(id, type, index);
|
||||
const auth = await getApiAuth(type, index);
|
||||
|
||||
const { info } = await r.json()
|
||||
return info.url as string
|
||||
}
|
||||
const r = await _fetch(url, {
|
||||
headers: {
|
||||
Authorization: auth,
|
||||
},
|
||||
});
|
||||
|
||||
const { info } = await r.json();
|
||||
return info.url as string;
|
||||
};
|
||||
|
35
src/gm.ts
35
src/gm.ts
@ -1,22 +1,29 @@
|
||||
|
||||
/**
|
||||
* UserScript APIs
|
||||
*/
|
||||
declare const GM: {
|
||||
/** https://www.tampermonkey.net/documentation.php#GM_info */
|
||||
info: Record<string, any>;
|
||||
/** 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://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
|
||||
/** 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
|
||||
type GM = typeof GM;
|
||||
|
||||
export const isGmAvailable = (requiredMethod: keyof GM = 'info'): boolean => {
|
||||
return typeof GM !== 'undefined' &&
|
||||
typeof GM[requiredMethod] !== 'undefined'
|
||||
}
|
||||
export const isGmAvailable = (requiredMethod: keyof GM = "info"): boolean => {
|
||||
return (
|
||||
typeof GM !== "undefined" && typeof GM[requiredMethod] !== "undefined"
|
||||
);
|
||||
};
|
||||
|
@ -1,37 +1,36 @@
|
||||
|
||||
import { createLocale } from './utils'
|
||||
import { createLocale } from "./utils";
|
||||
|
||||
export default createLocale({
|
||||
'PROCESSING' () {
|
||||
return 'Processing…' as const
|
||||
},
|
||||
'BTN_ERROR' () {
|
||||
return '❌Download Failed!' as const
|
||||
},
|
||||
PROCESSING() {
|
||||
return "Processing…" as const;
|
||||
},
|
||||
BTN_ERROR() {
|
||||
return "⚠️GET THE APP!" as const;
|
||||
},
|
||||
|
||||
'DEPRECATION_NOTICE' (btnName: string) {
|
||||
return `DEPRECATED!\nUse \`${btnName}\` inside \`Individual Parts\` instead.\n(This may still work. Click \`OK\` to continue.)` as const
|
||||
},
|
||||
DEPRECATION_NOTICE(btnName: string) {
|
||||
return `DEPRECATED!\nUse \`${btnName}\` inside \`Individual Parts\` instead.\n(This may still work. Click \`OK\` to continue.)` as const;
|
||||
},
|
||||
|
||||
'DOWNLOAD' <T extends string> (fileType: T) {
|
||||
return `Download ${fileType}` as const
|
||||
},
|
||||
'DOWNLOAD_AUDIO' <T extends string> (fileType: T) {
|
||||
return `Download ${fileType} Audio` as const
|
||||
},
|
||||
DOWNLOAD<T extends string>(fileType: T) {
|
||||
return `Download ${fileType}` as const;
|
||||
},
|
||||
DOWNLOAD_AUDIO<T extends string>(fileType: T) {
|
||||
return `Download ${fileType} Audio` as const;
|
||||
},
|
||||
|
||||
'IND_PARTS' () {
|
||||
return 'Individual Parts' as const
|
||||
},
|
||||
'IND_PARTS_TOOLTIP' () {
|
||||
return 'Download individual parts (BETA)' as const
|
||||
},
|
||||
IND_PARTS() {
|
||||
return "Individual Parts" as const;
|
||||
},
|
||||
IND_PARTS_TOOLTIP() {
|
||||
return "Download individual parts (BETA)" as const;
|
||||
},
|
||||
|
||||
'VIEW_IN_LIBRESCORE' () {
|
||||
return 'View in LibreScore' as const
|
||||
},
|
||||
VIEW_IN_LIBRESCORE() {
|
||||
return "View in LibreScore" as const;
|
||||
},
|
||||
|
||||
'FULL_SCORE' () {
|
||||
return 'Full score' as const
|
||||
},
|
||||
})
|
||||
FULL_SCORE() {
|
||||
return "Full score" as const;
|
||||
},
|
||||
});
|
||||
|
@ -1,37 +1,36 @@
|
||||
|
||||
import { createLocale } from './utils'
|
||||
import { createLocale } from "./utils";
|
||||
|
||||
export default createLocale({
|
||||
'PROCESSING' () {
|
||||
return 'Cargando…' as const
|
||||
},
|
||||
'BTN_ERROR' () {
|
||||
return '❌¡Descarga Fallida!' as const
|
||||
},
|
||||
PROCESSING() {
|
||||
return "Cargando…" as const;
|
||||
},
|
||||
BTN_ERROR() {
|
||||
return "⚠️¡OBTENER LA APLICACIÓN!" as const;
|
||||
},
|
||||
|
||||
'DEPRECATION_NOTICE' (btnName: string) {
|
||||
return `¡OBSOLETO!\nUtilizar \`${btnName}\` dentro de \`Partes Indivduales\` en su lugar.\n(Esto todavía puede funcionar. Pulsa \`Aceptar\` para continuar.)` as const
|
||||
},
|
||||
DEPRECATION_NOTICE(btnName: string) {
|
||||
return `¡OBSOLETO!\nUtilizar \`${btnName}\` dentro de \`Partes Indivduales\` en su lugar.\n(Esto todavía puede funcionar. Pulsa \`Aceptar\` para continuar.)` as const;
|
||||
},
|
||||
|
||||
'DOWNLOAD' <T extends string> (fileType: T) {
|
||||
return `Descargar ${fileType}` as const
|
||||
},
|
||||
'DOWNLOAD_AUDIO' <T extends string> (fileType: T) {
|
||||
return `Descargar Audio ${fileType}` as const
|
||||
},
|
||||
DOWNLOAD<T extends string>(fileType: T) {
|
||||
return `Descargar ${fileType}` as const;
|
||||
},
|
||||
DOWNLOAD_AUDIO<T extends string>(fileType: T) {
|
||||
return `Descargar Audio ${fileType}` as const;
|
||||
},
|
||||
|
||||
'IND_PARTS' () {
|
||||
return 'Partes individuales' as const
|
||||
},
|
||||
'IND_PARTS_TOOLTIP' () {
|
||||
return 'Descargar partes individuales (BETA)' as const
|
||||
},
|
||||
IND_PARTS() {
|
||||
return "Partes individuales" as const;
|
||||
},
|
||||
IND_PARTS_TOOLTIP() {
|
||||
return "Descargar partes individuales (BETA)" as const;
|
||||
},
|
||||
|
||||
'VIEW_IN_LIBRESCORE' () {
|
||||
return 'Visualizar en LibreScore' as const
|
||||
},
|
||||
VIEW_IN_LIBRESCORE() {
|
||||
return "Visualizar en LibreScore" as const;
|
||||
},
|
||||
|
||||
'FULL_SCORE' () {
|
||||
return 'Partitura Completa' as const
|
||||
},
|
||||
})
|
||||
FULL_SCORE() {
|
||||
return "Partitura Completa" as const;
|
||||
},
|
||||
});
|
||||
|
@ -1,60 +1,64 @@
|
||||
import isNodeJs from "detect-node";
|
||||
|
||||
import isNodeJs from 'detect-node'
|
||||
|
||||
import en from './en'
|
||||
import es from './es'
|
||||
import it from './it'
|
||||
import zh from './zh'
|
||||
import en from "./en";
|
||||
import es from "./es";
|
||||
import it from "./it";
|
||||
import zh from "./zh";
|
||||
|
||||
export interface LOCALE {
|
||||
'PROCESSING' (): string;
|
||||
'BTN_ERROR' (): string;
|
||||
"PROCESSING"(): string;
|
||||
"BTN_ERROR"(): string;
|
||||
|
||||
'DEPRECATION_NOTICE' (btnName: string): string;
|
||||
"DEPRECATION_NOTICE"(btnName: string): string;
|
||||
|
||||
'DOWNLOAD' (fileType: string): string;
|
||||
'DOWNLOAD_AUDIO' (fileType: string): string;
|
||||
"DOWNLOAD"(fileType: string): string;
|
||||
"DOWNLOAD_AUDIO"(fileType: string): string;
|
||||
|
||||
'IND_PARTS' (): string;
|
||||
'IND_PARTS_TOOLTIP' (): string;
|
||||
"IND_PARTS"(): string;
|
||||
"IND_PARTS_TOOLTIP"(): string;
|
||||
|
||||
'VIEW_IN_LIBRESCORE' (): string;
|
||||
"VIEW_IN_LIBRESCORE"(): string;
|
||||
|
||||
'FULL_SCORE' (): string;
|
||||
"FULL_SCORE"(): string;
|
||||
}
|
||||
|
||||
const locales = (<L extends { [n: string]: LOCALE } /** type checking */> (l: L) => Object.freeze(l))({
|
||||
en,
|
||||
es,
|
||||
it,
|
||||
zh,
|
||||
})
|
||||
const locales = (<L extends { [n: string]: LOCALE } /** type checking */>(
|
||||
l: L
|
||||
) => Object.freeze(l))({
|
||||
en,
|
||||
es,
|
||||
it,
|
||||
zh,
|
||||
});
|
||||
|
||||
// detect browser language
|
||||
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)]
|
||||
}
|
||||
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 _lang = userLangs.find(l => {
|
||||
// find the first occurrence of valid languages
|
||||
return names.includes(l)
|
||||
})
|
||||
return _lang || 'en'
|
||||
})()
|
||||
const names = Object.keys(locales);
|
||||
const _lang = userLangs.find((l) => {
|
||||
// find the first occurrence of valid languages
|
||||
return names.includes(l);
|
||||
});
|
||||
return _lang || "en";
|
||||
})();
|
||||
|
||||
export type STR_KEYS = keyof LOCALE
|
||||
export type ALL_LOCALES = typeof locales
|
||||
export type LANGS = keyof ALL_LOCALES
|
||||
export type STR_KEYS = keyof LOCALE;
|
||||
export type ALL_LOCALES = typeof locales;
|
||||
export type LANGS = keyof ALL_LOCALES;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export default function i18n<K extends STR_KEYS, L extends LANGS = 'en'> (key: K) {
|
||||
const locale = locales[lang] as ALL_LOCALES[L]
|
||||
return locale[key]
|
||||
export default function i18n<K extends STR_KEYS, L extends LANGS = "en">(
|
||||
key: K
|
||||
) {
|
||||
const locale = locales[lang] as ALL_LOCALES[L];
|
||||
return locale[key];
|
||||
}
|
||||
|
@ -1,37 +1,36 @@
|
||||
|
||||
import { createLocale } from './utils'
|
||||
import { createLocale } from "./utils";
|
||||
|
||||
export default createLocale({
|
||||
'PROCESSING' () {
|
||||
return 'Caricamento…' as const
|
||||
},
|
||||
'BTN_ERROR' () {
|
||||
return '❌Download Fallito!' as const
|
||||
},
|
||||
PROCESSING() {
|
||||
return "Caricamento…" as const;
|
||||
},
|
||||
BTN_ERROR() {
|
||||
return "⚠️SCARICA L'APP!" 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
|
||||
},
|
||||
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
|
||||
},
|
||||
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
|
||||
},
|
||||
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
|
||||
},
|
||||
VIEW_IN_LIBRESCORE() {
|
||||
return "Visualizzare in LibreScore" as const;
|
||||
},
|
||||
|
||||
'FULL_SCORE' () {
|
||||
return 'Spartito Completo' as const
|
||||
},
|
||||
})
|
||||
FULL_SCORE() {
|
||||
return "Spartito Completo" as const;
|
||||
},
|
||||
});
|
||||
|
@ -1,9 +1,8 @@
|
||||
|
||||
import type { LOCALE } from './'
|
||||
import type { LOCALE } from "./";
|
||||
|
||||
/**
|
||||
* type checking only so no missing keys
|
||||
*/
|
||||
export function createLocale<OBJ extends LOCALE> (obj: OBJ): OBJ {
|
||||
return Object.freeze(obj)
|
||||
export function createLocale<OBJ extends LOCALE>(obj: OBJ): OBJ {
|
||||
return Object.freeze(obj);
|
||||
}
|
||||
|
@ -1,37 +1,36 @@
|
||||
|
||||
import { createLocale } from './utils'
|
||||
import { createLocale } from "./utils";
|
||||
|
||||
export default createLocale({
|
||||
'PROCESSING' () {
|
||||
return '处理中…' as const
|
||||
},
|
||||
'BTN_ERROR' () {
|
||||
return '❌下载失败!' as const
|
||||
},
|
||||
PROCESSING() {
|
||||
return "处理中…" as const;
|
||||
},
|
||||
BTN_ERROR() {
|
||||
return "⚠️下载这个软件!" as const;
|
||||
},
|
||||
|
||||
'DEPRECATION_NOTICE' (btnName: string) {
|
||||
return `不建议使用\n请使用 \`单独分谱\` 里的 \`${btnName}\` 按钮代替\n(这也许仍会起作用。单击\`确定\`以继续。)` 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
|
||||
},
|
||||
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
|
||||
},
|
||||
IND_PARTS() {
|
||||
return "单独分谱" as const;
|
||||
},
|
||||
IND_PARTS_TOOLTIP() {
|
||||
return "下载单独分谱 (BETA)" as const;
|
||||
},
|
||||
|
||||
'VIEW_IN_LIBRESCORE' () {
|
||||
return '在 LibreScore 中查看' as const
|
||||
},
|
||||
VIEW_IN_LIBRESCORE() {
|
||||
return "在 LibreScore 中查看" as const;
|
||||
},
|
||||
|
||||
'FULL_SCORE' () {
|
||||
return '完整乐谱' as const
|
||||
},
|
||||
})
|
||||
FULL_SCORE() {
|
||||
return "完整乐谱" as const;
|
||||
},
|
||||
});
|
||||
|
@ -1,26 +0,0 @@
|
||||
import { assertRes, getFetch } from './utils'
|
||||
import { getMainCid } from './mscz'
|
||||
import { ScoreInfo } from './scoreinfo'
|
||||
|
||||
const _getLink = (indexingInfo: string) => {
|
||||
const { scorepack } = JSON.parse(indexingInfo)
|
||||
return `https://librescore.org/score/${scorepack as string}`
|
||||
}
|
||||
|
||||
export const getLibreScoreLink = async (scoreinfo: ScoreInfo, _fetch = getFetch()): Promise<string> => {
|
||||
const mainCid = await getMainCid(scoreinfo, _fetch)
|
||||
const ref = scoreinfo.getScorepackRef(mainCid)
|
||||
const url = `https://ipfs.infura.io:5001/api/v0/dag/get?arg=${ref}`
|
||||
|
||||
const r0 = await _fetch(url)
|
||||
if (r0.status !== 500) {
|
||||
assertRes(r0)
|
||||
}
|
||||
const res: { Message: string } | string = await r0.json()
|
||||
if (typeof res !== 'string') {
|
||||
// read further error msg
|
||||
throw new Error(res.Message)
|
||||
}
|
||||
|
||||
return _getLink(res)
|
||||
}
|
192
src/main.ts
192
src/main.ts
@ -1,148 +1,72 @@
|
||||
import './meta'
|
||||
import "./meta";
|
||||
|
||||
import FileSaver from 'file-saver'
|
||||
import { waitForSheetLoaded, console } from './utils'
|
||||
import { downloadPDF } from './pdf'
|
||||
import { downloadMscz } from './mscz'
|
||||
import { getFileUrl } from './file'
|
||||
import { INDV_DOWNLOADS } from './mscore'
|
||||
import { getLibreScoreLink } from './librescore-link'
|
||||
import { BtnList, BtnAction, BtnListMode, ICON } from './btn'
|
||||
import { ScoreInfoInPage, SheetInfoInPage, getActualId } from './scoreinfo'
|
||||
import i18n from './i18n'
|
||||
import FileSaver from "file-saver";
|
||||
import { waitForSheetLoaded } from "./utils";
|
||||
import { downloadPDF } from "./pdf";
|
||||
import { getFileUrl } from "./file";
|
||||
import { BtnList, BtnAction, BtnListMode } from "./btn";
|
||||
import { ScoreInfoInPage, SheetInfoInPage } from "./scoreinfo";
|
||||
import i18n from "./i18n";
|
||||
|
||||
const { saveAs } = FileSaver
|
||||
const { saveAs } = FileSaver;
|
||||
|
||||
const main = (): void => {
|
||||
const btnList = new BtnList()
|
||||
const scoreinfo = new ScoreInfoInPage(document)
|
||||
const { fileName } = scoreinfo
|
||||
const btnList = new BtnList();
|
||||
const scoreinfo = new ScoreInfoInPage(document);
|
||||
const { fileName } = scoreinfo;
|
||||
|
||||
// eslint-disable-next-line no-void
|
||||
void getActualId(scoreinfo)
|
||||
let indvPartBtn: HTMLButtonElement | null = null;
|
||||
const fallback = () => {
|
||||
// btns fallback to load from MSCZ file (`Individual Parts`)
|
||||
return indvPartBtn?.click();
|
||||
};
|
||||
|
||||
let indvPartBtn: HTMLButtonElement | null = null
|
||||
const fallback = () => {
|
||||
// btns fallback to load from MSCZ file (`Individual Parts`)
|
||||
return indvPartBtn?.click()
|
||||
}
|
||||
btnList.add({
|
||||
name: i18n("DOWNLOAD")("MSCZ"),
|
||||
action: BtnAction.errorPopup(),
|
||||
});
|
||||
|
||||
btnList.add({
|
||||
name: i18n('DOWNLOAD')('MSCZ'),
|
||||
action: BtnAction.process(() => downloadMscz(scoreinfo, saveAs)),
|
||||
})
|
||||
btnList.add({
|
||||
name: i18n("DOWNLOAD")("PDF"),
|
||||
action: BtnAction.process(
|
||||
() => downloadPDF(scoreinfo, new SheetInfoInPage(document), saveAs),
|
||||
fallback,
|
||||
3 * 60 * 1000 /* 3min */
|
||||
),
|
||||
});
|
||||
|
||||
btnList.add({
|
||||
name: i18n('DOWNLOAD')('PDF'),
|
||||
action: BtnAction.process(() => downloadPDF(scoreinfo, new SheetInfoInPage(document), saveAs), fallback, 3 * 60 * 1000 /* 3min */),
|
||||
})
|
||||
btnList.add({
|
||||
name: i18n("DOWNLOAD")("MXL"),
|
||||
action: BtnAction.errorPopup(),
|
||||
});
|
||||
|
||||
btnList.add({
|
||||
name: i18n('DOWNLOAD')('MXL'),
|
||||
action: BtnAction.mscoreWindow(scoreinfo, async (w, score) => {
|
||||
const mxl = await score.saveMxl()
|
||||
const data = new Blob([mxl])
|
||||
saveAs(data, `${fileName}.mxl`)
|
||||
w.close()
|
||||
}),
|
||||
})
|
||||
btnList.add({
|
||||
name: i18n("DOWNLOAD")("MIDI"),
|
||||
action: BtnAction.download(
|
||||
() => getFileUrl(scoreinfo.id, "midi"),
|
||||
fallback,
|
||||
30 * 1000 /* 30s */
|
||||
),
|
||||
});
|
||||
|
||||
btnList.add({
|
||||
name: i18n('DOWNLOAD')('MIDI'),
|
||||
action: BtnAction.download(() => getFileUrl(scoreinfo.id, 'midi'), fallback, 30 * 1000 /* 30s */),
|
||||
})
|
||||
btnList.add({
|
||||
name: i18n("DOWNLOAD")("MP3"),
|
||||
action: BtnAction.download(
|
||||
() => getFileUrl(scoreinfo.id, "mp3"),
|
||||
fallback,
|
||||
30 * 1000 /* 30s */
|
||||
),
|
||||
});
|
||||
|
||||
btnList.add({
|
||||
name: i18n('DOWNLOAD')('MP3'),
|
||||
action: BtnAction.download(() => getFileUrl(scoreinfo.id, 'mp3'), fallback, 30 * 1000 /* 30s */),
|
||||
})
|
||||
indvPartBtn = btnList.add({
|
||||
name: i18n("IND_PARTS")(),
|
||||
tooltip: i18n("IND_PARTS_TOOLTIP")(),
|
||||
action: BtnAction.errorPopup(),
|
||||
});
|
||||
|
||||
indvPartBtn = btnList.add({
|
||||
name: i18n('IND_PARTS')(),
|
||||
tooltip: i18n('IND_PARTS_TOOLTIP')(),
|
||||
action: BtnAction.mscoreWindow(scoreinfo, async (w, score, txt) => {
|
||||
const metadata = await score.metadata()
|
||||
console.log('score metadata loaded by webmscore', metadata)
|
||||
|
||||
// add the "full score" option as a "part"
|
||||
metadata.excerpts.unshift({ id: -1, title: i18n('FULL_SCORE')(), parts: [] })
|
||||
|
||||
// render the part selection page
|
||||
txt.remove()
|
||||
const fieldset = w.document.createElement('fieldset')
|
||||
w.document.body.append(fieldset)
|
||||
|
||||
const downloads = INDV_DOWNLOADS
|
||||
|
||||
// part selection
|
||||
const DEFAULT_PART = -1 // initially select "full score"
|
||||
for (const excerpt of metadata.excerpts) {
|
||||
const id = excerpt.id
|
||||
const partName = excerpt.title
|
||||
|
||||
const e = w.document.createElement('input')
|
||||
e.name = 'score-part'
|
||||
e.type = 'radio'
|
||||
e.alt = partName
|
||||
e.checked = id === DEFAULT_PART
|
||||
e.onclick = () => {
|
||||
return score.setExcerptId(id) // set selected part
|
||||
}
|
||||
|
||||
const label = w.document.createElement('label')
|
||||
label.innerText = partName
|
||||
|
||||
const br = w.document.createElement('br')
|
||||
fieldset.append(e, label, br)
|
||||
}
|
||||
|
||||
await score.setExcerptId(DEFAULT_PART)
|
||||
|
||||
// submit buttons
|
||||
for (const d of downloads) {
|
||||
const submitBtn = w.document.createElement('input')
|
||||
submitBtn.type = 'submit'
|
||||
submitBtn.style.margin = '0.5em'
|
||||
fieldset.append(submitBtn)
|
||||
|
||||
const initBtn = () => {
|
||||
submitBtn.onclick = onSubmit
|
||||
submitBtn.disabled = false
|
||||
submitBtn.value = d.name
|
||||
}
|
||||
|
||||
const onSubmit = async (): Promise<void> => {
|
||||
// lock the button when processing
|
||||
submitBtn.onclick = null
|
||||
submitBtn.disabled = true
|
||||
submitBtn.value = i18n('PROCESSING')()
|
||||
|
||||
const checked = fieldset.querySelector('input:checked') as HTMLInputElement
|
||||
const partName = checked.alt
|
||||
|
||||
const data = new Blob([await d.action(score)])
|
||||
saveAs(data, `${fileName} - ${partName}.${d.fileExt}`)
|
||||
|
||||
// unlock button
|
||||
initBtn()
|
||||
}
|
||||
|
||||
initBtn()
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
btnList.commit(BtnListMode.InPage);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
waitForSheetLoaded().then(main)
|
||||
waitForSheetLoaded().then(main);
|
||||
|
12
src/meta.js
12
src/meta.js
@ -1,18 +1,18 @@
|
||||
// ==UserScript==
|
||||
// @name musescore-downloader
|
||||
// @namespace https://www.xmader.com/
|
||||
// @homepageURL https://github.com/Xmader/musescore-downloader/
|
||||
// @supportURL https://github.com/Xmader/musescore-downloader/issues
|
||||
// @namespace https://msdl.librescore.org/
|
||||
// @homepageURL https://github.com/LibreScore/musescore-downloader/
|
||||
// @supportURL https://github.com/LibreScore/musescore-downloader/issues
|
||||
// @updateURL https://msdl.librescore.org/install.user.js
|
||||
// @downloadURL https://msdl.librescore.org/install.user.js
|
||||
// @version %VERSION%
|
||||
// @description download sheet music from musescore.com for free, no login or Musescore Pro required | 免登录、免 Musescore Pro,免费下载 musescore.com 上的曲谱
|
||||
// @author Xmader
|
||||
// @description Download sheet music from Musescore
|
||||
// @author LibreScore
|
||||
// @icon https://librescore.org/img/icons/logo.svg
|
||||
// @match https://musescore.com/*/*
|
||||
// @match https://s.musescore.com/*/*
|
||||
// @license MIT
|
||||
// @copyright Copyright (c) 2019-2021 Xmader
|
||||
// @copyright Copyright (c) 2021 LibreScore
|
||||
// @grant unsafeWindow
|
||||
// @grant GM.registerMenuCommand
|
||||
// @grant GM.addElement
|
||||
|
238
src/mscore.ts
238
src/mscore.ts
@ -1,137 +1,149 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { fetchMscz } from './mscz'
|
||||
import { fetchData } from './utils'
|
||||
import { ScoreInfo } from './scoreinfo'
|
||||
import isNodeJs from 'detect-node'
|
||||
import i18n from './i18n'
|
||||
import { dependencies as depVers } from '../package.json'
|
||||
import { fetchMscz } from "./mscz";
|
||||
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@${depVers.webmscore}/webmscore.js`
|
||||
const WEBMSCORE_URL = `https://cdn.jsdelivr.net/npm/webmscore@${depVers.webmscore}/webmscore.js`;
|
||||
|
||||
// fonts for Chinese characters (CN) and Korean hangul (KR)
|
||||
// JP characters are included in the CN font
|
||||
const FONT_URLS = ['CN', 'KR'].map(l => `https://cdn.jsdelivr.net/npm/@librescore/fonts@${depVers['@librescore/fonts']}/SourceHanSans${l}.min.woff2`)
|
||||
const FONT_URLS = ["CN", "KR"].map(
|
||||
(l) =>
|
||||
`https://cdn.jsdelivr.net/npm/@librescore/fonts@${depVers["@librescore/fonts"]}/SourceHanSans${l}.min.woff2`
|
||||
);
|
||||
|
||||
const SF3_URL = `https://cdn.jsdelivr.net/npm/@librescore/sf3@${depVers['@librescore/sf3']}/FluidR3Mono_GM.sf3`
|
||||
const SOUND_FONT_LOADED = Symbol('SoundFont loaded')
|
||||
const SF3_URL = `https://cdn.jsdelivr.net/npm/@librescore/sf3@${depVers["@librescore/sf3"]}/FluidR3Mono_GM.sf3`;
|
||||
const SOUND_FONT_LOADED = Symbol("SoundFont loaded");
|
||||
|
||||
export type WebMscore = import('webmscore').default
|
||||
export type WebMscoreConstr = typeof import('webmscore').default
|
||||
export type WebMscore = import("webmscore").default;
|
||||
export type WebMscoreConstr = typeof import("webmscore").default;
|
||||
|
||||
const initMscore = async (w: Window): Promise<WebMscoreConstr> => {
|
||||
if (!isNodeJs) { // attached to a page
|
||||
if (!w['WebMscore']) {
|
||||
// init webmscore (https://github.com/LibreScore/webmscore)
|
||||
const script = w.document.createElement('script')
|
||||
script.src = WEBMSCORE_URL
|
||||
w.document.body.append(script)
|
||||
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
|
||||
const initFonts = () => {
|
||||
// load CJK fonts
|
||||
// CJK (East Asian) characters will be rendered as "tofu" if there is no font
|
||||
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>),
|
||||
)
|
||||
if (!isNodeJs) {
|
||||
// attached to a page
|
||||
if (!w["WebMscore"]) {
|
||||
// init webmscore (https://github.com/LibreScore/webmscore)
|
||||
const script = w.document.createElement("script");
|
||||
script.src = WEBMSCORE_URL;
|
||||
w.document.body.append(script);
|
||||
await new Promise((resolve) => {
|
||||
script.onload = resolve;
|
||||
});
|
||||
}
|
||||
return w["WebMscore"] as WebMscoreConstr;
|
||||
} else {
|
||||
fonts = Promise.all(
|
||||
FONT_URLS.map(url => fetchData(url)),
|
||||
)
|
||||
// nodejs
|
||||
return require("webmscore").default as WebMscoreConstr;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let fonts: Promise<Uint8Array[]> | undefined;
|
||||
const initFonts = () => {
|
||||
// load CJK fonts
|
||||
// CJK (East Asian) characters will be rendered as "tofu" if there is no font
|
||||
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(FONT_URLS.map((url) => fetchData(url)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const loadSoundFont = (score: WebMscore): Promise<void> => {
|
||||
if (!score[SOUND_FONT_LOADED]) {
|
||||
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)
|
||||
}
|
||||
if (!score[SOUND_FONT_LOADED]) {
|
||||
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(
|
||||
data,
|
||||
)
|
||||
})()
|
||||
score[SOUND_FONT_LOADED] = loadPromise
|
||||
}
|
||||
return score[SOUND_FONT_LOADED] as Promise<void>
|
||||
}
|
||||
await score.setSoundFont(data);
|
||||
})();
|
||||
score[SOUND_FONT_LOADED] = loadPromise;
|
||||
}
|
||||
return score[SOUND_FONT_LOADED] as Promise<void>;
|
||||
};
|
||||
|
||||
export const loadMscore = async (scoreinfo: ScoreInfo, w?: Window): Promise<WebMscore> => {
|
||||
initFonts()
|
||||
const WebMscore = await initMscore(w!)
|
||||
export const loadMscore = async (
|
||||
scoreinfo: ScoreInfo,
|
||||
w?: Window
|
||||
): Promise<WebMscore> => {
|
||||
initFonts();
|
||||
const WebMscore = await initMscore(w!);
|
||||
|
||||
// parse mscz data
|
||||
const data = new Uint8Array(
|
||||
new Uint8Array(await fetchMscz(scoreinfo)), // copy its ArrayBuffer
|
||||
)
|
||||
const score = await WebMscore.load('mscz', data, await fonts)
|
||||
await score.generateExcerpts()
|
||||
// parse mscz data
|
||||
const data = new Uint8Array(
|
||||
new Uint8Array(await fetchMscz(scoreinfo)) // copy its ArrayBuffer
|
||||
);
|
||||
const score = await WebMscore.load("mscz", data, await fonts);
|
||||
await score.generateExcerpts();
|
||||
|
||||
return score
|
||||
}
|
||||
return score;
|
||||
};
|
||||
|
||||
export interface IndividualDownload {
|
||||
name: string;
|
||||
fileExt: string;
|
||||
action (score: WebMscore): Promise<Uint8Array>;
|
||||
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')),
|
||||
},
|
||||
]
|
||||
{
|
||||
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")),
|
||||
},
|
||||
];
|
||||
|
119
src/mscz.ts
119
src/mscz.ts
@ -1,88 +1,49 @@
|
||||
import { assertRes, getFetch } from "./utils";
|
||||
import { ScoreInfo } from "./scoreinfo";
|
||||
|
||||
import { assertRes, getFetch } from './utils'
|
||||
import { ScoreInfo } from './scoreinfo'
|
||||
export const MSCZ_BUF_SYM = Symbol("msczBufferP");
|
||||
export const MSCZ_URL_SYM = Symbol("msczUrl");
|
||||
|
||||
export const MSCZ_BUF_SYM = Symbol('msczBufferP')
|
||||
export const MSCZ_URL_SYM = Symbol('msczUrl')
|
||||
export const MAIN_CID_SYM = Symbol('mainCid')
|
||||
|
||||
const IPNS_KEY = 'QmSdXtvzC8v8iTTZuj5cVmiugnzbR1QATYRcGix4bBsioP'
|
||||
const IPNS_RS_URL = `https://ipfs.io/api/v0/dag/resolve?arg=/ipns/${IPNS_KEY}`
|
||||
|
||||
export const getMainCid = async (scoreinfo: ScoreInfo, _fetch = getFetch()): Promise<string> => {
|
||||
// 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)
|
||||
}
|
||||
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)
|
||||
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;
|
||||
}
|
||||
}
|
||||
result = `https://ipfs.infura.io/ipfs/${cid}`
|
||||
|
||||
scoreinfo.store.set(MSCZ_URL_SYM, result) // persist to scoreinfo
|
||||
return result
|
||||
}
|
||||
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
|
||||
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)
|
||||
const data = await r.arrayBuffer()
|
||||
return data
|
||||
})()
|
||||
scoreinfo.store.set(MSCZ_BUF_SYM, msczBufferP)
|
||||
}
|
||||
if (!msczBufferP) {
|
||||
msczBufferP = (async (): Promise<ArrayBuffer> => {
|
||||
const url = await loadMsczUrl(scoreinfo, _fetch);
|
||||
const r = await _fetch(url);
|
||||
assertRes(r);
|
||||
const data = await r.arrayBuffer();
|
||||
return data;
|
||||
})();
|
||||
scoreinfo.store.set(MSCZ_BUF_SYM, msczBufferP);
|
||||
}
|
||||
|
||||
return msczBufferP
|
||||
}
|
||||
return msczBufferP;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
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
|
||||
saveAs(data, `${filename}.mscz`)
|
||||
}
|
||||
export const setMscz = async (
|
||||
scoreinfo: ScoreInfo,
|
||||
buffer: ArrayBuffer
|
||||
): Promise<void> => {
|
||||
scoreinfo.store.set(MSCZ_BUF_SYM, Promise.resolve(buffer));
|
||||
};
|
||||
|
@ -1,2 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
require("musescore-downloader/dist/cli.js")
|
||||
require("musescore-downloader/dist/cli.js");
|
||||
|
@ -1,19 +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%"
|
||||
}
|
||||
}
|
||||
"name": "msdl",
|
||||
"version": "%VERSION%",
|
||||
"author": "LibreScore",
|
||||
"bin": "cli.js",
|
||||
"description": "Alias for musescore-downloader",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/LibreScore/musescore-downloader.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/LibreScore/musescore-downloader/issues"
|
||||
},
|
||||
"homepage": "https://github.com/LibreScore/musescore-downloader#readme",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"musescore-downloader": "%VERSION%"
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +1,34 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { name as pkgName, version as pkgVer } from '../package.json'
|
||||
import { getFetch } from './utils'
|
||||
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'
|
||||
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 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 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 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,
|
||||
}
|
||||
export async function getVerInfo() {
|
||||
const installed = getSelfVer();
|
||||
const latest = await getLatestVer();
|
||||
return {
|
||||
installed,
|
||||
latest,
|
||||
isLatest: installed === latest,
|
||||
};
|
||||
}
|
||||
|
119
src/pdf.ts
119
src/pdf.ts
@ -1,64 +1,81 @@
|
||||
import isNodeJs from "detect-node";
|
||||
import { PDFWorker } from "../dist/cache/worker";
|
||||
import { PDFWorkerHelper } from "./worker-helper";
|
||||
import { getFileUrl } from "./file";
|
||||
import { ScoreInfo, SheetInfo, Dimensions } from "./scoreinfo";
|
||||
import { fetchBuffer } from "./utils";
|
||||
|
||||
import isNodeJs from 'detect-node'
|
||||
import { PDFWorker } from '../dist/cache/worker'
|
||||
import { PDFWorkerHelper } from './worker-helper'
|
||||
import { getFileUrl } from './file'
|
||||
import { ScoreInfo, SheetInfo, Dimensions } from './scoreinfo'
|
||||
import { fetchBuffer } from './utils'
|
||||
|
||||
type _ExFn = (imgURLs: string[], imgType: 'svg' | 'png', dimensions: Dimensions) => Promise<ArrayBuffer>
|
||||
type _ExFn = (
|
||||
imgURLs: string[],
|
||||
imgType: "svg" | "png",
|
||||
dimensions: Dimensions
|
||||
) => Promise<ArrayBuffer>;
|
||||
|
||||
const _exportPDFBrowser: _ExFn = async (imgURLs, imgType, dimensions) => {
|
||||
const worker = new PDFWorkerHelper()
|
||||
const pdfArrayBuffer = await worker.generatePDF(imgURLs, imgType, dimensions.width, dimensions.height)
|
||||
worker.terminate()
|
||||
return pdfArrayBuffer
|
||||
}
|
||||
const worker = new PDFWorkerHelper();
|
||||
const pdfArrayBuffer = await worker.generatePDF(
|
||||
imgURLs,
|
||||
imgType,
|
||||
dimensions.width,
|
||||
dimensions.height
|
||||
);
|
||||
worker.terminate();
|
||||
return pdfArrayBuffer;
|
||||
};
|
||||
|
||||
const _exportPDFNode: _ExFn = async (imgURLs, imgType, dimensions) => {
|
||||
const imgBufs = await Promise.all(imgURLs.map(url => fetchBuffer(url)))
|
||||
const imgBufs = await Promise.all(imgURLs.map((url) => fetchBuffer(url)));
|
||||
|
||||
const { generatePDF } = PDFWorker()
|
||||
const pdfArrayBuffer = await generatePDF(
|
||||
imgBufs,
|
||||
imgType,
|
||||
dimensions.width,
|
||||
dimensions.height,
|
||||
) as ArrayBuffer
|
||||
const { generatePDF } = PDFWorker();
|
||||
const pdfArrayBuffer = (await generatePDF(
|
||||
imgBufs,
|
||||
imgType,
|
||||
dimensions.width,
|
||||
dimensions.height
|
||||
)) as ArrayBuffer;
|
||||
|
||||
return pdfArrayBuffer
|
||||
}
|
||||
return pdfArrayBuffer;
|
||||
};
|
||||
|
||||
export const exportPDF = async (scoreinfo: ScoreInfo, sheet: SheetInfo): Promise<ArrayBuffer> => {
|
||||
const imgType = sheet.imgType
|
||||
const pageCount = sheet.pageCount
|
||||
export const exportPDF = async (
|
||||
scoreinfo: ScoreInfo,
|
||||
sheet: SheetInfo
|
||||
): Promise<ArrayBuffer> => {
|
||||
const imgType = sheet.imgType;
|
||||
const pageCount = sheet.pageCount;
|
||||
|
||||
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.
|
||||
return sheet.thumbnailUrl
|
||||
} else { // obtain image urls using the API
|
||||
return getFileUrl(scoreinfo.id, 'img', 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.
|
||||
return sheet.thumbnailUrl;
|
||||
} else {
|
||||
// obtain image urls using the API
|
||||
return getFileUrl(scoreinfo.id, "img", i);
|
||||
}
|
||||
});
|
||||
const sheetImgURLs = await Promise.all(rs);
|
||||
|
||||
const args = [sheetImgURLs, imgType, sheet.dimensions] as const;
|
||||
if (!isNodeJs) {
|
||||
return _exportPDFBrowser(...args);
|
||||
} else {
|
||||
return _exportPDFNode(...args);
|
||||
}
|
||||
})
|
||||
const sheetImgURLs = await Promise.all(rs)
|
||||
};
|
||||
|
||||
const args = [sheetImgURLs, imgType, sheet.dimensions] as const
|
||||
if (!isNodeJs) {
|
||||
return _exportPDFBrowser(...args)
|
||||
} else {
|
||||
return _exportPDFNode(...args)
|
||||
}
|
||||
}
|
||||
let pdfBlob: Blob;
|
||||
export const downloadPDF = async (
|
||||
scoreinfo: ScoreInfo,
|
||||
sheet: SheetInfo,
|
||||
saveAs: typeof import("file-saver").saveAs
|
||||
): Promise<void> => {
|
||||
const name = scoreinfo.fileName;
|
||||
if (pdfBlob) {
|
||||
return saveAs(pdfBlob, `${name}.pdf`);
|
||||
}
|
||||
|
||||
let pdfBlob: Blob
|
||||
export const downloadPDF = async (scoreinfo: ScoreInfo, sheet: SheetInfo, saveAs: typeof import('file-saver').saveAs): Promise<void> => {
|
||||
const name = scoreinfo.fileName
|
||||
if (pdfBlob) {
|
||||
return saveAs(pdfBlob, `${name}.pdf`)
|
||||
}
|
||||
const pdfArrayBuffer = await exportPDF(scoreinfo, sheet);
|
||||
|
||||
const pdfArrayBuffer = await exportPDF(scoreinfo, sheet)
|
||||
|
||||
pdfBlob = new Blob([pdfArrayBuffer])
|
||||
saveAs(pdfBlob, `${name}.pdf`)
|
||||
}
|
||||
pdfBlob = new Blob([pdfArrayBuffer]);
|
||||
saveAs(pdfBlob, `${name}.pdf`);
|
||||
};
|
||||
|
0
src/redirect.ts
Normal file
0
src/redirect.ts
Normal file
276
src/scoreinfo.ts
276
src/scoreinfo.ts
@ -1,195 +1,171 @@
|
||||
|
||||
import { getFetch, escapeFilename, assertRes } from './utils'
|
||||
import { getMainCid } from './mscz'
|
||||
import { getFetch, escapeFilename, assertRes } from "./utils";
|
||||
|
||||
export abstract class ScoreInfo {
|
||||
private readonly RADIX = 20;
|
||||
private readonly INDEX_RADIX = 32;
|
||||
abstract id: number;
|
||||
abstract title: string;
|
||||
|
||||
abstract id: number;
|
||||
abstract title: string;
|
||||
public store = new Map<symbol, any>();
|
||||
|
||||
public store = new Map<symbol, any>();
|
||||
|
||||
get idLastDigit (): number {
|
||||
return (+this.id) % this.RADIX
|
||||
}
|
||||
|
||||
get fileName (): string {
|
||||
return escapeFilename(this.title)
|
||||
}
|
||||
|
||||
public getMsczIpfsRef (mainCid: string): string {
|
||||
return `/ipfs/${mainCid}/${this.idLastDigit}/${this.id}.mscz`
|
||||
}
|
||||
|
||||
public getMsczCidUrl (mainCid: string): string {
|
||||
return `https://ipfs.infura.io:5001/api/v0/block/stat?arg=${this.getMsczIpfsRef(mainCid)}`
|
||||
}
|
||||
|
||||
public getScorepackRef (mainCid: string): string {
|
||||
return `/ipfs/${mainCid}/index/${(+this.id) % this.INDEX_RADIX}/${this.id}`
|
||||
}
|
||||
get fileName(): string {
|
||||
return escapeFilename(this.title);
|
||||
}
|
||||
}
|
||||
|
||||
export class ScoreInfoObj extends ScoreInfo {
|
||||
constructor (public id: number = 0, public title: string = '') { super() }
|
||||
constructor(public id: number = 0, public title: string = "") {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class ScoreInfoInPage extends ScoreInfo {
|
||||
constructor (private document: Document) { super() }
|
||||
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
|
||||
return +m[1]
|
||||
}
|
||||
get id(): number {
|
||||
const el = this.document.querySelector(
|
||||
"meta[property='al:ios:url']"
|
||||
) as HTMLMetaElement;
|
||||
const m = el.content.match(/(\d+)$/) as RegExpMatchArray;
|
||||
return +m[1];
|
||||
}
|
||||
|
||||
get title (): string {
|
||||
const el = this.document.querySelector("meta[property='og:title']") as HTMLMetaElement
|
||||
return el.content
|
||||
}
|
||||
get title(): string {
|
||||
const el = this.document.querySelector(
|
||||
"meta[property='og:title']"
|
||||
) as HTMLMetaElement;
|
||||
return el.content;
|
||||
}
|
||||
|
||||
get baseUrl (): string {
|
||||
const el = this.document.querySelector("meta[property='og:image']") as HTMLMetaElement
|
||||
const m = el.content.match(/^(.+\/)score_/) as RegExpMatchArray
|
||||
return m[1]
|
||||
}
|
||||
get baseUrl(): string {
|
||||
const el = this.document.querySelector(
|
||||
"meta[property='og:image']"
|
||||
) as HTMLMetaElement;
|
||||
const m = el.content.match(/^(.+\/)score_/) as RegExpMatchArray;
|
||||
return m[1];
|
||||
}
|
||||
}
|
||||
|
||||
export class ScoreInfoHtml extends ScoreInfo {
|
||||
private readonly ID_REG = /<meta property="al:ios:url" content="musescore:\/\/score\/(\d+)">/
|
||||
private readonly TITLE_REG = /<meta property="og:title" content="(.*)">/
|
||||
private readonly BASEURL_REG = /<meta property="og:image" content="(.+\/)score_.*">/
|
||||
private readonly ID_REG =
|
||||
/<meta property="al:ios:url" content="musescore:\/\/score\/(\d+)">/;
|
||||
private readonly TITLE_REG = /<meta property="og:title" content="(.*)">/;
|
||||
private readonly BASEURL_REG =
|
||||
/<meta property="og:image" content="(.+\/)score_.*">/;
|
||||
|
||||
constructor (private html: string) { super() }
|
||||
constructor(private html: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
get id (): number {
|
||||
const m = this.html.match(this.ID_REG)
|
||||
if (!m) return 0
|
||||
return +m[1]
|
||||
}
|
||||
get id(): number {
|
||||
const m = this.html.match(this.ID_REG);
|
||||
if (!m) return 0;
|
||||
return +m[1];
|
||||
}
|
||||
|
||||
get title (): string {
|
||||
const m = this.html.match(this.TITLE_REG)
|
||||
if (!m) return ''
|
||||
return m[1]
|
||||
}
|
||||
get title(): string {
|
||||
const m = this.html.match(this.TITLE_REG);
|
||||
if (!m) return "";
|
||||
return m[1];
|
||||
}
|
||||
|
||||
get baseUrl (): string {
|
||||
const m = this.html.match(this.BASEURL_REG)
|
||||
if (!m) return ''
|
||||
return m[1]
|
||||
}
|
||||
get baseUrl(): string {
|
||||
const m = this.html.match(this.BASEURL_REG);
|
||||
if (!m) return "";
|
||||
return m[1];
|
||||
}
|
||||
|
||||
get sheet (): SheetInfo {
|
||||
return new SheetInfoHtml(this.html)
|
||||
}
|
||||
get sheet(): SheetInfo {
|
||||
return new SheetInfoHtml(this.html);
|
||||
}
|
||||
|
||||
static async request (url: string, _fetch = getFetch()): Promise<ScoreInfoHtml> {
|
||||
const r = await _fetch(url)
|
||||
if (!r.ok) return new ScoreInfoHtml('')
|
||||
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)
|
||||
}
|
||||
const html = await r.text();
|
||||
return new ScoreInfoHtml(html);
|
||||
}
|
||||
}
|
||||
|
||||
export type Dimensions = { width: number; height: number }
|
||||
export type Dimensions = { width: number; height: number };
|
||||
|
||||
export abstract class SheetInfo {
|
||||
abstract pageCount: number;
|
||||
abstract pageCount: number;
|
||||
|
||||
/** url to the image of the first page */
|
||||
abstract thumbnailUrl: string;
|
||||
/** url to the image of the first page */
|
||||
abstract thumbnailUrl: string;
|
||||
|
||||
abstract dimensions: Dimensions;
|
||||
abstract dimensions: Dimensions;
|
||||
|
||||
get imgType (): 'svg' | 'png' {
|
||||
const thumbnail = this.thumbnailUrl
|
||||
const imgtype = thumbnail.match(/score_0\.(\w+)/)![1]
|
||||
return imgtype as 'svg' | 'png'
|
||||
}
|
||||
get imgType(): "svg" | "png" {
|
||||
const thumbnail = this.thumbnailUrl;
|
||||
const imgtype = thumbnail.match(/score_0\.(\w+)/)![1];
|
||||
return imgtype as "svg" | "png";
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
constructor(private document: Document) {
|
||||
super();
|
||||
}
|
||||
return this.document.getElementsByClassName(sheet0Div.className).length
|
||||
}
|
||||
|
||||
get thumbnailUrl (): string {
|
||||
const el = this.document.querySelector<HTMLLinkElement>('link[as=image]')
|
||||
const url = (el?.href || this.sheet0Img?.src) as string
|
||||
return url.split('@')[0]
|
||||
}
|
||||
private get sheet0Img(): HTMLImageElement | null {
|
||||
return this.document.querySelector("img[src*=score_]");
|
||||
}
|
||||
|
||||
get dimensions (): Dimensions {
|
||||
const { naturalWidth: width, naturalHeight: height } = this.sheet0Img as HTMLImageElement
|
||||
return { width, height }
|
||||
}
|
||||
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 {
|
||||
const el =
|
||||
this.document.querySelector<HTMLLinkElement>("link[as=image]");
|
||||
const url = (el?.href || this.sheet0Img?.src) as string;
|
||||
return url.split("@")[0];
|
||||
}
|
||||
|
||||
get dimensions(): Dimensions {
|
||||
const { naturalWidth: width, naturalHeight: height } = this
|
||||
.sheet0Img as HTMLImageElement;
|
||||
return { width, height };
|
||||
}
|
||||
}
|
||||
|
||||
export class SheetInfoHtml extends SheetInfo {
|
||||
private readonly PAGE_COUNT_REG = /pages(?:"|"):(\d+),/
|
||||
private readonly THUMBNAIL_REG = /<link (?:.*) href="(.*)" rel="preload" as="image"/
|
||||
private readonly PAGE_COUNT_REG = /pages(?:"|"):(\d+),/;
|
||||
private readonly THUMBNAIL_REG =
|
||||
/<link (?:.*) href="(.*)" rel="preload" as="image"/;
|
||||
|
||||
private readonly DIMENSIONS_REG = /dimensions(?:"|"):(?:"|")(\d+)x(\d+)(?:"|"),/
|
||||
private readonly DIMENSIONS_REG =
|
||||
/dimensions(?:"|"):(?:"|")(\d+)x(\d+)(?:"|"),/;
|
||||
|
||||
constructor (private html: string) { super() }
|
||||
constructor(private html: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
get pageCount (): number {
|
||||
const m = this.html.match(this.PAGE_COUNT_REG)
|
||||
if (!m) return NaN
|
||||
return +m[1]
|
||||
}
|
||||
get pageCount(): number {
|
||||
const m = this.html.match(this.PAGE_COUNT_REG);
|
||||
if (!m) return NaN;
|
||||
return +m[1];
|
||||
}
|
||||
|
||||
get thumbnailUrl (): string {
|
||||
const m = this.html.match(this.THUMBNAIL_REG)
|
||||
if (!m) return ''
|
||||
return m[1].split('@')[0]
|
||||
}
|
||||
get thumbnailUrl(): string {
|
||||
const m = this.html.match(this.THUMBNAIL_REG);
|
||||
if (!m) return "";
|
||||
return m[1].split("@")[0];
|
||||
}
|
||||
|
||||
get dimensions (): Dimensions {
|
||||
const m = this.html.match(this.DIMENSIONS_REG)
|
||||
if (!m) return { width: NaN, height: NaN }
|
||||
return { width: +m[1], height: +m[2] }
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
get dimensions(): Dimensions {
|
||||
const m = this.html.match(this.DIMENSIONS_REG);
|
||||
if (!m) return { width: NaN, height: NaN };
|
||||
return { width: +m[1], height: +m[2] };
|
||||
}
|
||||
}
|
||||
|
301
src/utils.ts
301
src/utils.ts
@ -1,169 +1,192 @@
|
||||
import isNodeJs from "detect-node";
|
||||
import { isGmAvailable, _GM } from "./gm";
|
||||
|
||||
import isNodeJs from 'detect-node'
|
||||
import { isGmAvailable, _GM } from './gm'
|
||||
|
||||
export const DISCORD_URL = 'https://discord.gg/gSsTUvJmD8'
|
||||
export const APP_URL =
|
||||
"https://github.com/LibreScore/librescore-app/releases/latest";
|
||||
|
||||
export const escapeFilename = (s: string): string => {
|
||||
return s.replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, '_')
|
||||
}
|
||||
return s.replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, "_");
|
||||
};
|
||||
|
||||
export const getIndexPath = (id: number): string => {
|
||||
const idStr = String(id)
|
||||
// 获取最后三位,倒序排列
|
||||
// x, y, z are the reversed last digits of the score id. Example: id 123456789, x = 9, y = 8, z = 7
|
||||
// https://developers.musescore.com/#/file-urls
|
||||
// "5449062" -> ["2", "6", "0"]
|
||||
const indexN = idStr.split('').reverse().slice(0, 3)
|
||||
return indexN.join('/')
|
||||
}
|
||||
const idStr = String(id);
|
||||
// 获取最后三位,倒序排列
|
||||
// x, y, z are the reversed last digits of the score id. Example: id 123456789, x = 9, y = 8, z = 7
|
||||
// https://developers.musescore.com/#/file-urls
|
||||
// "5449062" -> ["2", "6", "0"]
|
||||
const indexN = idStr.split("").reverse().slice(0, 3);
|
||||
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',
|
||||
}
|
||||
"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) => {
|
||||
if (typeof input === 'string' && !input.startsWith('http')) { // fix: Only absolute URLs are supported
|
||||
input = 'https://musescore.com' + input
|
||||
}
|
||||
init = Object.assign({ headers: NODE_FETCH_HEADERS }, init)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return nodeFetch(input, init)
|
||||
if (!isNodeJs) {
|
||||
return fetch;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const nodeFetch = require("node-fetch");
|
||||
return (input: RequestInfo, init?: RequestInit) => {
|
||||
if (typeof input === "string" && !input.startsWith("http")) {
|
||||
// fix: Only absolute URLs are supported
|
||||
input = "https://musescore.com" + input;
|
||||
}
|
||||
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> => {
|
||||
const _fetch = getFetch()
|
||||
const r = await _fetch(url, init)
|
||||
const data = await r.arrayBuffer()
|
||||
return new Uint8Array(data)
|
||||
}
|
||||
export const fetchData = async (
|
||||
url: string,
|
||||
init?: RequestInit
|
||||
): Promise<Uint8Array> => {
|
||||
const _fetch = getFetch();
|
||||
const r = await _fetch(url, init);
|
||||
const data = await r.arrayBuffer();
|
||||
return new Uint8Array(data);
|
||||
};
|
||||
|
||||
export const fetchBuffer = async (url: string, init?: RequestInit): Promise<Buffer> => {
|
||||
const d = await fetchData(url, init)
|
||||
return Buffer.from(d.buffer)
|
||||
}
|
||||
export const fetchBuffer = async (
|
||||
url: string,
|
||||
init?: RequestInit
|
||||
): Promise<Buffer> => {
|
||||
const d = await fetchData(url, init);
|
||||
return Buffer.from(d.buffer);
|
||||
};
|
||||
|
||||
export const assertRes = (r: Response): void => {
|
||||
if (!r.ok) throw new Error(`${r.url} ${r.status} ${r.statusText}`)
|
||||
}
|
||||
if (!r.ok) throw new Error(`${r.url} ${r.status} ${r.statusText}`);
|
||||
};
|
||||
|
||||
export const useTimeout = async <T> (promise: T | Promise<T>, ms: number): Promise<T> => {
|
||||
if (!(promise instanceof Promise)) {
|
||||
return promise
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const i = setTimeout(() => {
|
||||
reject(new Error('timeout'))
|
||||
}, ms)
|
||||
promise.then(resolve, reject).finally(() => clearTimeout(i))
|
||||
})
|
||||
}
|
||||
|
||||
export const getSandboxWindowAsync = async (targetEl: Element | undefined = undefined): Promise<Window> => {
|
||||
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'
|
||||
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
|
||||
resolve(w as Window)
|
||||
export const useTimeout = async <T>(
|
||||
promise: T | Promise<T>,
|
||||
ms: number
|
||||
): Promise<T> => {
|
||||
if (!(promise instanceof Promise)) {
|
||||
return promise;
|
||||
}
|
||||
|
||||
targetEl.setAttribute(eventName, `this['${id}'](document.createElement('iframe'))`)
|
||||
})
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const i = setTimeout(() => {
|
||||
reject(new Error("timeout"));
|
||||
}, ms);
|
||||
promise.then(resolve, reject).finally(() => clearTimeout(i));
|
||||
});
|
||||
};
|
||||
|
||||
export const getSandboxWindowAsync = async (
|
||||
targetEl: Element | undefined = undefined
|
||||
): Promise<Window> => {
|
||||
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";
|
||||
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;
|
||||
resolve(w as Window);
|
||||
};
|
||||
|
||||
targetEl.setAttribute(
|
||||
eventName,
|
||||
`this['${id}'](document.createElement('iframe'))`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const getUnsafeWindow = (): Window => {
|
||||
// eslint-disable-next-line no-eval
|
||||
return window.eval('window') as Window
|
||||
}
|
||||
// eslint-disable-next-line no-eval
|
||||
return window.eval("window") as Window;
|
||||
};
|
||||
|
||||
export const console: Console = (typeof window !== 'undefined' ? window : global).console // Object.is(window.console, unsafeWindow.console) == false
|
||||
export const console: Console = (
|
||||
typeof window !== "undefined" ? 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 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
|
||||
}
|
||||
return Element.prototype.attachShadow.call(el, {
|
||||
mode: "closed",
|
||||
}) as ShadowRoot;
|
||||
};
|
||||
|
||||
export const waitForDocumentLoaded = (): Promise<void> => {
|
||||
if (document.readyState !== 'complete') {
|
||||
return new Promise(resolve => {
|
||||
const cb = () => {
|
||||
if (document.readyState === 'complete') {
|
||||
resolve()
|
||||
document.removeEventListener('readystatechange', cb)
|
||||
}
|
||||
}
|
||||
document.addEventListener('readystatechange', cb)
|
||||
})
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
if (document.readyState !== "complete") {
|
||||
return new Promise((resolve) => {
|
||||
const cb = () => {
|
||||
if (document.readyState === "complete") {
|
||||
resolve();
|
||||
document.removeEventListener("readystatechange", cb);
|
||||
}
|
||||
};
|
||||
document.addEventListener("readystatechange", cb);
|
||||
});
|
||||
} else {
|
||||
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()
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
// entry file for the Web Extension
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
const MAIN_FILE = 'dist/main.js'
|
||||
const MAIN_FILE = "dist/main.js";
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = chrome.runtime.getURL(MAIN_FILE)
|
||||
script.addEventListener('load', () => script.remove())
|
||||
const script = document.createElement("script");
|
||||
script.src = chrome.runtime.getURL(MAIN_FILE);
|
||||
script.addEventListener("load", () => script.remove());
|
||||
|
||||
document.documentElement.appendChild(script)
|
||||
document.documentElement.appendChild(script);
|
||||
|
@ -1,173 +1,197 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { hookNative } from './anti-detection'
|
||||
import { console, getUnsafeWindow } from './utils'
|
||||
import { hookNative } from "./anti-detection";
|
||||
import { console, getUnsafeWindow } from "./utils";
|
||||
|
||||
const CHUNK_PUSH_FN = /^function [^r]\(\w\){/
|
||||
const CHUNK_PUSH_FN = /^function [^r]\(\w\){/;
|
||||
|
||||
interface Module {
|
||||
(module, exports, __webpack_require__): void;
|
||||
(module, exports, __webpack_require__): void;
|
||||
}
|
||||
|
||||
type WebpackJson = [(number | string)[], { [id: string]: Module }, any[]?][]
|
||||
type WebpackJson = [(number | string)[], { [id: string]: Module }, any[]?][];
|
||||
|
||||
const moduleLookup = (id: string, globalWebpackJson: WebpackJson) => {
|
||||
const pack = globalWebpackJson.find(x => x[1][id])!
|
||||
return pack[1][id]
|
||||
}
|
||||
const pack = globalWebpackJson.find((x) => x[1][id])!;
|
||||
return pack[1][id];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve (webpack_require) a module from the page's webpack package
|
||||
*
|
||||
*
|
||||
* I know this is super hacky.
|
||||
*/
|
||||
export const webpackHook = (moduleId: string, moduleOverrides: { [id: string]: Module } = {}, globalWebpackJson: WebpackJson = window['webpackJsonpmusescore']) => {
|
||||
const t = Object.assign((id: string, override = true) => {
|
||||
const r: any = {}
|
||||
const m: Module = (override && moduleOverrides[id])
|
||||
? moduleOverrides[id]
|
||||
: moduleLookup(id, globalWebpackJson)
|
||||
m(r, r, t)
|
||||
if (r.exports) return r.exports
|
||||
return r
|
||||
}, {
|
||||
d (exp, name, fn) {
|
||||
return Object.prototype.hasOwnProperty.call(exp, name) ||
|
||||
Object.defineProperty(exp, name, { enumerable: true, get: fn })
|
||||
},
|
||||
n (e) {
|
||||
const m = e.__esModule ? () => e.default : () => e
|
||||
t.d(m, 'a', m)
|
||||
return m
|
||||
},
|
||||
r (r) {
|
||||
Object.defineProperty(r, '__esModule', { value: true })
|
||||
},
|
||||
e () {
|
||||
return Promise.resolve()
|
||||
},
|
||||
})
|
||||
export const webpackHook = (
|
||||
moduleId: string,
|
||||
moduleOverrides: { [id: string]: Module } = {},
|
||||
globalWebpackJson: WebpackJson = window["webpackJsonpmusescore"]
|
||||
) => {
|
||||
const t = Object.assign(
|
||||
(id: string, override = true) => {
|
||||
const r: any = {};
|
||||
const m: Module =
|
||||
override && moduleOverrides[id]
|
||||
? moduleOverrides[id]
|
||||
: moduleLookup(id, globalWebpackJson);
|
||||
m(r, r, t);
|
||||
if (r.exports) return r.exports;
|
||||
return r;
|
||||
},
|
||||
{
|
||||
d(exp, name, fn) {
|
||||
return (
|
||||
Object.prototype.hasOwnProperty.call(exp, name) ||
|
||||
Object.defineProperty(exp, name, {
|
||||
enumerable: true,
|
||||
get: fn,
|
||||
})
|
||||
);
|
||||
},
|
||||
n(e) {
|
||||
const m = e.__esModule ? () => e.default : () => e;
|
||||
t.d(m, "a", m);
|
||||
return m;
|
||||
},
|
||||
r(r) {
|
||||
Object.defineProperty(r, "__esModule", { value: true });
|
||||
},
|
||||
e() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return t(moduleId)
|
||||
}
|
||||
return t(moduleId);
|
||||
};
|
||||
|
||||
export const ALL = '*'
|
||||
export const ALL = "*";
|
||||
|
||||
export const [webpackGlobalOverride, onPackLoad] = (() => {
|
||||
type OnPackLoadFn = (pack: WebpackJson[0]) => any
|
||||
type OnPackLoadFn = (pack: WebpackJson[0]) => any;
|
||||
|
||||
const moduleOverrides: { [id: string]: Module } = {}
|
||||
const onPackLoadFns: OnPackLoadFn[] = []
|
||||
const moduleOverrides: { [id: string]: Module } = {};
|
||||
const onPackLoadFns: OnPackLoadFn[] = [];
|
||||
|
||||
function applyOverride (pack: WebpackJson[0]) {
|
||||
let entries = Object.entries(moduleOverrides)
|
||||
// apply to all
|
||||
const all = moduleOverrides[ALL]
|
||||
if (all) {
|
||||
entries = Object.keys(pack[1]).map(id => [id, all])
|
||||
function applyOverride(pack: WebpackJson[0]) {
|
||||
let entries = Object.entries(moduleOverrides);
|
||||
// apply to all
|
||||
const all = moduleOverrides[ALL];
|
||||
if (all) {
|
||||
entries = Object.keys(pack[1]).map((id) => [id, all]);
|
||||
}
|
||||
|
||||
entries.forEach(([id, override]) => {
|
||||
const mod = pack[1][id];
|
||||
if (mod) {
|
||||
pack[1][id] = function (n, r, t) {
|
||||
// make exports configurable
|
||||
t = Object.assign(t, {
|
||||
d(exp, name, fn) {
|
||||
return Object.defineProperty(exp, name, {
|
||||
enumerable: true,
|
||||
get: fn,
|
||||
configurable: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
mod(n, r, t);
|
||||
override(n, r, t);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
entries.forEach(([id, override]) => {
|
||||
const mod = pack[1][id]
|
||||
if (mod) {
|
||||
pack[1][id] = function (n, r, t) {
|
||||
// make exports configurable
|
||||
t = Object.assign(t, {
|
||||
d (exp, name, fn) {
|
||||
return Object.defineProperty(exp, name, { enumerable: true, get: fn, configurable: true })
|
||||
},
|
||||
})
|
||||
mod(n, r, t)
|
||||
override(n, r, t)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// hook `webpackJsonpmusescore.push` as soon as `webpackJsonpmusescore` is available
|
||||
const _w = getUnsafeWindow();
|
||||
let jsonp = _w["webpackJsonpmusescore"];
|
||||
let hooked = false;
|
||||
Object.defineProperty(_w, "webpackJsonpmusescore", {
|
||||
get() {
|
||||
return jsonp;
|
||||
},
|
||||
set(v: WebpackJson) {
|
||||
jsonp = v;
|
||||
if (!hooked && v.push.toString().match(CHUNK_PUSH_FN)) {
|
||||
hooked = true;
|
||||
hookNative(v, "push", (_fn) => {
|
||||
return function (pack) {
|
||||
onPackLoadFns.forEach((fn) => fn(pack));
|
||||
applyOverride(pack);
|
||||
return _fn.call(this, pack);
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// hook `webpackJsonpmusescore.push` as soon as `webpackJsonpmusescore` is available
|
||||
const _w = getUnsafeWindow()
|
||||
let jsonp = _w['webpackJsonpmusescore']
|
||||
let hooked = false
|
||||
Object.defineProperty(_w, 'webpackJsonpmusescore', {
|
||||
get () { return jsonp },
|
||||
set (v: WebpackJson) {
|
||||
jsonp = v
|
||||
if (!hooked && v.push.toString().match(CHUNK_PUSH_FN)) {
|
||||
hooked = true
|
||||
hookNative(v, 'push', (_fn) => {
|
||||
return function (pack) {
|
||||
onPackLoadFns.forEach(fn => fn(pack))
|
||||
applyOverride(pack)
|
||||
return _fn.call(this, pack)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return [
|
||||
// set overrides
|
||||
(moduleId: string, override: Module) => {
|
||||
moduleOverrides[moduleId] = override
|
||||
},
|
||||
// set onPackLoad listeners
|
||||
(fn: OnPackLoadFn) => {
|
||||
onPackLoadFns.push(fn)
|
||||
},
|
||||
] as const
|
||||
})()
|
||||
return [
|
||||
// set overrides
|
||||
(moduleId: string, override: Module) => {
|
||||
moduleOverrides[moduleId] = override;
|
||||
},
|
||||
// set onPackLoad listeners
|
||||
(fn: OnPackLoadFn) => {
|
||||
onPackLoadFns.push(fn);
|
||||
},
|
||||
] as const;
|
||||
})();
|
||||
|
||||
export const webpackContext = new Promise<any>((resolve) => {
|
||||
webpackGlobalOverride(ALL, (n, r, t) => {
|
||||
resolve(t)
|
||||
})
|
||||
})
|
||||
webpackGlobalOverride(ALL, (n, r, t) => {
|
||||
resolve(t);
|
||||
});
|
||||
});
|
||||
|
||||
const PACK_ID_REG = /\+(\{.*?"\})\[\w\]\+/
|
||||
const PACK_ID_REG = /\+(\{.*?"\})\[\w\]\+/;
|
||||
|
||||
export const loadAllPacks = () => {
|
||||
return webpackContext.then((ctx) => {
|
||||
return webpackContext.then((ctx) => {
|
||||
try {
|
||||
const fn = ctx.e.toString();
|
||||
const packsData = fn.match(PACK_ID_REG)[1] as string;
|
||||
// eslint-disable-next-line no-new-func, @typescript-eslint/no-implied-eval
|
||||
const packs = Function(`return (${packsData})`)() as {
|
||||
[id: string]: string;
|
||||
};
|
||||
|
||||
Object.keys(packs).forEach((id) => {
|
||||
ctx.e(id);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const OBF_FN_REG =
|
||||
/\w\(".{4}"\),(\w)=(\[".+?\]);\w=\1,\w=(\d+).+?\);var (\w=.+?,\w\})/;
|
||||
export const OBFUSCATED_REG = /(\w)\((\d+),"(.{4})"\)/g;
|
||||
|
||||
export const getObfuscationCtx = (
|
||||
mod: Module
|
||||
): ((n: number, s: string) => string) => {
|
||||
const str = mod.toString();
|
||||
const m = str.match(OBF_FN_REG);
|
||||
if (!m) return () => "";
|
||||
|
||||
try {
|
||||
const fn = ctx.e.toString()
|
||||
const packsData = fn.match(PACK_ID_REG)[1] as string
|
||||
// eslint-disable-next-line no-new-func, @typescript-eslint/no-implied-eval
|
||||
const packs = Function(`return (${packsData})`)() as { [id: string]: string }
|
||||
const arrVar = m[1];
|
||||
const arr = JSON.parse(m[2]);
|
||||
|
||||
Object.keys(packs).forEach((id) => {
|
||||
ctx.e(id)
|
||||
})
|
||||
let n = +m[3] + 1;
|
||||
for (; --n; ) arr.push(arr.shift());
|
||||
|
||||
const fnStr = m[4];
|
||||
const ctxStr = `var ${arrVar}=${JSON.stringify(arr)};return (${fnStr})`;
|
||||
// eslint-disable-next-line no-new-func, @typescript-eslint/no-implied-eval
|
||||
const fn = new Function(ctxStr)();
|
||||
|
||||
return fn;
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
console.error(err);
|
||||
return () => "";
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const OBF_FN_REG = /\w\(".{4}"\),(\w)=(\[".+?\]);\w=\1,\w=(\d+).+?\);var (\w=.+?,\w\})/
|
||||
export const OBFUSCATED_REG = /(\w)\((\d+),"(.{4})"\)/g
|
||||
|
||||
export const getObfuscationCtx = (mod: Module): (n: number, s: string) => string => {
|
||||
const str = mod.toString()
|
||||
const m = str.match(OBF_FN_REG)
|
||||
if (!m) return () => ''
|
||||
|
||||
try {
|
||||
const arrVar = m[1]
|
||||
const arr = JSON.parse(m[2])
|
||||
|
||||
let n = +m[3] + 1
|
||||
for (; --n;) arr.push(arr.shift())
|
||||
|
||||
const fnStr = m[4]
|
||||
const ctxStr = `var ${arrVar}=${JSON.stringify(arr)};return (${fnStr})`
|
||||
// eslint-disable-next-line no-new-func, @typescript-eslint/no-implied-eval
|
||||
const fn = new Function(ctxStr)()
|
||||
|
||||
return fn
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return () => ''
|
||||
}
|
||||
}
|
||||
|
||||
export default webpackHook
|
||||
export default webpackHook;
|
||||
|
@ -1,35 +1,36 @@
|
||||
|
||||
import { PDFWorkerMessage } from './worker'
|
||||
import { PDFWorker } from '../dist/cache/worker'
|
||||
import { PDFWorkerMessage } from "./worker";
|
||||
import { PDFWorker } from "../dist/cache/worker";
|
||||
|
||||
const scriptUrlFromFunction = (fn: () => any): string => {
|
||||
const blob = new Blob(['(' + fn.toString() + ')()'], { type: 'application/javascript' })
|
||||
return window.URL.createObjectURL(blob)
|
||||
}
|
||||
const blob = new Blob(["(" + fn.toString() + ")()"], {
|
||||
type: "application/javascript",
|
||||
});
|
||||
return window.URL.createObjectURL(blob);
|
||||
};
|
||||
|
||||
// Node.js fix
|
||||
if (typeof Worker === 'undefined') {
|
||||
globalThis.Worker = class { } as any // noop shim
|
||||
if (typeof Worker === "undefined") {
|
||||
globalThis.Worker = class {} as any; // noop shim
|
||||
}
|
||||
|
||||
export class PDFWorkerHelper extends Worker {
|
||||
constructor () {
|
||||
const url = scriptUrlFromFunction(PDFWorker)
|
||||
super(url)
|
||||
}
|
||||
constructor() {
|
||||
const url = scriptUrlFromFunction(PDFWorker);
|
||||
super(url);
|
||||
}
|
||||
|
||||
generatePDF (imgURLs: string[], imgType: 'svg' | 'png', width: number, height: number): Promise<ArrayBuffer> {
|
||||
const msg: PDFWorkerMessage = [
|
||||
imgURLs,
|
||||
imgType,
|
||||
width,
|
||||
height,
|
||||
]
|
||||
this.postMessage(msg)
|
||||
return new Promise((resolve) => {
|
||||
this.addEventListener('message', (e) => {
|
||||
resolve(e.data)
|
||||
})
|
||||
})
|
||||
}
|
||||
generatePDF(
|
||||
imgURLs: string[],
|
||||
imgType: "svg" | "png",
|
||||
width: number,
|
||||
height: number
|
||||
): Promise<ArrayBuffer> {
|
||||
const msg: PDFWorkerMessage = [imgURLs, imgType, width, height];
|
||||
this.postMessage(msg);
|
||||
return new Promise((resolve) => {
|
||||
this.addEventListener("message", (e) => {
|
||||
resolve(e.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
166
src/worker.ts
166
src/worker.ts
@ -1,113 +1,119 @@
|
||||
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
import PDFDocument from 'pdfkit/lib/document'
|
||||
import SVGtoPDF from 'svg-to-pdfkit'
|
||||
import PDFDocument from "pdfkit/lib/document";
|
||||
import SVGtoPDF from "svg-to-pdfkit";
|
||||
|
||||
type ImgType = 'svg' | 'png'
|
||||
type ImgType = "svg" | "png";
|
||||
|
||||
type DataResultType = 'dataUrl' | 'text'
|
||||
type DataResultType = "dataUrl" | "text";
|
||||
|
||||
const readData = (data: Blob | Buffer, type: DataResultType): string | Promise<string> => {
|
||||
if (!(data instanceof Uint8Array)) { // blob
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (): void => {
|
||||
const result = reader.result
|
||||
resolve(result as string)
|
||||
}
|
||||
reader.onerror = reject
|
||||
if (type === 'dataUrl') {
|
||||
reader.readAsDataURL(data)
|
||||
} else {
|
||||
reader.readAsText(data)
|
||||
}
|
||||
})
|
||||
} else { // buffer
|
||||
if (type === 'dataUrl') {
|
||||
return 'data:image/png;base64,' + data.toString('base64')
|
||||
const readData = (
|
||||
data: Blob | Buffer,
|
||||
type: DataResultType
|
||||
): string | Promise<string> => {
|
||||
if (!(data instanceof Uint8Array)) {
|
||||
// blob
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (): void => {
|
||||
const result = reader.result;
|
||||
resolve(result as string);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
if (type === "dataUrl") {
|
||||
reader.readAsDataURL(data);
|
||||
} else {
|
||||
reader.readAsText(data);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return data.toString('utf-8')
|
||||
// buffer
|
||||
if (type === "dataUrl") {
|
||||
return "data:image/png;base64," + data.toString("base64");
|
||||
} else {
|
||||
return data.toString("utf-8");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @platform browser
|
||||
*/
|
||||
const fetchBlob = async (imgUrl: string): Promise<Blob> => {
|
||||
const r = await fetch(imgUrl, {
|
||||
cache: 'no-cache',
|
||||
})
|
||||
return r.blob()
|
||||
}
|
||||
const r = await fetch(imgUrl, {
|
||||
cache: "no-cache",
|
||||
});
|
||||
return r.blob();
|
||||
};
|
||||
|
||||
/**
|
||||
* @example
|
||||
* @example
|
||||
* import { PDFWorker } from '../dist/cache/worker'
|
||||
* const { generatePDF } = PDFWorker()
|
||||
* const pdfData = await generatePDF(...)
|
||||
*/
|
||||
export const generatePDF = async (imgBlobs: Blob[] | Buffer[], imgType: ImgType, width: number, height: number): Promise<ArrayBuffer> => {
|
||||
// @ts-ignore
|
||||
const pdf = new (PDFDocument as typeof import('pdfkit'))({
|
||||
// compress: true,
|
||||
size: [width, height],
|
||||
autoFirstPage: false,
|
||||
margin: 0,
|
||||
layout: 'portrait',
|
||||
})
|
||||
export const generatePDF = async (
|
||||
imgBlobs: Blob[] | Buffer[],
|
||||
imgType: ImgType,
|
||||
width: number,
|
||||
height: number
|
||||
): Promise<ArrayBuffer> => {
|
||||
// @ts-ignore
|
||||
const pdf = new (PDFDocument as typeof import("pdfkit"))({
|
||||
// compress: true,
|
||||
size: [width, height],
|
||||
autoFirstPage: false,
|
||||
margin: 0,
|
||||
layout: "portrait",
|
||||
});
|
||||
|
||||
if (imgType === 'png') {
|
||||
const imgDataUrlList: string[] = await Promise.all(imgBlobs.map(b => readData(b, 'dataUrl')))
|
||||
if (imgType === "png") {
|
||||
const imgDataUrlList: string[] = await Promise.all(
|
||||
imgBlobs.map((b) => readData(b, "dataUrl"))
|
||||
);
|
||||
|
||||
imgDataUrlList.forEach((data) => {
|
||||
pdf.addPage()
|
||||
pdf.image(data, {
|
||||
width,
|
||||
height,
|
||||
})
|
||||
})
|
||||
} else { // imgType == "svg"
|
||||
const svgList = await Promise.all(imgBlobs.map(b => readData(b, 'text')))
|
||||
imgDataUrlList.forEach((data) => {
|
||||
pdf.addPage();
|
||||
pdf.image(data, {
|
||||
width,
|
||||
height,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// imgType == "svg"
|
||||
const svgList = await Promise.all(
|
||||
imgBlobs.map((b) => readData(b, "text"))
|
||||
);
|
||||
|
||||
svgList.forEach((svg) => {
|
||||
pdf.addPage()
|
||||
SVGtoPDF(pdf, svg, 0, 0, {
|
||||
preserveAspectRatio: 'none',
|
||||
})
|
||||
})
|
||||
}
|
||||
svgList.forEach((svg) => {
|
||||
pdf.addPage();
|
||||
SVGtoPDF(pdf, svg, 0, 0, {
|
||||
preserveAspectRatio: "none",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const buf: Uint8Array = await pdf.getBuffer()
|
||||
// @ts-ignore
|
||||
const buf: Uint8Array = await pdf.getBuffer();
|
||||
|
||||
return buf.buffer
|
||||
}
|
||||
return buf.buffer;
|
||||
};
|
||||
|
||||
export type PDFWorkerMessage = [string[], ImgType, number, number];
|
||||
|
||||
/**
|
||||
* @platform browser (web worker)
|
||||
*/
|
||||
if (typeof onmessage !== 'undefined') {
|
||||
onmessage = async (e): Promise<void> => {
|
||||
const [
|
||||
imgUrls,
|
||||
imgType,
|
||||
width,
|
||||
height,
|
||||
] = e.data as PDFWorkerMessage
|
||||
if (typeof onmessage !== "undefined") {
|
||||
onmessage = async (e): Promise<void> => {
|
||||
const [imgUrls, imgType, width, height] = e.data as PDFWorkerMessage;
|
||||
|
||||
const imgBlobs = await Promise.all(imgUrls.map(url => fetchBlob(url)))
|
||||
const imgBlobs = await Promise.all(
|
||||
imgUrls.map((url) => fetchBlob(url))
|
||||
);
|
||||
|
||||
const pdfBuf = await generatePDF(
|
||||
imgBlobs,
|
||||
imgType,
|
||||
width,
|
||||
height,
|
||||
)
|
||||
const pdfBuf = await generatePDF(imgBlobs, imgType, width, height);
|
||||
|
||||
postMessage(pdfBuf, [pdfBuf])
|
||||
}
|
||||
postMessage(pdfBuf, [pdfBuf]);
|
||||
};
|
||||
}
|
||||
|
@ -1,52 +1,60 @@
|
||||
/* eslint-disable */
|
||||
const w = typeof unsafeWindow == 'object' ? unsafeWindow : window;
|
||||
const w = typeof unsafeWindow == "object" ? unsafeWindow : window;
|
||||
|
||||
// GM APIs glue
|
||||
const _GM = typeof GM == 'object' ? GM : undefined;
|
||||
const gmId = '' + Math.random();
|
||||
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 })
|
||||
)
|
||||
// add buttons to the userscript manager menu
|
||||
_GM.registerMenuCommand(`** Version: ${_GM.info.script.version} **`, () =>
|
||||
_GM.openInTab(
|
||||
"https://github.com/LibreScore/musescore-downloader/releases",
|
||||
{ active: true }
|
||||
)
|
||||
);
|
||||
|
||||
_GM.registerMenuCommand(
|
||||
'** Source Code **',
|
||||
() => _GM.openInTab(_GM.info.script.homepage, { 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 })
|
||||
)
|
||||
_GM.registerMenuCommand("** Discord **", () =>
|
||||
_GM.openInTab("https://discord.gg/DKu7cUZ4XQ", { active: true })
|
||||
);
|
||||
}
|
||||
|
||||
function getRandL () {
|
||||
return String.fromCharCode(97 + Math.floor(Math.random() * 26))
|
||||
function getRandL() {
|
||||
return String.fromCharCode(97 + Math.floor(Math.random() * 26));
|
||||
}
|
||||
|
||||
// script loader
|
||||
new Promise(resolve => {
|
||||
const id = '' + Math.random();
|
||||
w[id] = resolve;
|
||||
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"
|
||||
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() + ')()')})
|
||||
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() +
|
||||
")()"
|
||||
);
|
||||
});
|
||||
|
@ -1,17 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"es2019",
|
||||
],
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"strictNullChecks": true,
|
||||
"sourceMap": false,
|
||||
"newLine": "lf",
|
||||
}
|
||||
}
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"lib": ["dom", "dom.iterable", "es2019"],
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"strictNullChecks": true,
|
||||
"sourceMap": false,
|
||||
"newLine": "lf"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user