├── .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 | LibreScore logo 12 | 13 | [![Discord](https://img.shields.io/discord/774491656643674122?color=5865F2&label=&labelColor=555555&logo=discord&logoColor=FFFFFF)](https://discord.gg/DKu7cUZ4XQ) [![Weblate](https://weblate.librescore.org/widgets/librescore/-/dl-librescore/svg-badge.svg)](https://weblate.librescore.org/engage/librescore) [![Github All Releases](https://img.shields.io/github/downloads/LibreScore/app-librescore/total.svg?label=App)](https://github.com/LibreScore/app-librescore/releases/latest) [![Github All Releases](https://img.shields.io/github/downloads/LibreScore/dl-librescore/total.svg?label=Userscript)](https://github.com/LibreScore/dl-librescore/releases/latest) [![npm](https://img.shields.io/npm/dt/dl-librescore?label=Command-line+tool)](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 | Logo LibreScore 12 | 13 | [![Discord](https://img.shields.io/discord/774491656643674122?color=5865F2&label=&labelColor=555555&logo=discord&logoColor=FFFFFF)](https://discord.gg/DKu7cUZ4XQ) [![Weblate](https://weblate.librescore.org/widgets/librescore/-/dl-librescore/svg-badge.svg)](https://weblate.librescore.org/engage/librescore) [![GitHub Všechna Vydání](https://img.shields.io/github/downloads/LibreScore/app-librescore/total.svg?label=App)](https://github.com/LibreScore/app-librescore/releases/latest) [![GitHub Všechna Vydání](https://img.shields.io/github/downloads/LibreScore/dl-librescore/total.svg?label=Uživatelský+skript)](https://github.com/LibreScore/dl-librescore/releases/latest) [![npm](https://img.shields.io/npm/dt/dl-librescore?label=Nástroj+příkazového+řádku)](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 | LibreScore logo 12 | 13 | [![Discord](https://img.shields.io/discord/774491656643674122?color=5865F2&label=&labelColor=555555&logo=discord&logoColor=FFFFFF)](https://discord.gg/DKu7cUZ4XQ) [![Weblate](https://weblate.librescore.org/widgets/librescore/-/dl-librescore/svg-badge.svg)](https://weblate.librescore.org/engage/librescore) [![Github All Releases](https://img.shields.io/github/downloads/LibreScore/app-librescore/total.svg?label=App)](https://github.com/LibreScore/app-librescore/releases/latest) [![Github All Releases](https://img.shields.io/github/downloads/LibreScore/dl-librescore/total.svg?label=Userscript)](https://github.com/LibreScore/dl-librescore/releases/latest) [![npm](https://img.shields.io/npm/dt/dl-librescore?label=Command-line+tool)](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 | Logotipo de LibreScore 12 | 13 | [![Discord](https://img.shields.io/discord/774491656643674122?color=5865F2&label=&labelColor=555555&logo=discord&logoColor=FFFFFF)](https://discord.gg/DKu7cUZ4XQ) [![Weblate](https://weblate.librescore.org/widgets/librescore/-/dl-librescore/svg-badge.svg)](https://weblate.librescore.org/engage/librescore) [![GitHub todos los lanzamientos](https://img.shields.io/github/downloads/LibreScore/app-librescore/total.svg?label=App)](https://github.com/LibreScore/app-librescore/releases/latest) [![GitHub todos los lanzamientos](https://img.shields.io/github/downloads/LibreScore/dl-librescore/total.svg?label=Guión+de+usuario)](https://github.com/LibreScore/dl-librescore/releases/latest) [![npm](https://img.shields.io/npm/dt/dl-librescore?label=Herramienta+de+línea-de-comandos)](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 | Logo de LibreScore 12 | 13 | [![Discord](https://img.shields.io/discord/774491656643674122?color=5865F2&label=&labelColor=555555&logo=discord&logoColor=FFFFFF)](https://discord.gg/DKu7cUZ4XQ) [![Weblate](https://weblate.librescore.org/widgets/librescore/-/dl-librescore/svg-badge.svg)](https://weblate.librescore.org/engage/librescore) [![GitHub toutes les versions](https://img.shields.io/github/downloads/LibreScore/app-librescore/total.svg?label=App)](https://github.com/LibreScore/app-librescore/releases/latest) [![GitHub toutes les versions](https://img.shields.io/github/downloads/LibreScore/dl-librescore/total.svg?label=Script+utilisateur)](https://github.com/LibreScore/dl-librescore/releases/latest) [![npm](https://img.shields.io/npm/dt/dl-librescore?label=Outil+de+ligne-de-commande)](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 | LibreScore logó 12 | 13 | [![Discord](https://img.shields.io/discord/774491656643674122?color=5865F2&label=&labelColor=555555&logo=discord&logoColor=FFFFFF)](https://discord.gg/DKu7cUZ4XQ) [![Weblate](https://weblate.librescore.org/widgets/librescore/-/dl-librescore/svg-badge.svg)](https://weblate.librescore.org/engage/librescore) [![Github minden verzió](https://img.shields.io/github/downloads/LibreScore/app-librescore/total.svg?label=App)](https://github.com/LibreScore/app-librescore/releases/latest) [![Github minden verzió](https://img.shields.io/github/downloads/LibreScore/dl-librescore/total.svg?label=Userscript)](https://github.com/LibreScore/dl-librescore/releases/latest) [![npm](https://img.shields.io/npm/dt/dl-librescore?label=Parancssoros+eszköz)](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 | LibreScore logo 12 | 13 | [![Discord](https://img.shields.io/discord/774491656643674122?color=5865F2&label=&labelColor=555555&logo=discord&logoColor=FFFFFF)](https://discord.gg/DKu7cUZ4XQ) [![Weblate](https://weblate.librescore.org/widgets/librescore/-/dl-librescore/svg-badge.svg)](https://weblate.librescore.org/engage/librescore) [![Github Semua Keluaran](https://img.shields.io/github/downloads/LibreScore/app-librescore/total.svg?label=Apl)](https://github.com/LibreScore/app-librescore/releases/latest) [![Github Semua Keluaran](https://img.shields.io/github/downloads/LibreScore/dl-librescore/total.svg?label=Skrip+pengguna)](https://github.com/LibreScore/dl-librescore/releases/latest) [![npm](https://img.shields.io/npm/dt/dl-librescore?label=Alat+baris+arahan)](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://img.shields.io/discord/774491656643674122?color=5865F2&label=&labelColor=555555&logo=discord&logoColor=FFFFFF)](https://discord.gg/DKu7cUZ4XQ) [![Weblate](https://weblate.librescore.org/widgets/librescore/-/dl-librescore/svg-badge.svg)](https://weblate.librescore.org/engage/librescore) [![Github All Releases](https://img.shields.io/github/downloads/LibreScore/app-librescore/total.svg?label=App)](https://github.com/LibreScore/app-librescore/releases/latest) [![Github All Releases](https://img.shields.io/github/downloads/LibreScore/dl-librescore/total.svg?label=Userscript)](https://github.com/LibreScore/dl-librescore/releases/latest) [![npm](https://img.shields.io/npm/dt/dl-librescore?label=Command-line+tool)](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 | LibreScore 标志 12 | 13 | [![Discord](https://img.shields.io/discord/774491656643674122?color=5865F2&label=&labelColor=555555&logo=discord&logoColor=FFFFFF)](https://discord.gg/DKu7cUZ4XQ) [![Weblate(翻译)](https://weblate.librescore.org/widgets/librescore/-/dl-librescore/svg-badge.svg)](https://weblate.librescore.org/engage/librescore) [![Github 全部发布版](https://img.shields.io/github/downloads/LibreScore/app-librescore/total.svg?label=应用)](https://github.com/LibreScore/app-librescore/releases/latest) [![Github 全部发布版](https://img.shields.io/github/downloads/LibreScore/dl-librescore/total.svg?label=用户脚本)](https://github.com/LibreScore/dl-librescore/releases/latest) [![node包管理器](https://img.shields.io/npm/dt/dl-librescore?label=命令行工具)](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 | --------------------------------------------------------------------------------