├── .eslintignore
├── .eslintrc
├── .github
└── workflows
│ ├── publish_new_release.yaml
│ └── update_translations.yaml
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── dist
└── package.json
├── docs
├── cs
│ └── PŘEČTĚTEMĚ.md
├── en
│ └── README.md
├── es
│ └── LÉAME.md
├── fr
│ └── LISEZMOI.md
├── hu
│ └── OLVASSAEL.md
├── ms
│ └── BACASAYA.md
├── ru
│ └── ПРОЧТИМЕНЯ.md
└── zh-Hans
│ └── 自述文件.md
├── images
└── logo.png
├── lang.sh
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── anti-detection.ts
├── btn.css
├── btn.ts
├── cli.ts
├── file-magics.ts
├── file.ts
├── gm.ts
├── i18n
│ ├── ar.json
│ ├── cs.json
│ ├── de.json
│ ├── en.json
│ ├── es.json
│ ├── fr.json
│ ├── hu.json
│ ├── id.json
│ ├── index.ts
│ ├── it.json
│ ├── ja.json
│ ├── ko.json
│ ├── ms.json
│ ├── nl.json
│ ├── pl.json
│ ├── pt.json
│ ├── ru.json
│ ├── tr.json
│ ├── zh-Hans.json
│ └── zh_Hant.json
├── main.ts
├── meta.js
├── mscore.ts
├── mscz.ts
├── musescore-dl
│ └── cli.js
├── npm-data.ts
├── pdf.ts
├── scoreinfo.ts
├── utils.ts
├── webpack-hook.ts
├── worker-helper.ts
├── worker.ts
└── wrapper.js
└── tsconfig.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/*
2 | rollup*
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es6": true,
6 | "node": true
7 | },
8 | "parser": "@typescript-eslint/parser",
9 | "plugins": ["@typescript-eslint"],
10 | "extends": [
11 | "standard",
12 | "plugin:@typescript-eslint/recommended",
13 | "plugin:@typescript-eslint/recommended-requiring-type-checking"
14 | ],
15 | "rules": {
16 | "dot-notation": "off",
17 | "camelcase": "off",
18 | "no-useless-constructor": "off",
19 | "@typescript-eslint/no-useless-constructor": "error",
20 | "no-dupe-class-members": "off",
21 | "no-void": "off",
22 | "no-use-before-define": "off",
23 | "@typescript-eslint/no-dupe-class-members": "error",
24 | "@typescript-eslint/no-floating-promises": "warn",
25 | "@typescript-eslint/member-delimiter-style": "warn",
26 | "@typescript-eslint/ban-ts-ignore": "off",
27 | "@typescript-eslint/ban-ts-comment": "off",
28 | "@typescript-eslint/no-explicit-any": "off",
29 | "@typescript-eslint/prefer-regexp-exec": "off",
30 | "@typescript-eslint/no-unsafe-assignment": "off",
31 | "@typescript-eslint/no-unsafe-call": "off",
32 | "@typescript-eslint/no-unsafe-member-access": "off",
33 | "no-trailing-spaces": [
34 | "error",
35 | {
36 | "ignoreComments": true
37 | }
38 | ],
39 | "comma-dangle": ["warn", "always-multiline"]
40 | },
41 | "parserOptions": {
42 | "project": ["./tsconfig.json"]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.github/workflows/publish_new_release.yaml:
--------------------------------------------------------------------------------
1 | name: Publish new release
2 |
3 | on:
4 | schedule:
5 | - cron: "0 0 * * 0"
6 | workflow_dispatch:
7 | inputs:
8 | version:
9 | description: "The version number. default: the version number in package.json"
10 | required: false
11 |
12 | concurrency:
13 | group: ${{ github.workflow }}-${{ github.ref }}
14 | cancel-in-progress: true
15 |
16 | env:
17 | VERSION: ${{ github.event.inputs.version }}
18 | NPM_TAG: latest
19 | REF: ${{ github.sha }}
20 | ARTIFACTS_DIR: ./.artifacts
21 |
22 | jobs:
23 | publish:
24 | runs-on: ubuntu-latest
25 | steps:
26 | - uses: actions/checkout@v3
27 | with:
28 | ref: ${{ env.REF }}
29 | - name: Check for new release
30 | run: |
31 | echo "updated=false" >> $GITHUB_ENV
32 | if [[ "$(npm show dl-librescore version)" != "$(node -p "require('./package.json').version")" ]]; then
33 | echo "updated=true" >> $GITHUB_ENV
34 | fi
35 | - uses: actions/setup-node@v2.4.1
36 | if: env.updated == 'true'
37 | with:
38 | node-version: 16
39 | registry-url: https://registry.npmjs.org/
40 | - name: Build userscript and command-line tool
41 | if: env.updated == 'true'
42 | run: |
43 | VER=$(node -p "require('./package.json').version")
44 | echo "VERSION=$VER" >> $GITHUB_ENV
45 | npm install
46 | npm version --allow-same-version --no-git-tag $VERSION
47 | npm run build
48 | npm run pack:ext
49 | - name: Publish command-line tool to NPM
50 | if: env.updated == 'true'
51 | run: npm publish --tag $NPM_TAG
52 | env:
53 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
54 | - name: Publish GitHub Release
55 | if: env.updated == 'true'
56 | env:
57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58 | run: |
59 | mkdir -p $ARTIFACTS_DIR
60 | cp dist/main.user.js $ARTIFACTS_DIR/dl-librescore.user.js
61 | wget -q https://github.com/LibreScore/dl-librescore/archive/$REF.tar.gz -O $ARTIFACTS_DIR/source.tar.gz
62 | cd $ARTIFACTS_DIR
63 | rm *.tar.gz
64 | files=$(ls .)
65 | assets=()
66 | for f in $files; do [ -f "$f" ] && assets+=("$f"); done
67 | SHORT_SHA=$(echo $REF | cut -c 1-7)
68 | gh release create v$VERSION "${assets[@]}" --title v$VERSION --target $REF
69 | - name: Delete workflow run
70 | if: env.updated == 'false'
71 | run: |
72 | curl -s -i -u ${{ secrets.LIBRESCORE_USERNAME }}:${{ secrets.LIBRESCORE_TOKEN }} -d '{"event_type":"delete_action","client_payload":{"run_id":"'"${{ github.run_id }}"'","repo":"LibreScore/dl-librescore"}}' -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/LibreScore/actions/dispatches
73 |
--------------------------------------------------------------------------------
/.github/workflows/update_translations.yaml:
--------------------------------------------------------------------------------
1 | name: Update translations
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | paths:
7 | - "src/i18n/*.json"
8 | pull_request:
9 | paths:
10 | - "src/i18n/*.json"
11 |
12 | concurrency:
13 | group: ${{ github.workflow }}-${{ github.ref }}
14 | cancel-in-progress: true
15 |
16 | jobs:
17 | translate:
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v3
21 | - name: Update translations
22 | run: |
23 | sudo npm i -g prettier
24 | bash ./lang.sh
25 | sed -ri 's/"version": "'"([[:digit:]]+)\.([[:digit:]]+)\.([[:digit:]]+)"'",/echo \\"version\\": \\"\1.\2.$((\3+1))\\",/e' package.json
26 | prettier --write package.json
27 | git config user.name github-actions
28 | git config user.email github-actions@github.com
29 | git add -A
30 | git commit -m "chore: update translations"
31 | git push
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | # parcel build cache
64 | .cache
65 |
66 | 1.*
67 |
68 | dist/
69 |
70 | .vscode/
71 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.options": {},
3 | "eslint.validate": ["javascript", "typescript"],
4 | "editor.tabSize": 2,
5 | "javascript.format.insertSpaceBeforeFunctionParenthesis": true,
6 | "typescript.format.insertSpaceBeforeFunctionParenthesis": true,
7 | "javascript.format.insertSpaceAfterConstructor": true,
8 | "typescript.format.insertSpaceAfterConstructor": true,
9 | "[typescript]": {
10 | "editor.formatOnSave": true
11 | },
12 | "editor.codeActionsOnSave": {
13 | "source.fixAll.eslint": true
14 | },
15 | "typescript.tsdk": "node_modules/typescript/lib",
16 | "i18n-ally.localesPaths": ["src/i18n"]
17 | }
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 LibreScore
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [čeština](/docs/cs/PŘEČTĚTEMĚ.md) | **English** | [español](/docs/es/LÉAME.md) | [français](/docs/fr/LISEZMOI.md) | [magyar](/docs/hu/OLVASSAEL.md) | [Melayu](/docs/ms/BACASAYA.md) | [русский](/docs/ru/ПРОЧТИМЕНЯ.md) | [简体中文](/docs/zh-Hans/自述文件.md) | [[+]](https://weblate.librescore.org/projects/librescore/docs)
4 |
5 | [//]: # "\+\_==!|!=_=!|!==_/+/ ***DO NOT EDIT ABOVE THIS LINE*** /+/^^+#|#+^+#|#+^^\+\"
6 |
7 | # dl-librescore
8 |
9 |
10 |
11 |

12 |
13 | [](https://discord.gg/DKu7cUZ4XQ) [](https://weblate.librescore.org/engage/librescore) [](https://github.com/LibreScore/app-librescore/releases/latest) [](https://github.com/LibreScore/dl-librescore/releases/latest) [](https://www.npmjs.com/package/dl-librescore)
14 |
15 | Download sheet music
16 |
17 |
18 |
19 | > DISCLAIMER: This is not an officially endorsed MuseScore product
20 |
21 | ## Installation
22 |
23 | There are 4 different installable programs:
24 |
25 | | Program | MSCZ | MIDI | MP3 | PDF | Conversion | | Windows | macOS | Linux | Android | iOS/iPadOS |
26 | | ---------------------------------------------------------------------------------- | ---- | ---- | --- | --- | ---------- | --- | ------- | ----- | ----- | ------- | ---------- |
27 | | [App](#app) | ✔️ | ✔️ | ✔️ | ❌ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
28 | | [Userscript](#userscript) | ❌ | ✔️ | ✔️ | ✔️ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
29 | | [Command-line tool](#command-line-tool) | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
30 | | [Webmscore website](#webmscore-website) | ❌ | ❌ | ❌ | ❌ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
31 |
32 | > Note: `Conversion` refers to the ability to convert files into other file types, including those not downloadable in the program.
33 | > Conversion types include: Individual Parts, PDF, PNG, SVG, MP3, WAV, FLAC, OGG, MIDI, MusicXML, MSCZ, and MSCX.
34 |
35 | ### App
36 |
37 | 1. Go to the [README](https://github.com/LibreScore/app-librescore#installation) page of the `app-librescore` repository
38 | 2. Follow the installation instructions for your device
39 |
40 | ### Userscript
41 |
42 | > Note: If your device is on iOS or iPadOS, please follow the [Shortcut](#shortcut) instructions.
43 | >
44 | > Note: If you cannot install browser extensions on your device, please follow the [Bookmark](#bookmark) instructions instead.
45 |
46 | #### Browser extension
47 |
48 | 1. Install [Tampermonkey](https://www.tampermonkey.net)
49 |
50 | > Note: If you already installed an old version of the script called "musescore-downloader", "mcsz downloader", or "musescore-dl", please uninstall it from the Tampermonkey dashboard
51 |
52 | 2. Go to the latest [dl-librescore.user.js](https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js) file
53 | 3. Press the Install button
54 |
55 | #### Shortcut
56 |
57 | 1. Install the [LibreScore shortcut](https://www.icloud.com/shortcuts/901d8778d2da4f7db9272d3b2232d0fe)
58 | 2. In Safari, when viewing a song on MuseScore, tap

59 | 3. Tap the LibreScore shortcut to activate the extension
60 |
61 | > Note: Before you can run JavaScript from a shortcut you must turn on Allow Running Scripts
62 | >
63 | > 1. Go to Settings

> Shortcuts > Advanced
64 | > 2. Turn on Allow Running Scripts
65 |
66 | #### Bookmark
67 |
68 | 1. Create a new bookmark (usually Ctrl+D)
69 | 2. Type `LibreScore` for the Name field
70 | 3. Type `javascript:(function () {let code = document.createElement('script');code.src = 'https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js';document.body.appendChild(code);}())` for the URL field
71 | 4. Save the bookmark
72 | 5. When viewing a song on MuseScore, click the bookmark to activate the extension
73 |
74 | ### Command-line tool
75 |
76 | 1. Install [Node.js LTS](https://nodejs.org)
77 | 2. Open a terminal (do _not_ open the Node.js application)
78 | 3. Type `npx dl-librescore@latest`, then press `Enter ↵`
79 |
80 | ### Webmscore website
81 |
82 | 1. Open [Webmscore](https://webmscore-pwa.librescore.org)
83 |
84 | > Note: You can access the website offline by installing it as a PWA
85 |
86 | ## Building
87 |
88 | 1. Install [Node.js LTS](https://nodejs.org)
89 | 2. `npm install` to install packages
90 | 3. `npm run build` to build
91 |
92 | - Install `./dist/main.user.js` with Tampermonkey
93 | - `node ./dist/cli.js` to run command-line tool
94 |
95 |
96 |
--------------------------------------------------------------------------------
/dist/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "commonjs"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/cs/PŘEČTĚTEMĚ.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | **čeština** | [English](/docs/en/README.md) | [español](/docs/es/LÉAME.md) | [français](/docs/fr/LISEZMOI.md) | [magyar](/docs/hu/OLVASSAEL.md) | [Melayu](/docs/ms/BACASAYA.md) | [русский](/docs/ru/ПРОЧТИМЕНЯ.md) | [简体中文](/docs/zh-Hans/自述文件.md) | [[+]](https://weblate.librescore.org/projects/librescore/docs)
4 |
5 | [//]: # "\+\_==!|!=_=!|!==_/+/ ***NEUPRAVUJTE NAD TENTO ŘÁDEK*** /+/^^+#|#+^+#|#+^^\+\"
6 |
7 | # dl-librescore
8 |
9 |
10 |
11 |

12 |
13 | [](https://discord.gg/DKu7cUZ4XQ) [](https://weblate.librescore.org/engage/librescore) [](https://github.com/LibreScore/app-librescore/releases/latest) [](https://github.com/LibreScore/dl-librescore/releases/latest) [](https://www.npmjs.com/package/dl-librescore)
14 |
15 | Stáhnout noty
16 |
17 |
18 |
19 | > UPOZORNĚNÍ: Toto není oficiálně schválený produkt MuseScore
20 |
21 | ## Instalace
22 |
23 | Existují 4 různé instalovatelné programy:
24 |
25 | | Program | MSCZ | MIDI | MP3 | PDF | Konverze | | Windows | macOS | Linux | Android | iOS/iPadOS |
26 | | ---------------------------------------------------------------------------------- | ---- | ---- | --- | --- | ---------- | --- | ------- | ----- | ----- | ------- | ---------- |
27 | | [Aplikace](#aplikace) | ✔️ | ✔️ | ✔️ | ❌ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
28 | | [Uživatelský skript](#uživatelský-skript) | ❌ | ✔️ | ✔️ | ✔️ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
29 | | [Nástroj příkazového řádku](#nástroj-příkazového-řádku) | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
30 | | [Webové stránky Webmscore](#webové-stránky-webmscore) | ❌ | ❌ | ❌ | ❌ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
31 |
32 | > Poznámka: `Konverze` odkazuje na schopnost převádět soubory na jiné typy souborů, včetně těch, které nelze stáhnout v programu.
33 | > Mezi typy převodu patří: Jednotlivé části, PDF, PNG, SVG, MP3, WAV, FLAC, OGG, MIDI, MusicXML, MSCZ a MSCX.
34 |
35 | ### Aplikace
36 |
37 | 1. Přejděte na stránku [PŘEČTĚTEMĚ](https://github.com/LibreScore/app-librescore/blob/master/docs/cs/PŘEČTĚTEMĚ.md#instalace) v repozitáři `app-librescore`
38 | 2. Postupujte podle pokynů k instalaci vašeho zařízení
39 |
40 | ### Uživatelský skript
41 |
42 | > Poznámka: Pokud máte zařízení se systémem iOS nebo iPadOS, postupujte podle pokynů [Zkratka](#zkratka).
43 | >
44 | > Poznámka: Pokud do zařízení nemůžete nainstalovat rozšíření prohlížeče, postupujte podle pokynů [Záložka](#záložka).
45 |
46 | #### Rozšíření prohlížeče
47 |
48 | 1. Nainstalujte [Tampermonkey](https://www.tampermonkey.net)
49 |
50 | > Poznámka: Pokud jste již nainstalovali starou verzi skriptu s názvem „musescore-downloader“, „mcsz downloader“ nebo „musescore-dl“, odinstalujte ji prosím z řídicího panelu Tampermonkey
51 |
52 | 2. Přejděte na nejnovější soubor [dl-librescore.user.js](https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js)
53 | 3. Stiskněte tlačítko Instalovat
54 |
55 | #### Zkratka
56 |
57 | 1. Nainstalujte [zkratku LibreScore](https://www.icloud.com/shortcuts/901d8778d2da4f7db9272d3b2232d0fe)
58 | 2. V Safari při prohlížení skladby na MuseScore klepněte na

59 | 3. Klepnutím na zkratku LibreScore aktivujte rozšíření
60 |
61 | > Poznámka: Před spuštěním JavaScriptu pomocí zkratky musíte zapnout volbu Povolit spouštění skriptů
62 | >
63 | > 1. Přejděte do Nastavení

> Zkratky > Pokročilé
64 | > 2. Zapněte „Povolit spouštění skriptů“
65 |
66 | #### Záložka
67 |
68 | 1. Vytvořte novou záložku (obvykle Ctrl+D)
69 | 2. Do pole Název napište `LibreScore`
70 | 3. Zadejte `javascript:(function () {let code = document.createElement('script');code.src = 'https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js';document.body.appendChild(code);}())` pro pole URL
71 | 4. Uložte záložku
72 | 5. Při prohlížení skladby v MuseScore aktivujte rozšíření kliknutím na záložku
73 |
74 | ### Nástroj příkazového řádku
75 |
76 | 1. Nainstalujte [Node.js LTS](https://nodejs.org)
77 | 2. Otevřete terminál (*ne*otevírejte aplikaci Node.js)
78 | 3. Napište `npx dl-librescore@latest` a stiskněte `Enter ↵`
79 |
80 | ### Webové stránky Webmscore
81 |
82 | 1. Otevřete [Webmscore](https://webmscore-pwa.librescore.org)
83 |
84 | > Poznámka: K webu můžete přistupovat offline, pokud si jej nainstalujete jako PWA
85 |
86 | ## Kompilace
87 |
88 | 1. Nainstalujte [Node.js LTS](https://nodejs.org)
89 | 2. `npm install` pro instalaci balíčků
90 | 3. `npm run build` ke kompilaci
91 |
92 | - Nainstalujte `./dist/main.user.js` pomocí Tampermonkey
93 | - `node ./dist/cli.js` pro spuštění nástroje příkazového řádku
94 |
95 |
96 |
--------------------------------------------------------------------------------
/docs/en/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [čeština](/docs/cs/PŘEČTĚTEMĚ.md) | **English** | [español](/docs/es/LÉAME.md) | [français](/docs/fr/LISEZMOI.md) | [magyar](/docs/hu/OLVASSAEL.md) | [Melayu](/docs/ms/BACASAYA.md) | [русский](/docs/ru/ПРОЧТИМЕНЯ.md) | [简体中文](/docs/zh-Hans/自述文件.md) | [[+]](https://weblate.librescore.org/projects/librescore/docs)
4 |
5 | [//]: # "\+\_==!|!=_=!|!==_/+/ ***DO NOT EDIT ABOVE THIS LINE*** /+/^^+#|#+^+#|#+^^\+\"
6 |
7 | # dl-librescore
8 |
9 |
10 |
11 |

12 |
13 | [](https://discord.gg/DKu7cUZ4XQ) [](https://weblate.librescore.org/engage/librescore) [](https://github.com/LibreScore/app-librescore/releases/latest) [](https://github.com/LibreScore/dl-librescore/releases/latest) [](https://www.npmjs.com/package/dl-librescore)
14 |
15 | Download sheet music
16 |
17 |
18 |
19 | > DISCLAIMER: This is not an officially endorsed MuseScore product
20 |
21 | ## Installation
22 |
23 | There are 4 different installable programs:
24 |
25 | | Program | MSCZ | MIDI | MP3 | PDF | Conversion | | Windows | macOS | Linux | Android | iOS/iPadOS |
26 | | ---------------------------------------------------------------------------------- | ---- | ---- | --- | --- | ---------- | --- | ------- | ----- | ----- | ------- | ---------- |
27 | | [App](#app) | ✔️ | ✔️ | ✔️ | ❌ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
28 | | [Userscript](#userscript) | ❌ | ✔️ | ✔️ | ✔️ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
29 | | [Command-line tool](#command-line-tool) | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
30 | | [Webmscore website](#webmscore-website) | ❌ | ❌ | ❌ | ❌ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
31 |
32 | > Note: `Conversion` refers to the ability to convert files into other file types, including those not downloadable in the program.
33 | > Conversion types include: Individual Parts, PDF, PNG, SVG, MP3, WAV, FLAC, OGG, MIDI, MusicXML, MSCZ, and MSCX.
34 |
35 | ### App
36 |
37 | 1. Go to the [README](https://github.com/LibreScore/app-librescore#installation) page of the `app-librescore` repository
38 | 2. Follow the installation instructions for your device
39 |
40 | ### Userscript
41 |
42 | > Note: If your device is on iOS or iPadOS, please follow the [Shortcut](#shortcut) instructions.
43 | >
44 | > Note: If you cannot install browser extensions on your device, please follow the [Bookmark](#bookmark) instructions instead.
45 |
46 | #### Browser extension
47 |
48 | 1. Install [Tampermonkey](https://www.tampermonkey.net)
49 |
50 | > Note: If you already installed an old version of the script called "musescore-downloader", "mcsz downloader", or "musescore-dl", please uninstall it from the Tampermonkey dashboard
51 |
52 | 2. Go to the latest [dl-librescore.user.js](https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js) file
53 | 3. Press the Install button
54 |
55 | #### Shortcut
56 |
57 | 1. Install the [LibreScore shortcut](https://www.icloud.com/shortcuts/901d8778d2da4f7db9272d3b2232d0fe)
58 | 2. In Safari, when viewing a song on MuseScore, tap

59 | 3. Tap the LibreScore shortcut to activate the extension
60 |
61 | > Note: Before you can run JavaScript from a shortcut you must turn on Allow Running Scripts
62 | >
63 | > 1. Go to Settings

> Shortcuts > Advanced
64 | > 2. Turn on Allow Running Scripts
65 |
66 | #### Bookmark
67 |
68 | 1. Create a new bookmark (usually Ctrl+D)
69 | 2. Type `LibreScore` for the Name field
70 | 3. Type `javascript:(function () {let code = document.createElement('script');code.src = 'https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js';document.body.appendChild(code);}())` for the URL field
71 | 4. Save the bookmark
72 | 5. When viewing a song on MuseScore, click the bookmark to activate the extension
73 |
74 | ### Command-line tool
75 |
76 | 1. Install [Node.js LTS](https://nodejs.org)
77 | 2. Open a terminal (do _not_ open the Node.js application)
78 | 3. Type `npx dl-librescore@latest`, then press `Enter ↵`
79 |
80 | ### Webmscore website
81 |
82 | 1. Open [Webmscore](https://webmscore-pwa.librescore.org)
83 |
84 | > Note: You can access the website offline by installing it as a PWA
85 |
86 | ## Building
87 |
88 | 1. Install [Node.js LTS](https://nodejs.org)
89 | 2. `npm install` to install packages
90 | 3. `npm run build` to build
91 |
92 | - Install `./dist/main.user.js` with Tampermonkey
93 | - `node ./dist/cli.js` to run command-line tool
94 |
95 |
96 |
--------------------------------------------------------------------------------
/docs/es/LÉAME.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [čeština](/docs/cs/PŘEČTĚTEMĚ.md) | [English](/docs/en/README.md) | **español** | [français](/docs/fr/LISEZMOI.md) | [magyar](/docs/hu/OLVASSAEL.md) | [Melayu](/docs/ms/BACASAYA.md) | [русский](/docs/ru/ПРОЧТИМЕНЯ.md) | [简体中文](/docs/zh-Hans/自述文件.md) | [[+]](https://weblate.librescore.org/projects/librescore/docs)
4 |
5 | [//]: # "\+\_==!|!=_=!|!==_/+/ ***NO EDITAR ENCIMA DE ESTA LÍNEA*** /+/^^+#|#+^+#|#+^^\+\"
6 |
7 | # dl-librescore
8 |
9 |
10 |
11 |

12 |
13 | [](https://discord.gg/DKu7cUZ4XQ) [](https://weblate.librescore.org/engage/librescore) [](https://github.com/LibreScore/app-librescore/releases/latest) [](https://github.com/LibreScore/dl-librescore/releases/latest) [](https://www.npmjs.com/package/dl-librescore)
14 |
15 | Descargar partituras
16 |
17 |
18 |
19 | > DESCARGO DE RESPONSABILIDAD: Este no es un producto de MuseScore patrocinado oficialmente
20 |
21 | ## Instalación
22 |
23 | Hay 4 programas instalables diferentes:
24 |
25 | | Programa | MSCZ | MIDI | MP3 | PDF | Conversión | | Windows | macOS | Linux | Android | iOS/iPadOS |
26 | | ---------------------------------------------------------------------------------- | ---- | ---- | --- | --- | ---------- | --- | ------- | ----- | ----- | ------- | ---------- |
27 | | [App](#app) | ✔️ | ✔️ | ✔️ | ❌ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
28 | | [Guión de usuario](#guión-de-usuario) | ❌ | ✔️ | ✔️ | ✔️ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
29 | | [Herramienta de línea-de-comandos](#herramienta-de-línea-de-comandos) | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
30 | | [Sitio web de Webmscore](#sitio-web-de-webmscore) | ❌ | ❌ | ❌ | ❌ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
31 |
32 | > Nota: `Conversión` se refiere a la capacidad de convertir archivos en otros tipos de archivos, incluidos los que no se pueden descargar en el programa.
33 | > Los tipos de conversión incluyen: Partes individuales, PDF, PNG, SVG, MP3, WAV, FLAC, OGG, MIDI, MusicXML, MSCZ y MSCX.
34 |
35 | ### App
36 |
37 | 1. Vaya a la página [LÉAME](https://github.com/LibreScore/app-librescore/blob/master/docs/es/LÉAME.md#instalación) del repositorio `app-librescore`
38 | 2. Sigue las instrucciones de instalación de su dispositivo
39 |
40 | ### Guión de usuario
41 |
42 | > Nota: Si su dispositivo está en iOS o iPadOS, por favor, siga las instrucciones de [Atajo](#atajo).
43 | >
44 | > Nota: Si no puede instalar extensiones de navegador en su dispositivo, por favor, siga las instrucciones de [Marcador](#marcador) en su lugar.
45 |
46 | #### Extensión del navegador
47 |
48 | 1. Instale [Tampermonkey](https://www.tampermonkey.net/?locale=es)
49 |
50 | > Nota: Si ya instaló una versión antigua del script llamado "musescore-downloader", "mcsz downloader" o "musescore-dl", por favor, desinstálelo desde el panel de control de Tampermonkey
51 |
52 | 2. Vaya al último archivo [dl-librescore.user.js](https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js)
53 | 3. Pulse el botón Instalar
54 |
55 | #### Atajo
56 |
57 | 1. Instale el [atajo de LibreScore](https://www.icloud.com/shortcuts/901d8778d2da4f7db9272d3b2232d0fe)
58 | 2. En Safari, cuando esté viendo una canción en MuseScore, toque

.
59 | 3. Toque el atajo de LibreScore para activar la extensión
60 |
61 | > Nota: Para poder ejecutar JavaScript desde un atajo, debes activar la opción "Permitir ejecutar scripts"
62 | >
63 | > 1. Ve a Configuración

> Atajos > Avanzado
64 | > 2. Activa "Permitir ejecutar scripts"
65 |
66 | #### Marcador
67 |
68 | 1. Cree un nuevo marcador (normalmente Ctrl+D)
69 | 2. Escriba `LibreScore` para el campo Nombre
70 | 3. Escriba `javascript:(function () {let code = document.createElement('script');code.src = 'https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js';document.body.appendChild(code);}())` para el campo URL
71 | 4. Guarde el marcador
72 | 5. Cuando vea una canción en MuseScore, haga clic en el marcador para activar la extensión
73 |
74 | ### Herramienta de línea-de-comandos
75 |
76 | 1. Instale [Node.js LTS](https://nodejs.org/es)
77 | 2. Abra una terminal (_no_ abra la aplicación Node.js)
78 | 3. Escriba `npx dl-librescore@latest`, luego presione `Entrar ↵`
79 |
80 | ### Sitio web de Webmscore
81 |
82 | 1. Abra [Webmscore](https://webmscore-pwa.librescore.org)
83 |
84 | > Nota: Puede acceder al sitio web sin conexión instalándolo como un PWA
85 |
86 | ## Compilación
87 |
88 | 1. Instale [Node.js LTS](https://nodejs.org/es)
89 | 2. `npm install` para instalar paquetes
90 | 3. `npm run build` para compilar
91 |
92 | - Instale `./dist/main.user.js` con Tampermonkey
93 | - `node ./dist/cli.js` para ejecutar la herramienta de línea-de-comandos
94 |
95 |
96 |
--------------------------------------------------------------------------------
/docs/fr/LISEZMOI.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [čeština](/docs/cs/PŘEČTĚTEMĚ.md) | [English](/docs/en/README.md) | [español](/docs/es/LÉAME.md) | **français** | [magyar](/docs/hu/OLVASSAEL.md) | [Melayu](/docs/ms/BACASAYA.md) | [русский](/docs/ru/ПРОЧТИМЕНЯ.md) | [简体中文](/docs/zh-Hans/自述文件.md) | [[+]](https://weblate.librescore.org/projects/librescore/docs)
4 |
5 | [//]: # "\+\_==!|!=_=!|!==_/+/ ***NE MODIFIEZ PAS AU-DESSUS DE CETTE LIGNE*** /+/^^+#|#+^+#|#+^^\+\"
6 |
7 | # dl-librescore
8 |
9 |
10 |
11 |

12 |
13 | [](https://discord.gg/DKu7cUZ4XQ) [](https://weblate.librescore.org/engage/librescore) [](https://github.com/LibreScore/app-librescore/releases/latest) [](https://github.com/LibreScore/dl-librescore/releases/latest) [](https://www.npmjs.com/package/dl-librescore)
14 |
15 | Télécharger des partitions
16 |
17 |
18 |
19 | > AVIS DE NON-RESPONSABILITÉ : Ceci n'est pas un produit MuseScore officiellement approuvé
20 |
21 | ## Installation
22 |
23 | Il existe 4 programmes installables différents :
24 |
25 | | Programme | MSCZ | MIDI | MP3 | PDF | Conversion | | Windows | macOS | Linux | Android | iOS/iPadOS |
26 | | ---------------------------------------------------------------------------------- | ---- | ---- | --- | --- | ---------- | --- | ------- | ----- | ----- | ------- | ---------- |
27 | | [App](#app) | ✔️ | ✔️ | ✔️ | ❌ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
28 | | [Script utilisateur](#script-utilisateur) | ❌ | ✔️ | ✔️ | ✔️ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
29 | | [Outil de ligne-de-commande](#outil-de-ligne-de-commande) | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
30 | | [Site web de Webmscore](#site-web-de-webmscore) | ❌ | ❌ | ❌ | ❌ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
31 |
32 | > Remarque : `Conversion` fait référence à la possibilité de convertir des fichiers en d'autres types de fichiers, y compris ceux qui ne sont pas téléchargeables dans le programme.
33 | > Les types de conversion incluent : Parties individuelles, PDF, PNG, SVG, MP3, WAV, FLAC, OGG, MIDI, MusicXML, MSCZ et MSCX.
34 |
35 | ### App
36 |
37 | 1. Allez sur la page [LISEZMOI](https://github.com/LibreScore/app-librescore/blob/master/docs/fr/LISEZMOI.md#installation) du dépôt `app-librescore`
38 | 2. Suivez les instructions d'installation de votre appareil
39 |
40 | ### Script utilisateur
41 |
42 | > Remarque : Si votre appareil est sous iOS ou iPadOS, veuillez suivre les instructions [Raccourci](#raccourci).
43 | >
44 | > Remarque : Si vous ne parvenez pas à installer les extensions de navigateur sur votre appareil, veuillez plutôt suivre les instructions [Marque-page](#marque-page).
45 |
46 | #### Extension de navigateur
47 |
48 | 1. Installez [Tampermonkey](https://www.tampermonkey.net)
49 |
50 | > Remarque : Si vous avez déjà installé une ancienne version du script appelée « musescore-downloader », « mcsz downloader » ou « musescore-dl », veuillez la désinstaller depuis le tableau de bord Tampermonkey
51 |
52 | 2. Accédez au dernier fichier [dl-librescore.user.js](https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js)
53 | 3. Appuyez sur le bouton Installer
54 |
55 | #### Raccourci
56 |
57 | 1. Installez le [raccourci LibreScore](https://www.icloud.com/shortcuts/901d8778d2da4f7db9272d3b2232d0fe)
58 | 2. Dans Safari, lors de l'affichage d'une chanson sur MuseScore, appuyez sur

59 | 3. Appuyez sur le raccourci LibreScore pour activer l'extension
60 |
61 | > Remarque : Avant de pouvoir exécuter JavaScript depuis un raccourci, vous devez activer « Autoriser l’exécution de scripts »
62 | >
63 | > 1. Accédez à Réglages

> Raccourcis > Avancés
64 | > 2. Activez « Autoriser l’exécution de scripts »
65 |
66 | #### Marque-page
67 |
68 | 1. Créez un nouveau marque-page (généralement Ctrl+D)
69 | 2. Tapez `LibreScore` pour le champ Nom
70 | 3. Tapez `javascript:(function () {let code = document.createElement('script');code.src = 'https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js';document.body.appendChild(code);}())` pour le champ URL
71 | 4. Enregistrez le marque-page
72 | 5. Lors de l'affichage d'une chanson sur MuseScore, cliquez sur le marque-page pour activer l'extension
73 |
74 | ### Outil de ligne-de-commande
75 |
76 | 1. Installez [Node.js LTS](https://nodejs.org/fr)
77 | 2. Ouvrez un terminal (n'ouvrez _pas_ l'application Node.js)
78 | 3. Tapez `npx dl-librescore@latest`, puis appuyez sur `Entrée ↵`
79 |
80 | ### Site web de Webmscore
81 |
82 | 1. Ouvrez [Webmscore](https://webmscore-pwa.librescore.org)
83 |
84 | > Remarque : Vous pouvez accéder au site web hors ligne en l'installant en tant que PWA
85 |
86 | ## Compilation
87 |
88 | 1. Installez [Node.js LTS](https://nodejs.org/fr)
89 | 2. `npm install` pour installer les packages
90 | 3. `npm run build` pour compiler
91 |
92 | - Installez `./dist/main.user.js` avec Tampermonkey
93 | - `node ./dist/cli.js` pour exécuter l'outil de ligne-de-commande
94 |
95 |
96 |
--------------------------------------------------------------------------------
/docs/hu/OLVASSAEL.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [čeština](/docs/cs/PŘEČTĚTEMĚ.md) | [English](/docs/en/README.md) | [español](/docs/es/LÉAME.md) | [français](/docs/fr/LISEZMOI.md) | **magyar** | [Melayu](/docs/ms/BACASAYA.md) | [русский](/docs/ru/ПРОЧТИМЕНЯ.md) | [简体中文](/docs/zh-Hans/自述文件.md) | [[+]](https://weblate.librescore.org/projects/librescore/docs)
4 |
5 | [//]: # "\+\_==!|!=_=!|!==_/+/ ***NE MÓDOSÍTS SEMMIT E SOR FELETT*** /+/^^+#|#+^+#|#+^^\+\"
6 |
7 | # dl-librescore
8 |
9 |
10 |
11 |

12 |
13 | [](https://discord.gg/DKu7cUZ4XQ) [](https://weblate.librescore.org/engage/librescore) [](https://github.com/LibreScore/app-librescore/releases/latest) [](https://github.com/LibreScore/dl-librescore/releases/latest) [](https://www.npmjs.com/package/dl-librescore)
14 |
15 | Kotta letöltése
16 |
17 |
18 |
19 | > NYILATKOZAT: Ez nem egy hivatalosan jóváhagyott MuseScore termék
20 |
21 | ## Telepítés
22 |
23 | 4 különböző telepíthető program van:
24 |
25 | | Program | MSCZ | MIDI | MP3 | PDF | Konvertálás | | Windows | macOS | Linux | Android | iOS/iPadOS |
26 | | ---------------------------------------------------------------------------------- | ---- | ---- | --- | --- | ---------- | --- | ------- | ----- | ----- | ------- | ---------- |
27 | | [App](#app) | ✔️ | ✔️ | ✔️ | ❌ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
28 | | [Userscript](#userscript) | ❌ | ✔️ | ✔️ | ✔️ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
29 | | [Parancssoros eszköz](#parancssoros-eszköz) | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
30 | | [Webmscore webhely](#webmscore-webhely) | ❌ | ❌ | ❌ | ❌ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
31 |
32 | > Megjegyzés: A `Konvertálás` a fájltípus megváltoztatására használható, olyan fájl típusra is konvertálhatsz amiben esetleg nem is tudtad letölteni a kottát.
33 | > Ezekre lehet konvertálni: Külön kivonatok, PDF, PNG, SVG, MP3, WAV, FLAC, OGG, MIDI, MusicXML, MSCZ és MSCX.
34 |
35 | ### App
36 |
37 | 1. Menjen aa `app-librescore` tárhely [OLVASSAEL](https://github.com/LibreScore/app-librescore/blob/master/docs/hu/OLVASSAEL.md#telepítés) oldalára
38 | 2. Kövesse a telepítési utasításokat az eszközödhöz
39 |
40 | ### Userscript
41 |
42 | > Megjegyzés: Ha eszköze iOS vagy iPadOS operációs rendszert használ, kövesse a [Parancsok](#parancsok) utasításokat.
43 | >
44 | > Megjegyzés: Ha nem tud böngésző bővítményeket telepíteni eszközére, kövesse a [Könyvjelző](#könyvjelző) utasításokat.
45 |
46 | #### Böngésző bővítmény
47 |
48 | 1. Telepítse [Tampermonkey](https://www.tampermonkey.net)
49 |
50 | > Megjegyzés: Ha korábban már telepítetted a "musescore-downloader", "mcsz downloader", "mcsz downloader" vagy "musescore-dl" nevű script egy régebbi verzióját, távolítsd el a Tampermonkey kezelőfalról.
51 |
52 | 2. Nyissa meg a legújabb [dl-librescore.user.js](https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js) fájlt
53 | 3. Nyomja meg a Telepítés gombot
54 |
55 | #### Parancsok
56 |
57 | 1. Telepítse a [LibreScore parancsot](https://www.icloud.com/shortcuts/901d8778d2da4f7db9272d3b2232d0fe)
58 | 2. A Safariban, amikor egy dalt néz meg a MuseScore-on, érintse meg a

59 | 3. Érintse meg a LibreScore parancsot a bővítmény aktiválásához
60 |
61 | > Megjegyzés: Mielőtt futtathatna JavaScriptet egy parancsból, be kell kapcsolnia Szkriptek futtatásának engedélyezése lehetőséget.
62 | >
63 | > 1. Válassza a Beállítások

> Parancsok > Haladó menüpontot.
64 | > 2. Kapcsolja be a Szkriptek futtatásának engedélyezése lehetőséget.
65 |
66 | #### Könyvjelző
67 |
68 | 1. Hozzon létre egy új könyvjelzőt (általában Ctrl+D)
69 | 2. Írja be a `LibreScore` szót a Név mezőbe
70 | 3. Írja be a következőt `javascript:(function () {let code = document.createElement('script');code.src = 'https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js';document.body.appendChild(code);}())` az URL mezőben
71 | 4. Mentse el a könyvjelzőt
72 | 5. Amikor egy dalt néz meg a MuseScore-on, kattintson a könyvjelzőre a bővítmény aktiválásához
73 |
74 | ### Parancssoros eszköz
75 |
76 | 1. Telepítse a [Node.js LTS-t](https://nodejs.org)
77 | 2. Nyisson meg egy terminált (_ne_ nyissa meg a Node.js alkalmazást)
78 | 3. Írja be, hogy `npx dl-librescore@latest`, majd nyomja meg az `Enter ↵` billentyűt
79 |
80 | ### Webmscore webhely
81 |
82 | 1. [Webmscore](https://webmscore-pwa.librescore.org) megnyitása
83 |
84 | > Megjegyzés: A webhelyet offline is elérheti, ha PWA-ként telepíti
85 |
86 | ## Összeállítás
87 |
88 | 1. Telepítse a [Node.js LTS-t](https://nodejs.org)
89 | 2. `npm install` a csomagok telepítéséhez
90 | 3. `npm run build` a fordításhoz
91 |
92 | - Telepítse a `./dist/main.user.js` fájlt a Tampermonkey segítségével
93 | - `node ./dist/cli.js` a parancssori eszköz futtatásához
94 |
95 |
96 |
--------------------------------------------------------------------------------
/docs/ms/BACASAYA.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [čeština](/docs/cs/PŘEČTĚTEMĚ.md) | [English](/docs/en/README.md) | [español](/docs/es/LÉAME.md) | [français](/docs/fr/LISEZMOI.md) | [magyar](/docs/hu/OLVASSAEL.md) | **Melayu** | [русский](/docs/ru/ПРОЧТИМЕНЯ.md) | [简体中文](/docs/zh-Hans/自述文件.md) | [[+]](https://weblate.librescore.org/projects/librescore/docs)
4 |
5 | [//]: # "\+\_==!|!=_=!|!==_/+/ ***JANGAN EDIT DI ATAS BARIS INI*** /+/^^+#|#+^+#|#+^^\+\"
6 |
7 | # dl-librescore
8 |
9 |
10 |
11 |

12 |
13 | [](https://discord.gg/DKu7cUZ4XQ) [](https://weblate.librescore.org/engage/librescore) [](https://github.com/LibreScore/app-librescore/releases/latest) [](https://github.com/LibreScore/dl-librescore/releases/latest) [](https://www.npmjs.com/package/dl-librescore)
14 |
15 | Muat turun lembaran muzik
16 |
17 |
18 |
19 | > PENAFIAN: Ini bukan produk MuseScore yang disahkan secara rasmi
20 |
21 | ## Pemasangan
22 |
23 | Terdapat 4 program boleh dipasang yang berbeza:
24 |
25 | | Program | MSCZ | MIDI | MP3 | PDF | Penukaran | | Windows | macOS | Linux | Android | iOS/iPadOS |
26 | | ---------------------------------------------------------------------------------- | ---- | ---- | --- | --- | ---------- | --- | ------- | ----- | ----- | ------- | ---------- |
27 | | [Apl](#apl) | ✔️ | ✔️ | ✔️ | ❌ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
28 | | [Skrip pengguna](#skrip-pengguna) | ❌ | ✔️ | ✔️ | ✔️ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
29 | | [Alat baris arahan](#alat-baris-arahan) | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
30 | | [Laman web Webmscore](#laman-web-webmscore) | ❌ | ❌ | ❌ | ❌ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
31 |
32 | > Nota: `Penukaran` merujuk kepada keupayaan untuk menukar fail kepada jenis fail lain, termasuk yang tidak boleh dimuat turun dalam program.
33 | > Jenis penukaran termasuk: Bahagian Individu, PDF, PNG, SVG, MP3, WAV, FLAC, OGG, MIDI, MusicXML, MSCZ dan MSCX.
34 |
35 | ### Apl
36 |
37 | 1. Pergi ke halaman [BACASAYA](https://github.com/LibreScore/app-librescore/blob/master/docs/ms/BACASAYA.md#pemasangan) repositori `app-librescore`
38 | 2. Ikut arahan pemasangan untuk peranti anda
39 |
40 | ### Skrip pengguna
41 |
42 | > Nota: Jika peranti anda menggunakan iOS atau iPadOS, sila ikut arahan [Pintasan](#pintasan).
43 | >
44 | > Nota: Jika anda tidak boleh memasang sambungan penyemak imbas pada peranti anda, sila ikut arahan [Penanda Halaman](#penanda-halaman) sebaliknya.
45 |
46 | #### Sambungan penyemak imbas
47 |
48 | 1. Pasang [Tampermonkey](https://www.tampermonkey.net)
49 |
50 | > Nota: Jika anda telah memasang versi lama skrip yang dipanggil "musescore-downloader", "mcsz downloader", atau "musescore-dl", sila nyahpasangnya daripada papan pemuka Tampermonkey
51 |
52 | 2. Pergi ke fail [dl-librescore.user.js](https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js) terkini
53 | 3. Tekan butang Pasang
54 |
55 | #### Pintasan
56 |
57 | 1. Pasang [pintasan LibreScore](https://www.icloud.com/shortcuts/901d8778d2da4f7db9272d3b2232d0fe)
58 | 2. Dalam Safari, apabila melihat lagu di MuseScore, ketik

59 | 3. Ketik pintasan LibreScore untuk mengaktifkan sambungan
60 |
61 | > Nota: Sebelum anda boleh menjalankan JavaScript daripada pintasan, anda mesti menghidupkan Benarkan Skrip Berjalan
62 | >
63 | > 1. Pergi ke Tetapan

> Pintasan > Advanced
64 | > 2. Menyalakan Izinkan Menjalankan Skrip
65 |
66 | #### Penanda buku
67 |
68 | 1. Buat penanda halaman baharu (biasanya Ctrl+D)
69 | 2. Taipkan `LibreScore` untuk medan Nama
70 | 3. Taipkan `javascript:(function () {let code = document.createElement('script');code.src = 'https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js';document.body.appendChild(code);}())` untuk medan URL
71 | 4. Simpan penanda halaman
72 | 5. Apabila melihat lagu di MuseScore, klik penanda halaman untuk mengaktifkan sambungan
73 |
74 | ### Alat baris arahan
75 |
76 | 1. Pasang [Node.js LTS](https://nodejs.org)
77 | 2. Buka terminal (_jangan_ buka aplikasi Node.js)
78 | 3. Taip `npx dl-librescore@latest`, kemudian tekan `Enter ↵`
79 |
80 | ### Laman web Webmscore
81 |
82 | 1. Buka [Webmscore](https://webmscore-pwa.librescore.org)
83 |
84 | > Nota: Anda boleh mengakses tapak web di luar talian dengan memasangnya sebagai PWA
85 |
86 | ## Penyusunan
87 |
88 | 1. Pasang [Node.js LTS](https://nodejs.org)
89 | 2. `npm install` untuk memasang pakej
90 | 3. `npm run build` untuk membina
91 |
92 | - Pasang `./dist/main.user.js` dengan Tampermonkey
93 | - `node ./dist/cli.js` untuk menjalankan alat baris arahan
94 |
95 |
96 |
--------------------------------------------------------------------------------
/docs/ru/ПРОЧТИМЕНЯ.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [čeština](/docs/cs/PŘEČTĚTEMĚ.md) | [English](/docs/en/README.md) | [español](/docs/es/LÉAME.md) | [français](/docs/fr/LISEZMOI.md) | [magyar](/docs/hu/OLVASSAEL.md) | [Melayu](/docs/ms/BACASAYA.md) | **русский** | [简体中文](/docs/zh-Hans/自述文件.md) | [[+]](https://weblate.librescore.org/projects/librescore/docs)
4 |
5 | [//]: # "\+\_==!|!=_=!|!==_/+/ ***НЕ РЕДАКТИРОВАТЬ ВЫШЕ ЭТОЙ ЛИНИИ*** /+/^^+#|#+^+#|#+^^\+\"
6 |
7 | # dl-librescore
8 |
9 |
10 |
11 |

12 |
13 | [](https://discord.gg/DKu7cUZ4XQ) [](https://weblate.librescore.org/engage/librescore) [](https://github.com/LibreScore/app-librescore/releases/latest) [](https://github.com/LibreScore/dl-librescore/releases/latest) [](https://www.npmjs.com/package/dl-librescore)
14 |
15 | Скачать ноты
16 |
17 |
18 |
19 | > ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Это не официально одобренный продукт MuseScore↵
20 |
21 | ## Установка
22 |
23 | Существует 4 различных устанавливаемых программы:
24 |
25 | | Программа | MSCZ | MIDI | MP3 | PDF | Преобразование | | Windows | macOS | Linux | Android | iOS/iPadOS |
26 | | ---------------------------------------------------------------------------------- | ---- | ---- | --- | --- | ---------- | --- | ------- | ----- | ----- | ------- | ---------- |
27 | | [Приложение](#приложение) | ✔️ | ✔️ | ✔️ | ❌ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
28 | | [Юзерскрипт](#юзерскрипт) | ❌ | ✔️ | ✔️ | ✔️ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
29 | | [Инструмент командной-строки](#инструмент-командной-строки) | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
30 | | [Веб-сайт Вэбмскор](#веб-сайт-вэбмскор) | ❌ | ❌ | ❌ | ❌ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
31 |
32 | > Примечание: `Преобразование` относится к возможности преобразовывать файлы в другие типы файлов, в том числе те, которые нельзя загрузить в программе.
33 | > Типы преобразования включают: отдельные партии, PDF, PNG, SVG, MP3, WAV, FLAC, OGG, MIDI, MusicXML, MSCZ и MSCX.
34 |
35 | ### Приложение
36 |
37 | 1. Перейдите на страницу [ПРОЧТИМЕНЯ](https://github.com/LibreScore/app-librescore/blob/master/docs/ru/ПРОЧТИМЕНЯ.md#установка) репозитория `app-librescore`
38 | 2. Следуйте инструкциям по установке для вашего устройства
39 |
40 | ### Юзерскрипт
41 |
42 | > Примечание: Если ваше устройство работает под управлением iOS или iPadOS, следуйте инструкциям [Быстрая команда](#быстрая-команда).
43 | >
44 | > Примечание: Если вы не можете установить расширения браузера на своё устройство, следуйте инструкциям [Закладка](#закладка).
45 |
46 | #### Расширение для браузера
47 |
48 | 1. Установите [Tampermonkey](https://www.tampermonkey.net/?locale=ru)
49 |
50 | > Примечание: Если вы уже установили старую версию скрипта под названием «musescore-downloader», «mscz downloader» или «musescore-dl», удалите её с панели инструментов Tampermonkey
51 |
52 | 2. Перейдите к последнему файлу [dl-librescore.user.js](https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js)
53 | 3. Нажмите кнопку Установить
54 |
55 | #### Быстрая команда
56 |
57 | 1. Установите [ярлык LibreScore](https://www.icloud.com/shortcuts/901d8778d2da4f7db9272d3b2232d0fe).
58 | 2. В Safari при просмотре песни на MuseScore нажмите

59 | 3. Нажмите ярлык LibreScore, чтобы активировать расширение
60 |
61 | > Примечание: Чтобы выполнять код JavaScript из быстрой команды, необходимо включить настройку «Запуск скриптов»
62 | >
63 | > 1. Откройте «Настройки»

> «Быстрые команды» > «Дополнения»
64 | > 2. Включите настройку «Запуск скриптов»
65 |
66 | #### Закладка
67 |
68 | 1. Создайте новуб закладку (обычно Ctrl+D)
69 | 2. Введите `LibreScore` для поля Name
70 | 3. Введите `javascript:(function () {let code = document.createElement('script');code.src = 'https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js';document.body.appendChild(code);}())` для поля URL
71 | 4. Сохраните закладку
72 | 5. Во время просмотра песни на MuseScore, кликните на закладку, чтобы активировать расширение
73 |
74 | ### Инструмент командной-строки
75 |
76 | 1. Установите [Node.js LTS](https://nodejs.org).
77 | 2. Откройте терминал (не открывайте приложение Node.js)
78 | 3. Введите `npx dl-librescore@latest`, затем нажмите `Enter ↵`
79 |
80 | ### Веб-сайт Вэбмскор
81 |
82 | 1. Откройте [Webmscore](https://webmscore-pwa.librescore.org)
83 |
84 | > Примечание. Вы можете получить доступ к веб-сайту в автономном режиме, установив его как PWA.
85 |
86 | ## Сборка
87 |
88 | 1.Установите [Node.js LTS](https://nodejs.org/ru)
89 | 2. `npm install`,чтобы установить пакеты
90 | 3. `npm run build` чтобы собрать
91 |
92 | - Установите `./dist/main.user.js`, используя Tampermonkey
93 | - Введите `node ./dist/cli.js`, чтобы запустить утилиту командной строки
94 |
95 |
96 |
--------------------------------------------------------------------------------
/docs/zh-Hans/自述文件.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [čeština](/docs/cs/PŘEČTĚTEMĚ.md) | [English](/docs/en/README.md) | [español](/docs/es/LÉAME.md) | [français](/docs/fr/LISEZMOI.md) | [magyar](/docs/hu/OLVASSAEL.md) | [Melayu](/docs/ms/BACASAYA.md) | [русский](/docs/ru/ПРОЧТИМЕНЯ.md) | **简体中文** | [[+]](https://weblate.librescore.org/projects/librescore/docs)
4 |
5 | [//]: # "\+\_==!|!=_=!|!==_/+/ ***不要在此行上方编辑*** /+/^^+#|#+^+#|#+^^\+\"
6 |
7 | # dl-librescore
8 |
9 |
10 |
11 |

12 |
13 | [](https://discord.gg/DKu7cUZ4XQ) [](https://weblate.librescore.org/engage/librescore) [](https://github.com/LibreScore/app-librescore/releases/latest) [](https://github.com/LibreScore/dl-librescore/releases/latest) [](https://www.npmjs.com/package/dl-librescore)
14 |
15 | 下载乐谱
16 |
17 |
18 |
19 | > 免责声明:这不是官方认可的 MuseScore 产品
20 |
21 | ## 安装
22 |
23 | 有4种不同的安装程序:
24 |
25 | | 程序 | MSCZ | MIDI | MP3 | PDF | 转换 | | Windows | macOS | Linux | 安卓 | iOS/iPadOS |
26 | | ---------------------------------------------------------------------------------- | ---- | ---- | --- | --- | ---------- | --- | ------- | ----- | ----- | ------- | ---------- |
27 | | [应用程序](#应用程序) | ✔️ | ✔️ | ✔️ | ❌ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
28 | | [用户脚本](#用户脚本) | ❌ | ✔️ | ✔️ | ✔️ | ❌ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
29 | | [命令行工具](#命令行工具) | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
30 | | [Webmscore 尔网站](#webmscore-尔网站) | ❌ | ❌ | ❌ | ❌ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
31 |
32 | > 注意:`轉換`是指將文件轉換為其他文件類型的能力,包括那些不能在此程式中下載的文件。
33 | > 轉換類型包括:樂譜中的單個樂器、PDF、PNG、SVG、MP3、WAV、FLAC、OGG、MIDI、MusicXML、MSCZ 和 MSCX。
34 |
35 | ### 应用程序
36 |
37 | 1. 前往 [README](https://github.com/LibreScore/app-librescore#installation) 页面找到 `app-librescore` 储存库
38 | 2. 按照您设备的安装流程进行安装
39 |
40 | ### 用户脚本
41 |
42 | > 注意:如果您的设备运行的系统是 iOS 或者 iPadOS,请参考: [Shortcut](#shortcut)
43 | >
44 | > 注意:如果您无法在您的设备上安装浏览器扩展插件,请参考以下替代方案: [Bookmark](#bookmark)
45 |
46 | #### 浏览器扩展插件
47 |
48 | 1. 安装 [Tampermonkey](https://www.tampermonkey.net)
49 |
50 | > 提示:如果您已经安装了名为“musescore-downloader”、“mcsz downloader”或“musescore-dl”的旧版本脚本,请从Tampermonkey管理面板中卸载它
51 |
52 | 2. 访问最新的 [dl-librescore.user.js](https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js) 文件
53 | 3. 按下安装按钮
54 |
55 | #### 快捷方式
56 |
57 | 1. 安装 [LibreScore快捷方式](https://www.icloud.com/shortcuts/901d8778d2da4f7db9272d3b2232d0fe)
58 | 2. 在Safari中,当在MuseScore上查看歌曲时,点击

59 | 3. 点击LibreScore快捷方式激活扩展
60 |
61 | > 注意:您必须先打开“允许运行脚本”,才能从快捷指令运行 JavaScript
62 | >
63 | > 1. 前往“设置”

> “快捷指令” > “高级”
64 | > 2. 打开“允许运行脚本”
65 |
66 | #### 书签
67 |
68 | 1. 创建一个新书签(通常是Ctrl+D)
69 | 2. 在Name字段中输入 `LibreScore`
70 | 3. 键入 `javascript:(function () {let code = document.createElement('script');code.src = 'https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js';document.body.appendChild(code);}())` 用于URL字段
71 | 4. 保存书签
72 | 5. 在MuseScore上查看歌曲时,单击书签以激活扩展
73 |
74 | ### 命令行工具
75 |
76 | 1. 安装 [Node.js LTS](https://nodejs.org)
77 | 2. 打开一个终端(Terminal)窗口 (不要打开 the Node.js 应用程序)
78 | 3. 在窗口中输入 `npx dl-librescore@latest`,然后按 `回车键(Enter) ↵`
79 |
80 | ### Webmscore 网站
81 |
82 | 1. 打开 [Webmscore](https://webmscore-pwa.librescore.org)网站
83 |
84 | > 提示:通过把它安装为渐进式Web应用(PWA),您可以离线访问这个网站
85 |
86 | ## 构建方法
87 |
88 | 1. 安装 [Node.js LTS](https://nodejs.org)
89 | 2.运行 `npm install` 来安装程序包
90 | 3. 运行`npm run build` 来构建程序
91 |
92 | - 用油猴(篡改猴,Tampermonkey)安装 `./dist/main.user.js`
93 | - 运行`node ./dist/cli.js` 来启动命令行工具
94 |
95 |
96 |
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LibreScore/dl-librescore/5d7eb5a022b0bc3ae0db5af2381af0ab7c46644e/images/logo.png
--------------------------------------------------------------------------------
/lang.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #* STEP 0
4 | # sudo npm i -g prettier
5 | I18N_PATH="./src/i18n"
6 |
7 | #* STEP 1
8 | shopt -s nullglob
9 |
10 | #* STEP 3
11 | import_first_line=$(awk '/'"import i18n from"'/,/'"function getLocale"'/ {printf NR "\n"}' src/i18n/index.ts | head -n 1)
12 | import_last_line=$(awk '/'"import i18n from"'/,/'"function getLocale"'/ {printf NR "\n"}' src/i18n/index.ts | tail -n 3 | head -n 1)
13 | sed -i "${import_first_line},${import_last_line}d" src/i18n/index.ts
14 | sed -i "${import_first_line}i import i18n from "'"'"i18next"'"'";" src/i18n/index.ts
15 |
16 | map_first_line=$(awk '/'"languageMap = \["'/,/'"\];"'/ {printf NR "\n"}' src/i18n/index.ts | head -n 1)
17 | map_last_line=$(awk '/'"languageMap = \["'/,/'"\];"'/ {printf NR "\n"}' src/i18n/index.ts | tail -n 1 | head -n 1)
18 | sed -i "${map_first_line},${map_last_line}d" src/i18n/index.ts
19 | sed -i "${map_first_line}i let languageMap = [" src/i18n/index.ts
20 |
21 | resources_first_line=$(awk '/'"resources: \{"'/,/'" \},"'/ {printf NR "\n"}' src/i18n/index.ts | head -n 1)
22 | resources_last_line=$(awk '/'"resources: \{"'/,/'" \},"'/ {printf NR "\n"}' src/i18n/index.ts | tail -n 1 | head -n 1)
23 | sed -i "${resources_first_line},${resources_last_line}d" src/i18n/index.ts
24 | sed -i "${resources_first_line}i resources: {" src/i18n/index.ts
25 |
26 | for file in $I18N_PATH/*.json; do
27 | filename=${file##*/}
28 | filename=${filename%%\.*}
29 | unsanitized_filename=${filename//-/$'_'}
30 | sed -i "${import_first_line}s"'/$/'"import $unsanitized_filename from "'"'".\/$filename.json"'"'";"'/' src/i18n/index.ts
31 | sed -i "${map_first_line}s"'/$/"'"$filename"'",/' src/i18n/index.ts
32 | sed -i "${resources_first_line}s"'/$/'""'"'"$filename"'"'": { translation: $unsanitized_filename },"'/' src/i18n/index.ts
33 | done
34 | sed -i "${map_first_line}s"'/$/'"];"'/' src/i18n/index.ts
35 | sed -i "${resources_first_line}s"'/$/'"},"'/' src/i18n/index.ts
36 |
37 | #* STEP 4
38 | prettier --write ./src/i18n/index.ts
39 |
40 | shopt -u nullglob
41 |
42 | exit 0
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dl-librescore",
3 | "version": "0.35.29",
4 | "description": "Download sheet music",
5 | "main": "dist/main.user.js",
6 | "bin": "dist/cli.js",
7 | "type": "module",
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/LibreScore/dl-librescore.git"
11 | },
12 | "author": "LibreScore",
13 | "license": "MIT",
14 | "bugs": {
15 | "url": "https://github.com/LibreScore/dl-librescore/issues"
16 | },
17 | "homepage": "https://github.com/LibreScore/dl-librescore#readme",
18 | "homepage_url": "https://github.com/LibreScore/dl-librescore#readme",
19 | "manifest_version": 2,
20 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
21 | "content_scripts": [
22 | {
23 | "matches": [
24 | "*://*.musescore.com/*/*"
25 | ],
26 | "js": [
27 | "src/web-ext.js"
28 | ],
29 | "run_at": "document_start"
30 | }
31 | ],
32 | "web_accessible_resources": [
33 | "dist/main.user.js"
34 | ],
35 | "dependencies": {
36 | "@librescore/fonts": "^0.4.1",
37 | "@librescore/sf3": "^0.8.0",
38 | "detect-node": "^2.1.0",
39 | "i18next": "^23.16.8",
40 | "inquirer": "^8.2.6",
41 | "md5": "^2.3.0",
42 | "node-fetch": "^2.7.0",
43 | "ora": "^5.4.1",
44 | "proxy-agent": "^5.0.0",
45 | "webmscore": "^1.2.1",
46 | "yargs": "^17.7.2"
47 | },
48 | "devDependencies": {
49 | "@crokita/rollup-plugin-node-builtins": "^2.1.3",
50 | "@rollup/plugin-json": "^6.1.0",
51 | "@types/file-saver": "^2.0.7",
52 | "@types/inquirer": "^9.0.7",
53 | "@types/md5": "^2.3.5",
54 | "@types/pdfkit": "^0.13.8",
55 | "@types/yargs": "^17.0.33",
56 | "pdfkit": "git+https://github.com/LibreScore/pdfkit.git",
57 | "rollup": "^4.29.1",
58 | "@rollup/plugin-commonjs": "^28.0.2",
59 | "rollup-plugin-node-globals": "^1.4.0",
60 | "@rollup/plugin-node-resolve": "^16.0.0",
61 | "rollup-plugin-string": "^3.0.0",
62 | "rollup-plugin-typescript": "^1.0.1",
63 | "svg-to-pdfkit": "^0.1.8",
64 | "tslib": "^2.8.1",
65 | "typescript": "^4.7.2"
66 | },
67 | "overrides": {
68 | "whatwg-url": "14.x"
69 | },
70 | "scripts": {
71 | "build": "rollup -c",
72 | "watch": "npm run build -- --watch",
73 | "start:ext": "web-ext run --url https://musescore.com/",
74 | "start:ext-chrome": "npm run start:ext -- -t chromium",
75 | "pack:ext": "zip -r dist/ext.zip manifest.json src/web-ext.js dist/main.user.js",
76 | "bump-version:patch": "npm version patch --no-git-tag && npm run build",
77 | "bump-version:minor": "npm version minor --no-git-tag && npm run build"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from "rollup-plugin-typescript";
2 | import resolve from "@rollup/plugin-node-resolve";
3 | import commonjs from "@rollup/plugin-commonjs";
4 | import builtins from "@crokita/rollup-plugin-node-builtins";
5 | import nodeGlobals from "rollup-plugin-node-globals";
6 | import json from "@rollup/plugin-json";
7 | import { string } from "rollup-plugin-string";
8 | import fs from "fs";
9 |
10 | const getBannerText = () => {
11 | const packageJson = JSON.parse(fs.readFileSync("./package.json", "utf-8"));
12 | const { version } = packageJson;
13 | let bannerText = fs.readFileSync("./src/meta.js", "utf-8");
14 | bannerText = bannerText.replace("%VERSION%", version);
15 | return bannerText;
16 | };
17 |
18 | const getWrapper = (startL, endL) => {
19 | const js = fs.readFileSync("./src/wrapper.js", "utf-8");
20 | return js.split(/\n/g).slice(startL, endL).join("\n");
21 | };
22 |
23 | const basePlugins = [
24 | typescript({
25 | target: "ES6",
26 | sourceMap: false,
27 | allowJs: true,
28 | lib: ["ES6", "dom"],
29 | }),
30 | resolve({
31 | preferBuiltins: true,
32 | jsnext: true,
33 | extensions: [".js", ".ts"],
34 | }),
35 | commonjs({
36 | extensions: [".js", ".ts"],
37 | }),
38 | json(),
39 | string({
40 | include: "**/*.css",
41 | }),
42 | {
43 | /**
44 | * remove tslib license comments
45 | * @param {string} code
46 | * @param {string} id
47 | */
48 | transform(code, id) {
49 | if (id.includes("tslib")) {
50 | code = code.split(/\r?\n/g).slice(15).join("\n");
51 | }
52 | return {
53 | code,
54 | };
55 | },
56 | },
57 | ];
58 |
59 | const plugins = [
60 | ...basePlugins,
61 | builtins(),
62 | nodeGlobals({
63 | dirname: false,
64 | filename: false,
65 | baseDir: false,
66 | }),
67 | ];
68 |
69 | export default [
70 | {
71 | input: "src/worker.ts",
72 | output: {
73 | file: "dist/cache/worker.js",
74 | format: "iife",
75 | name: "worker",
76 | banner: "export const PDFWorker = function () { ",
77 | footer: "return worker\n}\n",
78 | sourcemap: false,
79 | },
80 | plugins,
81 | },
82 | {
83 | input: "src/main.ts",
84 | output: {
85 | file: "dist/main.user.js",
86 | format: "iife",
87 | sourcemap: false,
88 | banner: getBannerText,
89 | intro: () => getWrapper(0, -5),
90 | outro: () => getWrapper(-5),
91 | },
92 | plugins,
93 | },
94 | {
95 | input: "src/cli.ts",
96 | output: {
97 | file: "dist/cli.js",
98 | format: "cjs",
99 | banner: "#!/usr/bin/env node",
100 | sourcemap: false,
101 | },
102 | plugins: basePlugins,
103 | },
104 | ];
105 |
--------------------------------------------------------------------------------
/src/anti-detection.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-extend-native */
2 | /* eslint-disable @typescript-eslint/ban-types */
3 |
4 | /**
5 | * make hooked methods "native"
6 | */
7 | export const makeNative = (() => {
8 | const l = new Map();
9 |
10 | hookNative(
11 | Function.prototype,
12 | "toString",
13 | (_toString) => {
14 | return function () {
15 | if (l.has(this)) {
16 | const _fn = l.get(this) || parseInt; // "function () {\n [native code]\n}"
17 | if (l.has(_fn)) {
18 | // nested
19 | return _fn.toString();
20 | } else {
21 | return _toString.call(_fn) as string;
22 | }
23 | }
24 | return _toString.call(this) as string;
25 | };
26 | },
27 | true
28 | );
29 |
30 | return (fn: Function, original: Function) => {
31 | l.set(fn, original);
32 | };
33 | })();
34 |
35 | export function hookNative(
36 | target: T,
37 | method: M,
38 | hook: (originalFn: T[M], detach: () => void) => T[M],
39 | async = false
40 | ): void {
41 | // reserve for future hook update
42 | const _fn = target[method];
43 | const detach = () => {
44 | target[method] = _fn; // detach
45 | };
46 |
47 | // This script can run before anything on the page,
48 | // so setting this function to be non-configurable and non-writable is no use.
49 | const hookedFn = hook(_fn, detach);
50 | target[method] = hookedFn;
51 |
52 | if (!async) {
53 | makeNative(hookedFn as any, _fn as any);
54 | } else {
55 | setTimeout(() => {
56 | makeNative(hookedFn as any, _fn as any);
57 | });
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/btn.css:
--------------------------------------------------------------------------------
1 | section {
2 | --msd-html-width: 330px;
3 | --msd-html-height: 50px;
4 | --msd-color-animation: msd-background-migration 3s ease alternate infinite;
5 | --msd-color-background: linear-gradient(60deg, #f79533, #f37055, #ef4e7b, #a166ab, #5073b8, #1098ad, #07b39b, #6fba82);
6 | -webkit-tap-highlight-color: transparent;
7 | font-family: Muse Sans, Inter, Helvetica neue, Helvetica, sans-serif;
8 | font-variant-ligatures: none;
9 | font-feature-settings: "liga" 0;
10 | -webkit-text-size-adjust: 100%;
11 | font-synthesis: none;
12 | color: #180036;
13 | box-sizing: inherit;
14 | /* margin-bottom: 12px; */
15 | gap: 6px;
16 | align-items: center;
17 | display: flex;
18 | flex-wrap: wrap;
19 | }
20 |
21 | button {
22 | --msd-html-width: 330px;
23 | --msd-html-height: 50px;
24 | --msd-color-animation: msd-background-migration 3s ease alternate infinite;
25 | --msd-color-background: linear-gradient(60deg, #f79533, #f37055, #ef4e7b, #a166ab, #5073b8, #1098ad, #07b39b, #6fba82);
26 | -webkit-tap-highlight-color: transparent;
27 | outline: none;
28 | font-variant-ligatures: none;
29 | font-feature-settings: "liga" 0;
30 | -webkit-text-size-adjust: 100%;
31 | font-synthesis: none;
32 | color-scheme: light dark;
33 | align-items: center;
34 | -webkit-appearance: none;
35 | border: 0;
36 | box-sizing: border-box;
37 | display: inline-flex;
38 | flex-wrap: nowrap;
39 | font-family: inherit;
40 | line-height: 16px;
41 | position: relative;
42 | text-align: center;
43 | text-decoration: none;
44 | user-select: none;
45 | vertical-align: baseline;
46 | white-space: nowrap;
47 | font-size: 16px;
48 | height: 40px;
49 | padding: 4px 12px;
50 | border-radius: 100px;
51 | align-self: center;
52 | min-width: 188px !important;
53 | margin: 0 !important;
54 | cursor: pointer;
55 | justify-content: center;
56 | background: #0777f2;
57 | color: #fff;
58 | }
59 |
60 | @media (min-width: 962px) {
61 | button {
62 | min-width: 183px !important;
63 | }
64 | }
65 |
66 | @media (max-width: 767px) {
67 | button {
68 | min-width: calc(50% - 8px) !important;
69 | }
70 | }
71 |
72 | button:hover {
73 | background: #0060d5;
74 | transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1), color, box-shadow;
75 | }
76 |
77 | svg {
78 | --msd-html-width: 330px;
79 | --msd-html-height: 50px;
80 | --msd-color-animation: msd-background-migration 3s ease alternate infinite;
81 | --msd-color-background: linear-gradient(60deg, #f79533, #f37055, #ef4e7b, #a166ab, #5073b8, #1098ad, #07b39b, #6fba82);
82 | -webkit-tap-highlight-color: transparent;
83 | font-variant-ligatures: none;
84 | font-feature-settings: "liga" 0;
85 | -webkit-text-size-adjust: 100%;
86 | font-synthesis: none;
87 | color-scheme: light dark;
88 | font-family: inherit;
89 | line-height: 16px;
90 | text-align: center;
91 | user-select: none;
92 | white-space: nowrap;
93 | font-size: 16px;
94 | cursor: pointer;
95 | box-sizing: inherit;
96 | background: no-repeat 50%;
97 | display: inline-block;
98 | height: 24px;
99 | max-width: none;
100 | min-width: 24px;
101 | width: 24px;
102 | fill: #fff;
103 | color: #fff;
104 | position: relative;
105 | z-index: 2;
106 | vertical-align: top;
107 | margin-right: 4px;
108 | }
109 |
110 | path {
111 | --msd-html-width: 330px;
112 | --msd-html-height: 50px;
113 | --msd-color-animation: msd-background-migration 3s ease alternate infinite;
114 | --msd-color-background: linear-gradient(60deg, #f79533, #f37055, #ef4e7b, #a166ab, #5073b8, #1098ad, #07b39b, #6fba82);
115 | -webkit-tap-highlight-color: transparent;
116 | font-variant-ligatures: none;
117 | font-feature-settings: "liga" 0;
118 | -webkit-text-size-adjust: 100%;
119 | font-synthesis: none;
120 | color-scheme: light dark;
121 | font-family: inherit;
122 | line-height: 16px;
123 | text-align: center;
124 | user-select: none;
125 | white-space: nowrap;
126 | font-size: 16px;
127 | cursor: pointer;
128 | box-sizing: inherit;
129 | fill: #fff;
130 | color: #fff;
131 | }
132 |
133 | span {
134 | --msd-html-width: 330px;
135 | --msd-html-height: 50px;
136 | --msd-color-animation: msd-background-migration 3s ease alternate infinite;
137 | --msd-color-background: linear-gradient(60deg, #f79533, #f37055, #ef4e7b, #a166ab, #5073b8, #1098ad, #07b39b, #6fba82);
138 | -webkit-tap-highlight-color: transparent;
139 | font-variant-ligatures: none;
140 | font-feature-settings: "liga" 0;
141 | -webkit-text-size-adjust: 100%;
142 | font-synthesis: none;
143 | color-scheme: light dark;
144 | font-family: inherit;
145 | line-height: 16px;
146 | text-align: center;
147 | user-select: none;
148 | white-space: nowrap;
149 | font-size: 16px;
150 | cursor: pointer;
151 | color: #fff;
152 | box-sizing: inherit;
153 | position: relative;
154 | z-index: 2;
155 | }
156 |
--------------------------------------------------------------------------------
/src/btn.ts:
--------------------------------------------------------------------------------
1 | import { useTimeout, windowOpenAsync, console, attachShadow } from "./utils";
2 | import { isGmAvailable, _GM } from "./gm";
3 | // @ts-ignore
4 | import btnListCss from "./btn.css";
5 | import i18nextInit, { i18next } from "./i18n/index";
6 |
7 | (async () => {
8 | await i18nextInit;
9 | })();
10 |
11 | type BtnElement = HTMLButtonElement;
12 |
13 | export enum ICON {
14 | DOWNLOAD_TOP = "M9.479 4.225v7.073L8.15 9.954a.538.538 0 00-.756.766l2.214 2.213a.52.52 0 00.745 0l2.198-2.203a.526.526 0 10-.745-.745l-1.287 1.308V4.225a.52.52 0 00-1.041 0z",
15 | DOWNLOAD_BOTTOM = "M16.25 11.516v5.209a.52.52 0 01-.521.52H4.27a.521.521 0 01-.521-.52v-5.209a.52.52 0 10-1.042 0v5.209a1.562 1.562 0 001.563 1.562h11.458a1.562 1.562 0 001.562-1.562v-5.209a.52.52 0 10-1.041 0z",
16 | }
17 |
18 | const buildDownloadBtn = () => {
19 | const btn = document.createElement("button") as HTMLButtonElement;
20 | btn.type = "button";
21 | btn.append(document.createElementNS("http://www.w3.org/2000/svg", "svg"));
22 | btn.append(document.createElement("span"));
23 | let btnSvg = btn.querySelector("svg")!;
24 | btnSvg.setAttribute("viewBox", "0 0 20 20");
25 | let svgPath = document.createElementNS(
26 | "http://www.w3.org/2000/svg",
27 | "path"
28 | );
29 | svgPath.setAttribute("d", ICON.DOWNLOAD_TOP);
30 | svgPath.setAttribute("fill", "#fff");
31 | btnSvg.append(svgPath);
32 | svgPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
33 | svgPath.setAttribute("d", ICON.DOWNLOAD_BOTTOM);
34 | svgPath.setAttribute("fill", "#fff");
35 | btnSvg.append(svgPath);
36 | return btn;
37 | };
38 |
39 | const cloneBtn = (btn: HTMLButtonElement) => {
40 | const n = btn.cloneNode(true) as HTMLButtonElement;
41 | n.onclick = btn.onclick;
42 | return n;
43 | };
44 |
45 | interface BtnOptions {
46 | readonly name: string;
47 | readonly action: BtnAction;
48 | readonly disabled?: boolean;
49 | readonly tooltip?: string;
50 | readonly icon?: ICON;
51 | readonly lightTheme?: boolean;
52 | }
53 |
54 | export enum BtnListMode {
55 | InPage,
56 | ExtWindow,
57 | }
58 |
59 | export class BtnList {
60 | private readonly list: BtnElement[] = [];
61 |
62 | add(options: BtnOptions): BtnElement {
63 | const btnTpl = buildDownloadBtn();
64 | const setText = (btn: BtnElement) => {
65 | const textNode = btn.querySelector("span");
66 | return (str: string): void => {
67 | if (textNode) textNode.textContent = str;
68 | };
69 | };
70 |
71 | setText(btnTpl)(options.name);
72 |
73 | btnTpl.onclick = function () {
74 | const btn = this as BtnElement;
75 | options.action(options.name, btn, setText(btn));
76 | };
77 |
78 | this.list.push(btnTpl);
79 |
80 | if (options.disabled) {
81 | btnTpl.disabled = options.disabled;
82 | }
83 |
84 | if (options.tooltip) {
85 | btnTpl.title = options.tooltip;
86 | }
87 |
88 | // add buttons to the userscript manager menu
89 | if (isGmAvailable("registerMenuCommand")) {
90 | // eslint-disable-next-line no-void
91 | void _GM.registerMenuCommand(options.name, () => {
92 | options.action(options.name, btnTpl, () => undefined);
93 | });
94 | }
95 |
96 | return btnTpl;
97 | }
98 |
99 | private _commit() {
100 | const btnParent = document.querySelector(
101 | "#ELEMENT_ID_SCORE_DOWNLOAD_SECTION > section"
102 | ) as HTMLElement;
103 |
104 | let shadow = attachShadow(btnParent);
105 |
106 | // Inject the collected styles into the shadow DOM
107 | const style = document.createElement("style");
108 | style.textContent = btnListCss;
109 | shadow.appendChild(style);
110 |
111 | // hide buttons using the shadow DOM
112 | const slot = document.createElement("slot");
113 | shadow.append(slot);
114 |
115 | shadow.append(...this.list.map((e) => cloneBtn(e)));
116 |
117 | return btnParent;
118 | }
119 |
120 | /**
121 | * replace the template button with the list of new buttons
122 | */
123 | async commit(mode: BtnListMode = BtnListMode.InPage): Promise {
124 | switch (mode) {
125 | case BtnListMode.InPage: {
126 | let el: Element;
127 | el = this._commit();
128 | break;
129 | }
130 |
131 | default:
132 | throw new Error(i18next.t("unknown_button_list_mode"));
133 | }
134 | }
135 | }
136 |
137 | type BtnAction = (
138 | btnName: string,
139 | btnEl: BtnElement,
140 | setText: (str: string) => void
141 | ) => any;
142 |
143 | // eslint-disable-next-line @typescript-eslint/no-namespace
144 | export namespace BtnAction {
145 | type Promisable = T | Promise;
146 | type UrlInput = Promisable | (() => Promisable);
147 |
148 | const normalizeUrlInput = (url: UrlInput) => {
149 | if (typeof url === "function") return url();
150 | else return url;
151 | };
152 |
153 | export const download = (
154 | url: UrlInput,
155 | fileName: string,
156 | fallback?: () => Promisable,
157 | timeout?: number,
158 | target?: "_blank"
159 | ): BtnAction =>
160 | process(
161 | async (name, setText): Promise => {
162 | const _url = await normalizeUrlInput(url);
163 |
164 | if (isGmAvailable("xmlHttpRequest")) {
165 | _GM.xmlHttpRequest({
166 | method: "GET",
167 | url: _url,
168 | headers: { Range: "bytes=0-0" },
169 | onload: function (response) {
170 | const headers = response.responseHeaders;
171 | const contentDisposition = headers
172 | .split("\n")
173 | .find((header) =>
174 | header
175 | .toLowerCase()
176 | .startsWith("content-disposition")
177 | );
178 |
179 | if (contentDisposition) {
180 | const a = document.createElement("a");
181 | a.href = _url;
182 | a.download = fileName;
183 | if (target) a.target = target;
184 | a.dispatchEvent(new MouseEvent("click"));
185 | } else {
186 | fetch(_url)
187 | .then((response) => {
188 | if (!response.ok) {
189 | throw new Error(
190 | "Failed to fetch the file"
191 | );
192 | }
193 | const contentLength =
194 | response.headers.get(
195 | "Content-Length"
196 | );
197 |
198 | if (!contentLength) {
199 | return response.blob();
200 | }
201 |
202 | const contentType =
203 | response.headers.get(
204 | "Content-Type"
205 | ) || "application/octet-stream";
206 | const total = parseInt(
207 | contentLength,
208 | 10
209 | );
210 | let loaded = 0;
211 |
212 | const reader =
213 | response.body!.getReader();
214 |
215 | const stream = new ReadableStream({
216 | start(controller) {
217 | const percent = Math.round(
218 | (loaded / total) * 100
219 | );
220 | setText(`${percent}%`);
221 |
222 | function push() {
223 | reader
224 | .read()
225 | .then(
226 | ({
227 | done,
228 | value,
229 | }) => {
230 | if (done) {
231 | controller.close();
232 | setText(
233 | name
234 | );
235 | return;
236 | }
237 |
238 | loaded +=
239 | value.byteLength;
240 |
241 | // Update progress
242 | const percent =
243 | Math.round(
244 | (loaded /
245 | total) *
246 | 100
247 | );
248 | setText(
249 | `${percent}%`
250 | );
251 |
252 | controller.enqueue(
253 | value
254 | );
255 | push();
256 | }
257 | );
258 | }
259 | push();
260 | },
261 | });
262 | return new Response(stream)
263 | .blob()
264 | .then((blob) => {
265 | return new Blob([blob], {
266 | type: contentType,
267 | });
268 | });
269 | })
270 | .then((blob) => {
271 | // Create a blob URL
272 | const blobUrl =
273 | window.URL.createObjectURL(blob);
274 |
275 | // Create a temporary element
276 | const a = document.createElement("a");
277 | a.href = blobUrl;
278 | a.download = fileName; // Set the desired filename
279 | if (target) a.target = target;
280 | a.dispatchEvent(
281 | new MouseEvent("click")
282 | );
283 |
284 | window.URL.revokeObjectURL(blobUrl);
285 | })
286 | .catch((error) => {
287 | console.error(
288 | "Error downloading the file:",
289 | error
290 | );
291 | });
292 | }
293 | },
294 | });
295 | }
296 | },
297 | fallback,
298 | timeout
299 | );
300 |
301 | export const openUrl = (
302 | url: UrlInput,
303 | fallback?: () => Promisable,
304 | timeout?: number,
305 | target?: "_blank"
306 | ): BtnAction => {
307 | return process(
308 | async (): Promise => {
309 | const _url = await normalizeUrlInput(url);
310 | const a = document.createElement("a");
311 | a.href = _url;
312 | if (target) a.target = target;
313 | a.dispatchEvent(new MouseEvent("click"));
314 | },
315 | fallback,
316 | timeout
317 | );
318 | };
319 |
320 | export const process = (
321 | fn: (btnName: string, setText: (str: string) => void) => any,
322 | fallback?: () => Promisable,
323 | timeout = 0 /* 10min */
324 | ): BtnAction => {
325 | return async (name, btn, setText): Promise => {
326 | const _onclick = btn.onclick;
327 |
328 | btn.onclick = null;
329 | setText(i18next.t("processing"));
330 |
331 | try {
332 | await useTimeout(fn(name, setText), timeout);
333 | setText(name);
334 | } catch (err) {
335 | console.error(err);
336 | if (fallback) {
337 | // use fallback
338 | await fallback();
339 | setText(name);
340 | }
341 | }
342 |
343 | btn.onclick = _onclick;
344 | };
345 | };
346 | }
347 |
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | /* eslint-disable no-void */
3 |
4 | import fs from "fs";
5 | import path from "path";
6 | import os from "os";
7 | import { setMscz } from "./mscz";
8 | import { loadMscore, INDV_DOWNLOADS, WebMscore } from "./mscore";
9 | import { ScoreInfoHtml, ScoreInfoObj } from "./scoreinfo";
10 | import { fetchBuffer } from "./utils";
11 | import { isNpx, getVerInfo } from "./npm-data";
12 | import { getFileUrl } from "./file";
13 | import { exportPDF } from "./pdf";
14 | import i18nextInit, { i18next } from "./i18n/index";
15 | import { InputFileFormat } from "webmscore/schemas";
16 |
17 | (async () => {
18 | await i18nextInit;
19 | })();
20 |
21 | const inquirer: typeof import("inquirer") = require("inquirer");
22 | const ora: typeof import("ora") = require("ora");
23 | const chalk: typeof import("chalk") = require("chalk");
24 | const yargs = require("yargs");
25 | const { hideBin } = require("yargs/helpers");
26 | const argv: any = yargs(hideBin(process.argv))
27 | .usage(i18next.t("cli_usage_hint", { bin: "$0" }))
28 | .example(
29 | "$0 -i https://musescore.com/user/123/scores/456 -t mp3 -o " +
30 | process.cwd(),
31 | i18next.t("cli_example_url")
32 | )
33 | .example(
34 | "$0 -i " + i18next.t("path_to_folder") + " -t midi pdf",
35 | i18next.t("cli_example_folder")
36 | )
37 | .example(
38 | "$0 -i " + i18next.t("path_to_file") + ".mxl -t flac",
39 | i18next.t("cli_example_file")
40 | )
41 | .option("input", {
42 | alias: "i",
43 | type: "string",
44 | description: i18next.t("cli_option_input_description"),
45 | requiresArg: true,
46 | })
47 | .option("type", {
48 | alias: "t",
49 | type: "array",
50 | description: i18next.t("cli_option_type_description"),
51 | requiresArg: true,
52 | choices: [
53 | "midi",
54 | "mp3",
55 | "pdf",
56 | "mscz",
57 | "mscx",
58 | "musicxml",
59 | "flac",
60 | "ogg",
61 | ],
62 | })
63 | .option("output", {
64 | alias: "o",
65 | type: "string",
66 | description: i18next.t("cli_option_output_description"),
67 | requiresArg: true,
68 | default: process.cwd(),
69 | })
70 | .option("verbose", {
71 | alias: "v",
72 | type: "boolean",
73 | description: i18next.t("cli_option_verbose_description"),
74 | })
75 | .alias("help", "h")
76 | .alias("version", "V").argv;
77 |
78 | const SCORE_URL_REG = /^(?:https?:\/\/)(?:(?:s|www)\.)?musescore\.com\/[^\s]+$/;
79 |
80 | type ExpDlType = "midi" | "mp3" | "pdf";
81 |
82 | interface Params {
83 | fileInit: string;
84 | confirmed: boolean;
85 | useExpDL: boolean;
86 | expDlTypes: ExpDlType[];
87 | part: number;
88 | types: number[];
89 | output: string;
90 | }
91 |
92 | const createSpinner = () => {
93 | return ora({
94 | text: i18next.t("processing"),
95 | color: "blue",
96 | spinner: "bounce",
97 | indent: 0,
98 | }).start();
99 | };
100 |
101 | const checkboxValidate = (input: number[]) => {
102 | return input.length >= 1;
103 | };
104 |
105 | const createDirectoryIfNotExist = (input: string) => {
106 | const dirExists = fs.existsSync(input);
107 |
108 | if (!dirExists) {
109 | fs.mkdirSync(input, { recursive: true });
110 | }
111 | };
112 |
113 | const getOutputDir = async (defaultOutput: string) => {
114 | let dirNotExistsTries = 0;
115 | let lastTryDir: string | null = null;
116 |
117 | const { output } = await inquirer.prompt({
118 | type: "input",
119 | name: "output",
120 | message: i18next.t("cli_output_message"),
121 | async validate(input: string) {
122 | if (!input) return false;
123 |
124 | const dirExists = fs.existsSync(input);
125 |
126 | if (!dirExists) {
127 | if (lastTryDir !== input) {
128 | lastTryDir = input;
129 | dirNotExistsTries = 0;
130 | }
131 |
132 | dirNotExistsTries++;
133 |
134 | if (dirNotExistsTries >= 2) {
135 | fs.mkdirSync(input, { recursive: true });
136 | } else {
137 | try {
138 | fs.accessSync(input);
139 | } catch (e) {
140 | return (
141 | `${e.message}` +
142 | "\n " +
143 | `${chalk.bold(i18next.t("cli_confirm_message"))}` +
144 | `${chalk.dim(" (Enter ↵)")}`
145 | );
146 | }
147 | }
148 | } else if (!fs.statSync(input).isDirectory()) return false;
149 |
150 | dirNotExistsTries = 0;
151 |
152 | try {
153 | fs.accessSync(input);
154 | } catch (e) {
155 | return e.message;
156 | }
157 |
158 | return true;
159 | },
160 | default: defaultOutput,
161 | });
162 |
163 | return output;
164 | };
165 |
166 | void (async () => {
167 | if (!isNpx()) {
168 | const { installed, latest, isLatest } = await getVerInfo();
169 | if (!isLatest) {
170 | console.log(
171 | chalk.yellowBright(
172 | i18next.t("cli_outdated_version_message", {
173 | installed: installed,
174 | latest: latest,
175 | })
176 | )
177 | );
178 | }
179 | }
180 |
181 | let isInteractive = true;
182 | let types;
183 | let filetypes;
184 |
185 | // Check if both input and type arguments are used
186 | if (argv.input && argv.type) {
187 | isInteractive = false;
188 | }
189 |
190 | if (isInteractive) {
191 | argv.verbose = true;
192 | // Determine platform and paste message
193 | const platform = os.platform();
194 | let pasteMessage = "";
195 | if (platform === "win32") {
196 | pasteMessage = i18next.t("cli_windows_paste_hint");
197 | } else if (platform === "linux") {
198 | pasteMessage = i18next.t("cli_linux_paste_hint");
199 | } // For MacOS, no hint is needed because the paste shortcut is universal.
200 |
201 | // ask for the page url or path to local file
202 | const { fileInit } = await inquirer.prompt({
203 | type: "input",
204 | name: "fileInit",
205 | message: i18next.t("cli_input_message"),
206 | suffix:
207 | "\n (" +
208 | i18next.t("cli_input_suffix") +
209 | `) ${chalk.bgGray(pasteMessage)}\n `,
210 | validate(input: string) {
211 | return (
212 | input &&
213 | (!!input.match(SCORE_URL_REG) ||
214 | fs.statSync(input).isFile() ||
215 | fs.statSync(input).isDirectory())
216 | );
217 | },
218 | default: argv.input,
219 | });
220 |
221 | argv.input = fileInit;
222 | }
223 |
224 | const spinner = createSpinner();
225 |
226 | // Check if input is a file or directory
227 | let isFile: boolean;
228 | let isDir: boolean;
229 | try {
230 | isFile = fs.lstatSync(argv.input).isFile();
231 | isDir = fs.lstatSync(argv.input).isDirectory();
232 | } catch (_) {
233 | isFile = false;
234 | isDir = false;
235 | }
236 |
237 | // Check if local file or directory
238 | if (isFile || isDir) {
239 | let filePaths: string[] = [];
240 |
241 | if (isDir) {
242 | if (!(argv.input.endsWith("/") || argv.input.endsWith("\\"))) {
243 | argv.input += "/";
244 | }
245 | await fs.promises
246 | .readdir(argv.input, { withFileTypes: true })
247 | .then((files) =>
248 | files.forEach((file) => {
249 | try {
250 | if (file.isDirectory()) {
251 | return;
252 | }
253 | } catch (err) {
254 | spinner.fail(err.message);
255 | return;
256 | }
257 | filePaths.push(argv.input + file.name);
258 | })
259 | );
260 |
261 | if (isInteractive) {
262 | if (argv.type) {
263 | argv.type[argv.type.findIndex((e) => e === "musicxml")] =
264 | "mxl";
265 | argv.type[argv.type.findIndex((e) => e === "midi")] = "mid";
266 | types = argv.type.map((e) =>
267 | INDV_DOWNLOADS.findIndex((f) => f.fileExt === e)
268 | );
269 | }
270 | // build filetype choices
271 | const typeChoices = INDV_DOWNLOADS.map((d, i) => ({
272 | name: d.name,
273 | value: i,
274 | }));
275 | // filetype selection
276 | spinner.stop();
277 | types = await inquirer.prompt({
278 | type: "checkbox",
279 | name: "types",
280 | message: i18next.t("cli_types_message"),
281 | choices: typeChoices,
282 | validate: checkboxValidate,
283 | pageSize: Infinity,
284 | default: types,
285 | });
286 | spinner.start();
287 |
288 | types = types.types;
289 |
290 | // output directory
291 | spinner.stop();
292 | const output = await getOutputDir(argv.output);
293 | spinner.start();
294 | argv.output = output;
295 | }
296 | } else {
297 | filePaths.push(argv.input);
298 | }
299 |
300 | await Promise.all(
301 | filePaths.map(async (filePath) => {
302 | createDirectoryIfNotExist(filePath);
303 | // validate input file
304 | if (!fs.statSync(filePath).isFile()) {
305 | spinner.fail(i18next.t("cli_file_error"));
306 | return;
307 | }
308 |
309 | if (!isInteractive) {
310 | // validate types
311 | if (argv.type.length === 0) {
312 | spinner.fail(i18next.t("cli_type_error"));
313 | return;
314 | }
315 | }
316 |
317 | let inputFileExt = path.extname(filePath).substring(1);
318 |
319 | if (inputFileExt === "mid") {
320 | inputFileExt = "midi";
321 | }
322 | if (
323 | ![
324 | "gp",
325 | "gp3",
326 | "gp4",
327 | "gp5",
328 | "gpx",
329 | "gtp",
330 | "kar",
331 | "midi",
332 | "mscx",
333 | "mscz",
334 | "musicxml",
335 | "mxl",
336 | "ptb",
337 | "xml",
338 | ].includes(inputFileExt)
339 | ) {
340 | spinner.fail(i18next.t("cli_file_extension_error"));
341 | return;
342 | }
343 |
344 | // get scoreinfo
345 | let scoreinfo = new ScoreInfoObj(
346 | 0,
347 | path.basename(filePath, "." + inputFileExt)
348 | );
349 |
350 | // load file
351 | let score: WebMscore;
352 | let metadata: import("webmscore/schemas").ScoreMetadata;
353 | try {
354 | // load local file
355 | const data = await fs.promises.readFile(filePath);
356 | await setMscz(scoreinfo, data.buffer);
357 | if (argv.verbose) {
358 | spinner.info(i18next.t("cli_file_loaded_message"));
359 | spinner.start();
360 | }
361 | // load score using webmscore
362 | score = await loadMscore(
363 | inputFileExt as InputFileFormat,
364 | scoreinfo
365 | );
366 |
367 | if (isInteractive && isFile) {
368 | metadata = await score.metadata();
369 | }
370 |
371 | if (argv.verbose) {
372 | spinner.info(i18next.t("cli_score_loaded_message"));
373 | }
374 | } catch (err) {
375 | if (isFile || argv.verbose) {
376 | spinner.fail(err.message);
377 | }
378 | if (argv.verbose) {
379 | spinner.info(i18next.t("cli_input_error"));
380 | }
381 | return;
382 | }
383 |
384 | let parts;
385 | if (isInteractive && isFile) {
386 | // build part choices
387 | const partChoices = metadata.excerpts.map((p) => ({
388 | name: p.title,
389 | value: p.id,
390 | }));
391 | // console.log(partChoices);
392 | // add the "full score" option as a "part"
393 | partChoices.unshift({
394 | value: -1,
395 | name: i18next.t("full_score"),
396 | });
397 |
398 | // part selection
399 | spinner.stop();
400 | parts = await inquirer.prompt({
401 | type: "checkbox",
402 | name: "parts",
403 | message: i18next.t("cli_parts_message"),
404 | choices: partChoices,
405 | validate: checkboxValidate,
406 | pageSize: Infinity,
407 | });
408 | spinner.start();
409 | // console.log(parts);
410 | parts = partChoices.filter((e) =>
411 | parts.parts.includes(e.value)
412 | );
413 | // console.log(parts);
414 | } else {
415 | parts = [{ name: i18next.t("full_score"), value: -1 }];
416 | }
417 |
418 | if (argv.type) {
419 | argv.type[argv.type.findIndex((e) => e === "musicxml")] =
420 | "mxl";
421 | argv.type[argv.type.findIndex((e) => e === "midi")] = "mid";
422 | types = argv.type.map((e) =>
423 | INDV_DOWNLOADS.findIndex((f) => f.fileExt === e)
424 | );
425 | }
426 | if (isInteractive && isFile) {
427 | // build filetype choices
428 | const typeChoices = INDV_DOWNLOADS.map((d, i) => ({
429 | name: d.name,
430 | value: i,
431 | }));
432 | // filetype selection
433 | spinner.stop();
434 | types = await inquirer.prompt({
435 | type: "checkbox",
436 | name: "types",
437 | message: i18next.t("cli_types_message"),
438 | choices: typeChoices,
439 | validate: checkboxValidate,
440 | pageSize: Infinity,
441 | default: types,
442 | });
443 | spinner.start();
444 |
445 | types = types.types;
446 | }
447 |
448 | filetypes = types.map((i) => INDV_DOWNLOADS[i]);
449 |
450 | if (isInteractive && isFile) {
451 | // output directory
452 | spinner.stop();
453 | const output = await getOutputDir(argv.output);
454 | spinner.start();
455 | argv.output = output;
456 | }
457 |
458 | createDirectoryIfNotExist(argv.output);
459 |
460 | // validate output directory
461 | try {
462 | await fs.promises.access(argv.output);
463 | } catch (err) {
464 | spinner.fail(err.message);
465 | return;
466 | }
467 |
468 | // export files
469 | const fileName =
470 | scoreinfo.fileName || (await score.titleFilenameSafe());
471 | // spinner.start();
472 | for (const type of filetypes) {
473 | for (const part of parts) {
474 | // select part
475 | await score.setExcerptId(part.value);
476 |
477 | // generate file data
478 | const data = await type.action(score);
479 |
480 | // save to filesystem
481 | const n = `${fileName} - ${part.name}.${type.fileExt}`;
482 | const f = path.join(argv.output, n);
483 | await fs.promises.writeFile(f, data);
484 | if (argv.verbose) {
485 | spinner.info(
486 | i18next.t("cli_saved_message", {
487 | file: chalk.underline(f),
488 | })
489 | );
490 | }
491 | }
492 | }
493 | })
494 | );
495 | spinner.succeed(i18next.t("cli_done_message"));
496 | return;
497 | } else {
498 | // validate input URL
499 | if (!argv.input.match(SCORE_URL_REG)) {
500 | spinner.fail(i18next.t("cli_url_error"));
501 | return;
502 | }
503 | argv.input = argv.input.match(SCORE_URL_REG)[0];
504 |
505 | // validate types
506 | if (!isInteractive) {
507 | if (argv.type.length === 0) {
508 | spinner.fail(i18next.t("cli_type_error"));
509 | return;
510 | } else if (
511 | ["mscz", "mscx", "musicxml", "flac", "ogg"].some((e) =>
512 | argv.type.includes(e)
513 | )
514 | ) {
515 | // Fail since user cannot download these types from a URL
516 | spinner.fail(i18next.t("cli_url_type_error"));
517 | return;
518 | }
519 | }
520 |
521 | // request scoreinfo
522 | let scoreinfo: ScoreInfoHtml = await ScoreInfoHtml.request(argv.input);
523 |
524 | // validate musescore URL
525 | if (scoreinfo.id === 0) {
526 | spinner.fail(i18next.t("cli_score_not_found"));
527 | return;
528 | }
529 |
530 | if (isInteractive) {
531 | // confirmation
532 | spinner.stop();
533 | const { confirmed } = await inquirer.prompt({
534 | type: "confirm",
535 | name: "confirmed",
536 | message: i18next.t("cli_confirm_message"),
537 | prefix:
538 | `${chalk.yellow("!")} ` +
539 | i18next.t("id", { id: scoreinfo.id }) +
540 | "\n " +
541 | i18next.t("title", { title: scoreinfo.title }) +
542 | "\n ",
543 | default: true,
544 | });
545 | if (!confirmed) return;
546 |
547 | // print a blank line
548 | console.log();
549 | spinner.start();
550 | } else {
551 | // print message if verbosity is enabled
552 | if (argv.verbose) {
553 | spinner.stop();
554 | console.log(
555 | `${chalk.yellow("!")} ` +
556 | i18next.t("id", { id: scoreinfo.id }) +
557 | "\n " +
558 | i18next.t("title", { title: scoreinfo.title }) +
559 | "\n "
560 | );
561 | spinner.start();
562 | }
563 | }
564 |
565 | if (argv.type) {
566 | types = argv.type;
567 | }
568 | if (isInteractive) {
569 | // filetype selection
570 | spinner.stop();
571 | types = await inquirer.prompt({
572 | type: "checkbox",
573 | name: "types",
574 | message: i18next.t("cli_types_message"),
575 | choices: ["midi", "mp3", "pdf"],
576 | validate: checkboxValidate,
577 | pageSize: Infinity,
578 | default: types,
579 | });
580 | types = types.types;
581 |
582 | // output directory
583 | const output = await getOutputDir(argv.output);
584 | spinner.start();
585 | argv.output = output;
586 | }
587 |
588 | createDirectoryIfNotExist(argv.output);
589 |
590 | // validate output directory
591 | try {
592 | await fs.promises.access(argv.output);
593 | } catch (err) {
594 | spinner.fail(err.message);
595 | return;
596 | }
597 |
598 | await Promise.all(
599 | types.map(async (type) => {
600 | // download/generate file data
601 | let fileExt: String;
602 | let fileData: Buffer;
603 | switch (type) {
604 | case "midi": {
605 | fileExt = "mid";
606 | const fileUrl = await getFileUrl(
607 | scoreinfo.id,
608 | "midi",
609 | argv.input
610 | );
611 | fileData = await fetchBuffer(fileUrl);
612 | break;
613 | }
614 | case "mp3": {
615 | fileExt = "mp3";
616 | const fileUrl = await getFileUrl(
617 | scoreinfo.id,
618 | "mp3",
619 | argv.input
620 | );
621 | fileData = await fetchBuffer(fileUrl);
622 | break;
623 | }
624 | case "pdf": {
625 | fileExt = "pdf";
626 | fileData = Buffer.from(
627 | await exportPDF(
628 | scoreinfo,
629 | scoreinfo.sheet,
630 | argv.input
631 | )
632 | );
633 | break;
634 | }
635 | }
636 |
637 | // save to filesystem
638 | const f = path.join(
639 | argv.output,
640 | `${scoreinfo.fileName}.${fileExt}`
641 | );
642 | await fs.promises.writeFile(f, fileData);
643 | if (argv.verbose) {
644 | spinner.info(
645 | i18next.t("cli_saved_message", {
646 | file: chalk.underline(f),
647 | })
648 | );
649 | }
650 | })
651 | );
652 |
653 | spinner.succeed(i18next.t("cli_done_message"));
654 | return;
655 | }
656 | })();
657 |
--------------------------------------------------------------------------------
/src/file-magics.ts:
--------------------------------------------------------------------------------
1 | import isNodeJs from "detect-node";
2 | import { hookNative } from "./anti-detection";
3 | import type { FileType } from "./file";
4 |
5 | const TYPE_REG = /type=(img|mp3|midi)/;
6 | // first page has different URL
7 | const INIT_PAGE_REG = /(score_0\.png@0|score_0\.svg)/;
8 | const INDEX_REG = /index=(\d+)/;
9 |
10 | export const auths = {};
11 |
12 | (() => {
13 | if (isNodeJs) {
14 | // noop in CLI
15 | return () => Promise.resolve("");
16 | }
17 |
18 | try {
19 | const p = Object.getPrototypeOf(document.body);
20 | Object.setPrototypeOf(document.body, null);
21 |
22 | hookNative(document.body, "append", () => {
23 | return function (...nodes: Node[]) {
24 | p.append.call(this, ...nodes);
25 |
26 | if (nodes[0].nodeName === "IFRAME") {
27 | const iframe = nodes[0] as HTMLIFrameElement;
28 | const w = iframe.contentWindow as Window;
29 |
30 | hookNative(w, "fetch", () => {
31 | return function (url, init) {
32 | let token = init?.headers?.Authorization;
33 | if (
34 | typeof url === "string" &&
35 | (token || url.match(INIT_PAGE_REG))
36 | ) {
37 | let m = url.match(TYPE_REG);
38 | let i = url.match(INDEX_REG);
39 | if (m && i) {
40 | // console.log(url, token, m[1], i[1]);
41 | const type = m[1];
42 | const index = i[1];
43 | auths[type + index] = token;
44 | } else if (url.match(INIT_PAGE_REG)) {
45 | auths["img0"] = url;
46 | }
47 | }
48 | return fetch(url, init);
49 | };
50 | });
51 | }
52 | };
53 | });
54 |
55 | Object.setPrototypeOf(document.body, p);
56 | } catch (err) {
57 | console.error(err);
58 | }
59 | })();
60 |
--------------------------------------------------------------------------------
/src/file.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-extend-native */
2 | /* eslint-disable @typescript-eslint/no-unsafe-return */
3 |
4 | import md5 from "md5";
5 | import { getFetch } from "./utils";
6 | import { auths } from "./file-magics";
7 |
8 | export type FileType = "img" | "mp3" | "midi";
9 |
10 | const getSuffix = async (scoreUrl: string): Promise => {
11 | let suffixUrls: string[] = [];
12 | if (scoreUrl !== "") {
13 | const response = await fetch(scoreUrl);
14 | const text = await response.text();
15 | suffixUrls = [
16 | ...text.matchAll(
17 | /link.+?href=["'](https:\/\/musescore\.com\/static\/public\/build\/musescore.*?(?:_es6)?\/20.+?\.js)["']/g
18 | ),
19 | ].map((match) => match[1]);
20 | } else {
21 | suffixUrls = [
22 | ...document.head.innerHTML.matchAll(
23 | /link.+?href=["'](https:\/\/musescore\.com\/static\/public\/build\/musescore.*?(?:_es6)?\/20.+?\.js)["']/g
24 | ),
25 | ].map((match) => match[1]);
26 | }
27 |
28 | for (const url of suffixUrls) {
29 | const response = await fetch(url);
30 | const text = await response.text();
31 |
32 | const match = text.match(/"([^"]+)"\)\.substr\(0,4\)/);
33 | if (match) {
34 | return match[1];
35 | }
36 | }
37 |
38 | return null;
39 | };
40 |
41 | const getApiUrl = (id: number, type: FileType, index: number): string => {
42 | return `/api/jmuse?id=${id}&type=${type}&index=${index}`;
43 | };
44 |
45 | const getApiAuth = async (
46 | id: number,
47 | type: FileType,
48 | index: number,
49 | scoreUrl: string
50 | ): Promise => {
51 | const code = `${id}${type}${index}${await getSuffix(scoreUrl)}`;
52 | return md5(code).slice(0, 4);
53 | };
54 |
55 | let imgInProgress = false;
56 |
57 | const getApiAuthNetwork = async (
58 | type: FileType,
59 | index: number
60 | ): Promise => {
61 | let numPages = 0;
62 | let pageCooldown = 25;
63 | if (!auths[type + index]) {
64 | try {
65 | switch (type) {
66 | case "midi": {
67 | const fsBtn = document.querySelector(
68 | 'button[title="Toggle Fullscreen"]'
69 | ) as HTMLButtonElement;
70 | if (!fsBtn) {
71 | // mobile device
72 | document
73 | .querySelector("button[title='Open Mixer']")
74 | ?.click();
75 | const observer = new MutationObserver(() => {
76 | if (
77 | document.querySelector(
78 | "body > article[role='dialog']"
79 | )
80 | ) {
81 | let audioSources = document.querySelector(
82 | "body > article[role='dialog'] select"
83 | );
84 |
85 | if (audioSources !== null) {
86 | audioSources.querySelector(
87 | "option[value='0']"
88 | )?.selected = true;
89 |
90 | audioSources.dispatchEvent(
91 | new Event("change")
92 | );
93 | }
94 | document
95 | .querySelector(
96 | "article[role='dialog'] header > button"
97 | )
98 | ?.click();
99 | }
100 | });
101 | observer.observe(document.body, {
102 | childList: true,
103 | subtree: true,
104 | });
105 | } else {
106 | const el =
107 | fsBtn.parentElement?.parentElement?.querySelector(
108 | "button"
109 | ) as HTMLButtonElement;
110 | el.click();
111 | }
112 | break;
113 | }
114 | case "mp3": {
115 | const el = document.querySelector(
116 | 'button[title="Toggle Play"]'
117 | ) as HTMLButtonElement;
118 | if (!el) {
119 | // mobile device
120 | document.querySelector("#scorePlayButton")?.click();
121 | } else {
122 | el.click();
123 | }
124 | break;
125 | }
126 | case "img": {
127 | if (!imgInProgress) {
128 | imgInProgress = true;
129 | let parentDiv = document.querySelector(
130 | "#jmuse-scroller-component"
131 | )!;
132 |
133 | numPages = parentDiv.children.length - 3;
134 | let i = 0;
135 |
136 | function scrollToNextChild() {
137 | let childDiv = parentDiv.children[i];
138 | if (childDiv) {
139 | childDiv.scrollIntoView();
140 | }
141 |
142 | i++;
143 |
144 | if (i < numPages) {
145 | setTimeout(scrollToNextChild, pageCooldown);
146 | }
147 | }
148 |
149 | scrollToNextChild();
150 | }
151 | imgInProgress = false;
152 | break;
153 | }
154 | }
155 | } catch (err) {
156 | console.error(err);
157 | throw Error;
158 | }
159 | }
160 |
161 | try {
162 | return new Promise((resolve, reject) => {
163 | let timer = setTimeout(
164 | () => {
165 | reject(new Error("token timeout"));
166 | },
167 | type === "img"
168 | ? numPages * pageCooldown * 2 + 2100
169 | : 5 * 1000 /* 5s */
170 | );
171 |
172 | // Check the auths object periodically
173 | let interval = setInterval(() => {
174 | if (auths.hasOwnProperty(type + index)) {
175 | clearTimeout(timer);
176 | clearInterval(interval);
177 | setInterval(
178 | () => {
179 | resolve(auths[type + index]);
180 | },
181 | // long delay for images to give time for them to load fully
182 | type === "img" ? 2000 : 100
183 | );
184 | }
185 | }, 100);
186 | });
187 | } catch {
188 | console.error(type, "token timeout");
189 | throw Error;
190 | }
191 | };
192 |
193 | export const getFileUrl = async (
194 | id: number,
195 | type: FileType,
196 | scoreUrl = "",
197 | index = 0,
198 | _fetch = getFetch(),
199 | setText?: (str: string) => void,
200 | pageCount?: number
201 | ): Promise => {
202 | const url = getApiUrl(id, type, index);
203 | let auth = await getApiAuth(id, type, index, scoreUrl);
204 | if (setText && pageCount) {
205 | const percent = Math.round(((index + 1) / pageCount) * 83);
206 | setText(`${percent}%`);
207 | }
208 |
209 | let r = await _fetch(url, {
210 | headers: {
211 | Authorization: auth,
212 | },
213 | });
214 |
215 | if (!r.ok) {
216 | auth = md5(`${id}${type}${index}%3(3`).slice(0, 4);
217 | r = await _fetch(url, {
218 | headers: {
219 | Authorization: auth,
220 | },
221 | });
222 |
223 | if (!r.ok) {
224 | auth = await getApiAuthNetwork(type, index);
225 | if (type === "img" && index === 0) {
226 | // auth is the URL for the first page
227 | r = await _fetch(auth);
228 | } else {
229 | r = await _fetch(url, {
230 | headers: {
231 | Authorization: auth,
232 | },
233 | });
234 | }
235 | }
236 | }
237 |
238 | const { info } = await r.json();
239 | return info.url as string;
240 | };
241 |
--------------------------------------------------------------------------------
/src/gm.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * UserScript APIs
3 | */
4 | declare const GM: {
5 | /** https://www.tampermonkey.net/documentation.php#GM_info */
6 | info: Record;
7 |
8 | /** https://www.tampermonkey.net/documentation.php#GM_registerMenuCommand */
9 | registerMenuCommand(
10 | name: string,
11 | fn: () => any,
12 | accessKey?: string
13 | ): Promise;
14 |
15 | /** https://github.com/Tampermonkey/tampermonkey/issues/881#issuecomment-639705679 */
16 | addElement(
17 | tagName: K,
18 | properties: Record
19 | ): Promise;
20 |
21 | /** https://www.tampermonkey.net/documentation.php#GM_xmlhttpRequest */
22 | xmlHttpRequest(details: GMXMLHttpRequestOptions): { abort: () => void };
23 | };
24 |
25 | export interface GMXMLHttpRequestOptions {
26 | method: string;
27 | url: string;
28 | headers?: Record;
29 | data?: string;
30 | responseType?: "arraybuffer" | "blob" | "json" | "text";
31 | timeout?: number;
32 | onload?: (response: GMXMLHttpRequestResponse) => void;
33 | onerror?: (error: any) => void;
34 | onprogress?: (progress: ProgressEvent) => void;
35 | }
36 |
37 | export interface GMXMLHttpRequestResponse {
38 | readyState: number;
39 | responseHeaders: string;
40 | responseText: string;
41 | status: number;
42 | statusText: string;
43 | finalUrl: string;
44 | }
45 |
46 | export const _GM = (typeof GM === "object" ? GM : undefined) as GM;
47 |
48 | type GM = typeof GM;
49 |
50 | export const isGmAvailable = (requiredMethod: keyof GM = "info"): boolean => {
51 | return (
52 | typeof GM !== "undefined" && typeof GM[requiredMethod] !== "undefined"
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/src/i18n/ar.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli_usage_hint": "",
3 | "cli_example_folder": "",
4 | "cli_example_file": "",
5 | "cli_option_input_description": "",
6 | "cli_option_type_description": "",
7 | "cli_option_output_description": "",
8 | "cli_option_verbose_description": "",
9 | "cli_outdated_version_message": "",
10 | "cli_windows_paste_hint": "",
11 | "cli_linux_paste_hint": "",
12 | "cli_input_message": "",
13 | "cli_input_suffix": "",
14 | "cli_types_message": "",
15 | "cli_output_message": "",
16 | "cli_file_error": "",
17 | "cli_input_error": "",
18 | "cli_parts_message": "",
19 | "cli_saved_message": "",
20 | "cli_done_message": "",
21 | "cli_url_error": "",
22 | "cli_url_type_error": "",
23 | "cli_score_not_found": "",
24 | "button_parent_not_found": "",
25 | "unknown_button_list_mode": "",
26 | "cli_example_url": "",
27 | "path_to_folder": "",
28 | "path_to_file": "",
29 | "cli_type_error": "",
30 | "cli_file_extension_error": "",
31 | "cli_file_loaded_message": "",
32 | "cli_score_loaded_message": "",
33 | "cli_confirm_message": "",
34 | "id": "",
35 | "title": "",
36 | "no_sheet_images_error": "",
37 | "source_code": "",
38 | "version": "",
39 | "processing": "",
40 | "download": "تنزيل {{fileType}}",
41 | "full_score": "تدوين كامل",
42 | "download_audio": "تنزيل صوت {{fileType}}",
43 | "official_button": "الاشتراك المطلوبة",
44 | "official_tooltip": "تتطلب النتائج الرسمية اشتراك MuseScore Pro+ (يتوفر إصدار تجريبي مجاني) للتنزيل."
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/cs.json:
--------------------------------------------------------------------------------
1 | {
2 | "processing": "Zpracovávání…",
3 | "download": "Stáhnout {{fileType}}",
4 | "official_button": "Vyžadováno předplatné",
5 | "official_tooltip": "Oficiální partitury vyžadují ke stažení předplatné MuseScore Pro+ (dostupná bezplatná zkušební doba).",
6 | "download_audio": "Stáhnout zvuk {{fileType}}",
7 | "full_score": "Celá partitura",
8 | "button_parent_not_found": "Nadřazený prvek tlačítka nenalezen",
9 | "unknown_button_list_mode": "Neznámý režim seznamu tlačítka",
10 | "cli_usage_hint": "Použití: {{bin}} [možnosti]",
11 | "cli_example_url": "stáhnout MP3 nebo adresu URL do určitého adresáře",
12 | "path_to_folder": "cesta/ke/složce",
13 | "path_to_file": "cesta/k/souboru",
14 | "cli_example_folder": "exportovat MIDI a PDF ze všech souborů v zadané složce do aktuální složky",
15 | "cli_example_file": "exportovat FLAC nebo zadaný soubor MusicXML do aktuální složky",
16 | "cli_option_input_description": "URL, soubor nebo složka ke stažení nebo k převodu",
17 | "cli_option_type_description": "Typ souborů ke stažení",
18 | "cli_option_output_description": "Složka, do které ukládat soubory",
19 | "cli_option_verbose_description": "Spustit s podrobným protokolováním",
20 | "cli_outdated_version_message": "\nJe dostupná nová verze! Aktuální verze je {{installed}}\nSpusťte npm i -g dl-librescore@{{latest}} pro aktualizaci",
21 | "cli_windows_paste_hint": "klikněte pravým pro vložení",
22 | "cli_linux_paste_hint": "obvykle Ctrl+Shift+V pro vložení",
23 | "cli_input_message": "Adresa URL MuseScore nebo cesta k souboru nebo složce:",
24 | "cli_input_suffix": "začíná na https://musescore.com/ nebo je cesta",
25 | "cli_types_message": "Výběr typu souboru",
26 | "cli_output_message": "Výstupní adresář:",
27 | "cli_file_error": "Soubor neexistuje",
28 | "cli_type_error": "Nejsou vybrány žádné typy",
29 | "cli_file_extension_error": "Neplatná přípona souboru, jsou podporovány pouze gp, gp3, gp4, gp5, gpx, gtp, kar, mid, midi, mscx, mscz, musicxml, mxl, ptb a xml",
30 | "cli_file_loaded_message": "Soubor načten",
31 | "cli_score_loaded_message": "Partitura načtena službou Webmscore",
32 | "cli_input_error": "Zkuste místo toho použít stránku Webmscore: https://webmscore-pwa.librescore.org",
33 | "cli_parts_message": "Výběr části",
34 | "cli_saved_message": "Soubor {{file}} uložen",
35 | "cli_done_message": "Hotovo",
36 | "cli_url_error": "Neplatná adresa URL",
37 | "cli_url_type_error": "Z adresy URL lze stáhnout pouze soubory MIDI, MP3, a PDF",
38 | "cli_score_not_found": "Partitura nenalezena",
39 | "cli_confirm_message": "Pokračovat?",
40 | "id": "ID: {{id}}",
41 | "title": "Název: {{title}}",
42 | "no_sheet_images_error": "Nenalezeny žádné obrázky not",
43 | "source_code": "Zdrojový kód",
44 | "version": "Verze: {{version}}"
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "processing": "Wird bearbeitet…",
3 | "official_button": "Abonnement erforderlich",
4 | "official_tooltip": "",
5 | "cli_score_loaded_message": "",
6 | "cli_input_error": "",
7 | "cli_url_type_error": "",
8 | "cli_score_not_found": "",
9 | "cli_confirm_message": "",
10 | "id": "",
11 | "title": "",
12 | "no_sheet_images_error": "",
13 | "source_code": "",
14 | "download_audio": "",
15 | "full_score": "",
16 | "button_parent_not_found": "",
17 | "unknown_button_list_mode": "",
18 | "cli_usage_hint": "",
19 | "cli_example_url": "",
20 | "path_to_folder": "",
21 | "path_to_file": "",
22 | "cli_example_folder": "",
23 | "cli_example_file": "",
24 | "download": "",
25 | "version": "",
26 | "cli_parts_message": "",
27 | "cli_saved_message": "",
28 | "cli_done_message": "",
29 | "cli_url_error": "",
30 | "cli_option_input_description": "",
31 | "cli_option_type_description": "",
32 | "cli_option_output_description": "",
33 | "cli_option_verbose_description": "",
34 | "cli_outdated_version_message": "",
35 | "cli_windows_paste_hint": "",
36 | "cli_linux_paste_hint": "",
37 | "cli_input_message": "",
38 | "cli_input_suffix": "",
39 | "cli_types_message": "",
40 | "cli_output_message": "",
41 | "cli_file_error": "",
42 | "cli_type_error": "",
43 | "cli_file_extension_error": "",
44 | "cli_file_loaded_message": ""
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "processing": "Processing…",
3 | "download": "Download {{fileType}}",
4 | "official_button": "Subscription required",
5 | "official_tooltip": "Official Scores require a MuseScore Pro+ subscription (free trial available) to download.",
6 | "download_audio": "Download {{fileType}} audio",
7 | "full_score": "Full score",
8 | "button_parent_not_found": "Button parent not found",
9 | "unknown_button_list_mode": "Unknown button list mode",
10 | "cli_usage_hint": "Usage: {{bin}} [options]",
11 | "cli_example_url": "download MP3 of URL to specified directory",
12 | "path_to_folder": "path/to/folder",
13 | "path_to_file": "path/to/file",
14 | "cli_example_folder": "export MIDI and PDF of all files in specified folder to current folder",
15 | "cli_example_file": "export FLAC of specified MusicXML file to current folder",
16 | "cli_option_input_description": "URL, file, or folder to download or convert from",
17 | "cli_option_type_description": "Type of files to download",
18 | "cli_option_output_description": "Folder to save files to",
19 | "cli_option_verbose_description": "Run with verbose logging",
20 | "cli_outdated_version_message": "\nNew version is available! Current version is {{installed}}\nRun npm i -g dl-librescore@{{latest}} to update",
21 | "cli_windows_paste_hint": "right-click to paste",
22 | "cli_linux_paste_hint": "usually Ctrl+Shift+V to paste",
23 | "cli_input_message": "MuseScore URL or path to file or folder:",
24 | "cli_input_suffix": "starts with https://musescore.com/ or is a path",
25 | "cli_types_message": "Filetype Selection",
26 | "cli_output_message": "Output Directory:",
27 | "cli_file_error": "File does not exist",
28 | "cli_type_error": "No types chosen",
29 | "cli_file_extension_error": "Invalid file extension, only gp, gp3, gp4, gp5, gpx, gtp, kar, mid, midi, mscx, mscz, musicxml, mxl, ptb, and xml are supported",
30 | "cli_file_loaded_message": "File loaded",
31 | "cli_score_loaded_message": "Score loaded by Webmscore",
32 | "cli_input_error": "Try using the Webmscore website instead: https://webmscore-pwa.librescore.org",
33 | "cli_parts_message": "Part Selection",
34 | "cli_saved_message": "Saved {{file}}",
35 | "cli_done_message": "Done",
36 | "cli_url_error": "Invalid URL",
37 | "cli_url_type_error": "Only MIDI, MP3, and PDF are downloadable from a URL",
38 | "cli_score_not_found": "Score not found",
39 | "cli_confirm_message": "Continue?",
40 | "id": "ID: {{id}}",
41 | "title": "Title: {{title}}",
42 | "no_sheet_images_error": "No sheet images found",
43 | "source_code": "Source Code",
44 | "version": "Version: {{version}}"
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/es.json:
--------------------------------------------------------------------------------
1 | {
2 | "processing": "Cargando…",
3 | "download": "Descargar {{fileType}}",
4 | "download_audio": "Descargar audio {{fileType}}",
5 | "full_score": "Partitura completa",
6 | "cli_file_error": "El archivo no existe",
7 | "cli_type_error": "No hay tipos elegidos",
8 | "cli_file_extension_error": "Extensión de archivo no válida, solo se admiten gp, gp3, gp4, gp5, gpx, gtp, kar, mid, midi, mscx, mscz, musicxml, mxl, ptb y xml",
9 | "cli_file_loaded_message": "Archivo cargado",
10 | "cli_score_loaded_message": "Partitura cargada por Webmscore",
11 | "cli_input_error": "Prueba utilizar la página web de Webmscore en lugar de: https://webmscore-pwa.librescore.org",
12 | "cli_parts_message": "Selección de partes",
13 | "cli_saved_message": "Guardado {{file}}",
14 | "cli_done_message": "Hecho",
15 | "cli_url_error": "URL no válido",
16 | "cli_url_type_error": "Solo MIDI, MP3, y PDF son descargables desde una URL",
17 | "cli_score_not_found": "Partitura no encontrada",
18 | "cli_confirm_message": "Continuar?",
19 | "id": "ID: {{id}}",
20 | "title": "Titulo: {{title}}",
21 | "no_sheet_images_error": "No se han encontrado imágenes de las hojas",
22 | "source_code": "Codigo fuente",
23 | "version": "Versión: {{version}}",
24 | "cli_usage_hint": "Uso: {{bin}} [opciones]",
25 | "button_parent_not_found": "No se encontró el padre del botón",
26 | "unknown_button_list_mode": "Modo de lista de botones desconocido",
27 | "cli_example_url": "descargar MP3 de la URL al directorio especificado",
28 | "path_to_folder": "ruta/a/carpeta",
29 | "path_to_file": "ruta/a/archivo",
30 | "cli_example_folder": "exportar MIDI y PDF del todos los archivos en la carpeta a especificada a la carpeta actual",
31 | "cli_example_file": "exportar FLAC del archivo MusicXML especificado a la carpeta actual",
32 | "cli_option_input_description": "URL, archivo o carpeta para descargar o convertir",
33 | "cli_option_type_description": "Tipo de archivos a descargar",
34 | "cli_option_output_description": "Carpeta para guardar los archivos",
35 | "cli_option_verbose_description": "Ejecutar con registro verboso",
36 | "cli_outdated_version_message": "\nHay una nueva version disponible! La version actual es {{installed}}\nEjecute npm i -g dl-librescore@{{latest}} para actualizar",
37 | "cli_windows_paste_hint": "clic derecho para pegar",
38 | "cli_linux_paste_hint": "generalmente Ctrl+Shift+V para pegar",
39 | "cli_input_message": "URL de MuseScore o ruta al archivo o carpeta:",
40 | "cli_input_suffix": "inicia con https://musescore.com/ o es una ruta de acceso",
41 | "cli_types_message": "Selección de tipo de archivo",
42 | "cli_output_message": "Directorio de salida:",
43 | "official_button": "Se requiere suscripción",
44 | "official_tooltip": "Las Partituras oficiales requieren una suscripción a MuseScore Pro+ (prueba gratuita disponible) para descargar."
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/fr.json:
--------------------------------------------------------------------------------
1 | {
2 | "processing": "En traitement…",
3 | "download": "Télécharger {{fileType}}",
4 | "full_score": "Partition complète",
5 | "download_audio": "Télécharger audio {{fileType}}",
6 | "cli_usage_hint": "",
7 | "button_parent_not_found": "Bouton parent non trouvé",
8 | "unknown_button_list_mode": "Mode de liste de bouton inconnu",
9 | "cli_example_url": "",
10 | "path_to_folder": "",
11 | "path_to_file": "chemin/vers/fichier",
12 | "cli_example_folder": "exporter le MIDI et le PDF de tous les fichiers du dossier spécifié vers le dossier actuel",
13 | "cli_example_file": "exporter le FLAC du fichier MusicXML spécifié vers le dossier actuel",
14 | "cli_option_input_description": "",
15 | "cli_option_type_description": "",
16 | "cli_option_output_description": "",
17 | "cli_option_verbose_description": "",
18 | "cli_outdated_version_message": "",
19 | "cli_windows_paste_hint": "",
20 | "cli_linux_paste_hint": "",
21 | "cli_input_message": "",
22 | "cli_input_suffix": "",
23 | "cli_types_message": "",
24 | "cli_output_message": "",
25 | "cli_file_error": "",
26 | "cli_type_error": "",
27 | "cli_file_extension_error": "",
28 | "cli_file_loaded_message": "",
29 | "cli_score_loaded_message": "",
30 | "cli_input_error": "",
31 | "cli_parts_message": "",
32 | "cli_saved_message": "",
33 | "cli_done_message": "",
34 | "cli_url_error": "",
35 | "cli_url_type_error": "",
36 | "cli_score_not_found": "",
37 | "cli_confirm_message": "",
38 | "id": "",
39 | "title": "",
40 | "no_sheet_images_error": "",
41 | "source_code": "",
42 | "version": "",
43 | "official_button": "Abonnement requis",
44 | "official_tooltip": "Les Partitions officielles nécessitent un abonnement MuseScore Pro+ (essai gratuit disponible) pour être téléchargées."
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/hu.json:
--------------------------------------------------------------------------------
1 | {
2 | "download": "Letöltés {{fileType}}",
3 | "download_audio": "{{fileType}} audió letöltése",
4 | "full_score": "Teljes kotta",
5 | "processing": "Feldolgozás…",
6 | "button_parent_not_found": "A gomb szülője nem található",
7 | "unknown_button_list_mode": "Ismeretlen gomb lista mód",
8 | "cli_usage_hint": "Használat: {{bin}} [opciók]",
9 | "cli_example_url": "az MP3 letöltése URL-ből a kiválasztott mappába",
10 | "path_to_folder": "elérési_út/a/mappához",
11 | "path_to_file": "elérési_út/a/fájlhoz",
12 | "cli_option_input_description": "URL, fájl vagy mappa a letöltéshez vagy konvertáláshoz",
13 | "cli_option_type_description": "A letöltendő fájlok típusa",
14 | "cli_option_output_description": "Mentési mappa elérési útja",
15 | "cli_option_verbose_description": "Futtatás teljes naplózással",
16 | "cli_windows_paste_hint": "jobb klikk a beillesztés",
17 | "cli_linux_paste_hint": "általában Ctrl+Shift+V a beillesztés",
18 | "cli_input_message": "MuseScore URL vagy a fájl vagy mappa elérési útja:",
19 | "cli_input_suffix": "https://musescore.com/ kell kezdődnie vagy egy elérési út",
20 | "cli_types_message": "Fájltípus kiválasztása",
21 | "cli_output_message": "Kimeneti könyvtár:",
22 | "cli_file_error": "A fájl nem létezik",
23 | "cli_type_error": "Nem lett fájltípus kiválasztva",
24 | "cli_file_loaded_message": "Fájl betöltve",
25 | "cli_score_loaded_message": "Webmscore betöltötte a kottát",
26 | "cli_input_error": "Inkább próbálja meg a Webmscore webhelyet használni: https://webmscore-pwa.librescore.org",
27 | "cli_parts_message": "Kivonat kijelölése",
28 | "cli_saved_message": "Mentve {{file}}",
29 | "cli_done_message": "Kész",
30 | "cli_url_error": "Érvénytelen URL",
31 | "cli_url_type_error": "Csak MIDI, MP3 és PDF formátumokban lehet letölteni az URL-ből",
32 | "cli_confirm_message": "Folytatod?",
33 | "id": "Azonosító: {{id}}",
34 | "title": "Cím: {{title}}",
35 | "no_sheet_images_error": "Nem található kottakép",
36 | "source_code": "Forráskód",
37 | "version": "Verzió: {{version}}",
38 | "cli_example_file": "a kiválasztott MusicXML fájl mentése FLAC fájlként a jelenlegi mappába",
39 | "cli_example_folder": "minden fájl mentése MIDI és PDF formátumba a kiválasztott mappából a jelenlegi mappába",
40 | "cli_outdated_version_message": "\nÚj verzió érhető el! A jelenlegi verzió {{installed}}\nFuttasd a npm i -g dl-librescore@{{latest}} parancsot a frissítéshez",
41 | "cli_file_extension_error": "Érvénytelen fájltípus, csak a gp, gp3, gp4, gp5, gpx, gtp, kar, mid, midi, mscx, mscz, musicxml, mxl, ptb és xml fájltípusok támogatottak",
42 | "cli_score_not_found": "A kotta nem található",
43 | "official_button": "Előfizetés szükséges",
44 | "official_tooltip": "A Hivatalos partiturák letöltéséhez MuseScore Pro+ előfizetés szükséges (ingyenes próbaverzió elérhető)."
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/id.json:
--------------------------------------------------------------------------------
1 | {
2 | "official_tooltip": "",
3 | "download_audio": "",
4 | "full_score": "",
5 | "button_parent_not_found": "",
6 | "unknown_button_list_mode": "",
7 | "cli_usage_hint": "",
8 | "cli_example_url": "",
9 | "path_to_folder": "",
10 | "path_to_file": "",
11 | "cli_example_folder": "",
12 | "cli_example_file": "",
13 | "cli_option_input_description": "",
14 | "cli_option_type_description": "",
15 | "cli_option_output_description": "",
16 | "cli_option_verbose_description": "",
17 | "cli_outdated_version_message": "",
18 | "cli_windows_paste_hint": "",
19 | "cli_linux_paste_hint": "",
20 | "cli_input_message": "",
21 | "cli_input_suffix": "",
22 | "cli_types_message": "",
23 | "cli_output_message": "",
24 | "cli_file_error": "",
25 | "cli_type_error": "",
26 | "cli_file_extension_error": "",
27 | "cli_file_loaded_message": "",
28 | "cli_score_loaded_message": "",
29 | "cli_input_error": "",
30 | "cli_parts_message": "",
31 | "cli_saved_message": "",
32 | "cli_done_message": "",
33 | "cli_url_error": "",
34 | "cli_url_type_error": "",
35 | "cli_score_not_found": "",
36 | "cli_confirm_message": "",
37 | "id": "",
38 | "title": "",
39 | "no_sheet_images_error": "",
40 | "source_code": "",
41 | "version": "",
42 | "processing": "",
43 | "download": "",
44 | "official_button": ""
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import i18n from "i18next";
2 | import ar from "./ar.json";
3 | import cs from "./cs.json";
4 | import de from "./de.json";
5 | import en from "./en.json";
6 | import es from "./es.json";
7 | import fr from "./fr.json";
8 | import hu from "./hu.json";
9 | import id from "./id.json";
10 | import it from "./it.json";
11 | import ja from "./ja.json";
12 | import ko from "./ko.json";
13 | import ms from "./ms.json";
14 | import nl from "./nl.json";
15 | import pl from "./pl.json";
16 | import pt from "./pt.json";
17 | import ru from "./ru.json";
18 | import tr from "./tr.json";
19 | import zh_Hans from "./zh-Hans.json";
20 | import zh_Hant from "./zh_Hant.json";
21 |
22 | function getLocale(): string {
23 | let languageMap = [
24 | "ar",
25 | "cs",
26 | "de",
27 | "en",
28 | "es",
29 | "fr",
30 | "hu",
31 | "id",
32 | "it",
33 | "ja",
34 | "ko",
35 | "ms",
36 | "nl",
37 | "pl",
38 | "pt",
39 | "ru",
40 | "tr",
41 | "zh-Hans",
42 | "zh_Hant",
43 | ];
44 |
45 | let locale: string = "en";
46 | if (typeof window !== "undefined") {
47 | let localeOrder = navigator.languages?.concat(
48 | Intl.DateTimeFormat().resolvedOptions().locale,
49 | ) ?? ["en"];
50 | let localeArray = Object.values(languageMap).map((arr) => arr[0]);
51 |
52 | localeOrder.some((localeItem) => {
53 | if (localeArray.includes(localeItem)) {
54 | locale = localeItem;
55 | return true;
56 | } else if (localeArray.includes(localeItem.substring(0, 2))) {
57 | locale = localeItem.substring(0, 2);
58 | return true;
59 | } else if (
60 | localeArray.some((locale) =>
61 | locale.startsWith(localeItem.substring(0, 2)),
62 | )
63 | ) {
64 | locale = localeArray.find((locale) =>
65 | locale.startsWith(localeItem.substring(0, 2)),
66 | )!;
67 | return true;
68 | }
69 | });
70 | if (locale === "en") {
71 | if (
72 | [
73 | "ab",
74 | "be",
75 | "et",
76 | "hy",
77 | "kk",
78 | "ky",
79 | "lv",
80 | "os",
81 | "ro-MD",
82 | "ru",
83 | "tg",
84 | "tk",
85 | "uk",
86 | "uz",
87 | ].some((e) => localeOrder[0].startsWith(e)) &&
88 | localeArray.includes("ru")
89 | ) {
90 | locale = "ru";
91 | } else {
92 | locale = "en";
93 | }
94 | }
95 | }
96 | return locale;
97 | }
98 |
99 | export default i18n.init({
100 | compatibilityJSON: "v3",
101 | lng: getLocale(),
102 | fallbackLng: "en",
103 | resources: {
104 | ar: { translation: ar },
105 | cs: { translation: cs },
106 | de: { translation: de },
107 | en: { translation: en },
108 | es: { translation: es },
109 | fr: { translation: fr },
110 | hu: { translation: hu },
111 | id: { translation: id },
112 | it: { translation: it },
113 | ja: { translation: ja },
114 | ko: { translation: ko },
115 | ms: { translation: ms },
116 | nl: { translation: nl },
117 | pl: { translation: pl },
118 | pt: { translation: pt },
119 | ru: { translation: ru },
120 | tr: { translation: tr },
121 | "zh-Hans": { translation: zh_Hans },
122 | zh_Hant: { translation: zh_Hant },
123 | },
124 | interpolation: {
125 | // Fix output path `path/to/file` being displayed as `path/to/file` in cli (verbose mode)
126 | escapeValue: false,
127 | },
128 | });
129 |
130 | export const i18next = i18n;
131 |
--------------------------------------------------------------------------------
/src/i18n/it.json:
--------------------------------------------------------------------------------
1 | {
2 | "processing": "Caricamento…",
3 | "download": "Scaricare {{fileType}}",
4 | "download_audio": "Scaricare {{fileType}} audio",
5 | "full_score": "Partitura completa",
6 | "cli_usage_hint": "",
7 | "button_parent_not_found": "",
8 | "unknown_button_list_mode": "",
9 | "cli_example_url": "",
10 | "path_to_folder": "",
11 | "path_to_file": "",
12 | "cli_example_folder": "",
13 | "cli_example_file": "",
14 | "cli_option_input_description": "",
15 | "cli_option_type_description": "",
16 | "cli_option_output_description": "",
17 | "cli_option_verbose_description": "",
18 | "cli_outdated_version_message": "",
19 | "cli_windows_paste_hint": "",
20 | "cli_linux_paste_hint": "",
21 | "cli_input_message": "",
22 | "cli_input_suffix": "",
23 | "cli_types_message": "",
24 | "cli_output_message": "",
25 | "cli_file_error": "",
26 | "cli_type_error": "",
27 | "cli_file_extension_error": "",
28 | "cli_file_loaded_message": "",
29 | "cli_score_loaded_message": "",
30 | "cli_input_error": "",
31 | "cli_parts_message": "",
32 | "cli_saved_message": "",
33 | "cli_done_message": "",
34 | "cli_url_error": "",
35 | "cli_url_type_error": "",
36 | "cli_score_not_found": "",
37 | "cli_confirm_message": "",
38 | "id": "",
39 | "title": "",
40 | "no_sheet_images_error": "",
41 | "source_code": "",
42 | "version": "",
43 | "official_button": "Abbonamento richiesto",
44 | "official_tooltip": "Gli Spartiti ufficiali richiedono un abbonamento a MuseScore Pro+ (prova gratuita disponibile) per essere scaricati."
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/ja.json:
--------------------------------------------------------------------------------
1 | {
2 | "processing": "処理中…",
3 | "download": "{{fileType}}をダウンロード",
4 | "download_audio": "{{fileType}}オーディオをダウンロード",
5 | "full_score": "フルスコア",
6 | "cli_url_error": "",
7 | "cli_url_type_error": "",
8 | "cli_score_not_found": "",
9 | "cli_usage_hint": "",
10 | "button_parent_not_found": "",
11 | "unknown_button_list_mode": "",
12 | "cli_example_url": "",
13 | "path_to_folder": "",
14 | "path_to_file": "",
15 | "cli_example_folder": "",
16 | "cli_example_file": "",
17 | "cli_option_input_description": "",
18 | "cli_option_type_description": "",
19 | "cli_option_output_description": "",
20 | "cli_option_verbose_description": "",
21 | "cli_outdated_version_message": "",
22 | "cli_windows_paste_hint": "",
23 | "cli_linux_paste_hint": "",
24 | "cli_input_message": "",
25 | "cli_input_suffix": "",
26 | "cli_types_message": "",
27 | "cli_output_message": "",
28 | "cli_file_error": "",
29 | "cli_type_error": "",
30 | "cli_file_extension_error": "",
31 | "cli_file_loaded_message": "",
32 | "cli_score_loaded_message": "",
33 | "cli_input_error": "",
34 | "cli_parts_message": "",
35 | "cli_saved_message": "",
36 | "cli_done_message": "",
37 | "id": "",
38 | "cli_confirm_message": "",
39 | "title": "",
40 | "no_sheet_images_error": "",
41 | "source_code": "",
42 | "version": "",
43 | "official_button": "サブスクリプションが必要です",
44 | "official_tooltip": "公式楽譜をダウンロードするには、MuseScore Pro+ サブスクリプション (無料トライアルあり) が必要です。"
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/ko.json:
--------------------------------------------------------------------------------
1 | {
2 | "processing": "처리 중…",
3 | "download": "{{fileType}} 다운로드",
4 | "download_audio": "{{fileType}} 오디오 다운로드",
5 | "full_score": "전체 악보",
6 | "cli_done_message": "완료",
7 | "cli_score_not_found": "악보 찾을 수 없음",
8 | "cli_confirm_message": "계속하시겠습니까?",
9 | "cli_usage_hint": "사용법: {{bin}} [옵션]",
10 | "button_parent_not_found": "버튼 parent를 찾을 수 없음",
11 | "unknown_button_list_mode": "알려지지 않은 버튼 리스트 모드",
12 | "cli_example_url": "지정 폴더로 URL에서 MP3를 다운로드",
13 | "path_to_folder": "폴더/경로",
14 | "path_to_file": "파일/경로",
15 | "cli_example_folder": "지정된 폴더의 모든 파일에서 MIDI와 PDF를 현재 폴더로 내보내기",
16 | "cli_example_file": "지정된 MusicXML 파일에서 FLAC를 현재 폴더로 내보내기",
17 | "cli_option_input_description": "다운로드하거나 변환할 URL, 파일 또는 폴더",
18 | "cli_option_type_description": "다운로드할 파일 형식",
19 | "cli_option_output_description": "파일을 저장할 폴더",
20 | "cli_option_verbose_description": "자세한 로깅으로 실행",
21 | "cli_outdated_version_message": "\n새 버전이 출시되었습니다! 현재 버전은 {{installed}}입니다.\nnpm i -g dl-librescore@{{latest}} 를 실행해서 업데이트해 주세요",
22 | "cli_windows_paste_hint": "오른쪽 클릭으로 붙여넣기",
23 | "cli_linux_paste_hint": "대부분은 Ctrl+Shift+V 로 붙여넣기",
24 | "cli_input_message": "MuseScore URL 또는 파일/폴더의 경로:",
25 | "cli_input_suffix": "https://musescore.com/ 으로 시작하거나 경로임",
26 | "cli_types_message": "파일 포맷 선택",
27 | "cli_output_message": "출력 디렉토리:",
28 | "cli_file_error": "파일이 없습니다",
29 | "cli_type_error": "포맷이 선택되지 않음",
30 | "cli_file_extension_error": "잘못된 파일 확장자, gp, gp3, gp4, gp5, gpx, gtp, kar, mid, midi, mscx, mscz, musicxml, mxl, ptb, xml만 지원됨",
31 | "cli_file_loaded_message": "파일 로드 완료",
32 | "cli_score_loaded_message": "웹엠스코어로 악보 로드됨",
33 | "cli_input_error": "웹엠스코어 웹사이트를 이용해 보세요. https://webmscore-pwa.librescore.org",
34 | "cli_parts_message": "파트 선택",
35 | "cli_saved_message": "{{file}} 저장됨",
36 | "cli_url_error": "잘못된 URL",
37 | "cli_url_type_error": "URL에서는 MIDI, MP3, PDF만 다운로드 가능",
38 | "id": "식별자: {{id}}",
39 | "title": "제목: {{title}}",
40 | "no_sheet_images_error": "악보 이미지를 찾을 수 없음",
41 | "source_code": "소스 코드",
42 | "version": "버전: {{version}}",
43 | "official_button": "구독 필요",
44 | "official_tooltip": "공식 악보를 다운로드하려면 MuseScore Pro+ 구독(무료 평가판 이용 가능)이 필요합니다."
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/ms.json:
--------------------------------------------------------------------------------
1 | {
2 | "processing": "Memproses…",
3 | "download": "Muat turun {{fileType}}",
4 | "download_audio": "Muat turun audio {{fileType}}",
5 | "button_parent_not_found": "Induk butang tidak ditemui",
6 | "unknown_button_list_mode": "Mod senarai butang tidak diketahui",
7 | "cli_usage_hint": "Penggunaan: {{bin}} [pilihan]",
8 | "path_to_folder": "laluan/ke/folder",
9 | "path_to_file": "laluan/ke/fail",
10 | "cli_example_folder": "eksport MIDI dan PDF semua fail dalam folder tertentu ke folder semasa",
11 | "cli_example_file": "eksport FLAC fail MusicXML yang ditentukan ke folder semasa",
12 | "cli_option_type_description": "Jenis fail untuk dimuat turun",
13 | "cli_option_output_description": "Folder untuk menyimpan fail ke",
14 | "cli_option_verbose_description": "Jalankan dengan pengelogan verbose",
15 | "cli_windows_paste_hint": "klik kanan untuk menampal",
16 | "cli_linux_paste_hint": "biasanya Ctrl+Shift+V untuk menampal",
17 | "cli_input_message": "URL MuseScore atau laluan ke fail atau folder:",
18 | "cli_input_suffix": "bermula dengan https://musescore.com/ atau ialah laluan",
19 | "cli_types_message": "Pemilihan jenis fail",
20 | "cli_output_message": "Direktori Output:",
21 | "cli_file_error": "Fail tidak wujud",
22 | "cli_type_error": "Tiada jenis yang dipilih",
23 | "cli_file_loaded_message": "Fail dimuatkan",
24 | "cli_score_loaded_message": "Skor dimuatkan oleh Webmscore",
25 | "cli_parts_message": "Pemilihan Bahagian",
26 | "cli_saved_message": "{{file}} disimpan",
27 | "cli_done_message": "Selesai",
28 | "cli_url_error": "URL tidak sah",
29 | "cli_input_error": "Cuba gunakan tapak web Webmscore: https://webmscore-pwa.librescore.org",
30 | "cli_score_not_found": "Skor tidak ditemui",
31 | "cli_confirm_message": "Teruskan?",
32 | "title": "Tajuk: {{title}}",
33 | "source_code": "Kod sumber",
34 | "version": "Versi: {{version}}",
35 | "id": "ID: {{id}}",
36 | "full_score": "Skor penuh",
37 | "cli_example_url": "muat turun MP3 URL ke direktori tertentu",
38 | "cli_option_input_description": "URL, fail atau folder untuk memuat turun atau menukar daripada",
39 | "cli_outdated_version_message": "\nVersi baharu tersedia! Versi semasa {{installed}}\nJalankan npm i -g dl-librescore@{{latest}} untuk mengemas kini",
40 | "cli_file_extension_error": "Sambungan fail tidak sah, hanya gp, gp3, gp4, gp5, gpx, gtp, kar, mid, midi, mscx, mscz, musicxml, mxl, ptb dan xml disokong",
41 | "cli_url_type_error": "Hanya MIDI, MP3 dan PDF boleh dimuat turun daripada URL",
42 | "no_sheet_images_error": "Tiada imej helaian ditemui",
43 | "official_button": "Langganan diperlukan",
44 | "official_tooltip": "Skor Rasmi memerlukan langganan MuseScore Pro+ (percubaan percuma tersedia) untuk dimuat turun."
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/nl.json:
--------------------------------------------------------------------------------
1 | {
2 | "download": "Download {{fileType}}",
3 | "download_audio": "Download {{fileType}} audio",
4 | "full_score": "Volledige partituur",
5 | "unknown_button_list_mode": "Ongekende knop lijst modus",
6 | "processing": "Verwerken…",
7 | "button_parent_not_found": "Parent knop niet gevonden",
8 | "official_button": "Abonnement vereist",
9 | "official_tooltip": "Officiële partituren vereisen een MuseScore Pro+ abonnement (gratis proefversie beschikbaar) om te downloaden.",
10 | "cli_file_loaded_message": "",
11 | "cli_score_loaded_message": "",
12 | "cli_input_error": "",
13 | "cli_parts_message": "",
14 | "cli_saved_message": "",
15 | "cli_done_message": "",
16 | "no_sheet_images_error": "",
17 | "source_code": "",
18 | "version": "",
19 | "cli_example_file": "",
20 | "cli_usage_hint": "",
21 | "cli_example_url": "",
22 | "path_to_folder": "",
23 | "path_to_file": "",
24 | "cli_example_folder": "",
25 | "cli_option_input_description": "",
26 | "cli_option_type_description": "",
27 | "cli_option_output_description": "",
28 | "cli_option_verbose_description": "",
29 | "cli_outdated_version_message": "",
30 | "cli_windows_paste_hint": "",
31 | "cli_linux_paste_hint": "",
32 | "cli_input_message": "",
33 | "cli_input_suffix": "",
34 | "cli_types_message": "",
35 | "cli_output_message": "",
36 | "cli_file_error": "",
37 | "cli_type_error": "",
38 | "cli_file_extension_error": "",
39 | "cli_url_error": "",
40 | "cli_url_type_error": "",
41 | "cli_score_not_found": "",
42 | "cli_confirm_message": "",
43 | "id": "",
44 | "title": ""
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/pl.json:
--------------------------------------------------------------------------------
1 | {
2 | "processing": "",
3 | "download": "",
4 | "official_button": "",
5 | "official_tooltip": "",
6 | "download_audio": "",
7 | "full_score": "",
8 | "button_parent_not_found": "",
9 | "unknown_button_list_mode": "",
10 | "cli_usage_hint": "",
11 | "cli_example_url": "",
12 | "path_to_folder": "",
13 | "path_to_file": "",
14 | "cli_example_folder": "",
15 | "cli_example_file": "",
16 | "cli_option_input_description": "",
17 | "cli_option_type_description": "",
18 | "cli_option_output_description": "",
19 | "cli_option_verbose_description": "",
20 | "cli_outdated_version_message": "",
21 | "cli_windows_paste_hint": "",
22 | "cli_linux_paste_hint": "",
23 | "cli_input_message": "",
24 | "cli_input_suffix": "",
25 | "cli_types_message": "",
26 | "cli_output_message": "",
27 | "cli_file_error": "",
28 | "cli_type_error": "",
29 | "cli_file_extension_error": "",
30 | "cli_file_loaded_message": "",
31 | "cli_score_loaded_message": "",
32 | "cli_input_error": "",
33 | "cli_parts_message": "",
34 | "cli_saved_message": "",
35 | "cli_done_message": "",
36 | "cli_url_error": "",
37 | "cli_url_type_error": "",
38 | "cli_score_not_found": "",
39 | "cli_confirm_message": "",
40 | "id": "",
41 | "title": "",
42 | "no_sheet_images_error": "",
43 | "source_code": "",
44 | "version": ""
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/pt.json:
--------------------------------------------------------------------------------
1 | {
2 | "processing": "",
3 | "download": "",
4 | "official_button": "",
5 | "official_tooltip": "",
6 | "download_audio": "",
7 | "full_score": "",
8 | "button_parent_not_found": "",
9 | "unknown_button_list_mode": "",
10 | "cli_usage_hint": "",
11 | "cli_example_url": "",
12 | "path_to_folder": "",
13 | "path_to_file": "",
14 | "cli_example_folder": "",
15 | "cli_example_file": "",
16 | "cli_option_input_description": "",
17 | "cli_option_type_description": "",
18 | "cli_option_output_description": "",
19 | "cli_option_verbose_description": "",
20 | "cli_outdated_version_message": "",
21 | "cli_windows_paste_hint": "",
22 | "cli_linux_paste_hint": "",
23 | "cli_input_message": "",
24 | "cli_input_suffix": "",
25 | "cli_types_message": "",
26 | "cli_output_message": "",
27 | "cli_file_error": "",
28 | "cli_type_error": "",
29 | "cli_file_extension_error": "",
30 | "cli_file_loaded_message": "",
31 | "cli_score_loaded_message": "",
32 | "cli_input_error": "",
33 | "cli_parts_message": "",
34 | "cli_saved_message": "",
35 | "cli_done_message": "",
36 | "cli_url_error": "",
37 | "cli_url_type_error": "",
38 | "cli_score_not_found": "",
39 | "cli_confirm_message": "",
40 | "id": "",
41 | "title": "",
42 | "no_sheet_images_error": "",
43 | "source_code": "",
44 | "version": ""
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/ru.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli_usage_hint": "Применение: {{bin}} [опции]",
3 | "processing": "Обработка…",
4 | "download": "Загрузить {{fileType}}",
5 | "full_score": "Партитура целиком",
6 | "download_audio": "Скачать {{fileType}}-аудио",
7 | "button_parent_not_found": "Параметр кнопки не найден",
8 | "unknown_button_list_mode": "Неизвестные режимы кнопок",
9 | "cli_example_url": "скачать MP3 по URL в указанный каталог",
10 | "path_to_folder": "путь/к/папке",
11 | "path_to_file": "путь/к/файлу",
12 | "cli_example_folder": "экспортировать все файлы MIDI и PDF из указанной папки в текущую папку",
13 | "cli_example_file": "экспортировать FLAC указанного MusicXML-файла в текущую папку",
14 | "cli_option_input_description": "URL-адрес, файл или папка для загрузки или преобразования",
15 | "cli_option_type_description": "Тип файлов для скачивания",
16 | "cli_option_output_description": "Папка для сохранения файлов",
17 | "cli_option_verbose_description": "Запуск с подробным ведением логов",
18 | "cli_outdated_version_message": "\nДоступна новая версия! Текущая версия: {{installed}}\nВыполните npm i -g dl-librescore@{{latest}} для обновления",
19 | "cli_windows_paste_hint": "щёлкните правой клавишей мыши для вставки",
20 | "cli_linux_paste_hint": "обычно Ctrl+Shift+V для вставки",
21 | "cli_input_message": "URL-адрес из MuseScore или путь к файлу или папке:",
22 | "cli_input_suffix": "начинается с https://musescore.com/ или полного пути до файла",
23 | "cli_types_message": "Укажите типы файлов",
24 | "cli_output_message": "Укажите каталог для сохранения:",
25 | "cli_file_error": "Данный файл не существует",
26 | "cli_type_error": "Не указан ни один тип",
27 | "cli_file_extension_error": "Неверное расширение файла. Поддерживаются только gp, gp3, gp4, gp5, gpx, gtp, kar, mid, midi, mscx, mscz, musicxml, mxl, ptb и xml",
28 | "cli_file_loaded_message": "Файл загружен",
29 | "cli_score_loaded_message": "Партитура загружена через Webmscore",
30 | "cli_input_error": "Попробуйте воспользоваться веб-сайтом Webmscore: https://webmscore-pwa.librescore.org",
31 | "cli_parts_message": "Укажите партии",
32 | "cli_saved_message": "Сохранено: {{file}}",
33 | "cli_done_message": "Готово",
34 | "cli_url_error": "Неверный URL",
35 | "cli_url_type_error": "По данному URL-адресу можно загрузить только MIDI, MP3 и PDF",
36 | "cli_score_not_found": "Партитура не обнаружена",
37 | "cli_confirm_message": "Продолжить?",
38 | "id": "ID: {{id}}",
39 | "title": "Название: {{title}}",
40 | "no_sheet_images_error": "Изображения нот не обнаружены",
41 | "source_code": "Исходный код",
42 | "version": "Версия: {{version}}",
43 | "official_button": "Требуется подписка",
44 | "official_tooltip": "Для загрузки официальных партитур требуется подписка MuseScore Pro+ (доступна бесплатная пробная версия)."
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/tr.json:
--------------------------------------------------------------------------------
1 | {
2 | "processing": "",
3 | "download": "",
4 | "official_button": "",
5 | "official_tooltip": "",
6 | "download_audio": "",
7 | "full_score": "",
8 | "button_parent_not_found": "",
9 | "unknown_button_list_mode": "",
10 | "cli_usage_hint": "",
11 | "cli_example_url": "",
12 | "path_to_folder": "",
13 | "path_to_file": "",
14 | "cli_example_folder": "",
15 | "cli_example_file": "",
16 | "cli_option_input_description": "",
17 | "cli_option_type_description": "",
18 | "cli_option_output_description": "",
19 | "cli_option_verbose_description": "",
20 | "cli_outdated_version_message": "",
21 | "cli_windows_paste_hint": "",
22 | "cli_linux_paste_hint": "",
23 | "cli_input_message": "",
24 | "cli_input_suffix": "",
25 | "cli_types_message": "",
26 | "cli_output_message": "",
27 | "cli_file_error": "",
28 | "cli_type_error": "",
29 | "cli_file_extension_error": "",
30 | "cli_file_loaded_message": "",
31 | "cli_score_loaded_message": "",
32 | "cli_input_error": "",
33 | "cli_parts_message": "",
34 | "cli_saved_message": "",
35 | "cli_done_message": "",
36 | "cli_url_error": "",
37 | "cli_url_type_error": "",
38 | "cli_score_not_found": "",
39 | "cli_confirm_message": "",
40 | "id": "",
41 | "title": "",
42 | "no_sheet_images_error": "",
43 | "source_code": "",
44 | "version": ""
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/zh-Hans.json:
--------------------------------------------------------------------------------
1 | {
2 | "processing": "处理中…",
3 | "download": "下载 {{fileType}}",
4 | "download_audio": "下载 {{fileType}} 音频",
5 | "full_score": "完整乐谱",
6 | "cli_usage_hint": "使用方式:{{bin}} [选项]",
7 | "button_parent_not_found": "未找到按钮父级",
8 | "unknown_button_list_mode": "未知的按钮模式列表",
9 | "cli_example_url": "从URL中下载MP3到指定位置",
10 | "path_to_folder": "路径/到/文件夹",
11 | "path_to_file": "路径/到/文件",
12 | "cli_example_folder": "将指定文件夹中所有文件的 MIDI 和 PDF 导出到当前文件夹",
13 | "cli_example_file": "导出指定的FLAC格式的MusicXML文件到当前文件夹",
14 | "cli_option_input_description": "要下载或转换的 URL、文件或文件夹",
15 | "cli_option_type_description": "需要下载的文件类型",
16 | "cli_option_output_description": "保存文件的文件夹位置",
17 | "cli_option_verbose_description": "运行时启用详细日志记录",
18 | "cli_outdated_version_message": "\n新版本可用!当前版本为 {{installed}}\n运行 npm i -g dl-librescore@{{latest}} 命令来进行更新",
19 | "cli_windows_paste_hint": "右键粘贴",
20 | "cli_linux_paste_hint": "通常情况下使用 Ctrl+Shift+V来进行粘贴",
21 | "cli_input_message": "MuseScore的URL或文件或文件夹的路径:",
22 | "cli_input_suffix": "以https://musescore.com/ 开始或者是一个路径",
23 | "cli_types_message": "文件类型选择",
24 | "cli_output_message": "输出目录:",
25 | "cli_file_error": "文件不存在",
26 | "cli_type_error": "未选择文件类型",
27 | "cli_file_extension_error": "文件扩展名无效,只支持gp、gp3、gp4、gp5、gpx、gtp、kar、mid、midi、mscx、mscz、musicxml、mxl、ptb和xml",
28 | "cli_file_loaded_message": "文件已加载",
29 | "cli_score_loaded_message": "由 Webmscore 尔加载的乐谱",
30 | "cli_input_error": "试试用 Webmscore 尔网站:https://webmscore-pwa.librescore.org",
31 | "cli_parts_message": "选择分谱",
32 | "cli_saved_message": "已储存{{file}}",
33 | "cli_done_message": "完成",
34 | "cli_url_error": "无效网址",
35 | "cli_url_type_error": "URL只可以从下载MIDI、MP3和PDF",
36 | "cli_score_not_found": "乐谱不存在",
37 | "cli_confirm_message": "继续?",
38 | "id": "ID:{{id}}",
39 | "title": "标题:{{title}}",
40 | "no_sheet_images_error": "没有发现乐谱图像",
41 | "source_code": "源码",
42 | "version": "版本:{{version}}",
43 | "official_button": "需要订阅",
44 | "official_tooltip": "官方乐谱需要订阅 MuseScore Pro+(可免费试用)才能下载。"
45 | }
46 |
--------------------------------------------------------------------------------
/src/i18n/zh_Hant.json:
--------------------------------------------------------------------------------
1 | {
2 | "full_score": "全份樂譜",
3 | "processing": "處理中……",
4 | "official_button": "需要訂閱",
5 | "download_audio": "下載 {{fileType}} 音頻",
6 | "button_parent_not_found": "找不到按鍵的上級",
7 | "download": "下載 {{fileType}}",
8 | "official_tooltip": "官方樂譜需要訂閱 MuseScore Pro+(可免費試用)才能下載。",
9 | "cli_usage_hint": "用法: {{bin}} [options]",
10 | "unknown_button_list_mode": "未知的按鈕清單模式",
11 | "cli_example_url": "",
12 | "path_to_folder": "",
13 | "path_to_file": "",
14 | "cli_example_folder": "",
15 | "cli_example_file": "",
16 | "cli_option_input_description": "",
17 | "cli_option_type_description": "",
18 | "cli_option_output_description": "",
19 | "cli_option_verbose_description": "",
20 | "cli_outdated_version_message": "",
21 | "cli_windows_paste_hint": "",
22 | "cli_linux_paste_hint": "",
23 | "cli_input_message": "",
24 | "cli_input_suffix": "",
25 | "cli_types_message": "",
26 | "cli_output_message": "",
27 | "cli_file_error": "",
28 | "cli_type_error": "",
29 | "cli_file_loaded_message": "",
30 | "cli_file_extension_error": "",
31 | "cli_score_loaded_message": "",
32 | "cli_input_error": "",
33 | "cli_parts_message": "",
34 | "cli_saved_message": "",
35 | "cli_done_message": "",
36 | "cli_url_error": "",
37 | "cli_url_type_error": "",
38 | "cli_score_not_found": "",
39 | "cli_confirm_message": "",
40 | "id": "",
41 | "title": "",
42 | "no_sheet_images_error": "",
43 | "source_code": "",
44 | "version": ""
45 | }
46 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import "./meta";
2 |
3 | import FileSaver from "file-saver";
4 | import { waitForSheetLoaded } from "./utils";
5 | import { downloadPDF } from "./pdf";
6 | import { getFileUrl } from "./file";
7 | import { BtnList, BtnAction, BtnListMode } from "./btn";
8 | import { ScoreInfoInPage, SheetInfoInPage } from "./scoreinfo";
9 | import i18nextInit, { i18next } from "./i18n/index";
10 | import { isGmAvailable, _GM } from "./gm";
11 |
12 | (async () => {
13 | await i18nextInit;
14 | })();
15 |
16 | if (isGmAvailable("registerMenuCommand")) {
17 | // add buttons to the userscript manager menu
18 | _GM.registerMenuCommand(
19 | "** " +
20 | i18next.t("version", { version: _GM.info.script.version }) +
21 | " **",
22 | () =>
23 | _GM.openInTab(
24 | "https://github.com/LibreScore/dl-librescore/releases",
25 | {
26 | active: true,
27 | }
28 | )
29 | );
30 |
31 | _GM.registerMenuCommand("** " + i18next.t("source_code") + " **", () =>
32 | _GM.openInTab(_GM.info.script.homepage, { active: true })
33 | );
34 |
35 | _GM.registerMenuCommand("** Discord **", () =>
36 | _GM.openInTab("https://discord.gg/DKu7cUZ4XQ", { active: true })
37 | );
38 | }
39 |
40 | const { saveAs } = FileSaver;
41 |
42 | const main = (): void => {
43 | let isOfficial = !!(
44 | document.querySelector(
45 | "meta[property='musescore:author'][content='Official Scores']"
46 | ) ||
47 | document.querySelector(
48 | "meta[property='musescore:author'][content='Official Author']"
49 | )
50 | );
51 | let isMobile = !document.querySelector('button[title="Toggle Fullscreen"]');
52 | if (isMobile) {
53 | isMobile = !Array.from([
54 | ...document.querySelector("#jmuse-layout")!.querySelectorAll("div"),
55 | ]).some((div) => div.querySelectorAll("button").length === 3);
56 | }
57 | let isPDFOnly = false;
58 | if (isOfficial) {
59 | isPDFOnly = !document.querySelector("#playerControls");
60 | }
61 | new Promise(() => {
62 | let noSub = isMobile
63 | ? document.querySelector("#jmuse-scroller-component")!
64 | .childElementCount <= 1
65 | : Array.from([
66 | ...document.querySelectorAll("#jmuse-scroller-component div"),
67 | ]).some((el: HTMLDivElement) =>
68 | el.innerText.startsWith("End of preview")
69 | );
70 | const scoreinfo = new ScoreInfoInPage(document);
71 | const btnList = new BtnList();
72 | let indvPartBtn: HTMLButtonElement | null = null;
73 | const fallback = () => {
74 | // btns fallback to load from MSCZ file (`Individual Parts`)
75 | return indvPartBtn?.click();
76 | };
77 |
78 | if (noSub && isOfficial) {
79 | btnList.add({
80 | name:
81 | i18next.t("download", { fileType: "PDF" }) +
82 | "\n(" +
83 | i18next.t("official_button") +
84 | ")",
85 | action: BtnAction.openUrl("https://musescore.com/upgrade"),
86 | tooltip: i18next.t("official_tooltip"),
87 | });
88 | } else {
89 | btnList.add({
90 | name: i18next.t("download", {
91 | fileType: "PDF",
92 | }),
93 | action: BtnAction.process(
94 | async (_, setText): Promise => {
95 | return downloadPDF(
96 | scoreinfo,
97 | new SheetInfoInPage(document),
98 | saveAs,
99 | setText
100 | );
101 | },
102 |
103 | fallback,
104 | 3 * 60 * 1000 /* 3min */
105 | ),
106 | });
107 | }
108 |
109 | if (!isPDFOnly) {
110 | btnList.add({
111 | name: i18next.t("download", { fileType: "MIDI" }),
112 | action: BtnAction.download(
113 | () => getFileUrl(scoreinfo.id, "midi"),
114 | scoreinfo.fileName,
115 | fallback,
116 | 30 * 1000 /* 30s */
117 | ),
118 | });
119 |
120 | btnList.add({
121 | name: i18next.t("download", { fileType: "MP3" }),
122 | action: BtnAction.download(
123 | () => getFileUrl(scoreinfo.id, "mp3"),
124 | scoreinfo.fileName,
125 | fallback,
126 | 30 * 1000 /* 30s */
127 | ),
128 | });
129 | }
130 | // eslint-disable-next-line @typescript-eslint/no-floating-promises
131 | btnList.commit(BtnListMode.InPage);
132 | });
133 | };
134 |
135 | // eslint-disable-next-line @typescript-eslint/no-floating-promises
136 | waitForSheetLoaded().then(main);
137 |
--------------------------------------------------------------------------------
/src/meta.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name dl-librescore
3 | // @namespace https://github.com/LibreScore/dl-librescore
4 | // @homepageURL https://github.com/LibreScore/dl-librescore
5 | // @supportURL https://github.com/LibreScore/dl-librescore/issues
6 | // @updateURL https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js
7 | // @downloadURL https://github.com/LibreScore/dl-librescore/releases/latest/download/dl-librescore.user.js
8 | // @version %VERSION%
9 | // @description Download sheet music
10 | // @author LibreScore
11 | // @icon https://librescore.org/img/icons/logo.svg
12 | // @match https://musescore.com/*/*
13 | // @match https://s.musescore.com/*/*
14 | // @license MIT
15 | // @copyright Copyright (c) 2024 LibreScore
16 | // @grant unsafeWindow
17 | // @grant GM.registerMenuCommand
18 | // @grant GM.addElement
19 | // @grant GM.openInTab
20 | // @grant GM.xmlHttpRequest
21 | // @connect self
22 | // @connect musescore.com
23 | // @connect ultimate-guitar.com
24 | // @run-at document-start
25 | // ==/UserScript==
26 |
--------------------------------------------------------------------------------
/src/mscore.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
3 |
4 | import { fetchMscz } from "./mscz";
5 | import { fetchData } from "./utils";
6 | import { ScoreInfo } from "./scoreinfo";
7 | import { dependencies as depVers } from "../package.json";
8 | import isNodeJs from "detect-node";
9 | import i18nextInit, { i18next } from "./i18n/index";
10 | import { InputFileFormat } from "webmscore/schemas";
11 |
12 | (async () => {
13 | await i18nextInit;
14 | })();
15 |
16 | const WEBMSCORE_URL = `https://cdn.jsdelivr.net/npm/webmscore@${depVers.webmscore}/webmscore.js`;
17 |
18 | // fonts for Chinese characters (CN) and Korean hangul (KR)
19 | // JP characters are included in the CN font
20 | const FONT_URLS = ["CN", "KR"].map(
21 | (l) =>
22 | `https://cdn.jsdelivr.net/npm/@librescore/fonts@${depVers["@librescore/fonts"]}/SourceHanSans${l}.min.woff2`
23 | );
24 |
25 | const SF3_URL = `https://cdn.jsdelivr.net/npm/@librescore/sf3@${depVers["@librescore/sf3"]}/FluidR3Mono_GM.sf3`;
26 | const SOUND_FONT_LOADED = Symbol("SoundFont loaded");
27 |
28 | export type WebMscore = import("webmscore").default;
29 | export type WebMscoreConstr = typeof import("webmscore").default;
30 |
31 | const initMscore = async (w: Window): Promise => {
32 | if (!isNodeJs) {
33 | // attached to a page
34 | if (!w["WebMscore"]) {
35 | // init webmscore (https://github.com/LibreScore/webmscore)
36 | const script = w.document.createElement("script");
37 | script.src = WEBMSCORE_URL;
38 | w.document.body.append(script);
39 | await new Promise((resolve) => {
40 | script.onload = resolve;
41 | });
42 | }
43 | return w["WebMscore"] as WebMscoreConstr;
44 | } else {
45 | // nodejs
46 | return require("webmscore").default as WebMscoreConstr;
47 | }
48 | };
49 |
50 | let fonts: Promise | undefined;
51 | const initFonts = () => {
52 | // load CJK fonts
53 | // CJK (East Asian) characters will be rendered as "tofu" if there is no font
54 | if (!fonts) {
55 | if (isNodeJs) {
56 | // module.exports.CN = ..., module.exports.KR = ...
57 | const FONTS = Object.values(require("@librescore/fonts"));
58 |
59 | const fs = require("fs");
60 | fonts = Promise.all(
61 | FONTS.map(
62 | (path: string) =>
63 | fs.promises.readFile(path) as Promise
64 | )
65 | );
66 | } else {
67 | fonts = Promise.all(FONT_URLS.map((url) => fetchData(url)));
68 | }
69 | }
70 | };
71 |
72 | export const loadSoundFont = (score: WebMscore): Promise => {
73 | if (!score[SOUND_FONT_LOADED]) {
74 | const loadPromise = (async () => {
75 | let data: Uint8Array;
76 | if (isNodeJs) {
77 | // module.exports.FluidR3Mono = ...
78 | const SF3 = Object.values(require("@librescore/sf3"))[0];
79 | const fs = require("fs");
80 | data = await fs.promises.readFile(SF3);
81 | } else {
82 | data = await fetchData(SF3_URL);
83 | }
84 |
85 | await score.setSoundFont(data);
86 | })();
87 | score[SOUND_FONT_LOADED] = loadPromise;
88 | }
89 | return score[SOUND_FONT_LOADED] as Promise;
90 | };
91 |
92 | export const loadMscore = async (
93 | fileExt: InputFileFormat,
94 | scoreinfo: ScoreInfo,
95 | w?: Window
96 | ): Promise => {
97 | initFonts();
98 | const WebMscore = await initMscore(w!);
99 |
100 | // parse mscz data
101 | const data = new Uint8Array(
102 | new Uint8Array(await fetchMscz(scoreinfo)) // copy its ArrayBuffer
103 | );
104 | const score = await WebMscore.load(fileExt, data, await fonts);
105 | await score.generateExcerpts();
106 |
107 | return score;
108 | };
109 |
110 | export interface IndividualDownload {
111 | name: string;
112 | fileExt: string;
113 | action(score: WebMscore): Promise;
114 | }
115 |
116 | export const INDV_DOWNLOADS: IndividualDownload[] = [
117 | {
118 | name: i18next.t("download", { fileType: "PDF" }),
119 | fileExt: "pdf",
120 | action: (score) => score.savePdf(),
121 | },
122 | {
123 | name: i18next.t("download", { fileType: "MSCZ" }),
124 | fileExt: "mscz",
125 | action: (score) => score.saveMsc("mscz"),
126 | },
127 | {
128 | name: i18next.t("download", { fileType: "MSCX" }),
129 | fileExt: "mscx",
130 | action: (score) => score.saveMsc("mscx"),
131 | },
132 | {
133 | name: i18next.t("download", { fileType: "MusicXML" }),
134 | fileExt: "mxl",
135 | action: (score) => score.saveMxl(),
136 | },
137 | {
138 | name: i18next.t("download", { fileType: "MIDI" }),
139 | fileExt: "mid",
140 | action: (score) => score.saveMidi(true, true),
141 | },
142 | {
143 | name: i18next.t("download_audio", { fileType: "MP3" }),
144 | fileExt: "mp3",
145 | action: (score) =>
146 | loadSoundFont(score).then(() => score.saveAudio("mp3")),
147 | },
148 | {
149 | name: i18next.t("download_audio", { fileType: "FLAC" }),
150 | fileExt: "flac",
151 | action: (score) =>
152 | loadSoundFont(score).then(() => score.saveAudio("flac")),
153 | },
154 | {
155 | name: i18next.t("download_audio", { fileType: "OGG" }),
156 | fileExt: "ogg",
157 | action: (score) =>
158 | loadSoundFont(score).then(() => score.saveAudio("ogg")),
159 | },
160 | ];
161 |
--------------------------------------------------------------------------------
/src/mscz.ts:
--------------------------------------------------------------------------------
1 | import { assertRes, getFetch } from "./utils";
2 | import { ScoreInfo } from "./scoreinfo";
3 |
4 | export const MSCZ_BUF_SYM = Symbol("msczBufferP");
5 | export const MSCZ_URL_SYM = Symbol("msczUrl");
6 |
7 | export const loadMsczUrl = async (
8 | scoreinfo: ScoreInfo,
9 | _fetch = getFetch()
10 | ): Promise => {
11 | // look for the persisted msczUrl inside scoreinfo
12 | let result = scoreinfo.store.get(MSCZ_URL_SYM) as string;
13 | if (result) {
14 | return result;
15 | }
16 |
17 | scoreinfo.store.set(MSCZ_URL_SYM, result); // persist to scoreinfo
18 | return result;
19 | };
20 |
21 | export const fetchMscz = async (
22 | scoreinfo: ScoreInfo,
23 | _fetch = getFetch()
24 | ): Promise => {
25 | let msczBufferP = scoreinfo.store.get(MSCZ_BUF_SYM) as
26 | | Promise
27 | | undefined;
28 |
29 | if (!msczBufferP) {
30 | msczBufferP = (async (): Promise => {
31 | const url = await loadMsczUrl(scoreinfo, _fetch);
32 | const r = await _fetch(url);
33 | assertRes(r);
34 | const data = await r.arrayBuffer();
35 | return data;
36 | })();
37 | scoreinfo.store.set(MSCZ_BUF_SYM, msczBufferP);
38 | }
39 |
40 | return msczBufferP;
41 | };
42 |
43 | // eslint-disable-next-line @typescript-eslint/require-await
44 | export const setMscz = async (
45 | scoreinfo: ScoreInfo,
46 | buffer: ArrayBuffer
47 | ): Promise => {
48 | scoreinfo.store.set(MSCZ_BUF_SYM, Promise.resolve(buffer));
49 | };
50 |
--------------------------------------------------------------------------------
/src/musescore-dl/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | require("dl-librescore/dist/cli.js");
3 |
--------------------------------------------------------------------------------
/src/npm-data.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2 |
3 | import { name as pkgName, version as pkgVer } from "../package.json";
4 | import { getFetch } from "./utils";
5 |
6 | const IS_NPX_REG = /_npx(\/|\\)\d+\1/;
7 | const NPM_REGISTRY = "https://registry.npmjs.org";
8 |
9 | export function isNpx(): boolean {
10 | // file is in a npx cache dir
11 | // TODO: installed locally?
12 | return __dirname.match(IS_NPX_REG) !== null;
13 | }
14 |
15 | export function getSelfVer(): string {
16 | return pkgVer;
17 | }
18 |
19 | export async function getLatestVer(_fetch = getFetch()): Promise {
20 | // fetch pkg info from the npm registry
21 | const r = await _fetch(`${NPM_REGISTRY}/${pkgName}`);
22 | const json = await r.json();
23 | return json["dist-tags"].latest as string;
24 | }
25 |
26 | export async function getVerInfo() {
27 | const installed = getSelfVer();
28 | const latest = await getLatestVer();
29 | return {
30 | installed,
31 | latest,
32 | isLatest: installed === latest,
33 | };
34 | }
35 |
--------------------------------------------------------------------------------
/src/pdf.ts:
--------------------------------------------------------------------------------
1 | import isNodeJs from "detect-node";
2 | import { PDFWorker } from "../dist/cache/worker";
3 | import { PDFWorkerHelper } from "./worker-helper";
4 | import { getFileUrl } from "./file";
5 | import { ScoreInfo, SheetInfo, Dimensions } from "./scoreinfo";
6 | import { fetchBuffer } from "./utils";
7 |
8 | type _ExFn = (
9 | imgURLs: string[],
10 | imgType: "svg" | "png",
11 | dimensions: Dimensions,
12 | setText?: (str: string) => void
13 | ) => Promise;
14 |
15 | const _exportPDFBrowser: _ExFn = async (
16 | imgURLs,
17 | imgType,
18 | dimensions,
19 | setText
20 | ) => {
21 | const worker = new PDFWorkerHelper();
22 | const pdfArrayBuffer = await worker.generatePDF(
23 | imgURLs,
24 | imgType,
25 | dimensions.width,
26 | dimensions.height,
27 | setText
28 | );
29 | worker.terminate();
30 | return pdfArrayBuffer;
31 | };
32 |
33 | const _exportPDFNode: _ExFn = async (imgURLs, imgType, dimensions) => {
34 | const imgBufs = await Promise.all(imgURLs.map((url) => fetchBuffer(url)));
35 |
36 | const { generatePDF } = PDFWorker();
37 | const pdfArrayBuffer = (await generatePDF(
38 | imgBufs,
39 | imgType,
40 | dimensions.width,
41 | dimensions.height
42 | )) as ArrayBuffer;
43 |
44 | return pdfArrayBuffer;
45 | };
46 |
47 | export const exportPDF = async (
48 | scoreinfo: ScoreInfo,
49 | sheet: SheetInfo,
50 | scoreUrl = "",
51 | setText: (str: string) => void
52 | ): Promise => {
53 | const imgType = sheet.imgType;
54 | const pageCount = sheet.pageCount;
55 |
56 | const rs = Array.from({ length: pageCount }).map(async (_, i) => {
57 | let url;
58 | if (i === 0) {
59 | // The url to the first page is static. We don't need to use API to obtain it.
60 | url = sheet.thumbnailUrl;
61 | if (setText) {
62 | setText(`${Math.round((1 / pageCount) * 83)}%`);
63 | }
64 | } else {
65 | // obtain image urls using the API
66 | url = await getFileUrl(
67 | scoreinfo.id,
68 | "img",
69 | scoreUrl,
70 | i,
71 | undefined,
72 | setText,
73 | pageCount
74 | );
75 | }
76 | return url;
77 | });
78 | const sheetImgURLs = await Promise.all(rs);
79 | const args = [sheetImgURLs, imgType, sheet.dimensions] as const;
80 | if (!isNodeJs) {
81 | return _exportPDFBrowser(...args, setText);
82 | } else {
83 | return _exportPDFNode(...args);
84 | }
85 | };
86 |
87 | let pdfBlob: Blob;
88 | export const downloadPDF = async (
89 | scoreinfo: ScoreInfo,
90 | sheet: SheetInfo,
91 | saveAs: typeof import("file-saver").saveAs,
92 | setText: (str: string) => void
93 | ): Promise => {
94 | const name = scoreinfo.fileName;
95 | if (pdfBlob) {
96 | return saveAs(pdfBlob, `${name}.pdf`);
97 | }
98 |
99 | const pdfArrayBuffer = await exportPDF(scoreinfo, sheet, "", setText);
100 | setText("100%");
101 |
102 | pdfBlob = new Blob([pdfArrayBuffer]);
103 | saveAs(pdfBlob, `${name}.pdf`);
104 | };
105 |
--------------------------------------------------------------------------------
/src/scoreinfo.ts:
--------------------------------------------------------------------------------
1 | import { getFetch, escapeFilename } from "./utils";
2 | import i18nextInit, { i18next } from "./i18n/index";
3 |
4 | (async () => {
5 | await i18nextInit;
6 | })();
7 |
8 | export abstract class ScoreInfo {
9 | abstract id: number;
10 | abstract title: string;
11 |
12 | public store = new Map();
13 |
14 | get fileName(): string {
15 | return escapeFilename(this.title);
16 | }
17 | }
18 |
19 | export class ScoreInfoObj extends ScoreInfo {
20 | constructor(public id: number = 0, public title: string = "") {
21 | super();
22 | }
23 | }
24 |
25 | export class ScoreInfoInPage extends ScoreInfo {
26 | constructor(private document: Document) {
27 | super();
28 | }
29 |
30 | get id(): number {
31 | const el = this.document.querySelector(
32 | "meta[property='al:ios:url']"
33 | ) as HTMLMetaElement;
34 | const m = el.content.match(/(\d+)$/) as RegExpMatchArray;
35 | return +m[1];
36 | }
37 |
38 | get title(): string {
39 | const el = this.document.querySelector(
40 | "meta[property='og:title']"
41 | ) as HTMLMetaElement;
42 | return el.content;
43 | }
44 |
45 | get baseUrl(): string {
46 | const el = this.document.querySelector(
47 | "meta[property='og:image']"
48 | ) as HTMLMetaElement;
49 | const m = el.content.match(/^(.+\/)score_/) as RegExpMatchArray;
50 | return m[1];
51 | }
52 | }
53 |
54 | export class ScoreInfoHtml extends ScoreInfo {
55 | private readonly ID_REG =
56 | //;
57 | private readonly TITLE_REG = //;
58 | private readonly BASEURL_REG =
59 | //;
60 |
61 | constructor(private html: string) {
62 | super();
63 | }
64 |
65 | get id(): number {
66 | const m = this.html.match(this.ID_REG);
67 | if (!m) return 0;
68 | return +m[1];
69 | }
70 |
71 | get title(): string {
72 | const m = this.html.match(this.TITLE_REG);
73 | if (!m) return "";
74 | return m[1];
75 | }
76 |
77 | get baseUrl(): string {
78 | const m = this.html.match(this.BASEURL_REG);
79 | if (!m) return "";
80 | return m[1];
81 | }
82 |
83 | get sheet(): SheetInfo {
84 | return new SheetInfoHtml(this.html);
85 | }
86 |
87 | static async request(
88 | url: string,
89 | _fetch = getFetch()
90 | ): Promise {
91 | const r = await _fetch(url);
92 | if (!r.ok) return new ScoreInfoHtml("");
93 |
94 | const html = await r.text();
95 | return new ScoreInfoHtml(html);
96 | }
97 | }
98 |
99 | export type Dimensions = { width: number; height: number };
100 |
101 | export abstract class SheetInfo {
102 | abstract pageCount: number;
103 |
104 | /** url to the image of the first page */
105 | abstract thumbnailUrl: string;
106 |
107 | abstract dimensions: Dimensions;
108 |
109 | get imgType(): "svg" | "png" {
110 | const thumbnail = this.thumbnailUrl;
111 | const imgtype = thumbnail.match(/score_0\.(\w+)/)![1];
112 | return imgtype as "svg" | "png";
113 | }
114 | }
115 |
116 | export class SheetInfoInPage extends SheetInfo {
117 | constructor(private document: Document) {
118 | super();
119 | }
120 |
121 | private get sheet0Img(): HTMLImageElement | null {
122 | return this.document.querySelector("img[src*=score_0]");
123 | }
124 |
125 | get pageCount(): number {
126 | const sheet0Div = this.sheet0Img?.parentElement;
127 | if (!sheet0Div) {
128 | throw new Error(i18next.t("no_sheet_images_error"));
129 | }
130 | return this.document.getElementsByClassName(sheet0Div.className).length;
131 | }
132 |
133 | get thumbnailUrl(): string {
134 | const el =
135 | this.document.querySelector("link[as=image]");
136 | const url = (el?.href || this.sheet0Img?.src) as string;
137 | return url.split("@")[0];
138 | }
139 |
140 | get dimensions(): Dimensions {
141 | const { naturalWidth: width, naturalHeight: height } = this
142 | .sheet0Img as HTMLImageElement;
143 | return { width, height };
144 | }
145 | }
146 |
147 | export class SheetInfoHtml extends SheetInfo {
148 | private readonly PAGE_COUNT_REG = /pages(?:"|"):(\d+)/;
149 | private readonly THUMBNAIL_REG =
150 | / {
5 | return s.replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, "_");
6 | };
7 |
8 | export const getIndexPath = (id: number): string => {
9 | const idStr = String(id);
10 | // 获取最后三位,倒序排列
11 | // x, y, z are the reversed last digits of the score id. Example: id 123456789, x = 9, y = 8, z = 7
12 | // https://developers.musescore.com/#/file-urls
13 | // "5449062" -> ["2", "6", "0"]
14 | const indexN = idStr.split("").reverse().slice(0, 3);
15 | return indexN.join("/");
16 | };
17 |
18 | const NODE_FETCH_HEADERS = {
19 | "User-Agent":
20 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.2535.85",
21 | "Accept-Language": "en-US;q=0.8",
22 | };
23 |
24 | export const getFetch = (): typeof fetch => {
25 | if (!isNodeJs) {
26 | return fetch;
27 | } else {
28 | // eslint-disable-next-line @typescript-eslint/no-var-requires
29 | const nodeFetch = require("node-fetch");
30 | // eslint-disable-next-line @typescript-eslint/no-var-requires
31 | // Use proxy based on standard proxy environment variables
32 | const ProxyAgent = require("proxy-agent");
33 | return (input: RequestInfo, init?: RequestInit) => {
34 | if (typeof input === "string" && !input.startsWith("http")) {
35 | // fix: Only absolute URLs are supported
36 | input = "https://musescore.com" + input;
37 | }
38 | init = Object.assign(
39 | {
40 | headers: NODE_FETCH_HEADERS,
41 | // Use the `HTTPS_PROXY` environment variable for no URL given
42 | // see: https://github.com/TooTallNate/node-proxy-agent#proxy-agent
43 | agent: new ProxyAgent(),
44 | },
45 | init
46 | );
47 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return
48 | return nodeFetch(input, init);
49 | };
50 | }
51 | };
52 |
53 | export const fetchData = async (
54 | url: string,
55 | init?: RequestInit
56 | ): Promise => {
57 | const _fetch = getFetch();
58 | const r = await _fetch(url, init);
59 | const data = await r.arrayBuffer();
60 | return new Uint8Array(data);
61 | };
62 |
63 | export const fetchBuffer = async (
64 | url: string,
65 | init?: RequestInit
66 | ): Promise => {
67 | const d = await fetchData(url, init);
68 | return Buffer.from(d.buffer);
69 | };
70 |
71 | export const assertRes = (r: Response): void => {
72 | if (!r.ok) throw new Error(`${r.url} ${r.status} ${r.statusText}`);
73 | };
74 |
75 | export const useTimeout = async (
76 | promise: T | Promise,
77 | ms: number
78 | ): Promise => {
79 | if (!(promise instanceof Promise)) {
80 | return promise;
81 | }
82 |
83 | return new Promise((resolve, reject) => {
84 | const i = setTimeout(() => {
85 | reject(new Error("timeout"));
86 | }, ms);
87 | promise.then(resolve, reject).finally(() => clearTimeout(i));
88 | });
89 | };
90 |
91 | export const getSandboxWindowAsync = async (
92 | targetEl: Element | undefined = undefined
93 | ): Promise => {
94 | if (typeof document === "undefined") return {} as any as Window;
95 |
96 | if (isGmAvailable("addElement")) {
97 | // create iframe using GM_addElement API
98 | const iframe = await _GM.addElement("iframe", {});
99 | iframe.style.display = "none";
100 | return iframe.contentWindow as Window;
101 | }
102 |
103 | if (!targetEl) {
104 | return new Promise((resolve) => {
105 | // You need ads in your pages, right?
106 | const observer = new MutationObserver(() => {
107 | for (let i = 0; i < window.frames.length; i++) {
108 | // find iframe windows created by ads
109 | const frame = frames[i];
110 | try {
111 | const href = frame.location.href;
112 | if (href === location.href || href === "about:blank") {
113 | resolve(frame);
114 | return;
115 | }
116 | } catch {}
117 | }
118 | });
119 | observer.observe(document.body, { subtree: true, childList: true });
120 | });
121 | }
122 |
123 | return new Promise((resolve) => {
124 | const eventName = "onmousemove";
125 | const id = Math.random().toString();
126 |
127 | targetEl[id] = (iframe: HTMLIFrameElement) => {
128 | delete targetEl[id];
129 | targetEl.removeAttribute(eventName);
130 |
131 | iframe.style.display = "none";
132 | targetEl.append(iframe);
133 | const w = iframe.contentWindow;
134 | resolve(w as Window);
135 | };
136 |
137 | targetEl.setAttribute(
138 | eventName,
139 | `this['${id}'](document.createElement('iframe'))`
140 | );
141 | });
142 | };
143 |
144 | export const getUnsafeWindow = (): Window => {
145 | // eslint-disable-next-line no-eval
146 | return window.eval("window") as Window;
147 | };
148 |
149 | export const console: Console = (
150 | typeof window !== "undefined" ? window : global
151 | ).console; // Object.is(window.console, unsafeWindow.console) == false
152 |
153 | export const windowOpenAsync = (
154 | targetEl: Element | undefined,
155 | ...args: Parameters
156 | ): Promise => {
157 | return getSandboxWindowAsync(targetEl).then((w) => w.open(...args));
158 | };
159 |
160 | export const attachShadow = (el: Element): ShadowRoot => {
161 | return Element.prototype.attachShadow.call(el, {
162 | mode: "closed",
163 | }) as ShadowRoot;
164 | };
165 |
166 | export const waitForDocumentLoaded = (): Promise => {
167 | if (document.readyState !== "complete") {
168 | return new Promise((resolve) => {
169 | const cb = () => {
170 | if (document.readyState === "complete") {
171 | resolve();
172 | document.removeEventListener("readystatechange", cb);
173 | }
174 | };
175 | document.addEventListener("readystatechange", cb);
176 | });
177 | } else {
178 | return Promise.resolve();
179 | }
180 | };
181 |
182 | /**
183 | * Run script before the page is fully loaded
184 | */
185 | export const waitForSheetLoaded = (): Promise => {
186 | return new Promise((resolve) => {
187 | const observer = new MutationObserver(() => {
188 | const meta =
189 | document.querySelector(
190 | "#ELEMENT_ID_SCORE_DOWNLOAD_SECTION > section > button"
191 | ) && document.querySelector("#jmuse-scroller-component div");
192 | if (meta) {
193 | resolve();
194 | observer.disconnect();
195 | }
196 | });
197 | observer.observe(document, { childList: true, subtree: true });
198 | });
199 | };
200 |
--------------------------------------------------------------------------------
/src/webpack-hook.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-return */
2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
3 |
4 | import { hookNative } from "./anti-detection";
5 | import { console, getUnsafeWindow } from "./utils";
6 |
7 | const CHUNK_PUSH_FN = /^function [^r]\(\w\){/;
8 |
9 | interface Module {
10 | (module, exports, __webpack_require__): void;
11 | }
12 |
13 | type WebpackJson = [(number | string)[], { [id: string]: Module }, any[]?][];
14 |
15 | const moduleLookup = (id: string, globalWebpackJson: WebpackJson) => {
16 | const pack = globalWebpackJson.find((x) => x[1][id])!;
17 | return pack[1][id];
18 | };
19 |
20 | /**
21 | * Retrieve (webpack_require) a module from the page's webpack package
22 | *
23 | * I know this is super hacky.
24 | */
25 | export const webpackHook = (
26 | moduleId: string,
27 | moduleOverrides: { [id: string]: Module } = {},
28 | globalWebpackJson: WebpackJson = window["webpackJsonpmusescore"]
29 | ) => {
30 | const t = Object.assign(
31 | (id: string, override = true) => {
32 | const r: any = {};
33 | const m: Module =
34 | override && moduleOverrides[id]
35 | ? moduleOverrides[id]
36 | : moduleLookup(id, globalWebpackJson);
37 | m(r, r, t);
38 | if (r.exports) return r.exports;
39 | return r;
40 | },
41 | {
42 | d(exp, name, fn) {
43 | return (
44 | Object.prototype.hasOwnProperty.call(exp, name) ||
45 | Object.defineProperty(exp, name, {
46 | enumerable: true,
47 | get: fn,
48 | })
49 | );
50 | },
51 | n(e) {
52 | const m = e.__esModule ? () => e.default : () => e;
53 | t.d(m, "a", m);
54 | return m;
55 | },
56 | r(r) {
57 | Object.defineProperty(r, "__esModule", { value: true });
58 | },
59 | e() {
60 | return Promise.resolve();
61 | },
62 | }
63 | );
64 |
65 | return t(moduleId);
66 | };
67 |
68 | export const ALL = "*";
69 |
70 | export const [webpackGlobalOverride, onPackLoad] = (() => {
71 | type OnPackLoadFn = (pack: WebpackJson[0]) => any;
72 |
73 | const moduleOverrides: { [id: string]: Module } = {};
74 | const onPackLoadFns: OnPackLoadFn[] = [];
75 |
76 | function applyOverride(pack: WebpackJson[0]) {
77 | let entries = Object.entries(moduleOverrides);
78 | // apply to all
79 | const all = moduleOverrides[ALL];
80 | if (all) {
81 | entries = Object.keys(pack[1]).map((id) => [id, all]);
82 | }
83 |
84 | entries.forEach(([id, override]) => {
85 | const mod = pack[1][id];
86 | if (mod) {
87 | pack[1][id] = function (n, r, t) {
88 | // make exports configurable
89 | t = Object.assign(t, {
90 | d(exp, name, fn) {
91 | return Object.defineProperty(exp, name, {
92 | enumerable: true,
93 | get: fn,
94 | configurable: true,
95 | });
96 | },
97 | });
98 | mod(n, r, t);
99 | override(n, r, t);
100 | };
101 | }
102 | });
103 | }
104 |
105 | // hook `webpackJsonpmusescore.push` as soon as `webpackJsonpmusescore` is available
106 | const _w = getUnsafeWindow();
107 | let jsonp = _w["webpackJsonpmusescore"];
108 | let hooked = false;
109 | Object.defineProperty(_w, "webpackJsonpmusescore", {
110 | get() {
111 | return jsonp;
112 | },
113 | set(v: WebpackJson) {
114 | jsonp = v;
115 | if (!hooked && v.push.toString().match(CHUNK_PUSH_FN)) {
116 | hooked = true;
117 | hookNative(v, "push", (_fn) => {
118 | return function (pack) {
119 | onPackLoadFns.forEach((fn) => fn(pack));
120 | applyOverride(pack);
121 | return _fn.call(this, pack);
122 | };
123 | });
124 | }
125 | },
126 | });
127 |
128 | return [
129 | // set overrides
130 | (moduleId: string, override: Module) => {
131 | moduleOverrides[moduleId] = override;
132 | },
133 | // set onPackLoad listeners
134 | (fn: OnPackLoadFn) => {
135 | onPackLoadFns.push(fn);
136 | },
137 | ] as const;
138 | })();
139 |
140 | export const webpackContext = new Promise((resolve) => {
141 | webpackGlobalOverride(ALL, (n, r, t) => {
142 | resolve(t);
143 | });
144 | });
145 |
146 | const PACK_ID_REG = /\+(\{.*?"\})\[\w\]\+/;
147 |
148 | export const loadAllPacks = () => {
149 | return webpackContext.then((ctx) => {
150 | try {
151 | const fn = ctx.e.toString();
152 | const packsData = fn.match(PACK_ID_REG)[1] as string;
153 | // eslint-disable-next-line no-new-func, @typescript-eslint/no-implied-eval
154 | const packs = Function(`return (${packsData})`)() as {
155 | [id: string]: string;
156 | };
157 |
158 | Object.keys(packs).forEach((id) => {
159 | ctx.e(id);
160 | });
161 | } catch (err) {
162 | console.error(err);
163 | }
164 | });
165 | };
166 |
167 | const OBF_FN_REG =
168 | /\w\(".{4}"\),(\w)=(\[".+?\]);\w=\1,\w=(\d+).+?\);var (\w=.+?,\w\})/;
169 | export const OBFUSCATED_REG = /(\w)\((\d+),"(.{4})"\)/g;
170 |
171 | export const getObfuscationCtx = (
172 | mod: Module
173 | ): ((n: number, s: string) => string) => {
174 | const str = mod.toString();
175 | const m = str.match(OBF_FN_REG);
176 | if (!m) return () => "";
177 |
178 | try {
179 | const arrVar = m[1];
180 | const arr = JSON.parse(m[2]);
181 |
182 | let n = +m[3] + 1;
183 | for (; --n; ) arr.push(arr.shift());
184 |
185 | const fnStr = m[4];
186 | const ctxStr = `var ${arrVar}=${JSON.stringify(arr)};return (${fnStr})`;
187 | // eslint-disable-next-line no-new-func, @typescript-eslint/no-implied-eval
188 | const fn = new Function(ctxStr)();
189 |
190 | return fn;
191 | } catch (err) {
192 | console.error(err);
193 | return () => "";
194 | }
195 | };
196 |
197 | export default webpackHook;
198 |
--------------------------------------------------------------------------------
/src/worker-helper.ts:
--------------------------------------------------------------------------------
1 | import { PDFWorkerMessage } from "./worker";
2 | import { PDFWorker } from "../dist/cache/worker";
3 |
4 | const scriptUrlFromFunction = (fn: () => any): string => {
5 | const blob = new Blob(["(" + fn.toString() + ")()"], {
6 | type: "application/javascript",
7 | });
8 | return window.URL.createObjectURL(blob);
9 | };
10 |
11 | // Node.js fix
12 | if (typeof Worker === "undefined") {
13 | globalThis.Worker = class {} as any; // noop shim
14 | }
15 |
16 | export class PDFWorkerHelper extends Worker {
17 | constructor() {
18 | const url = scriptUrlFromFunction(PDFWorker);
19 | super(url);
20 | }
21 |
22 | generatePDF(
23 | imgURLs: string[],
24 | imgType: "svg" | "png",
25 | width: number,
26 | height: number,
27 | setText?: (str: string) => void
28 | ): Promise {
29 | const msg: PDFWorkerMessage = [imgURLs, imgType, width, height];
30 | this.postMessage(msg);
31 |
32 | return new Promise((resolve) => {
33 | if (setText) {
34 | const onProgress = (e: MessageEvent) => {
35 | if (e.data.type === "fetchProgress") {
36 | // Call the setText callback with the progress percentage
37 | setText(`${e.data.progress}%`);
38 | } else if (e.data instanceof ArrayBuffer) {
39 | // If the data is the final PDF buffer, resolve the promise
40 | resolve(e.data);
41 |
42 | // Remove the event listener once we have resolved the promise
43 | this.removeEventListener("message", onProgress);
44 | }
45 | };
46 | this.addEventListener("message", onProgress);
47 | } else {
48 | this.addEventListener("message", (e) => {
49 | resolve(e.data);
50 | });
51 | }
52 | });
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/worker.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import isNodeJs from "detect-node";
4 | import PDFDocument from "pdfkit/lib/document";
5 | import SVGtoPDF from "svg-to-pdfkit";
6 |
7 | type ImgType = "svg" | "png";
8 |
9 | type DataResultType = "dataUrl" | "text";
10 |
11 | const readData = (
12 | data: Blob | Buffer,
13 | type: DataResultType
14 | ): string | Promise => {
15 | if (!(data instanceof Uint8Array)) {
16 | // blob
17 | return new Promise((resolve, reject) => {
18 | const reader = new FileReader();
19 | reader.onload = (): void => {
20 | const result = reader.result;
21 | resolve(result as string);
22 | };
23 | reader.onerror = reject;
24 | if (type === "dataUrl") {
25 | reader.readAsDataURL(data);
26 | } else {
27 | reader.readAsText(data);
28 | }
29 | });
30 | } else {
31 | // buffer
32 | if (type === "dataUrl") {
33 | return "data:image/png;base64," + data.toString("base64");
34 | } else {
35 | return data.toString("utf-8");
36 | }
37 | }
38 | };
39 |
40 | /**
41 | * @platform browser
42 | */
43 | const fetchBlob = async (imgUrl: string): Promise => {
44 | const r = await fetch(imgUrl, {
45 | cache: "no-cache",
46 | });
47 | return r.blob();
48 | };
49 |
50 | /**
51 | * @example
52 | * import { PDFWorker } from '../dist/cache/worker'
53 | * const { generatePDF } = PDFWorker()
54 | * const pdfData = await generatePDF(...)
55 | */
56 | export const generatePDF = async (
57 | imgBlobs: Blob[] | Buffer[],
58 | imgType: ImgType,
59 | width: number,
60 | height: number
61 | ): Promise => {
62 | // @ts-ignore
63 | const pdf = new (PDFDocument as typeof import("pdfkit"))({
64 | // compress: true,
65 | size: [width, height],
66 | autoFirstPage: false,
67 | margin: 0,
68 | layout: "portrait",
69 | });
70 |
71 | if (imgType === "png") {
72 | const imgDataUrlList: string[] = await Promise.all(
73 | imgBlobs.map((b) => readData(b, "dataUrl"))
74 | );
75 |
76 | imgDataUrlList.forEach((data) => {
77 | pdf.addPage();
78 | pdf.image(data, {
79 | width,
80 | height,
81 | });
82 | });
83 | } else {
84 | // imgType == "svg"
85 | const svgList = await Promise.all(
86 | imgBlobs.map((b) => readData(b, "text"))
87 | );
88 |
89 | svgList.forEach((svg) => {
90 | pdf.addPage();
91 | SVGtoPDF(pdf, svg, 0, 0, {
92 | preserveAspectRatio: "none",
93 | });
94 | });
95 | }
96 |
97 | // @ts-ignore
98 | const buf: Uint8Array = await pdf.getBuffer();
99 |
100 | return buf.buffer;
101 | };
102 |
103 | export type PDFWorkerMessage = [string[], ImgType, number, number];
104 |
105 | /**
106 | * @platform browser (web worker)
107 | */
108 | if (typeof onmessage !== "undefined") {
109 | onmessage = async (e): Promise => {
110 | const [imgUrls, imgType, width, height] = e.data as PDFWorkerMessage;
111 | let imgBlobs;
112 |
113 | if (isNodeJs) {
114 | imgBlobs = await Promise.all(imgUrls.map((url) => fetchBlob(url)));
115 | } else {
116 | let completedFetches = 0;
117 |
118 | const sendProgress = () => {
119 | const progress = Math.round(
120 | 83 + (completedFetches / imgUrls.length) * 17
121 | );
122 | postMessage({ type: "fetchProgress", progress });
123 | };
124 |
125 | imgBlobs = await Promise.all(
126 | imgUrls.map(async (url, i) => {
127 | const blob = await fetchBlob(url);
128 | completedFetches++;
129 | sendProgress();
130 | return blob;
131 | })
132 | );
133 | }
134 | const pdfBuf = await generatePDF(imgBlobs, imgType, width, height);
135 |
136 | postMessage(pdfBuf, [pdfBuf]);
137 | };
138 | }
139 |
--------------------------------------------------------------------------------
/src/wrapper.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | const w = typeof unsafeWindow == "object" ? unsafeWindow : window;
3 |
4 | // GM APIs glue
5 | const _GM = typeof GM == "object" ? GM : undefined;
6 | const gmId = "" + Math.random();
7 | w[gmId] = _GM;
8 |
9 | function getRandL() {
10 | return String.fromCharCode(97 + Math.floor(Math.random() * 26));
11 | }
12 |
13 | // script loader
14 | new Promise((resolve) => {
15 | const id = "" + Math.random();
16 | w[id] = resolve;
17 |
18 | const stackN = 9;
19 | let loaderIntro = "";
20 | for (let i = 0; i < stackN; i++) {
21 | loaderIntro += `(function ${getRandL()}(){`;
22 | }
23 | const loaderOutro = "})()".repeat(stackN);
24 | const mockUrl = "https://c.amazon-adsystem.com/aax2/apstag.js";
25 |
26 | Function(
27 | `${loaderIntro}const d=new Image();window['${id}'](d);delete window['${id}'];document.body.prepend(d)${loaderOutro}//# sourceURL=${mockUrl}`
28 | )();
29 | }).then((d) => {
30 | d.style.display = "none";
31 | d.src =
32 | "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
33 | d.once = false;
34 | d.setAttribute(
35 | "onload",
36 | `if(this.once)return;this.once=true;this.remove();const GM=window['${gmId}'];delete window['${gmId}'];(` +
37 | function a() {
38 | /** script code here */
39 | }.toString() +
40 | ")()"
41 | );
42 | });
43 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "lib": ["dom", "dom.iterable", "es2019"],
5 | "module": "commonjs",
6 | "moduleResolution": "node",
7 | "esModuleInterop": true,
8 | "resolveJsonModule": true,
9 | "strictNullChecks": true,
10 | "sourceMap": false,
11 | "newLine": "lf"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------