Compare commits

..

182 commits

Author SHA1 Message Date
Wenzheng Tang
8e3dd94c12
feat: add @icon metadata
Merge pull request #125 from Wupb/patch-1
2021-06-18 18:31:05 -04:00
Wupb
b1a3456646
Update @icon metadata 2021-06-18 13:09:15 -07:00
Wupb
d269ae9be9
Add @icon metadata 2021-06-18 11:46:01 -07:00
Xmader
20298e5c0e
v0.24.1 2021-06-06 12:48:20 -04:00
Xmader
49880d63da
fix: mp3 & midi download 2021-06-06 12:47:21 -04:00
Xmader
0c2106993d
chore: source tarball name consistency 2021-05-21 17:55:14 -04:00
Xmader
3ac3a51ad1
v0.24.0 2021-05-21 17:44:53 -04:00
Xmader
9c05dd5d3d
fix: btn position when logged in 2021-05-21 17:41:18 -04:00
Xmader
ae08ff847a
chore: .eslintrc 2021-05-21 17:40:52 -04:00
Xmader
8cc089357f
fix: default btn position 2021-05-21 16:50:04 -04:00
Xmader
4e92c06791
refactor: btn color theme 2021-05-19 02:10:28 -04:00
Xmader
5d1639dddf
feat: View in LibreScore btn light color 2021-05-19 02:02:52 -04:00
Xmader
88fdfff054
feat: match the new design 2021-05-19 01:59:10 -04:00
Xmader
893c64edda
v0.23.15 2021-05-13 23:56:00 -04:00
Xmader
1bf3050513
feat: update librescore link 2021-05-13 23:54:39 -04:00
Xmader
ffbac084fb
feat: direct discord url to the #dataset-bugs channel 2021-05-13 16:43:40 -04:00
Xmader
861af6d3f0
v0.23.13 2021-05-11 17:03:38 -04:00
Xmader
30514e9507
fix: tampermonkey script on Chrome
fixes: #121
2021-05-11 17:02:32 -04:00
Wenzheng Tang
7a397e068f
fix: discord message formatting
Merge pull request #120 from PeterNjeim/master
2021-05-10 21:23:42 -04:00
Peter Njeim
d97b3d86d8 fix: discord message formatting 2021-05-10 14:08:15 -03:00
Xmader
bef85c5c3e
v0.23.12 2021-05-10 10:57:16 -04:00
Xmader
b68ff38028
fix: mp3 & midi download
The result differed as connection speed differs.

race condition: this script code and `webpackChunkmusescore_es6` chunks
2021-05-10 10:55:24 -04:00
Xmader
a571fed456
v0.23.11 2021-05-09 14:29:26 -04:00
Xmader
f9be984d17
refactor: discord url 2021-05-09 14:27:33 -04:00
Xmader
9adba49de1
feat: ask user to send Discord message on error 2021-05-09 14:25:43 -04:00
Xmader
6d93907df0
chore: include source tarballs in release mirrors on ipfs
https://discord.com/channels/774491656643674122/774491656643674128/838118504535949312
2021-05-02 18:09:12 -04:00
Wenzheng Tang
c5d6d87b8c
Merge pull request #113 from RubenVerg/patch-2
Update cli.ts
By default, `npm i -g (already installed package)` only installs new patches, not minors or majors.
2021-05-02 18:01:24 -04:00
Ruben Vergani
8475941d47
Update cli.ts
by default, `npm i -g (already installed package)` only installs new patches, not minors or majors.
2021-04-10 19:07:50 +02:00
Xmader
71def367e8
v0.23.10 2021-03-13 01:29:30 -05:00
Xmader
d1c9634331
fix: ask user to send Discord message when score is missing from dataset 2021-03-13 01:28:37 -05:00
Wenzheng Tang
49aef0ccf3
feat: Ask user to send Discord message when score is missing from dataset
Merge pull request #110 from PeterNjeim/master
2021-03-12 21:24:54 -05:00
Peter Njeim
ec15d44811 revert: dist/main.js 2021-03-12 20:20:27 -04:00
Peter Njeim
4494e15540 feat: include discord link in message, npm audit fixes, and typescript handbook standards 2021-03-12 20:10:50 -04:00
Peter Njeim
6a6e2a5ea1 fix: packages and unnecessary if statement 2021-03-12 19:35:41 -04:00
Xmader
3260672412
v0.23.9 2021-02-06 05:15:31 -05:00
Xmader
004cb16fce
fix: button min width 2021-02-06 05:12:34 -05:00
Xmader
ec1c1ea87c
chore(deps): upgrade @librescore/fonts 2021-02-06 05:06:21 -05:00
Xmader
69e5bd0a78
fix(i18n): button text overflow
https://discord.com/channels/774491656643674122/776293233382653963/806828497242816522
2021-02-05 18:07:24 -05:00
Xmader
06a91b1c2d
v0.23.8 2021-01-27 14:03:58 -05:00
Xmader
41f5286d48
fix: CORS issue for PDF download
CORS requests would fail after a cached no-CORS request of the same urls
2021-01-27 12:57:35 -05:00
Xmader
29a09c2596
refactor: pdf worker fetch blobs 2021-01-27 12:35:52 -05:00
Xmader
f2a52dd514
refactor: pdf worker 2021-01-27 12:26:32 -05:00
Xmader
1e3e2d7581
v0.23.7 2021-01-27 11:41:14 -05:00
Xmader
eda8342a3d
fix: PDF download when the piano keyboard is open 2021-01-27 11:31:09 -05:00
Xmader
e9ed0812b9
fix: PDF page count
This will emit a malformed 642 byte PDF file.
2021-01-27 11:20:29 -05:00
Xmader
4bd5d55676
fix: MIDI download 2021-01-27 11:09:06 -05:00
Xmader
c571a49093
v0.23.6 2021-01-21 23:43:40 -05:00
Xmader
b5477a4059
fix: btns position 2021-01-21 23:40:53 -05:00
Xmader
d33c06c892
feat(cli): print musescore-downloader version, msdl -v 2021-01-21 21:24:08 -05:00
Xmader
04884a137f
chore(deps): upgrade webmscore to v0.18.0 2021-01-21 01:58:20 -05:00
Xmader
263f72dc7a
feat: lock downloaded dependency versions 2021-01-21 01:44:13 -05:00
Xmader
c46343b46c
v0.23.5 2021-01-20 11:32:57 -05:00
Xmader
6202321a42
refactor(npm-data): avoid running external npm cmd 2021-01-20 11:29:49 -05:00
Xmader
3c72b5a92f
refactor: check version info 2021-01-20 11:05:56 -05:00
Xmader
085c6a2d2a
refactor: add check for latest version 2021-01-20 10:52:08 -05:00
Xmader
1eb0f35bde
feat: add check for latest version
Merge pull request #106 from RubenVerg/master
2021-01-20 10:45:36 -05:00
Xmader
49fcb99160
fix: MIDI or PDF download not working (#107) 2021-01-20 10:44:54 -05:00
Xmader
bd943675d8
refactor: check for latest version 2021-01-20 10:44:00 -05:00
Xmader
030d37ddc0
refactor: read installed version using the package.json 2021-01-20 10:42:33 -05:00
Xmader
d014ade9ca
feat: msdl package to alias a specific version of musescore-downloader 2021-01-15 07:28:57 -05:00
Ruben Vergani
b8181f421d bump npm 2021-01-15 09:08:40 +01:00
Ruben Vergani
463ea5d416 for reasons to me unknown, eslint doesn't work,
so manually fixed the file
2021-01-15 09:06:31 +01:00
Ruben Vergani
da5d53898a add the check to the CLI 2021-01-15 09:02:48 +01:00
Ruben Vergani
2b842e267f expose the installed and latest version functions 2021-01-15 09:01:11 +01:00
Ruben Vergani
08294f564b spaces, not tabs 2021-01-15 08:55:06 +01:00
Ruben Vergani
46a7f50115 add functions to check if msdl is running 2021-01-15 08:54:01 +01:00
Xmader
7bb3aaf7b1
v0.23.4 2021-01-12 04:04:58 -05:00
Xmader
dd30454b5a
feat: update scorepack index 2021-01-12 04:02:52 -05:00
Xmader
d99848c6fc
v0.23.3 2021-01-07 03:39:56 -05:00
Xmader
c973d5d06f
chore: lock webmscore version to v0.16.2 2021-01-07 03:39:32 -05:00
Xmader
d919441966
chore: upgrade webmscore
There was a critical RuntimeError due to compiling bug in webmscore v0.16.1
2021-01-07 03:29:38 -05:00
Xmader
86c15d55fe
v0.23.2 2021-01-07 02:47:37 -05:00
Xmader
1faaf660c4
fix: pass the scroll event through the btns background
https://stackoverflow.com/questions/1009753/
2021-01-07 02:45:58 -05:00
Xmader
8250d80d4b
fix: btns position relative to the entire document instead of viewport 2021-01-07 02:38:06 -05:00
Xmader
d9d09c4e8f
perf: improve scroll performance
https://developers.google.com/web/updates/2016/06/passive-event-listeners
2021-01-07 01:57:13 -05:00
Xmader
20704eba75
chore: upgrade webmscore 2021-01-05 09:13:00 -05:00
Xmader
3b329301c1
v0.23.1 2021-01-02 10:47:45 -05:00
Xmader
86c5429dce
feat(i18n): add Chinese translations 2021-01-02 10:46:14 -05:00
Xmader
7f860faf66
feat(i18n): update translations
Co-authored-by:  ClaudioAmato <32262813+ClaudioAmato@users.noreply.github.com>
2021-01-02 10:25:38 -05:00
Xmader
de5ff6ff98
doc: add Italian to readme 2021-01-02 09:54:42 -05:00
Xmader
1b0d3d2eae
feat(i18n): update translations
Co-authored-by:  ClaudioAmato <32262813+ClaudioAmato@users.noreply.github.com>
2021-01-02 09:50:35 -05:00
Claudio Amato
28ac964ba3
Added italian Readme 2021-01-02 15:22:47 +01:00
Xmader
44f7edfaa3
feat(i18n): add Italian language 2021-01-02 07:41:46 -05:00
Xmader
46879ffacd
feat(i18n): add Italian language 2021-01-02 07:34:31 -05:00
Xmader
8945a53896
refactor: i18n "View in LibreScore" 2021-01-02 02:44:43 -05:00
Xmader
60d842bac7
feat: add View in LibreScore in msdl cli 2021-01-02 02:34:19 -05:00
Xmader
bca9f5e150
chore: update license year 2021-01-02 01:11:54 -05:00
PaoloC95
519a5c3671
it.ts
Italian Language
2021-01-01 21:08:19 +01:00
Xmader
13031c323b
v0.23.0 2020-12-31 22:01:18 -05:00
Xmader
2cfb060750
fix: open url 2020-12-31 21:59:10 -05:00
Xmader
047fbf06b6
feat: reposition btns when scrolling 2020-12-31 14:29:37 -05:00
Xmader
c01d797983
fix: open url 2020-12-31 14:08:00 -05:00
Xmader
bb39ebccc9
fix: btn position when window resizes 2020-12-31 13:13:26 -05:00
Xmader
8e0ce90093
feat: better visualization (#102)
Co-authored-by:  ClaudioAmato <32262813+ClaudioAmato@users.noreply.github.com>
2020-12-31 12:49:09 -05:00
Xmader
d50fb677b4
fix: run script before the page is fully loaded 2020-12-28 05:13:37 -05:00
Xmader
003377e9ec
feat: run script before the page is fully loaded 2020-12-28 00:51:36 -05:00
Xmader
2eca89e672
feat: view score in LibreScore 2020-12-27 23:51:37 -05:00
Xmader
eb0f2b4b1d
v0.22.1 2020-12-17 03:33:58 -05:00
Xmader
90d34bd05b
style: platform-specific paste hints 2020-12-17 03:32:18 -05:00
SealsRock12
541393eb0c
feat: platform-specific paste hints 2020-12-17 03:31:49 -05:00
Xmader
2df57606e3
fix: get the actual id of s.musescore.com urls
using pregenerated sid2id map
2020-12-16 22:11:26 -05:00
Xmader
8e6992ab27
v0.22.0 2020-12-14 02:17:58 -05:00
Xmader
2d75ae45be
feat: support MP3 export 2020-12-14 02:13:20 -05:00
Xmader
e123108928
chore: upgrade webmscore to v0.12.0 2020-12-14 02:12:49 -05:00
Xmader
1a4efa76fa
v0.21.4 2020-12-09 11:24:53 -05:00
Xmader
e5ffc4b9a2
fix: anti-detection 2020-12-09 11:22:09 -05:00
Xmader
e10e23ffec
feat: show version on the userscript manager menu 2020-12-09 11:20:48 -05:00
Xmader
99bec9b4b4
v0.21.3 2020-12-08 14:48:24 -05:00
Xmader
5ea64e81d7
chore: upload Chrome extension crx 2020-12-08 14:48:03 -05:00
Xmader
18da36a34a
fix: script wrapper anti-detection 2020-12-08 14:36:31 -05:00
Xmader
cc8bddd551
v0.21.2 2020-12-06 12:34:51 -05:00
Xmader
741684329a
fix: cli error 2020-12-06 12:32:13 -05:00
Xmader
557a531e5b
v0.21.1 2020-12-06 03:05:25 -05:00
Xmader
3f6818fde5
feat: remove get rid of Disable Tampermonkey 2020-12-06 03:03:13 -05:00
Xmader
f6b8ab4413
refactor: script wrapper 2020-12-06 02:49:55 -05:00
Xmader
05e147a03c
refactor: get rid of Disable Tampermonkey 2020-12-06 02:36:05 -05:00
Xmader
f224edbc49
fix: script load 2020-12-06 02:35:08 -05:00
Xmader
bdbcdd62e9
feat: add menu link to discord 2020-12-06 02:25:26 -05:00
Xmader
0522e5935c
feat: add menu link to source code 2020-12-06 02:22:54 -05:00
Xmader
63bf02046a
refactor: script load 2020-12-06 02:21:33 -05:00
Xmader
1831ffa7c8
v0.21.0 2020-12-06 01:48:14 -05:00
Xmader
334b452fcc
refactor: get sandbox window using GM APIs 2020-12-06 01:36:35 -05:00
Xmader
ffa8683dbe
refactor: GM APIs 2020-12-06 01:28:56 -05:00
Xmader
9491f6c848
fix: no playback (#92) 2020-12-06 00:51:51 -05:00
Xmader
63ac2030a9
feat: add buttons to the userscript manager menu 2020-12-06 00:48:52 -05:00
Xmader
95ec4725e1
v0.20.8 2020-12-05 18:53:59 -05:00
Xmader
baa2be3ca3
fix: anti-detection
modify error stack script
2020-12-05 18:53:16 -05:00
Xmader
c8467bf5b7
v0.20.7 2020-12-04 14:45:48 -05:00
Xmader
48c41d1c7c
fix: anti-detection 2020-12-04 14:44:28 -05:00
Xmader
73ad8618c2
v0.20.6 2020-12-03 21:05:12 -05:00
Xmader
0e1f4aeafd
feat: cli help right click to paste 2020-12-03 21:04:36 -05:00
Xmader
b9d5ffa6fb
feat: anti detecting-using-error-stack 2020-12-03 20:55:49 -05:00
Xmader
9ee64ab7d5
v0.20.5 2020-12-01 14:10:50 -05:00
Xmader
3871a03127
fix: pdf worker 2020-12-01 14:10:36 -05:00
Xmader
4eb5134b68
v0.20.4 2020-12-01 13:31:41 -05:00
Xmader
2d635039b2
fix: script load 2020-12-01 13:28:56 -05:00
Xmader
bc0b12d362
fix: script load 2020-12-01 13:22:12 -05:00
Xmader
c11d6d2c48
feat: get rid of Disable Tampermonkey 2020-12-01 13:20:14 -05:00
Xmader
2a29e378f2
fix: btn list 2020-12-01 12:43:48 -05:00
Xmader
b6837ee8e0
v0.20.3 2020-11-30 15:26:16 -05:00
Xmader
65c1e02d8b
fix: the loadstart event is not fired on <img> elements in Chrome 2020-11-30 15:26:01 -05:00
Xmader
7f12c76413
v0.20.2 2020-11-30 15:05:12 -05:00
Xmader
e142c7a3db
fix: load script 2020-11-30 15:04:11 -05:00
Xmader
4b91be54a6
fix: get file url 2020-11-30 14:55:29 -05:00
Xmader
baf187ee46
fix: error handling 2020-11-30 14:23:24 -05:00
Xmader
f1412cdffa
fix: cli error handling 2020-11-30 14:21:55 -05:00
Xmader
f53ff45057
feat: anti detecting-using-error-stack 2020-11-30 14:18:08 -05:00
Xmader
9a3d16f86c
doc: fix the url for chrome extension 2020-11-30 09:32:57 -05:00
Xmader
58dc01224d
v0.20.1 2020-11-30 09:25:03 -05:00
Xmader
32aa5e6938
fix: get file urls 2020-11-30 09:24:33 -05:00
Xmader
79f139e663
fix: btn not showing 2020-11-30 09:16:09 -05:00
Xmader
f4d43443b9
feat: remove the question mark in cli msg for people don't know regexp 2020-11-29 18:08:03 -05:00
Xmader
6b5f0d3321
v0.20.0 2020-11-29 17:48:21 -05:00
Xmader
7fc1a5fb6a
feat: support s.musescore.com urls 2020-11-29 17:47:39 -05:00
Xmader
7485d884bf
refactor: cli UA 2020-11-29 17:27:29 -05:00
Xmader
d171f46dfe
feat: cli UA 2020-11-29 17:00:29 -05:00
Xmader
bc4b25d3be
v0.19.5 2020-11-29 16:39:47 -05:00
Xmader
967e0b29f0
refactor: remove unused code 2020-11-29 16:39:17 -05:00
Xmader
a73da45f16
v0.19.4 2020-11-29 00:37:15 -05:00
Xmader
2a08dd5567
fix: some btns hidden 2020-11-29 00:36:22 -05:00
Xmader
d499c1bbb4
v0.19.3 2020-11-29 00:23:41 -05:00
Xmader
a5c0dc77de
fix: getBtnContainer 2020-11-28 23:51:47 -05:00
Xmader
8c9c92ecc5
v0.19.2 2020-11-27 12:24:18 -05:00
Xmader
01514f651b
feat: get sandbox window using ad iframes 2020-11-27 12:21:47 -05:00
Xmader
1f62f47dc8
fix: get btn container 2020-11-27 12:21:46 -05:00
Xmader
935b56e4d2
feat: add sandbox iframe under btn 2020-11-27 12:21:46 -05:00
Xmader
f6536114ae
chore: rollup config 2020-11-27 12:21:45 -05:00
Xmader
bc19c66be3
doc: cli usage 2020-11-26 17:55:15 -05:00
Xmader
a7d2b966a9
doc: cli usage 2020-11-26 17:12:28 -05:00
Xmader
bff23cc770
v0.19.1 2020-11-26 16:53:22 -05:00
Xmader
604678b29a
feat(cli): ask for the page url or path to local file 2020-11-26 16:53:01 -05:00
Xmader
a99bfc5923
v0.19.0 - cli local mscz file 2020-11-26 16:43:28 -05:00
Xmader
397326ce13
feat(cli): load local file 2020-11-26 16:40:25 -05:00
Xmader
7a9b4910b4
v0.18.0 - cli file destination 2020-11-26 15:08:44 -05:00
Xmader
515f5d940e
refactor(cli): file destination 2020-11-26 15:07:34 -05:00
Xmader
427a75f2dd
feat(cli): must select at least one filetype 2020-11-26 15:02:25 -05:00
Xmader
0d8ab40ba9
refactor: prompt params 2020-11-26 14:58:03 -05:00
Xmader
1680a35369
feat(cli): change destination directory 2020-11-26 14:49:54 -05:00
Xmader
6e48ef1253
v0.17.2 2020-11-26 14:24:50 -05:00
Xmader
4acff224a8
refactor: get sandbox 2020-11-26 14:21:19 -05:00
Xmader
ec4cf55696
fix: get sandbox 2020-11-26 14:18:41 -05:00
Xmader
27861a802d
doc: CLI usage 2020-11-26 13:24:37 -05:00
31 changed files with 1608 additions and 362 deletions

View file

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

View file

@ -13,12 +13,16 @@ on:
ref: ref:
description: 'The branch, tag or SHA to release from' description: 'The branch, tag or SHA to release from'
required: false required: false
chrome_ext_url:
description: 'URL to the Chrome Extension crx'
required: true
env: env:
VERSION: ${{ github.event.inputs.version }} VERSION: ${{ github.event.inputs.version }}
NPM_TAG: ${{ github.event.inputs.npm_tag }} NPM_TAG: ${{ github.event.inputs.npm_tag }}
REF: ${{ github.event.inputs.ref || github.sha }} REF: ${{ github.event.inputs.ref || github.sha }}
ARTIFACTS_DIR: ./.artifacts ARTIFACTS_DIR: ./.artifacts
CHROME_EXT_URL: ${{ github.event.inputs.chrome_ext_url }}
jobs: jobs:
release: release:
@ -49,6 +53,15 @@ jobs:
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # 0301... NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # 0301...
- name: NPM Publish msdl
run: |
cd ./src/msdl
sed -i "s/%VERSION%/$VERSION/" package.json
npm publish --tag $NPM_TAG
cd -
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish Firefox Extension - name: Publish Firefox Extension
id: web-ext-build id: web-ext-build
uses: kewisch/action-web-ext@v1 uses: kewisch/action-web-ext@v1
@ -64,6 +77,8 @@ jobs:
mkdir -p $ARTIFACTS_DIR mkdir -p $ARTIFACTS_DIR
cp dist/main.js $ARTIFACTS_DIR/musescore-downloader.user.js cp dist/main.js $ARTIFACTS_DIR/musescore-downloader.user.js
cp dist/ext.zip $ARTIFACTS_DIR/musescore-downloader.webextension.zip cp dist/ext.zip $ARTIFACTS_DIR/musescore-downloader.webextension.zip
wget -q $CHROME_EXT_URL -P $ARTIFACTS_DIR/
wget -q https://github.com/Xmader/musescore-downloader/archive/$REF.tar.gz -O $ARTIFACTS_DIR/source.tar.gz
- run: bash ./.github/workflows/get-signed-ext.sh - run: bash ./.github/workflows/get-signed-ext.sh
env: env:
EXT_ID: musescore-downloader EXT_ID: musescore-downloader
@ -83,6 +98,7 @@ jobs:
IPFS_HASH: ${{ steps.ipfs.outputs.hash }} IPFS_HASH: ${{ steps.ipfs.outputs.hash }}
run: | run: |
cd $ARTIFACTS_DIR cd $ARTIFACTS_DIR
rm *.tar.gz
files=$(ls .) files=$(ls .)
assets=() assets=()

View file

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

137
README.md
View file

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

861
dist/main.js vendored

File diff suppressed because it is too large Load diff

40
package-lock.json generated
View file

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

View file

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

View file

@ -15,6 +15,11 @@ const getBannerText = () => {
return bannerText return bannerText
} }
const getWrapper = (startL, endL) => {
const js = fs.readFileSync("./src/wrapper.js", "utf-8")
return js.split(/\n/g).slice(startL, endL).join("\n")
}
const basePlugins = [ const basePlugins = [
typescript({ typescript({
target: "ES6", target: "ES6",
@ -61,7 +66,6 @@ const plugins = [
dirname: false, dirname: false,
filename: false, filename: false,
baseDir: false, baseDir: false,
process: false,
}), }),
] ]
@ -84,6 +88,8 @@ export default [
format: "iife", format: "iife",
sourcemap: false, sourcemap: false,
banner: getBannerText, banner: getBannerText,
intro: () => getWrapper(0, -1),
outro: () => getWrapper(-1)
}, },
plugins, plugins,
}, },

View file

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

View file

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

View file

@ -3,48 +3,100 @@
/* eslint-disable no-void */ /* eslint-disable no-void */
import fs from 'fs' import fs from 'fs'
import { fetchMscz, MSCZ_URL_SYM } from './mscz' import path from 'path'
import os from 'os'
import { fetchMscz, setMscz, MSCZ_URL_SYM } from './mscz'
import { loadMscore, INDV_DOWNLOADS, WebMscore } from './mscore' import { loadMscore, INDV_DOWNLOADS, WebMscore } from './mscore'
import { ScoreInfoHtml } from './scoreinfo' import { ScoreInfo, ScoreInfoHtml, ScoreInfoObj, getActualId } from './scoreinfo'
import { escapeFilename } from './utils' import { getLibreScoreLink } from './librescore-link'
import { escapeFilename, DISCORD_URL } from './utils'
import { isNpx, getVerInfo, getSelfVer } from './npm-data'
import i18n from './i18n' import i18n from './i18n'
const inquirer: typeof import('inquirer') = require('inquirer') const inquirer: typeof import('inquirer') = require('inquirer')
const ora: typeof import('ora') = require('ora') const ora: typeof import('ora') = require('ora')
const chalk: typeof import('chalk') = require('chalk') const chalk: typeof import('chalk') = require('chalk')
const SCORE_URL_PREFIX = 'https://musescore.com/' const SCORE_URL_PREFIX = 'https://(s.)musescore.com/'
const SCORE_URL_REG = /https:\/\/(s\.)?musescore\.com\//
const EXT = '.mscz'
interface Params {
fileInit: string;
confirmed: boolean;
part: number;
types: number[];
dest: string;
}
void (async () => { void (async () => {
// ask for the page url const arg = process.argv[2]
const { url } = await inquirer.prompt({ if (['-v', '--version'].includes(arg)) { // ran with flag -v or --version, `msdl -v`
console.log(getSelfVer()) // print musescore-downloader version
return // exit process
}
// Determine platform and paste message
const platform = os.platform()
let pasteMessage = ''
if (platform === 'win32') {
pasteMessage = 'right-click to paste'
} else if (platform === 'linux') {
pasteMessage = 'usually Ctrl+Shift+V to paste'
} // For MacOS, no hint is needed because the paste shortcut is universal.
let scoreinfo: ScoreInfo
let librescoreLink: Promise<string> | undefined
// ask for the page url or path to local file
const { fileInit } = await inquirer.prompt<Params>({
type: 'input', type: 'input',
name: 'url', name: 'fileInit',
message: 'Score URL:', message: 'Score URL or path to local MSCZ file:',
suffix: ` (starts with "${SCORE_URL_PREFIX}")\n `, suffix: '\n ' +
`(starts with "${SCORE_URL_PREFIX}" or local filepath ends with "${EXT}") ` +
`${chalk.bgGray(pasteMessage)}\n `,
validate (input: string) { validate (input: string) {
return input && input.startsWith(SCORE_URL_PREFIX) return input &&
(
!!input.match(SCORE_URL_REG) ||
(input.endsWith(EXT) && fs.statSync(input).isFile())
)
}, },
default: process.argv[2], default: arg,
}) })
// request scoreinfo const isLocalFile = fileInit.endsWith(EXT)
const scoreinfo = await ScoreInfoHtml.request(url) if (!isLocalFile) {
const fileName = scoreinfo.fileName // request scoreinfo
scoreinfo = await ScoreInfoHtml.request(fileInit)
try {
await getActualId(scoreinfo as any)
} catch (err) {
console.error(err)
}
// confirmation // confirmation
const { confirmed } = await inquirer.prompt({ const { confirmed } = await inquirer.prompt<Params>({
type: 'confirm', type: 'confirm',
name: 'confirmed', name: 'confirmed',
message: 'Continue?', message: 'Continue?',
prefix: `${chalk.yellow('!')} ` + prefix: `${chalk.yellow('!')} ` +
`ID: ${scoreinfo.id}\n ` + `ID: ${scoreinfo.id}\n ` +
`Title: ${scoreinfo.title}\n `, `Title: ${scoreinfo.title}\n `,
default: true, default: true,
}) })
if (!confirmed) return if (!confirmed) return
// initiate LibreScore link request
librescoreLink = getLibreScoreLink(scoreinfo)
librescoreLink.catch(() => '') // silence this unhandled Promise rejection
// print a blank line to the terminal
console.log()
} else {
scoreinfo = new ScoreInfoObj(0, path.basename(fileInit, EXT))
}
console.log()
const spinner = ora({ const spinner = ora({
text: i18n('PROCESSING')(), text: i18n('PROCESSING')(),
color: 'blue', color: 'blue',
@ -55,11 +107,24 @@ void (async () => {
let score: WebMscore let score: WebMscore
let metadata: import('webmscore/schemas').ScoreMetadata let metadata: import('webmscore/schemas').ScoreMetadata
try { try {
// fetch mscz file from the dataset, and cache it for side effect if (!isLocalFile) {
await fetchMscz(scoreinfo) // fetch mscz file from the dataset, and cache it for side effect
await fetchMscz(scoreinfo)
} else {
// load local file
const data = await fs.promises.readFile(fileInit)
await setMscz(scoreinfo, data.buffer)
}
spinner.info('MSCZ file loaded') spinner.info('MSCZ file loaded')
spinner.info(`File URL: ${scoreinfo.store.get(MSCZ_URL_SYM) as string}`) if (!isLocalFile) {
spinner.info(`File URL: ${scoreinfo.store.get(MSCZ_URL_SYM) as string}`)
}
if (librescoreLink) {
try {
spinner.info(`${i18n('VIEW_IN_LIBRESCORE')()}: ${await librescoreLink}`)
} catch { } // it doesn't affect the main feature
}
spinner.start() spinner.start()
// load score using webmscore // load score using webmscore
@ -69,6 +134,10 @@ void (async () => {
spinner.info('Score loaded by webmscore') spinner.info('Score loaded by webmscore')
} catch (err) { } catch (err) {
spinner.fail(err.message) spinner.fail(err.message)
spinner.info(
'Send your URL to the #dataset-bugs channel in the LibreScore Community Discord server:\n ' +
DISCORD_URL,
)
return return
} }
spinner.succeed('OK\n') spinner.succeed('OK\n')
@ -81,7 +150,7 @@ void (async () => {
const typeChoices = INDV_DOWNLOADS.map((d, i) => ({ name: d.name, value: i })) const typeChoices = INDV_DOWNLOADS.map((d, i) => ({ name: d.name, value: i }))
// part selection // part selection
const { part }: { part: number } = await inquirer.prompt({ const { part } = await inquirer.prompt<Params>({
type: 'list', type: 'list',
name: 'part', name: 'part',
message: 'Part Selection', message: 'Part Selection',
@ -91,24 +160,47 @@ void (async () => {
await score.setExcerptId(part) await score.setExcerptId(part)
// filetype selection // filetype selection
const { types }: { types: number[] } = await inquirer.prompt({ const { types } = await inquirer.prompt<Params>({
type: 'checkbox', type: 'checkbox',
name: 'types', name: 'types',
message: 'Filetype Selection', message: 'Filetype Selection',
choices: typeChoices, choices: typeChoices,
validate (input: number[]) {
return input.length >= 1
},
}) })
const filetypes = types.map(i => INDV_DOWNLOADS[i]) const filetypes = types.map(i => INDV_DOWNLOADS[i])
// destination directory
const { dest } = await inquirer.prompt<Params>({
type: 'input',
name: 'dest',
message: 'Destination Directory:',
validate (input: string) {
return input && fs.statSync(input).isDirectory()
},
default: process.cwd(),
})
// export files // export files
const fileName = scoreinfo.fileName || await score.titleFilenameSafe()
spinner.start() spinner.start()
await Promise.all( await Promise.all(
filetypes.map(async (d) => { filetypes.map(async (d) => {
const data = await d.action(score) const data = await d.action(score)
const f = `${fileName} - ${escapeFilename(partName)}.${d.fileExt}` const n = `${fileName} - ${escapeFilename(partName)}.${d.fileExt}`
const f = path.join(dest, n)
await fs.promises.writeFile(f, data) await fs.promises.writeFile(f, data)
spinner.info(`Saved ${chalk.underline(f)}`) spinner.info(`Saved ${chalk.underline(f)}`)
spinner.start() spinner.start()
}), }),
) )
spinner.succeed('OK') spinner.succeed('OK')
if (!isNpx()) {
const { installed, latest, isLatest } = await getVerInfo()
if (!isLatest) {
console.log(chalk.yellowBright(`\nYour installed version (${installed}) of the musescore-downloader CLI is not the latest one (${latest})!\nRun npm i -g musescore-downloader@${latest} to update.`))
}
}
})() })()

View file

@ -1,12 +1,12 @@
/* eslint-disable no-extend-native */ /* eslint-disable no-extend-native */
/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-return */
import { ALL, webpackGlobalOverride } from './webpack-hook' import { hookNative } from './anti-detection'
import { console } from './utils' import { console } from './utils'
type FileType = 'img' | 'mp3' | 'midi' type FileType = 'img' | 'mp3' | 'midi'
const TYPE_REG = /id=(\d+)&type=(img|mp3|midi)/ const TYPE_REG = /type=(img|mp3|midi)/
/** /**
* I know this is super hacky. * I know this is super hacky.
@ -14,27 +14,41 @@ const TYPE_REG = /id=(\d+)&type=(img|mp3|midi)/
const magicHookConstr = (() => { const magicHookConstr = (() => {
const l = {} const l = {}
webpackGlobalOverride(ALL, (n, r, t) => { // override try {
const e = n.exports const p = Object.getPrototypeOf(document.body)
if (typeof e === 'object' && e.fetch) { Object.setPrototypeOf(document.body, null)
const fn = e.fetch
t.d(e, 'fetch', () => { hookNative(document.body, 'append', () => {
return function (...args) { return function (...nodes: Node[]) {
const [url, init] = args p.append.call(this, ...nodes)
const token = init?.headers?.Authorization
if (typeof url === 'string' && token) { if (nodes[0].nodeName === 'IFRAME') {
const m = url.match(TYPE_REG) const iframe = nodes[0] as HTMLIFrameElement
if (m) { const w = iframe.contentWindow as Window
const type = m[2]
// eslint-disable-next-line no-unused-expressions hookNative(w, 'fetch', () => {
l[type]?.(token) return function (url, init) {
const token = init?.headers?.Authorization
if (typeof url === 'string' && token) {
const m = url.match(TYPE_REG)
console.debug(url, token, m)
if (m) {
const type = m[1]
// eslint-disable-next-line no-unused-expressions
l[type]?.(token)
}
}
return fetch(url, init)
} }
} })
return fn(...args)
} }
}) }
} })
})
Object.setPrototypeOf(document.body, p)
} catch (err) {
console.error(err)
}
return async (type: FileType) => { return async (type: FileType) => {
return new Promise<string>((resolve) => { return new Promise<string>((resolve) => {
@ -65,12 +79,12 @@ const getApiAuth = async (type: FileType, index: number): Promise<string> => {
// force to retrieve the MAGIC // force to retrieve the MAGIC
switch (type) { switch (type) {
case 'midi': { case 'midi': {
const el = document.querySelectorAll('.SD7H- > button')[3] as HTMLButtonElement const el = document.querySelector('button[hasaccess]') as HTMLButtonElement
el.click() el.click()
break break
} }
case 'mp3': { case 'mp3': {
const el = document.querySelector('#playerBtnExprt') as HTMLButtonElement const el = document.querySelector('button[title="Toggle Play"]') as HTMLButtonElement
el.click() el.click()
break break
} }

22
src/gm.ts Normal file
View file

@ -0,0 +1,22 @@
/**
* UserScript APIs
*/
declare const GM: {
/** https://www.tampermonkey.net/documentation.php#GM_info */
info: Record<string, any>;
/** https://www.tampermonkey.net/documentation.php#GM_registerMenuCommand */
registerMenuCommand (name: string, fn: () => any, accessKey?: string): Promise<number>;
/** https://github.com/Tampermonkey/tampermonkey/issues/881#issuecomment-639705679 */
addElement<K extends keyof HTMLElementTagNameMap> (tagName: K, properties: Record<string, any>): Promise<HTMLElementTagNameMap[K]>;
}
export const _GM = (typeof GM === 'object' ? GM : undefined) as GM
type GM = typeof GM
export const isGmAvailable = (requiredMethod: keyof GM = 'info'): boolean => {
return typeof GM !== 'undefined' &&
typeof GM[requiredMethod] !== 'undefined'
}

View file

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

View file

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

View file

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

37
src/i18n/it.ts Normal file
View file

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

37
src/i18n/zh.ts Normal file
View file

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

26
src/librescore-link.ts Normal file
View file

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

View file

@ -1,13 +1,14 @@
import './meta' import './meta'
import FileSaver from 'file-saver' import FileSaver from 'file-saver'
import { waitForDocumentLoaded, console } from './utils' import { waitForSheetLoaded, console } from './utils'
import { downloadPDF } from './pdf' import { downloadPDF } from './pdf'
import { downloadMscz } from './mscz' import { downloadMscz } from './mscz'
import { getFileUrl } from './file' import { getFileUrl } from './file'
import { INDV_DOWNLOADS } from './mscore' import { INDV_DOWNLOADS } from './mscore'
import { BtnList, BtnAction, BtnListMode } from './btn' import { getLibreScoreLink } from './librescore-link'
import { ScoreInfoInPage, SheetInfoInPage } from './scoreinfo' import { BtnList, BtnAction, BtnListMode, ICON } from './btn'
import { ScoreInfoInPage, SheetInfoInPage, getActualId } from './scoreinfo'
import i18n from './i18n' import i18n from './i18n'
const { saveAs } = FileSaver const { saveAs } = FileSaver
@ -15,7 +16,10 @@ const { saveAs } = FileSaver
const main = (): void => { const main = (): void => {
const btnList = new BtnList() const btnList = new BtnList()
const scoreinfo = new ScoreInfoInPage(document) const scoreinfo = new ScoreInfoInPage(document)
const { fileName, id } = scoreinfo const { fileName } = scoreinfo
// eslint-disable-next-line no-void
void getActualId(scoreinfo)
let indvPartBtn: HTMLButtonElement | null = null let indvPartBtn: HTMLButtonElement | null = null
const fallback = () => { const fallback = () => {
@ -34,7 +38,7 @@ const main = (): void => {
}) })
btnList.add({ btnList.add({
name: i18n('DOWNLOAD')('MusicXML'), name: i18n('DOWNLOAD')('MXL'),
action: BtnAction.mscoreWindow(scoreinfo, async (w, score) => { action: BtnAction.mscoreWindow(scoreinfo, async (w, score) => {
const mxl = await score.saveMxl() const mxl = await score.saveMxl()
const data = new Blob([mxl]) const data = new Blob([mxl])
@ -45,12 +49,12 @@ const main = (): void => {
btnList.add({ btnList.add({
name: i18n('DOWNLOAD')('MIDI'), name: i18n('DOWNLOAD')('MIDI'),
action: BtnAction.download(() => getFileUrl(id, 'midi'), fallback, 30 * 1000 /* 30s */), action: BtnAction.download(() => getFileUrl(scoreinfo.id, 'midi'), fallback, 30 * 1000 /* 30s */),
}) })
btnList.add({ btnList.add({
name: i18n('DOWNLOAD')('MP3'), name: i18n('DOWNLOAD')('MP3'),
action: BtnAction.download(() => getFileUrl(id, 'mp3'), fallback, 30 * 1000 /* 30s */), action: BtnAction.download(() => getFileUrl(scoreinfo.id, 'mp3'), fallback, 30 * 1000 /* 30s */),
}) })
indvPartBtn = btnList.add({ indvPartBtn = btnList.add({
@ -128,8 +132,17 @@ const main = (): void => {
}), }),
}) })
btnList.add({
name: i18n('VIEW_IN_LIBRESCORE')(),
action: BtnAction.openUrl(() => getLibreScoreLink(scoreinfo)),
tooltip: 'BETA',
icon: ICON.LIBRESCORE,
lightTheme: true,
})
// eslint-disable-next-line @typescript-eslint/no-floating-promises
btnList.commit(BtnListMode.InPage) btnList.commit(BtnListMode.InPage)
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
waitForDocumentLoaded().then(main) waitForSheetLoaded().then(main)

View file

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

View file

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

View file

@ -48,7 +48,7 @@ export const loadMsczUrl = async (scoreinfo: ScoreInfo, _fetch = getFetch()): Pr
// read further error msg // read further error msg
const err = cidRes.Message const err = cidRes.Message
if (err.includes('no link named')) { // file not found if (err.includes('no link named')) { // file not found
throw new Error('score not in dataset') throw new Error('Score not in dataset')
} else { } else {
throw new Error(err) throw new Error(err)
} }
@ -76,6 +76,11 @@ export const fetchMscz = async (scoreinfo: ScoreInfo, _fetch = getFetch()): Prom
return msczBufferP return msczBufferP
} }
// eslint-disable-next-line @typescript-eslint/require-await
export const setMscz = async (scoreinfo: ScoreInfo, buffer: ArrayBuffer): Promise<void> => {
scoreinfo.store.set(MSCZ_BUF_SYM, Promise.resolve(buffer))
}
export const downloadMscz = async (scoreinfo: ScoreInfo, saveAs: typeof import('file-saver').saveAs): Promise<void> => { export const downloadMscz = async (scoreinfo: ScoreInfo, saveAs: typeof import('file-saver').saveAs): Promise<void> => {
const data = new Blob([await fetchMscz(scoreinfo)]) const data = new Blob([await fetchMscz(scoreinfo)])
const filename = scoreinfo.fileName const filename = scoreinfo.fileName

2
src/msdl/cli.js Normal file
View file

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

19
src/msdl/package.json Normal file
View file

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

34
src/npm-data.ts Normal file
View file

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

View file

@ -1,8 +1,10 @@
import { getFetch, escapeFilename } from './utils' import { getFetch, escapeFilename, assertRes } from './utils'
import { getMainCid } from './mscz'
export abstract class ScoreInfo { export abstract class ScoreInfo {
private readonly RADIX = 20; private readonly RADIX = 20;
private readonly INDEX_RADIX = 32;
abstract id: number; abstract id: number;
abstract title: string; abstract title: string;
@ -24,10 +26,14 @@ export abstract class ScoreInfo {
public getMsczCidUrl (mainCid: string): string { public getMsczCidUrl (mainCid: string): string {
return `https://ipfs.infura.io:5001/api/v0/block/stat?arg=${this.getMsczIpfsRef(mainCid)}` return `https://ipfs.infura.io:5001/api/v0/block/stat?arg=${this.getMsczIpfsRef(mainCid)}`
} }
public getScorepackRef (mainCid: string): string {
return `/ipfs/${mainCid}/index/${(+this.id) % this.INDEX_RADIX}/${this.id}`
}
} }
export class ScoreInfoObj extends ScoreInfo { export class ScoreInfoObj extends ScoreInfo {
constructor (public id: number, public title: string) { super() } constructor (public id: number = 0, public title: string = '') { super() }
} }
export class ScoreInfoInPage extends ScoreInfo { export class ScoreInfoInPage extends ScoreInfo {
@ -43,11 +49,18 @@ export class ScoreInfoInPage extends ScoreInfo {
const el = this.document.querySelector("meta[property='og:title']") as HTMLMetaElement const el = this.document.querySelector("meta[property='og:title']") as HTMLMetaElement
return el.content return el.content
} }
get baseUrl (): string {
const el = this.document.querySelector("meta[property='og:image']") as HTMLMetaElement
const m = el.content.match(/^(.+\/)score_/) as RegExpMatchArray
return m[1]
}
} }
export class ScoreInfoHtml extends ScoreInfo { export class ScoreInfoHtml extends ScoreInfo {
private readonly ID_REG = /<meta property="al:ios:url" content="musescore:\/\/score\/(\d+)">/ private readonly ID_REG = /<meta property="al:ios:url" content="musescore:\/\/score\/(\d+)">/
private readonly TITLE_REG = /<meta property="og:title" content="(.*)">/ private readonly TITLE_REG = /<meta property="og:title" content="(.*)">/
private readonly BASEURL_REG = /<meta property="og:image" content="(.+\/)score_.*">/
constructor (private html: string) { super() } constructor (private html: string) { super() }
@ -63,6 +76,12 @@ export class ScoreInfoHtml extends ScoreInfo {
return m[1] return m[1]
} }
get baseUrl (): string {
const m = this.html.match(this.BASEURL_REG)
if (!m) return ''
return m[1]
}
static async request (url: string, _fetch = getFetch()): Promise<ScoreInfoHtml> { static async request (url: string, _fetch = getFetch()): Promise<ScoreInfoHtml> {
const r = await _fetch(url) const r = await _fetch(url)
if (!r.ok) return new ScoreInfoHtml('') if (!r.ok) return new ScoreInfoHtml('')
@ -86,14 +105,50 @@ export abstract class SheetInfo {
export class SheetInfoInPage extends SheetInfo { export class SheetInfoInPage extends SheetInfo {
constructor (private document: Document) { super() } constructor (private document: Document) { super() }
private get sheet0Img (): HTMLImageElement | null {
return this.document.querySelector('img[src*=score_]')
}
get pageCount (): number { get pageCount (): number {
return this.document.querySelectorAll('.gXB83').length const sheet0Div = this.sheet0Img?.parentElement
if (!sheet0Div) {
throw new Error('no sheet images found')
}
return this.document.getElementsByClassName(sheet0Div.className).length
} }
get thumbnailUrl (): string { get thumbnailUrl (): string {
// url to the image of the first page // url to the image of the first page
const el = this.document.querySelector('link[as=image]') as HTMLLinkElement const el = this.document.querySelector<HTMLLinkElement>('link[as=image]')
const url = el.href const url = (el?.href || this.sheet0Img?.src) as string
return url.split('@')[0] return url.split('@')[0]
} }
} }
export const getActualId = async (scoreinfo: ScoreInfoInPage | ScoreInfoHtml, _fetch = getFetch()): Promise<number> => {
if (scoreinfo.id <= 1000000000000) {
// actual id already
return scoreinfo.id
}
const mainCid = await getMainCid(scoreinfo, _fetch)
const ref = `${mainCid}/sid2id/${scoreinfo.id}`
const url = `https://ipfs.infura.io:5001/api/v0/dag/get?arg=${ref}`
const r0 = await _fetch(url)
if (r0.status !== 500) {
assertRes(r0)
}
const res: { Message: string } | number = await r0.json()
if (typeof res !== 'number') {
// read further error msg
throw new Error(res.Message)
}
// assign the actual id back to scoreinfo
Object.defineProperty(scoreinfo, 'id', {
get () { return res },
})
return res
}

View file

@ -1,5 +1,8 @@
import isNodeJs from 'detect-node' import isNodeJs from 'detect-node'
import { isGmAvailable, _GM } from './gm'
export const DISCORD_URL = 'https://discord.gg/gSsTUvJmD8'
export const escapeFilename = (s: string): string => { export const escapeFilename = (s: string): string => {
return s.replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, '_') return s.replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, '_')
@ -15,12 +18,22 @@ export const getIndexPath = (id: number): string => {
return indexN.join('/') return indexN.join('/')
} }
const NODE_FETCH_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0',
'Accept-Language': 'en-US,en;q=0.8',
}
export const getFetch = (): typeof fetch => { export const getFetch = (): typeof fetch => {
if (!isNodeJs) { if (!isNodeJs) {
return fetch return fetch
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return // eslint-disable-next-line @typescript-eslint/no-var-requires
return require('node-fetch') const nodeFetch = require('node-fetch')
return (input: RequestInfo, init?: RequestInit) => {
init = Object.assign({ headers: NODE_FETCH_HEADERS }, init)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return nodeFetch(input, init)
}
} }
} }
@ -47,18 +60,51 @@ export const useTimeout = async <T> (promise: T | Promise<T>, ms: number): Promi
}) })
} }
export const getSandboxWindowAsync = async (): Promise<Window> => { export const getSandboxWindowAsync = async (targetEl: Element | undefined = undefined): Promise<Window> => {
if (typeof document === 'undefined') return {} as any as Window if (typeof document === 'undefined') return {} as any as Window
if (isGmAvailable('addElement')) {
// create iframe using GM_addElement API
const iframe = await _GM.addElement('iframe', {})
iframe.style.display = 'none'
return iframe.contentWindow as Window
}
if (!targetEl) {
return new Promise((resolve) => {
// You need ads in your pages, right?
const observer = new MutationObserver(() => {
for (let i = 0; i < window.frames.length; i++) {
// find iframe windows created by ads
const frame = frames[i]
try {
const href = frame.location.href
if (href === location.href || href === 'about:blank') {
resolve(frame)
return
}
} catch { }
}
})
observer.observe(document.body, { subtree: true, childList: true })
})
}
return new Promise((resolve) => { return new Promise((resolve) => {
window.onmouseover = () => { const eventName = 'onmousemove'
const iframe = document.createElement('iframe') const id = Math.random().toString()
targetEl[id] = (iframe: HTMLIFrameElement) => {
delete targetEl[id]
targetEl.removeAttribute(eventName)
iframe.style.display = 'none' iframe.style.display = 'none'
document.body.append(iframe) targetEl.append(iframe)
const w = iframe.contentWindow const w = iframe.contentWindow
window.onmouseover = null
resolve(w as Window) resolve(w as Window)
} }
targetEl.setAttribute(eventName, `this['${id}'](document.createElement('iframe'))`)
}) })
} }
@ -69,8 +115,8 @@ export const getUnsafeWindow = (): Window => {
export const console: Console = (window || global).console // Object.is(window.console, unsafeWindow.console) == false export const console: Console = (window || global).console // Object.is(window.console, unsafeWindow.console) == false
export const windowOpenAsync = (...args: Parameters<Window['open']>): Promise<Window | null> => { export const windowOpenAsync = (targetEl: Element | undefined, ...args: Parameters<Window['open']>): Promise<Window | null> => {
return getSandboxWindowAsync().then(w => w.open(...args)) return getSandboxWindowAsync(targetEl).then(w => w.open(...args))
} }
export const attachShadow = (el: Element): ShadowRoot => { export const attachShadow = (el: Element): ShadowRoot => {
@ -92,3 +138,23 @@ export const waitForDocumentLoaded = (): Promise<void> => {
return Promise.resolve() return Promise.resolve()
} }
} }
/**
* Run script before the page is fully loaded
*/
export const waitForSheetLoaded = (): Promise<void> => {
if (document.readyState !== 'complete') {
return new Promise(resolve => {
const observer = new MutationObserver(() => {
const img = document.querySelector('img')
if (img) {
resolve()
observer.disconnect()
}
})
observer.observe(document, { childList: true, subtree: true })
})
} else {
return Promise.resolve()
}
}

View file

@ -4,7 +4,7 @@ import { PDFWorker } from '../dist/cache/worker'
const scriptUrlFromFunction = (fn: () => any): string => { const scriptUrlFromFunction = (fn: () => any): string => {
const blob = new Blob(['(' + fn.toString() + ')()'], { type: 'application/javascript' }) const blob = new Blob(['(' + fn.toString() + ')()'], { type: 'application/javascript' })
return URL.createObjectURL(blob) return window.URL.createObjectURL(blob)
} }
export class PDFWorkerHelper extends Worker { export class PDFWorkerHelper extends Worker {

View file

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

52
src/wrapper.js Normal file
View file

@ -0,0 +1,52 @@
/* eslint-disable */
const w = typeof unsafeWindow == 'object' ? unsafeWindow : window;
// GM APIs glue
const _GM = typeof GM == 'object' ? GM : undefined;
const gmId = '' + Math.random();
w[gmId] = _GM;
if (_GM && _GM.registerMenuCommand && _GM.openInTab) {
// add buttons to the userscript manager menu
_GM.registerMenuCommand(
`** Version: ${_GM.info.script.version} **`,
() => _GM.openInTab("https://github.com/Xmader/musescore-downloader/releases", { active: true })
)
_GM.registerMenuCommand(
'** Source Code **',
() => _GM.openInTab(_GM.info.script.homepage, { active: true })
)
_GM.registerMenuCommand(
'** Discord **',
() => _GM.openInTab("https://discord.gg/DKu7cUZ4XQ", { active: true })
)
}
function getRandL () {
return String.fromCharCode(97 + Math.floor(Math.random() * 26))
}
// script loader
new Promise(resolve => {
const id = '' + Math.random();
w[id] = resolve;
const stackN = 9
let loaderIntro = ''
for (let i = 0; i < stackN; i++) {
loaderIntro += `(function ${getRandL()}(){`
}
const loaderOutro = '})()'.repeat(stackN)
const mockUrl = "https://c.amazon-adsystem.com/aax2/apstag.js"
Function(`${loaderIntro}const d=new Image();window['${id}'](d);delete window['${id}'];document.body.prepend(d)${loaderOutro}//# sourceURL=${mockUrl}`)()
}).then(d => {
d.style.display = 'none';
d.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
d.once = false;
d.setAttribute('onload', `if(this.once)return;this.once=true;this.remove();const GM=window['${gmId}'];delete window['${gmId}'];(` + function a () {
/** script code here */
}.toString() + ')()')})