Compare commits

...

341 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
Xmader 25a7db05fa
v0.17.1 2020-11-26 13:04:43 -05:00
Xmader 566ec96144
feat: sandbox 2020-11-26 13:04:04 -05:00
Xmader 548f7c02ba
v0.17.0 2020-11-26 00:02:05 -05:00
Xmader f3fc5aeb6a
fix: console 2020-11-25 23:57:39 -05:00
Xmader 1f132bdfbd
feat: global sandbox 2020-11-25 21:06:35 -05:00
Xmader aa7622e953
feat: remove compatible eval 2020-11-25 18:19:40 -05:00
Xmader b34931e1b6
v0.16.2 2020-11-25 16:25:02 -05:00
Xmader 6acfec8141
refactor: get ipfs ref 2020-11-25 16:24:27 -05:00
Xmader b658c00a09
fix: ipns resolve 2020-11-25 15:57:13 -05:00
Xmader bb31465fca
fix: sheet info match 2020-11-25 00:36:56 -05:00
Xmader 31d0fd5f23
v0.16.1 2020-11-24 18:01:40 -05:00
Xmader 8df441dfee
fix: shebang 2020-11-24 18:01:03 -05:00
Xmader adb7f5075d
v0.16.0 2020-11-24 17:44:52 -05:00
Xmader 3b9dd171c7
feat: cli param 2020-11-24 17:44:26 -05:00
Xmader 64a4ebeb16
refactor: commonjs require 2020-11-24 17:43:07 -05:00
Xmader 50ab62fff7
refactor: escape filename 2020-11-24 17:35:44 -05:00
Xmader 6926600f45
feat: interactive CLI 2020-11-24 17:32:44 -05:00
Xmader 6e895b3cc1
feat: persist mscz url 2020-11-24 16:50:52 -05:00
Xmader 0f4d032399
chore: upgrade packages 2020-11-24 13:13:27 -05:00
Xmader b6cd50450a
chore: build cli.js 2020-11-24 13:11:19 -05:00
Xmader bb6eba5fdb
refactor: file-saver 2020-11-24 12:35:14 -05:00
Xmader caba1c041a
refactor: individual downloads 2020-11-24 06:03:46 -05:00
Xmader 40d1ddbab8
feat: i18n for nodejs 2020-11-24 06:01:02 -05:00
Xmader 688e0a4c7e
refactor: i18n
no circular dependencies
2020-11-24 05:57:24 -05:00
Xmader 5d50c2337c
feat: request & parse scoreinfo from html 2020-11-24 05:35:23 -05:00
Xmader 36a05aac75
feat: load soundfont in nodejs 2020-11-24 05:22:56 -05:00
Xmader c96a6b69dd
chore: update package.json 2020-11-24 05:19:31 -05:00
Xmader 2fbfbede86
refactor: detect nodejs 2020-11-24 05:08:18 -05:00
Xmader bfdd80a364
fix: fetch in nodejs 2020-11-24 05:02:14 -05:00
Xmader 7f8f635677
refactor: scoreinfo from pure data 2020-11-24 04:43:31 -05:00
Xmader 297387b2b4
fix: mscoreWindow 2020-11-24 04:38:47 -05:00
Xmader 3b43957cee
feat: load webmscore in nodejs 2020-11-24 04:36:01 -05:00
Xmader 195f607817
feat: show `score not in dataset` error 2020-11-24 04:12:31 -05:00
Xmader 92619ab3f7
refactor: scoreinfo mscz url 2020-11-24 04:03:06 -05:00
Xmader 141dac44ac
refactor: sheetinfo 2020-11-24 03:17:50 -05:00
Xmader 9e9f25ff80
feat: abstract storage for scoreinfo instance 2020-11-24 02:59:19 -05:00
Xmader 64b0e4d441
refactor: get rid of global scoreinfo 2020-11-24 02:51:43 -05:00
Xmader aafb71fc4b
refactor: implement `sheetImgType` in abstract class 2020-11-24 02:31:31 -05:00
Xmader 120d57b0e0
refactor: abstract class ScoreInfo 2020-11-24 02:29:37 -05:00
Xmader c334d8d124
refactor: replace base url with thumbnail url 2020-11-24 02:10:56 -05:00
Xmader ddfe2039b1
refactor: scoreinfo 2020-11-24 02:06:46 -05:00
Xmader 565e28a70d
refactor: remove playerdata from scoreinfo 2020-11-24 01:24:34 -05:00
Xmader f2e45d1803
v0.15.20 2020-11-23 15:58:34 -05:00
Xmader 7a53e5d9ca
fix: btn text 2020-11-23 15:57:20 -05:00
Xmader baadfb4e1a
v0.15.19 2020-11-23 14:37:49 -05:00
Xmader d025ce59ae
refactor: sandboxed window 2020-11-23 14:37:11 -05:00
Xmader 2c2fc6748d
fix: detected calling attachShadow 2020-11-23 14:36:06 -05:00
Xmader 3e0ade9d34
v0.15.18 2020-11-23 12:02:56 -05:00
Xmader 89763ee762
fix: buttons do appear but they do nothing 2020-11-23 12:02:41 -05:00
Xmader f014ede1f0
v0.15.17 2020-11-23 11:53:00 -05:00
Xmader 03e7f1cae3
fix: btn list display
anti-detection
2020-11-23 11:52:28 -05:00
Xmader ddab46e69d
fix: mode switching 2020-11-23 11:06:35 -05:00
Xmader 963961b69c
v0.15.16 2020-11-21 15:33:52 -05:00
Xmader 71091c9205
fix: script detected 2020-11-21 15:31:57 -05:00
Xmader d873a9b89e
v0.15.15 2020-11-21 01:23:25 -05:00
Xmader c6eeb4d881
fix: get auth magics
using fetch hook
2020-11-21 01:22:35 -05:00
Xmader 6b0a04b152
refactor: fetch mscz res 2020-11-21 00:38:10 -05:00
Xmader 5cefd2081d
refactor: solve anti-debug 2020-11-20 11:45:35 -05:00
Xmader 704770780d
v0.15.14 2020-11-20 10:20:17 -05:00
Xmader 0832c9484d
feat: continuously try to show external window 2020-11-20 10:18:50 -05:00
Xmader d7e2a39775
v0.15.13 2020-11-20 09:30:30 -05:00
Xmader 3aaf8cbd73
fix: temporary fix 2020-11-20 09:30:06 -05:00
Xmader 63892a495a
v0.15.12 2020-11-19 18:54:49 -05:00
Xmader 4eade0ad46
fix: download disabled on some scores 2020-11-19 18:53:46 -05:00
Xmader 52e1f9511c
v0.15.11 2020-11-19 17:59:03 -05:00
Xmader 9818814a51
refactor: string matchAll compatibility 2020-11-19 17:58:13 -05:00
Xmader 17c1e877fb
feat: auth magics 2020-11-19 17:54:41 -05:00
Xmader 99f72c511e
feat: resolve obfuscation ctx 2020-11-19 17:54:25 -05:00
Xmader 7f6a1e07e2
refactor: idLastDigit 2020-11-19 13:22:17 -05:00
Xmader f68a2ddada
v0.15.10 2020-11-19 00:52:03 -05:00
Xmader 5426ad5059
fix: auth magics 2020-11-19 00:51:18 -05:00
Xmader f8221a0f50
feat: webpack hook load all chunks 2020-11-19 00:51:09 -05:00
Xmader eaff7b123c
v0.15.9 2020-11-18 18:36:01 -05:00
Xmader 06b7bda575
fix: auth magics 2020-11-18 18:35:45 -05:00
Xmader 0e96b9dc08
v0.15.8 2020-11-18 14:01:14 -05:00
Xmader 83befd2df4
fix: auth magics eval 2020-11-18 14:00:43 -05:00
Xmader 7e66e131e9
v0.15.7 2020-11-18 13:58:37 -05:00
Xmader ac99e8373d
fix: auth magics 2020-11-18 13:57:56 -05:00
Xmader 698cbecd11
fix: auth magics
quick temporary fix
2020-11-18 13:29:34 -05:00
Xmader ee518dc8f3
v0.15.6 2020-11-17 12:10:57 -05:00
Xmader 4764e0931b
fix: auth magic 2020-11-17 12:09:49 -05:00
Xmader 6bc841f132
feat: webpack ctx hook 2020-11-17 12:09:23 -05:00
Xmader 023f7bf215
feat: webpack hook onPackLoad 2020-11-17 12:00:41 -05:00
Xmader 0b7c423d8e
fix: mscz file ref 2020-11-17 11:13:16 -05:00
Xmader 7cb8cd227e
v0.15.5 2020-11-14 17:44:46 -05:00
Xmader e10b975eeb
fix: mscz file ref 2020-11-14 17:44:25 -05:00
Xmader 7705320ce9
fix: download pdf 2020-11-14 17:23:22 -05:00
Xmader 125013d455
v0.15.4 2020-11-14 13:38:45 -05:00
Xmader 59c4e62912
fix: mscz ipfs 2020-11-14 13:37:46 -05:00
Xmader 5003e6572f
fix: auth magic 2020-11-14 13:33:42 -05:00
Xmader a3b982be4b
v0.15.3 2020-11-13 18:50:59 -05:00
Xmader 8d36aead85
fix: timeout 2020-11-13 18:50:08 -05:00
Xmader 43d8b46768
v0.15.2 2020-11-13 18:28:19 -05:00
Xmader d2956ad9d5
fix: get file url 2020-11-13 18:27:28 -05:00
Xmader d31678d3e6
v0.15.1 2020-11-13 16:01:47 -05:00
Xmader 6dd2f37e2e
fix: window.open 2020-11-13 15:55:41 -05:00
Xmader 94305ff964
v0.15.0 2020-11-13 01:27:15 -05:00
Xmader e98d639000
feat: btns fallback to load from MSCZ file (`Individual Parts`) 2020-11-13 01:25:39 -05:00
Xmader 8cc5375033
v0.14.6 2020-11-13 01:24:39 -05:00
Xmader f5e308b964
refactor: btns fallback 2020-11-13 01:15:01 -05:00
Xmader 837b960e88
refactor: btns fallback 2020-11-13 01:00:45 -05:00
Xmader 9aa6c406c6
feat: btns fallback to load from MSCZ file (`Individual Parts`) 2020-11-13 00:54:20 -05:00
Xmader 0764117544
fix: get file url 2020-11-13 00:26:39 -05:00
Xmader 17ef255a09
v0.14.5 2020-11-12 13:33:26 -05:00
Xmader 7d05c76c8a
fix: auth magic 2020-11-12 13:33:11 -05:00
Xmader 13bdeb36c4
v0.14.4 2020-11-12 13:29:26 -05:00
Xmader e50f0e3e8e
fix: auth magic 2020-11-12 13:29:10 -05:00
Xmader 8beb23ae69
feat: webpack global override hook all 2020-11-12 13:28:40 -05:00
Xmader edde0d0d75
v0.14.3 2020-11-12 13:09:04 -05:00
Xmader dbcda27be4
refactor: btn list
page independent
2020-11-12 13:02:50 -05:00
Xmader 41e67d5763
refactor: build download btn 2020-11-12 12:49:45 -05:00
Xmader 55113b6b60
refactor: btn style 2020-11-12 12:35:11 -05:00
Xmader ecd7f909a4
chore: bundle css 2020-11-12 12:13:14 -05:00
Xmader 844358d9a6
v0.14.2 2020-11-12 11:41:17 -05:00
Xmader ef0109270d
fix: get btn 2020-11-12 11:41:03 -05:00
Xmader 3074ebb754
v0.14.1 2020-11-12 09:47:56 -05:00
Xmader 44774347a8
fix: auth magic
temporary fix
2020-11-12 09:46:06 -05:00
Xmader 6e99cee113
fix: btn add 2020-11-12 09:31:11 -05:00
Xmader f1555d3db0
fix: btn add 2020-11-12 09:26:57 -05:00
Xmader 37c8eb3baf
v0.14.0 2020-11-12 02:59:00 -05:00
Xmader df6bfaf22c
feat: remove recaptcha
(unused)
2020-11-12 02:55:42 -05:00
Xmader d79651a751
feat: mscz files are now fully on IPFS
ipns://QmSdXtvzC8v8iTTZuj5cVmiugnzbR1QATYRcGix4bBsioP/<id>.mscz
2020-11-12 02:54:49 -05:00
Xmader d1fc51b362
v0.13.1 2020-11-11 22:48:09 -05:00
Xmader 81fd45e6b4
fix: download PDF & MP3 & MIDI 2020-11-11 22:47:35 -05:00
Xmader 5b4cb76f59
v0.13.0 2020-11-10 14:33:20 -05:00
Xmader a5de589b6b
feat: modes of BtnList
able to show the buttons in a separate window
2020-11-10 14:32:59 -05:00
Xmader a296651c6f
refactor: commit btn list 2020-11-10 13:51:48 -05:00
Xmader df99718d74
v0.12.5 2020-11-10 13:48:33 -05:00
Xmader 1cfb417c66
refactor: remove unused anti-detection code 2020-11-10 13:47:19 -05:00
Xmader 1f44fcf449
v0.12.5 2020-11-10 13:37:16 -05:00
Xmader 942acd0842
refactor: commit btn list 2020-11-10 13:36:49 -05:00
Xmader 8b6c61c6bc
v0.12.4 2020-11-10 09:47:36 -05:00
Xmader 3076c6cffd
fix: buttons disappear after 1s 2020-11-10 09:46:35 -05:00
Xmader 8b274a169a
v0.12.3 2020-11-09 16:17:08 -05:00
Xmader 1672ba66c4
fix: webpack global override hook 2020-11-09 16:16:44 -05:00
Xmader 9cc22dc181
v0.12.2 2020-11-09 14:50:33 -05:00
Xmader 914c770ea4
refactor: webpack global override hook 2020-11-09 14:48:57 -05:00
Xmader b53cc0d345
v0.12.1 2020-11-09 14:12:13 -05:00
Xmader 45645fe630
refactor: auth magic 2020-11-09 14:10:01 -05:00
Xmader 4de6343cb8
fix: auth magic 2020-11-09 14:09:25 -05:00
Xmader b594dd9a1c
feat: webpack global override hook 2020-11-09 14:07:57 -05:00
Xmader 6a767ea7ae
feat: individual parts default to "full score" 2020-11-09 14:04:18 -05:00
Xmader cb3bca3566
feat: remove deprecation notice 2020-11-09 13:58:25 -05:00
Xmader f37039020a
doc: discord link 2020-11-08 23:55:20 -05:00
Xmader b524de6806
doc: discord link 2020-11-08 23:46:54 -05:00
Xmader b0999f9efa
doc: discord link 2020-11-08 23:25:23 -05:00
Xmader 4b2d721661
fix: anti-detection 2020-11-08 15:48:54 -05:00
Xmader 8da2e7a1c8
v0.12.0 2020-11-07 21:32:29 -05:00
Xmader 8763e86c39
feat: hide btns using shadow DOM
require Firefox >= 63, Chrome >= 53, Opera >= 40, Safari >= 10, or Chromium based Edge
2020-11-07 21:28:32 -05:00
Xmader 13afe431c8
refactor: hookNative 2020-11-07 21:03:26 -05:00
Xmader f1a6fd81eb
refactor: remove unused anti-detection code 2020-11-07 20:53:31 -05:00
Xmader 954d0d5e65
v0.11.6 2020-11-07 16:58:37 -05:00
Xmader 506b485e82
fix: anti-detection 2020-11-07 16:58:08 -05:00
Xmader 322af44d7a
chore: archive release messages to archive.org 2020-11-06 14:11:46 -05:00
Xmader 9ae2a4441e
chore: mirrors inside repos 2020-11-06 14:10:32 -05:00
Xmader 19df4e16a1
v0.11.5 2020-11-06 13:45:55 -05:00
Xmader cb7366228f
fix: anti-detection 2020-11-06 13:44:08 -05:00
Xmader a583f52a5e
fix: anti-detection 2020-11-06 13:43:56 -05:00
Xmader 33d53b73eb
chore: mirrors inside repos 2020-11-06 12:52:24 -05:00
37 changed files with 3432 additions and 1148 deletions

View File

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

View File

@ -13,12 +13,16 @@ on:
ref:
description: 'The branch, tag or SHA to release from'
required: false
chrome_ext_url:
description: 'URL to the Chrome Extension crx'
required: true
env:
VERSION: ${{ github.event.inputs.version }}
NPM_TAG: ${{ github.event.inputs.npm_tag }}
REF: ${{ github.event.inputs.ref || github.sha }}
ARTIFACTS_DIR: ./.artifacts
CHROME_EXT_URL: ${{ github.event.inputs.chrome_ext_url }}
jobs:
release:
@ -49,6 +53,15 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # 0301...
- name: NPM Publish msdl
run: |
cd ./src/msdl
sed -i "s/%VERSION%/$VERSION/" package.json
npm publish --tag $NPM_TAG
cd -
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish Firefox Extension
id: web-ext-build
uses: kewisch/action-web-ext@v1
@ -64,6 +77,8 @@ jobs:
mkdir -p $ARTIFACTS_DIR
cp dist/main.js $ARTIFACTS_DIR/musescore-downloader.user.js
cp dist/ext.zip $ARTIFACTS_DIR/musescore-downloader.webextension.zip
wget -q $CHROME_EXT_URL -P $ARTIFACTS_DIR/
wget -q https://github.com/Xmader/musescore-downloader/archive/$REF.tar.gz -O $ARTIFACTS_DIR/source.tar.gz
- run: bash ./.github/workflows/get-signed-ext.sh
env:
EXT_ID: musescore-downloader
@ -83,6 +98,7 @@ jobs:
IPFS_HASH: ${{ steps.ipfs.outputs.hash }}
run: |
cd $ARTIFACTS_DIR
rm *.tar.gz
files=$(ls .)
assets=()
@ -94,6 +110,18 @@ jobs:
"${assets[@]}" \
-m v$VERSION \
-m "IPFS Hash: [$IPFS_HASH](https://ipfs.io/ipfs/$IPFS_HASH)" \
-m "Guess what? Mirrors! <https://github.com/musescore/MuseScore/tree/$SHORT_SHA>" \
-m "Guess what? Mirrors!<br><https://github.com/musescore/MuseScore/tree/$SHORT_SHA><br><https://github.com/github/dmca/tree/$SHORT_SHA>" \
-t $REF \
v$VERSION
- name: Archive to archive.org
continue-on-error: true
env:
REPO: ${{ github.repository }}
run: |
URL="https://github.com/$REPO/releases/"
curl "https://web.archive.org/save/" \
--compressed -s \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-raw "url=$URL&capture_all=on" \
| grep github

1
.gitignore vendored
View File

@ -67,3 +67,4 @@ typings/
dist/cache
dist/ext*
dist/cli.js

View File

@ -1,6 +1,6 @@
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
of this software and associated documentation files (the "Software"), to deal

148
README.md
View File

@ -1,12 +1,13 @@
# 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
**Star this project on [Github](https://github.com/Xmader/musescore-downloader) and [Gitlab](https://gitlab.com/Xmader/musescore-downloader)** (Mirror)
**Rate this on [Greasy Fork](https://greasyfork.org/scripts/391931)**
[![Discord](https://img.shields.io/discord/774491656643674122?color=7289da&label=Discord&logo=discord)](https://discord.gg/DKu7cUZ4XQ)
Need dataset of musescore.com for analysis / machine learning? try [musescore-dataset](https://github.com/Xmader/musescore-dataset).
@ -37,6 +38,18 @@ There is an article on their website: [Score download becomes a part of the Pro
## 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
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.
@ -50,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.
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.
@ -111,7 +124,8 @@ No, the API document is on https://developers.musescore.com/.
> 免登录、免 Musescore Pro下载 musescore.com 上的曲谱
**在 [Github](https://github.com/Xmader/musescore-downloader) 和 [Gitlab](https://gitlab.com/Xmader/musescore-downloader)** (镜像) **上给项目打星**
**在 [Greasy Fork](https://greasyfork.org/scripts/391931) 上给项目评分**
[![Discord](https://img.shields.io/discord/774491656643674122?color=7289da&label=Discord&logo=discord)](https://discord.gg/DKu7cUZ4XQ)
![](https://cdn.statically.io/gh/Xmader/musescore-downloader/master/screenshot.png?env=dev)
@ -127,8 +141,6 @@ No, the API document is on https://developers.musescore.com/.
脚本以 [Userscript](https://en.wikipedia.org/wiki/Userscript) 的形式提供,需要事先安装一个 [用户脚本管理器](https://greasyfork.org/zh-CN/help/installing-user-scripts),例如 Tampermonkey
在 [Greasy Fork](https://greasyfork.org/scripts/391931) 上查看、安装
在 [Github](https://github.com/Xmader/musescore-downloader) 上查看、讨论、更新
## License
@ -144,7 +156,8 @@ MIT
> descarga partituras de musescore.com de forma gratuita, no se requiere iniciar sesión o Musescore Pro
**Dale una estrella a este proyecto en [Github](https://github.com/Xmader/musescore-downloader) y [Gitlab](https://gitlab.com/Xmader/musescore-downloader)** (Respaldo)
**Calificalo en [Greasy Fork](https://greasyfork.org/scripts/391931)**
[![Discord](https://img.shields.io/discord/774491656643674122?color=7289da&label=Discord&logo=discord)](https://discord.gg/DKu7cUZ4XQ)
¿Necesita un conjunto de datos de musescore.com para análisis / Machine Learning? prueba [musescore-dataset](https://github.com/Xmader/musescore-dataset).
@ -230,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.
> utilizo ilegalmente nuestra API privada con contenido de música licenciada.
No, el documento de la API está en https://developers.musescore.com/.
@ -238,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).**
**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.
---

1745
dist/main.js vendored

File diff suppressed because it is too large Load Diff

659
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,9 @@
{
"name": "musescore-downloader",
"version": "0.11.4",
"version": "0.24.1",
"description": "download sheet music from musescore.com for free, no login or Musescore Pro required | 免登录、免 Musescore Pro免费下载 musescore.com 上的曲谱",
"main": "dist/main.js",
"bin": "dist/cli.js",
"repository": {
"type": "git",
"url": "git+https://github.com/Xmader/musescore-downloader.git"
@ -24,7 +25,7 @@
"content_scripts": [
{
"matches": [
"*://musescore.com/*/*"
"*://*.musescore.com/*/*"
],
"js": [
"src/web-ext.js"
@ -36,22 +37,30 @@
"dist/main.js"
],
"dependencies": {
"pdfkit": "git+https://github.com/Xmader/pdfkit.git",
"svg-to-pdfkit": "^0.1.8",
"webmscore": "^0.10.4"
"@librescore/fonts": "^0.4.0",
"@librescore/sf3": "^0.3.0",
"detect-node": "^2.0.4",
"inquirer": "^7.3.3",
"node-fetch": "^2.6.1",
"ora": "^5.1.0",
"webmscore": "^0.18.0"
},
"devDependencies": {
"@rollup/plugin-json": "^4.0.0",
"@rollup/plugin-json": "^4.1.0",
"@types/file-saver": "^2.0.1",
"@types/pdfkit": "^0.10.4",
"rollup": "^1.26.3",
"@types/inquirer": "^7.3.1",
"@types/pdfkit": "^0.10.6",
"pdfkit": "git+https://github.com/Xmader/pdfkit.git",
"rollup": "^1.32.1",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-string": "^3.0.0",
"rollup-plugin-typescript": "^1.0.1",
"tslib": "^1.10.0",
"typescript": "^4.1.1-rc"
"svg-to-pdfkit": "^0.1.8",
"tslib": "^1.14.1",
"typescript": "^4.1.2"
},
"scripts": {
"build": "rollup -c",

View File

@ -4,6 +4,7 @@ import commonjs from "rollup-plugin-commonjs"
import builtins from "rollup-plugin-node-builtins"
import nodeGlobals from "rollup-plugin-node-globals"
import json from "@rollup/plugin-json"
import { string } from "rollup-plugin-string"
import fs from "fs"
const getBannerText = () => {
@ -14,7 +15,12 @@ const getBannerText = () => {
return bannerText
}
const plugins = [
const getWrapper = (startL, endL) => {
const js = fs.readFileSync("./src/wrapper.js", "utf-8")
return js.split(/\n/g).slice(startL, endL).join("\n")
}
const basePlugins = [
typescript({
target: "ES6",
sourceMap: false,
@ -33,11 +39,8 @@ const plugins = [
extensions: [".js", ".ts"]
}),
json(),
builtins(),
nodeGlobals({
dirname: false,
filename: false,
baseDir: false,
string({
include: "**/*.css",
}),
{
/**
@ -45,7 +48,7 @@ const plugins = [
* @param {string} code
* @param {string} id
*/
transform(code, id) {
transform (code, id) {
if (id.includes("tslib")) {
code = code.split(/\r?\n/g).slice(15).join("\n")
}
@ -56,6 +59,16 @@ const plugins = [
},
]
const plugins = [
...basePlugins,
builtins(),
nodeGlobals({
dirname: false,
filename: false,
baseDir: false,
}),
]
export default [
{
input: "src/worker.ts",
@ -75,9 +88,19 @@ export default [
format: "iife",
sourcemap: false,
banner: getBannerText,
intro: "// fix for Greasemonkey\nwindow.eval('(' + function () {",
outro: "}.toString() + ')()')"
intro: () => getWrapper(0, -1),
outro: () => getWrapper(-1)
},
plugins,
},
{
input: "src/cli.ts",
output: {
file: "dist/cli.js",
format: "cjs",
banner: "#!/usr/bin/env node",
sourcemap: false,
},
plugins: basePlugins,
},
]

View File

@ -5,20 +5,24 @@
* make hooked methods "native"
*/
export const makeNative = (() => {
const l = new Set<Function>()
const l = new Map<Function, Function>()
hookNative(Function.prototype, 'toString', (_toString) => {
return function () {
if (l.has(this)) {
// "function () {\n [native code]\n}"
return _toString.call(parseInt) as string
const _fn = l.get(this) || parseInt // "function () {\n [native code]\n}"
if (l.has(_fn)) { // nested
return _fn.toString()
} else {
return _toString.call(_fn) as string
}
}
return _toString.call(this) as string
}
}, true)
return (fn: Function) => {
l.add(fn)
return (fn: Function, original: Function) => {
l.set(fn, original)
}
})()
@ -40,28 +44,10 @@ export function hookNative<T extends object, M extends (keyof T)> (
target[method] = hookedFn
if (!async) {
makeNative(hookedFn as any)
makeNative(hookedFn as any, _fn as any)
} else {
setTimeout(() => {
makeNative(hookedFn as any)
makeNative(hookedFn as any, _fn as any)
})
}
}
export const hideFromArrFilter = (() => {
const l = new Set()
const qsaHook = (_fn) => {
return function (...args) {
const arr = _fn.apply(this, args)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Array.prototype.filter.call(arr, (e) => !l.has(e))
}
}
hookNative(Element.prototype, 'querySelectorAll', qsaHook)
hookNative(document, 'querySelectorAll', qsaHook)
return (item: any) => {
l.add(item)
}
})()

83
src/btn.css Normal file
View File

@ -0,0 +1,83 @@
div {
width: 422px;
right: 0;
margin: 0 18px 18px 0;
text-align: center;
align-items: center;
font-family: 'Inter', 'Helvetica neue', Helvetica, sans-serif;
position: absolute;
z-index: 9999;
background: #f6f6f6;
min-width: 230px;
/* pass the scroll event through the btns background */
pointer-events: none;
}
@media screen and (max-width: 950px) {
div {
width: auto !important;
}
}
button {
width: 178px !important;
min-width: 178px;
height: 40px;
color: #fff;
background: #2e68c0;
cursor: pointer;
pointer-events: auto;
margin-bottom: 8px;
margin-right: 8px;
padding: 4px 12px;
justify-content: start;
align-self: center;
font-size: 16px;
border-radius: 6px;
border: 0;
display: inline-flex;
position: relative;
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 {
display: inline-block;
margin-right: 5px;
width: 20px;
height: 20px;
margin-top: auto;
margin-bottom: auto;
}
span {
margin-top: auto;
margin-bottom: auto;
}

View File

@ -1,107 +1,216 @@
import { ScoreInfo } from './scoreinfo'
import { loadMscore, WebMscore } from './mscore'
import { hideFromArrFilter } from './anti-detection'
import { useTimeout, windowOpenAsync, console, attachShadow, DISCORD_URL } from './utils'
import { isGmAvailable, _GM } from './gm'
import i18n from './i18n'
// @ts-ignore
import btnListCss from './btn.css'
type BtnElement = HTMLButtonElement
/**
* Select the original Download Button
*/
export const getDownloadBtn = (): BtnElement => {
const btnsDiv = document.querySelector('.score-right .buttons-wrapper') || document.querySelectorAll('aside > section > section > div')[3]
const btn = btnsDiv.querySelector('button, .button') as BtnElement
btn.onclick = null
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',
}
// fix the icon of the download btn
// if the `btn` seleted was a `Print` btn, replace the `print` icon with the `download` icon
const svgPath: SVGPathElement | null = btn.querySelector('svg > path')
if (svgPath) {
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')
}
const getBtnContainer = (): HTMLDivElement => {
const els = [...document.querySelectorAll('span')]
const el = els.find(b => {
const text = b?.textContent?.replace(/\s/g, '') || ''
return text.includes('Download') || text.includes('Print')
}) as HTMLDivElement | null
const btnParent = el?.parentElement?.parentElement as HTMLDivElement | undefined
if (!btnParent || !(btnParent instanceof HTMLDivElement)) throw new Error('btn parent not found')
return btnParent
}
if (btn.nodeName.toLowerCase() === 'button') {
btn.setAttribute('style', 'width: 205px !important')
} else {
btn.dataset.target = ''
}
const buildDownloadBtn = (icon: ICON, lightTheme = false) => {
const btn = document.createElement('button')
btn.type = 'button'
if (lightTheme) btn.className = 'light'
// build icon svg element
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svg.setAttribute('viewBox', '0 0 24 24')
const svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path')
svgPath.setAttribute('d', icon)
svgPath.setAttribute('fill', lightTheme ? '#2e68c0' : '#fff')
svg.append(svgPath)
const textNode = document.createElement('span')
btn.append(svg, textNode)
return btn
}
const cloneBtn = (btn: HTMLButtonElement) => {
const n = btn.cloneNode(true) as HTMLButtonElement
n.onclick = btn.onclick
return n
}
function getScrollParent (node: HTMLElement): HTMLElement {
if (node.scrollHeight > node.clientHeight) {
return node
} else {
return getScrollParent(node.parentNode as HTMLElement)
}
}
function onPageRendered (getEl: () => HTMLElement) {
return new Promise<HTMLElement>((resolve) => {
const observer = new MutationObserver(() => {
try {
const el = getEl()
if (el) {
observer.disconnect()
resolve(el)
}
} catch { }
})
observer.observe(document.querySelector('div > section') ?? document.body, { childList: true, subtree: true })
})
}
interface BtnOptions {
readonly name: string;
readonly action: BtnAction;
readonly disabled?: boolean;
readonly tooltip?: string;
readonly icon?: ICON;
readonly lightTheme?: boolean;
}
export enum BtnListMode {
InPage,
ExtWindow,
}
export class BtnList {
private readonly list: BtnElement[] = [];
constructor (private templateBtn: BtnElement) { }
private antiDetectionText = 'Download'
private hide (el: HTMLElement) {
hideFromArrFilter(el)
}
constructor (private getBtnParent: () => HTMLDivElement = getBtnContainer) { }
add (options: BtnOptions): BtnElement {
const btn = this.templateBtn.cloneNode(true) as HTMLButtonElement
const textNode = [...btn.childNodes].find((x) => {
const txt = x.textContent as string
return txt.includes('Download') || txt.includes('Print')
}) as HTMLSpanElement
// Anti-detection:
// musescore will send a track event "MSCZDOWNLOADER_INSTALLED" to its backend
// if detected "Download MSCZ"
['textContent', 'innerHTML'].forEach((_property) => {
const _set = textNode['__lookupSetter__'](_property)
Object.defineProperty(textNode, _property, {
set (v) { _set.call(textNode, v) },
get: () => {
// first time only
const t = this.antiDetectionText
this.antiDetectionText = ' '
return t
},
})
})
// hide this button from Array.prototype.filter
this.hide(btn)
this.hide(textNode)
const setText = (str: string): void => {
textNode.textContent = str
const btnTpl = buildDownloadBtn(options.icon ?? ICON.DOWNLOAD, options.lightTheme)
const setText = (btn: BtnElement) => {
const textNode = btn.querySelector('span')
return (str: string): void => {
if (textNode) textNode.textContent = str
}
}
setText(options.name)
setText(btnTpl)(options.name)
btn.onclick = (): void => {
options.action(options.name, btn, setText)
btnTpl.onclick = function () {
const btn = this as BtnElement
options.action(options.name, btn, setText(btn))
}
this.list.push(btn)
this.list.push(btnTpl)
if (options.disabled) {
btn.disabled = options.disabled
btnTpl.disabled = options.disabled
}
if (options.tooltip) {
btn.title = options.tooltip
btnTpl.title = options.tooltip
}
return btn
// add buttons to the userscript manager menu
if (isGmAvailable('registerMenuCommand')) {
// eslint-disable-next-line no-void
void _GM.registerMenuCommand(options.name, () => {
options.action(options.name, btnTpl, () => undefined)
})
}
return btnTpl
}
private _positionBtns (anchorDiv: HTMLDivElement, newParent: HTMLDivElement) {
let { top } = anchorDiv.getBoundingClientRect()
top += window.scrollY // relative to the entire document instead of viewport
if (top > 0) {
newParent.style.top = `${top}px`
} else {
newParent.style.top = '0px'
}
}
private _commit () {
const btnParent = document.querySelector('div') as HTMLDivElement
const shadow = attachShadow(btnParent)
// style the shadow DOM
const style = document.createElement('style')
style.innerText = btnListCss
shadow.append(style)
// hide buttons using the shadow DOM
const slot = document.createElement('slot')
shadow.append(slot)
const newParent = document.createElement('div')
newParent.append(...this.list.map(e => cloneBtn(e)))
shadow.append(newParent)
// default position
newParent.style.top = `${window.innerHeight - newParent.getBoundingClientRect().height}px`
void onPageRendered(this.getBtnParent).then((anchorDiv: HTMLDivElement) => {
const pos = () => this._positionBtns(anchorDiv, newParent)
pos()
// reposition btns when window resizes
window.addEventListener('resize', pos, { passive: true })
// reposition btns when scrolling
const scroll = getScrollParent(anchorDiv)
scroll.addEventListener('scroll', pos, { passive: true })
})
return btnParent
}
/**
* replace the template button with the list of new buttons
*/
commit (): void {
this.templateBtn.replaceWith(...this.list)
async commit (mode: BtnListMode = BtnListMode.InPage): Promise<void> {
switch (mode) {
case BtnListMode.InPage: {
let el: Element
try {
el = this._commit()
} catch {
// fallback to BtnListMode.ExtWindow
return this.commit(BtnListMode.ExtWindow)
}
const observer = new MutationObserver(() => {
// check if the buttons are still in document when dom updates
if (!document.contains(el)) {
// re-commit
// performance issue?
el = this._commit()
}
})
observer.observe(document, { childList: true, subtree: true })
break
}
case BtnListMode.ExtWindow: {
const div = this._commit()
const w = await windowOpenAsync(undefined, '', undefined, 'resizable,width=230,height=270')
// eslint-disable-next-line no-unused-expressions
w?.document.body.append(div)
window.addEventListener('unload', () => w?.close())
break
}
default:
throw new Error('unknown BtnListMode')
}
}
}
@ -118,28 +227,25 @@ export namespace BtnAction {
else return url
}
export const openUrl = (url: UrlInput): BtnAction => {
return process(async (): Promise<any> => {
window.open(await normalizeUrlInput(url))
})
}
export const download = (url: UrlInput): BtnAction => {
return process(async (): Promise<any> => {
export const download = (url: UrlInput, fallback?: () => Promisable<void>, timeout?: number, target?: '_blank'): BtnAction => {
return process(async (): Promise<void> => {
const _url = await normalizeUrlInput(url)
const a = document.createElement('a')
a.href = _url
if (target) a.target = target
a.dispatchEvent(new MouseEvent('click'))
})
}, fallback, timeout)
}
export const mscoreWindow = (fn: (w: Window, score: WebMscore, processingTextEl: ChildNode) => any): BtnAction => {
export const openUrl = download
export const mscoreWindow = (scoreinfo: ScoreInfo, fn: (w: Window, score: WebMscore, processingTextEl: ChildNode) => any): BtnAction => {
return async (btnName, btn, setText) => {
const _onclick = btn.onclick
btn.onclick = null
setText(i18n('PROCESSING')())
const w = window.open('') as Window
const w = await windowOpenAsync(btn, '') as Window
const txt = document.createTextNode(i18n('PROCESSING')())
w.document.body.append(txt)
@ -158,13 +264,13 @@ export namespace BtnAction {
btn.onclick = _onclick
})
score = await loadMscore(w)
score = await loadMscore(scoreinfo, w)
fn(w, score, txt)
}
}
export const process = (fn: () => any): BtnAction => {
export const process = (fn: () => any, fallback?: () => Promisable<void>, timeout = 10 * 60 * 1000 /* 10min */): BtnAction => {
return async (name, btn, setText): Promise<void> => {
const _onclick = btn.onclick
@ -172,11 +278,28 @@ export namespace BtnAction {
setText(i18n('PROCESSING')())
try {
await fn()
await useTimeout(fn(), timeout)
setText(name)
} catch (err) {
setText(i18n('BTN_ERROR')())
console.error(err)
if (fallback) {
// use fallback
await fallback()
setText(name)
} else {
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'))
}
}
btn.onclick = _onclick

206
src/cli.ts Normal file
View File

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

View File

@ -1,60 +1,115 @@
/* eslint-disable no-extend-native */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import scoreinfo from './scoreinfo'
import { webpackHook } from './webpack-hook'
import { hookNative } from './anti-detection'
const FILE_URL_MODULE_ID = 'iNJA'
const MAGIC_REG = /^\d+(img|mp3|midi)\d(.+)$/
import { console } from './utils'
type FileType = 'img' | 'mp3' | 'midi'
const getApiUrl = (id: number, type: FileType, index: number): string => {
// proxy
return `https://musescore.now.sh/api/jmuse?id=${id}&type=${type}&index=${index}`
}
const TYPE_REG = /type=(img|mp3|midi)/
/**
* I know this is super hacky.
*/
let magic: Promise<string> | string = new Promise((resolve) => {
hookNative(String.prototype, 'charCodeAt', (_fn, detach) => {
return function (i: number) {
const m = this.match(MAGIC_REG)
if (m) {
resolve(m[2])
magic = m[2]
detach()
const magicHookConstr = (() => {
const l = {}
try {
const p = Object.getPrototypeOf(document.body)
Object.setPrototypeOf(document.body, null)
hookNative(document.body, 'append', () => {
return function (...nodes: Node[]) {
p.append.call(this, ...nodes)
if (nodes[0].nodeName === 'IFRAME') {
const iframe = nodes[0] as HTMLIFrameElement
const w = iframe.contentWindow as Window
hookNative(w, 'fetch', () => {
return function (url, init) {
const token = init?.headers?.Authorization
if (typeof url === 'string' && token) {
const m = url.match(TYPE_REG)
console.debug(url, token, m)
if (m) {
const type = m[1]
// eslint-disable-next-line no-unused-expressions
l[type]?.(token)
}
}
return fetch(url, init)
}
})
}
}
return _fn.call(this, i) as number
}
})
})
})
export const getFileUrl = async (type: FileType, index = 0): Promise<string> => {
const fileUrlModule = webpackHook(FILE_URL_MODULE_ID, {
'6Ulw' (_, r, t) { // override
t.d(r, 'a', () => {
return type
})
},
'VSrV' (_, r, t) { // override
t.d(r, 'b', () => {
return getApiUrl
})
},
})
const fn: (id: number, index: number, cb: (url: string) => any, magic: string) => string = fileUrlModule.a
if (typeof magic !== 'string') {
// force to retrieve the MAGIC
const el = document.querySelectorAll('.SD7H- > button')[3] as HTMLButtonElement
el.click()
magic = await magic
Object.setPrototypeOf(document.body, p)
} catch (err) {
console.error(err)
}
return new Promise((resolve) => {
return fn(scoreinfo.id, index, resolve, magic as string)
})
return async (type: FileType) => {
return new Promise<string>((resolve) => {
l[type] = (token) => {
resolve(token)
magics[type] = token
}
})
}
})()
const magics: Record<FileType, Promise<string>> = {
img: magicHookConstr('img'),
midi: magicHookConstr('midi'),
mp3: magicHookConstr('mp3'),
}
const getApiUrl = (id: number, type: FileType, index: number): string => {
return `/api/jmuse?id=${id}&type=${type}&index=${index}&v2=1`
}
const getApiAuth = async (type: FileType, index: number): Promise<string> => {
// eslint-disable-next-line no-void
void index
const magic = magics[type]
if (magic instanceof Promise) {
// force to retrieve the MAGIC
switch (type) {
case 'midi': {
const el = document.querySelector('button[hasaccess]') as HTMLButtonElement
el.click()
break
}
case 'mp3': {
const el = document.querySelector('button[title="Toggle Play"]') as HTMLButtonElement
el.click()
break
}
case 'img': {
const imgE = document.querySelector('img[src*=score_]')
const nextE = imgE?.parentElement?.nextElementSibling
if (nextE) nextE.scrollIntoView()
break
}
}
}
return magic
}
export const getFileUrl = async (id: number, type: FileType, index = 0): Promise<string> => {
const url = getApiUrl(id, type, index)
const auth = await getApiAuth(type, index)
const r = await fetch(url, {
headers: {
Authorization: auth,
},
})
const { info } = await r.json()
return info.url as string
}

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

@ -1,5 +1,5 @@
import { createLocale } from './'
import { createLocale } from './utils'
export default createLocale({
'PROCESSING' () {
@ -27,6 +27,10 @@ export default createLocale({
return 'Download individual parts (BETA)' as const
},
'VIEW_IN_LIBRESCORE' () {
return 'View in LibreScore' as const
},
'FULL_SCORE' () {
return 'Full score' as const
},

View File

@ -1,5 +1,5 @@
import { createLocale } from './'
import { createLocale } from './utils'
export default createLocale({
'PROCESSING' () {
@ -10,7 +10,7 @@ export default createLocale({
},
'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) {
@ -27,6 +27,10 @@ export default createLocale({
return 'Descargar partes individuales (BETA)' as const
},
'VIEW_IN_LIBRESCORE' () {
return 'Visualizar en LibreScore' as const
},
'FULL_SCORE' () {
return 'Partitura Completa' as const
},

View File

@ -1,6 +1,10 @@
import isNodeJs from 'detect-node'
import en from './en'
import es from './es'
import it from './it'
import zh from './zh'
export interface LOCALE {
'PROCESSING' (): string;
@ -14,25 +18,31 @@ export interface LOCALE {
'IND_PARTS' (): string;
'IND_PARTS_TOOLTIP' (): string;
'FULL_SCORE' (): string;
}
'VIEW_IN_LIBRESCORE' (): string;
/**
* type checking only so no missing keys
*/
export function createLocale<OBJ extends LOCALE> (obj: OBJ): OBJ {
return Object.freeze(obj)
'FULL_SCORE' (): string;
}
const locales = (<L extends { [n: string]: LOCALE } /** type checking */> (l: L) => Object.freeze(l))({
en,
es,
it,
zh,
})
// detect browser language
const lang = (() => {
let userLangs: readonly string[]
if (!isNodeJs) {
userLangs = navigator.languages
} else {
const env = process.env
const l = env.LC_ALL || env.LC_MESSAGES || env.LANG || env.LANGUAGE || ''
userLangs = [l.slice(0, 2)]
}
const names = Object.keys(locales)
const _lang = navigator.languages.find(l => {
const _lang = userLangs.find(l => {
// find the first occurrence of valid languages
return names.includes(l)
})

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
},
})

9
src/i18n/utils.ts Normal file
View File

@ -0,0 +1,9 @@
import type { LOCALE } from './'
/**
* type checking only so no missing keys
*/
export function createLocale<OBJ extends LOCALE> (obj: OBJ): OBJ {
return Object.freeze(obj)
}

37
src/i18n/zh.ts Normal file
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,61 +1,66 @@
import './meta'
import { waitForDocumentLoaded, saveAs } from './utils'
import FileSaver from 'file-saver'
import { waitForSheetLoaded, console } from './utils'
import { downloadPDF } from './pdf'
import { downloadMscz } from './mscz'
import { getFileUrl } from './file'
import { WebMscore, loadSoundFont } from './mscore'
import { getDownloadBtn, BtnList, BtnAction } from './btn'
import * as recaptcha from './recaptcha'
import scoreinfo from './scoreinfo'
import { INDV_DOWNLOADS } from './mscore'
import { getLibreScoreLink } from './librescore-link'
import { BtnList, BtnAction, BtnListMode, ICON } from './btn'
import { ScoreInfoInPage, SheetInfoInPage, getActualId } from './scoreinfo'
import i18n from './i18n'
const main = (): void => {
// init recaptcha
// eslint-disable-next-line @typescript-eslint/no-floating-promises
recaptcha.init()
const { saveAs } = FileSaver
const btnList = new BtnList(getDownloadBtn())
const filename = scoreinfo.fileName
const main = (): void => {
const btnList = new BtnList()
const scoreinfo = new ScoreInfoInPage(document)
const { fileName } = scoreinfo
// eslint-disable-next-line no-void
void getActualId(scoreinfo)
let indvPartBtn: HTMLButtonElement | null = null
const fallback = () => {
// btns fallback to load from MSCZ file (`Individual Parts`)
return indvPartBtn?.click()
}
btnList.add({
name: i18n('DOWNLOAD')('MSCZ'),
action: BtnAction.process(downloadMscz),
action: BtnAction.process(() => downloadMscz(scoreinfo, saveAs)),
})
btnList.add({
name: i18n('DOWNLOAD')('PDF'),
action: BtnAction.deprecate(
BtnAction.process(downloadPDF),
),
action: BtnAction.process(() => downloadPDF(scoreinfo, new SheetInfoInPage(document)), fallback, 3 * 60 * 1000 /* 3min */),
})
btnList.add({
name: i18n('DOWNLOAD')('MusicXML'),
action: BtnAction.mscoreWindow(async (w, score) => {
name: i18n('DOWNLOAD')('MXL'),
action: BtnAction.mscoreWindow(scoreinfo, async (w, score) => {
const mxl = await score.saveMxl()
const data = new Blob([mxl])
saveAs(data, `${filename}.mxl`)
saveAs(data, `${fileName}.mxl`)
w.close()
}),
})
btnList.add({
name: i18n('DOWNLOAD')('MIDI'),
action: BtnAction.deprecate(
BtnAction.download(() => getFileUrl('midi')),
),
action: BtnAction.download(() => getFileUrl(scoreinfo.id, 'midi'), fallback, 30 * 1000 /* 30s */),
})
btnList.add({
name: i18n('DOWNLOAD')('MP3'),
action: BtnAction.download(() => getFileUrl('mp3')),
action: BtnAction.download(() => getFileUrl(scoreinfo.id, 'mp3'), fallback, 30 * 1000 /* 30s */),
})
btnList.add({
indvPartBtn = btnList.add({
name: i18n('IND_PARTS')(),
tooltip: i18n('IND_PARTS_TOOLTIP')(),
action: BtnAction.mscoreWindow(async (w, score, txt) => {
action: BtnAction.mscoreWindow(scoreinfo, async (w, score, txt) => {
const metadata = await score.metadata()
console.log('score metadata loaded by webmscore', metadata)
@ -67,46 +72,10 @@ const main = (): void => {
const fieldset = w.document.createElement('fieldset')
w.document.body.append(fieldset)
interface IndividualDownload {
name: string;
fileExt: string;
action (score: WebMscore): Promise<Uint8Array>;
}
const downloads: IndividualDownload[] = [
{
name: i18n('DOWNLOAD')('PDF'),
fileExt: 'pdf',
action: (score) => score.savePdf(),
},
{
name: i18n('DOWNLOAD')('MSCZ'),
fileExt: 'mscz',
action: (score) => score.saveMsc('mscz'),
},
{
name: i18n('DOWNLOAD')('MusicXML'),
fileExt: 'mxl',
action: (score) => score.saveMxl(),
},
{
name: i18n('DOWNLOAD')('MIDI'),
fileExt: 'mid',
action: (score) => score.saveMidi(true, true),
},
{
name: i18n('DOWNLOAD_AUDIO')('FLAC'),
fileExt: 'flac',
action: (score) => loadSoundFont(score).then(() => score.saveAudio('flac')),
},
{
name: i18n('DOWNLOAD_AUDIO')('OGG'),
fileExt: 'ogg',
action: (score) => loadSoundFont(score).then(() => score.saveAudio('ogg')),
},
]
const downloads = INDV_DOWNLOADS
// part selection
const DEFAULT_PART = -1 // initially select "full score"
for (const excerpt of metadata.excerpts) {
const id = excerpt.id
const partName = excerpt.title
@ -115,7 +84,7 @@ const main = (): void => {
e.name = 'score-part'
e.type = 'radio'
e.alt = partName
e.checked = id === 0 // initially select the first part
e.checked = id === DEFAULT_PART
e.onclick = () => {
return score.setExcerptId(id) // set selected part
}
@ -127,7 +96,7 @@ const main = (): void => {
fieldset.append(e, label, br)
}
await score.setExcerptId(0) // initially select the first part
await score.setExcerptId(DEFAULT_PART)
// submit buttons
for (const d of downloads) {
@ -152,7 +121,7 @@ const main = (): void => {
const partName = checked.alt
const data = new Blob([await d.action(score)])
saveAs(data, `${filename} - ${partName}.${d.fileExt}`)
saveAs(data, `${fileName} - ${partName}.${d.fileExt}`)
// unlock button
initBtn()
@ -163,8 +132,17 @@ const main = (): void => {
}),
})
btnList.commit()
btnList.add({
name: i18n('VIEW_IN_LIBRESCORE')(),
action: BtnAction.openUrl(() => getLibreScoreLink(scoreinfo)),
tooltip: 'BETA',
icon: ICON.LIBRESCORE,
lightTheme: true,
})
// eslint-disable-next-line @typescript-eslint/no-floating-promises
btnList.commit(BtnListMode.InPage)
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
waitForDocumentLoaded().then(main)
waitForSheetLoaded().then(main)

View File

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

View File

@ -1,25 +1,37 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { fetchMscz } from './mscz'
import { fetchData } from './utils'
import { ScoreInfo } from './scoreinfo'
import isNodeJs from 'detect-node'
import i18n from './i18n'
import { dependencies as depVers } from '../package.json'
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)
// 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')
export type WebMscore = import('webmscore').default
export type WebMscoreConstr = typeof import('webmscore').default
const initMscore = async (w: Window) => {
if (!w['WebMscore']) {
// init webmscore (https://github.com/LibreScore/webmscore)
const script = w.document.createElement('script')
script.src = WEBMSCORE_URL
w.document.body.append(script)
await new Promise(resolve => { script.onload = resolve })
const initMscore = async (w: Window): Promise<WebMscoreConstr> => {
if (!isNodeJs) { // attached to a page
if (!w['WebMscore']) {
// init webmscore (https://github.com/LibreScore/webmscore)
const script = w.document.createElement('script')
script.src = WEBMSCORE_URL
w.document.body.append(script)
await new Promise(resolve => { script.onload = resolve })
}
return w['WebMscore'] as WebMscoreConstr
} else { // nodejs
return require('webmscore').default as WebMscoreConstr
}
}
@ -28,17 +40,37 @@ const initFonts = () => {
// load CJK fonts
// CJK (East Asian) characters will be rendered as "tofu" if there is no font
if (!fonts) {
fonts = Promise.all(
FONT_URLS.map(url => fetchData(url)),
)
if (isNodeJs) {
// module.exports.CN = ..., module.exports.KR = ...
const FONTS = Object.values(require('@librescore/fonts'))
const fs = require('fs')
fonts = Promise.all(
FONTS.map((path: string) => fs.promises.readFile(path) as Promise<Buffer>),
)
} else {
fonts = Promise.all(
FONT_URLS.map(url => fetchData(url)),
)
}
}
}
export const loadSoundFont = (score: WebMscore): Promise<void> => {
if (!score[SOUND_FONT_LOADED]) {
const loadPromise = (async () => {
let data: Uint8Array
if (isNodeJs) {
// module.exports.FluidR3Mono = ...
const SF3 = Object.values(require('@librescore/sf3'))[0]
const fs = require('fs')
data = await fs.promises.readFile(SF3)
} else {
data = await fetchData(SF3_URL)
}
await score.setSoundFont(
await fetchData(SF3_URL),
data,
)
})()
score[SOUND_FONT_LOADED] = loadPromise
@ -46,17 +78,60 @@ export const loadSoundFont = (score: WebMscore): Promise<void> => {
return score[SOUND_FONT_LOADED] as Promise<void>
}
export const loadMscore = async (w: Window): Promise<WebMscore> => {
export const loadMscore = async (scoreinfo: ScoreInfo, w?: Window): Promise<WebMscore> => {
initFonts()
await initMscore(w)
const WebMscore = await initMscore(w!)
const WebMscore: typeof import('webmscore').default = w['WebMscore']
// parse mscz data
const data = new Uint8Array(
new Uint8Array(await fetchMscz()), // copy its ArrayBuffer
new Uint8Array(await fetchMscz(scoreinfo)), // copy its ArrayBuffer
)
const score = await WebMscore.load('mscz', data, await fonts)
await score.generateExcerpts()
return score
}
export interface IndividualDownload {
name: string;
fileExt: string;
action (score: WebMscore): Promise<Uint8Array>;
}
export const INDV_DOWNLOADS: IndividualDownload[] = [
{
name: i18n('DOWNLOAD')('PDF'),
fileExt: 'pdf',
action: (score) => score.savePdf(),
},
{
name: i18n('DOWNLOAD')('MSCZ'),
fileExt: 'mscz',
action: (score) => score.saveMsc('mscz'),
},
{
name: i18n('DOWNLOAD')('MusicXML'),
fileExt: 'mxl',
action: (score) => score.saveMxl(),
},
{
name: i18n('DOWNLOAD')('MIDI'),
fileExt: 'mid',
action: (score) => score.saveMidi(true, true),
},
{
name: i18n('DOWNLOAD_AUDIO')('MP3'),
fileExt: 'mp3',
action: (score) => loadSoundFont(score).then(() => score.saveAudio('mp3')),
},
{
name: i18n('DOWNLOAD_AUDIO')('FLAC'),
fileExt: 'flac',
action: (score) => loadSoundFont(score).then(() => score.saveAudio('flac')),
},
{
name: i18n('DOWNLOAD_AUDIO')('OGG'),
fileExt: 'ogg',
action: (score) => loadSoundFont(score).then(() => score.saveAudio('ogg')),
},
]

View File

@ -1,26 +1,88 @@
import * as recaptcha from './recaptcha'
import { saveAs } from './utils'
import scoreinfo from './scoreinfo'
import { assertRes, getFetch } from './utils'
import { ScoreInfo } from './scoreinfo'
let msczBufferP: Promise<ArrayBuffer> | undefined
export const MSCZ_BUF_SYM = Symbol('msczBufferP')
export const MSCZ_URL_SYM = Symbol('msczUrl')
export const MAIN_CID_SYM = Symbol('mainCid')
const IPNS_KEY = 'QmSdXtvzC8v8iTTZuj5cVmiugnzbR1QATYRcGix4bBsioP'
const IPNS_RS_URL = `https://ipfs.io/api/v0/dag/resolve?arg=/ipns/${IPNS_KEY}`
export const getMainCid = async (scoreinfo: ScoreInfo, _fetch = getFetch()): Promise<string> => {
// look for the persisted msczUrl inside scoreinfo
let result = scoreinfo.store.get(MAIN_CID_SYM) as string
if (result) {
return result
}
const r = await _fetch(IPNS_RS_URL)
assertRes(r)
const json = await r.json()
result = json.Cid['/']
scoreinfo.store.set(MAIN_CID_SYM, result) // persist to scoreinfo
return result
}
export const loadMsczUrl = async (scoreinfo: ScoreInfo, _fetch = getFetch()): Promise<string> => {
// look for the persisted msczUrl inside scoreinfo
let result = scoreinfo.store.get(MSCZ_URL_SYM) as string
if (result) {
return result
}
const mainCid = await getMainCid(scoreinfo, _fetch)
const url = scoreinfo.getMsczCidUrl(mainCid)
const r0 = await _fetch(url)
// ipfs-http-gateway specific error
// may read further error msg as json
if (r0.status !== 500) {
assertRes(r0)
}
const cidRes: { Key: string; Message: string } = await r0.json()
const cid = cidRes.Key
if (!cid) {
// read further error msg
const err = cidRes.Message
if (err.includes('no link named')) { // file not found
throw new Error('Score not in dataset')
} else {
throw new Error(err)
}
}
result = `https://ipfs.infura.io/ipfs/${cid}`
scoreinfo.store.set(MSCZ_URL_SYM, result) // persist to scoreinfo
return result
}
export const fetchMscz = async (scoreinfo: ScoreInfo, _fetch = getFetch()): Promise<ArrayBuffer> => {
let msczBufferP = scoreinfo.store.get(MSCZ_BUF_SYM) as Promise<ArrayBuffer> | undefined
export const fetchMscz = async (): Promise<ArrayBuffer> => {
if (!msczBufferP) {
const url = scoreinfo.msczUrl
msczBufferP = (async (): Promise<ArrayBuffer> => {
const token = await recaptcha.execute()
const r = await fetch(url + token)
const url = await loadMsczUrl(scoreinfo, _fetch)
const r = await _fetch(url)
assertRes(r)
const data = await r.arrayBuffer()
return data
})()
scoreinfo.store.set(MSCZ_BUF_SYM, msczBufferP)
}
return msczBufferP
}
export const downloadMscz = async (): Promise<void> => {
const data = new Blob([await fetchMscz()])
// eslint-disable-next-line @typescript-eslint/require-await
export const setMscz = async (scoreinfo: ScoreInfo, buffer: ArrayBuffer): Promise<void> => {
scoreinfo.store.set(MSCZ_BUF_SYM, Promise.resolve(buffer))
}
export const downloadMscz = async (scoreinfo: ScoreInfo, saveAs: typeof import('file-saver').saveAs): Promise<void> => {
const data = new Blob([await fetchMscz(scoreinfo)])
const filename = scoreinfo.fileName
saveAs(data, `${filename}.mscz`)
}

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,14 +1,14 @@
import { PDFWorkerHelper } from './worker-helper'
import { getFileUrl } from './file'
import { saveAs } from './utils'
import scoreinfo from './scoreinfo'
import FileSaver from 'file-saver'
import { ScoreInfo, SheetInfo } from './scoreinfo'
let pdfBlob: Blob
const _downloadPDF = async (imgURLs: string[], imgType: 'svg' | 'png', name = ''): Promise<void> => {
if (pdfBlob) {
return saveAs(pdfBlob, `${name}.pdf`)
return FileSaver.saveAs(pdfBlob, `${name}.pdf`)
}
const cachedImg = document.querySelector('img[src*=score_]') as HTMLImageElement
@ -20,18 +20,18 @@ const _downloadPDF = async (imgURLs: string[], imgType: 'svg' | 'png', name = ''
pdfBlob = new Blob([pdfArrayBuffer])
saveAs(pdfBlob, `${name}.pdf`)
FileSaver.saveAs(pdfBlob, `${name}.pdf`)
}
export const downloadPDF = async (): Promise<void> => {
const imgType = scoreinfo.sheetImgType
const pageCount = scoreinfo.pageCount
export const downloadPDF = async (scoreinfo: ScoreInfo, sheet: SheetInfo): Promise<void> => {
const imgType = sheet.imgType
const pageCount = sheet.pageCount
const rs = Array.from({ length: pageCount }).map((_, i) => {
if (i === 0) { // The url to the first page is static. We don't need to use API to obtain it.
return scoreinfo.baseUrl + `score_${i}.${imgType}`
return sheet.thumbnailUrl
} else { // obtain image urls using the API
return getFileUrl('img', i)
return getFileUrl(scoreinfo.id, 'img', i)
}
})
const sheetImgURLs = await Promise.all(rs)

View File

@ -1,48 +0,0 @@
/**
* the site key for Google reCAPTCHA v3
*/
const SITE_KEY = '6Ldxtt8UAAAAALvcRqWTlVOVIB7MmEWwN-zw_9fM'
type token = string;
interface GRecaptcha {
ready (cb: () => any): void;
execute (siteKey: string, opts: { action: string }): Promise<token>;
}
let gr: GRecaptcha | Promise<GRecaptcha>
/**
* load reCAPTCHA
*/
const load = (): Promise<GRecaptcha> => {
// load script
const script = document.createElement('script')
script.src = `https://www.recaptcha.net/recaptcha/api.js?render=${SITE_KEY}`
script.async = true
document.body.appendChild(script)
// add css
const style = document.createElement('style')
style.innerHTML = '.grecaptcha-badge { display: none !important; }'
document.head.appendChild(style)
return new Promise((resolve) => {
script.onload = (): void => {
const grecaptcha: GRecaptcha = window['grecaptcha']
grecaptcha.ready(() => resolve(grecaptcha))
}
})
}
export const init = (): GRecaptcha | Promise<GRecaptcha> => {
if (!gr) {
gr = load()
}
return gr
}
export const execute = async (): Promise<token> => {
const captcha = await init()
return captcha.execute(SITE_KEY, { action: 'downloadmscz' })
}

View File

@ -1,87 +1,154 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
// run at document-start
export const ugappJsStore: Record<string, any> | null = (() => {
try {
const l = document.body.children as HTMLCollectionOf<HTMLElement>
const el = [...l].find(e => Object.keys(e.dataset).length > 0) as HTMLDivElement
const json = Object.values(el.dataset)[0] as string
return JSON.parse(json)
} catch (err) {
console.error(err)
return null
import { getFetch, escapeFilename, assertRes } from './utils'
import { getMainCid } from './mscz'
export abstract class ScoreInfo {
private readonly RADIX = 20;
private readonly INDEX_RADIX = 32;
abstract id: number;
abstract title: string;
public store = new Map<symbol, any>();
get idLastDigit (): number {
return (+this.id) % this.RADIX
}
})()
export const scoreinfo = {
get fileName (): string {
return escapeFilename(this.title)
}
get playerdata (): any {
// @ts-ignore
return ugappJsStore.store.page.data.score
},
public getMsczIpfsRef (mainCid: string): string {
return `/ipfs/${mainCid}/${this.idLastDigit}/${this.id}.mscz`
}
get id (this: typeof scoreinfo): number {
try {
return this.playerdata.id
} catch {
const el = document.querySelector("meta[property='al:ios:url']") as HTMLMetaElement
const m = el.content.match(/(\d+)$/) as RegExpMatchArray
return +m[1]
}
},
public getMsczCidUrl (mainCid: string): string {
return `https://ipfs.infura.io:5001/api/v0/block/stat?arg=${this.getMsczIpfsRef(mainCid)}`
}
get title (this: typeof scoreinfo): string {
try {
return this.playerdata.title
} catch {
const el = document.querySelector("meta[property='og:title']") as HTMLMetaElement
return el.content
}
},
get fileName (this: typeof scoreinfo): string {
return this.title.replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, '_')
},
get pageCount (this: typeof scoreinfo): number {
try {
return this.playerdata.pages_count
} catch {
return document.querySelectorAll('.gXB83').length
}
},
get baseUrl (this: typeof scoreinfo): string {
let thumbnailUrl: string
try {
thumbnailUrl = this.playerdata.thumbnails.original
} catch {
const el = document.querySelector("meta[property='og:image']") as HTMLMetaElement
thumbnailUrl = el.content
}
const { origin, pathname } = new URL(thumbnailUrl)
// remove the last part
return origin + pathname.split('/').slice(0, -1).join('/') + '/'
},
get msczUrl (this: typeof scoreinfo): string {
// https://github.com/Xmader/cloudflare-worker-musescore-mscz
return `https://musescore.now.sh/api/mscz?id=${this.id}&token=`
},
get sheetImgType (): 'svg' | 'png' {
try {
const imgE = document.querySelector('img[src*=score_]') as HTMLImageElement
const { pathname } = new URL(imgE.src)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const imgtype = pathname.match(/\.(\w+)$/)![1]
return imgtype as 'svg' | 'png'
} catch (_) {
// return null
return 'svg'
}
},
public getScorepackRef (mainCid: string): string {
return `/ipfs/${mainCid}/index/${(+this.id) % this.INDEX_RADIX}/${this.id}`
}
}
export default scoreinfo
export class ScoreInfoObj extends ScoreInfo {
constructor (public id: number = 0, public title: string = '') { super() }
}
export class ScoreInfoInPage extends ScoreInfo {
constructor (private document: Document) { super() }
get id (): number {
const el = this.document.querySelector("meta[property='al:ios:url']") as HTMLMetaElement
const m = el.content.match(/(\d+)$/) as RegExpMatchArray
return +m[1]
}
get title (): string {
const el = this.document.querySelector("meta[property='og:title']") as HTMLMetaElement
return el.content
}
get baseUrl (): string {
const el = this.document.querySelector("meta[property='og:image']") as HTMLMetaElement
const m = el.content.match(/^(.+\/)score_/) as RegExpMatchArray
return m[1]
}
}
export class ScoreInfoHtml extends ScoreInfo {
private readonly ID_REG = /<meta property="al:ios:url" content="musescore:\/\/score\/(\d+)">/
private readonly TITLE_REG = /<meta property="og:title" content="(.*)">/
private readonly BASEURL_REG = /<meta property="og:image" content="(.+\/)score_.*">/
constructor (private html: string) { super() }
get id (): number {
const m = this.html.match(this.ID_REG)
if (!m) return 0
return +m[1]
}
get title (): string {
const m = this.html.match(this.TITLE_REG)
if (!m) return ''
return m[1]
}
get baseUrl (): string {
const m = this.html.match(this.BASEURL_REG)
if (!m) return ''
return m[1]
}
static async request (url: string, _fetch = getFetch()): Promise<ScoreInfoHtml> {
const r = await _fetch(url)
if (!r.ok) return new ScoreInfoHtml('')
const html = await r.text()
return new ScoreInfoHtml(html)
}
}
export abstract class SheetInfo {
abstract pageCount: number;
abstract thumbnailUrl: string;
get imgType (): 'svg' | 'png' {
const thumbnail = this.thumbnailUrl
const imgtype = thumbnail.match(/score_0\.(\w+)/)![1]
return imgtype as 'svg' | 'png'
}
}
export class SheetInfoInPage extends SheetInfo {
constructor (private document: Document) { super() }
private get sheet0Img (): HTMLImageElement | null {
return this.document.querySelector('img[src*=score_]')
}
get pageCount (): number {
const sheet0Div = this.sheet0Img?.parentElement
if (!sheet0Div) {
throw new Error('no sheet images found')
}
return this.document.getElementsByClassName(sheet0Div.className).length
}
get thumbnailUrl (): string {
// url to the image of the first page
const el = this.document.querySelector<HTMLLinkElement>('link[as=image]')
const url = (el?.href || this.sheet0Img?.src) as string
return url.split('@')[0]
}
}
export const getActualId = async (scoreinfo: ScoreInfoInPage | ScoreInfoHtml, _fetch = getFetch()): Promise<number> => {
if (scoreinfo.id <= 1000000000000) {
// actual id already
return scoreinfo.id
}
const mainCid = await getMainCid(scoreinfo, _fetch)
const ref = `${mainCid}/sid2id/${scoreinfo.id}`
const url = `https://ipfs.infura.io:5001/api/v0/dag/get?arg=${ref}`
const r0 = await _fetch(url)
if (r0.status !== 500) {
assertRes(r0)
}
const res: { Message: string } | number = await r0.json()
if (typeof res !== 'number') {
// read further error msg
throw new Error(res.Message)
}
// assign the actual id back to scoreinfo
Object.defineProperty(scoreinfo, 'id', {
get () { return res },
})
return res
}

View File

@ -1,7 +1,12 @@
import FileSaver from 'file-saver/dist/FileSaver.js'
import isNodeJs from 'detect-node'
import { isGmAvailable, _GM } from './gm'
export const saveAs: typeof import('file-saver').saveAs = FileSaver.saveAs
export const DISCORD_URL = 'https://discord.gg/gSsTUvJmD8'
export const escapeFilename = (s: string): string => {
return s.replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, '_')
}
export const getIndexPath = (id: number): string => {
const idStr = String(id)
@ -13,12 +18,111 @@ export const getIndexPath = (id: number): string => {
return indexN.join('/')
}
const NODE_FETCH_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0',
'Accept-Language': 'en-US,en;q=0.8',
}
export const getFetch = (): typeof fetch => {
if (!isNodeJs) {
return fetch
} else {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const nodeFetch = require('node-fetch')
return (input: RequestInfo, init?: RequestInit) => {
init = Object.assign({ headers: NODE_FETCH_HEADERS }, init)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return nodeFetch(input, init)
}
}
}
export const fetchData = async (url: string, init?: RequestInit): Promise<Uint8Array> => {
const r = await fetch(url, init)
const data = await r.arrayBuffer()
return new Uint8Array(data)
}
export const assertRes = (r: Response): void => {
if (!r.ok) throw new Error(`${r.url} ${r.status} ${r.statusText}`)
}
export const useTimeout = async <T> (promise: T | Promise<T>, ms: number): Promise<T> => {
if (!(promise instanceof Promise)) {
return promise
}
return new Promise((resolve, reject) => {
const i = setTimeout(() => {
reject(new Error('timeout'))
}, ms)
promise.then(resolve, reject).finally(() => clearTimeout(i))
})
}
export const getSandboxWindowAsync = async (targetEl: Element | undefined = undefined): Promise<Window> => {
if (typeof document === 'undefined') return {} as any as Window
if (isGmAvailable('addElement')) {
// create iframe using GM_addElement API
const iframe = await _GM.addElement('iframe', {})
iframe.style.display = 'none'
return iframe.contentWindow as Window
}
if (!targetEl) {
return new Promise((resolve) => {
// You need ads in your pages, right?
const observer = new MutationObserver(() => {
for (let i = 0; i < window.frames.length; i++) {
// find iframe windows created by ads
const frame = frames[i]
try {
const href = frame.location.href
if (href === location.href || href === 'about:blank') {
resolve(frame)
return
}
} catch { }
}
})
observer.observe(document.body, { subtree: true, childList: true })
})
}
return new Promise((resolve) => {
const eventName = 'onmousemove'
const id = Math.random().toString()
targetEl[id] = (iframe: HTMLIFrameElement) => {
delete targetEl[id]
targetEl.removeAttribute(eventName)
iframe.style.display = 'none'
targetEl.append(iframe)
const w = iframe.contentWindow
resolve(w as Window)
}
targetEl.setAttribute(eventName, `this['${id}'](document.createElement('iframe'))`)
})
}
export const getUnsafeWindow = (): Window => {
// eslint-disable-next-line no-eval
return window.eval('window') as Window
}
export const console: Console = (window || global).console // Object.is(window.console, unsafeWindow.console) == false
export const windowOpenAsync = (targetEl: Element | undefined, ...args: Parameters<Window['open']>): Promise<Window | null> => {
return getSandboxWindowAsync(targetEl).then(w => w.open(...args))
}
export const attachShadow = (el: Element): ShadowRoot => {
return Element.prototype.attachShadow.call(el, { mode: 'closed' }) as ShadowRoot
}
export const waitForDocumentLoaded = (): Promise<void> => {
if (document.readyState !== 'complete') {
return new Promise(resolve => {
@ -34,3 +138,23 @@ export const waitForDocumentLoaded = (): Promise<void> => {
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

@ -1,11 +1,16 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { hookNative } from './anti-detection'
import { console, getUnsafeWindow } from './utils'
const CHUNK_PUSH_FN = /^function [^r]\(\w\){/
interface Module {
(module, exports, __webpack_require__): void;
}
type WebpackJson = [number, { [id: string]: Module }][]
type WebpackJson = [(number | string)[], { [id: string]: Module }, any[]?][]
const moduleLookup = (id: string, globalWebpackJson: WebpackJson) => {
const pack = globalWebpackJson.find(x => x[1][id])!
@ -47,4 +52,122 @@ export const webpackHook = (moduleId: string, moduleOverrides: { [id: string]: M
return t(moduleId)
}
export const ALL = '*'
export const [webpackGlobalOverride, onPackLoad] = (() => {
type OnPackLoadFn = (pack: WebpackJson[0]) => any
const moduleOverrides: { [id: string]: Module } = {}
const onPackLoadFns: OnPackLoadFn[] = []
function applyOverride (pack: WebpackJson[0]) {
let entries = Object.entries(moduleOverrides)
// apply to all
const all = moduleOverrides[ALL]
if (all) {
entries = Object.keys(pack[1]).map(id => [id, all])
}
entries.forEach(([id, override]) => {
const mod = pack[1][id]
if (mod) {
pack[1][id] = function (n, r, t) {
// make exports configurable
t = Object.assign(t, {
d (exp, name, fn) {
return Object.defineProperty(exp, name, { enumerable: true, get: fn, configurable: true })
},
})
mod(n, r, t)
override(n, r, t)
}
}
})
}
// hook `webpackJsonpmusescore.push` as soon as `webpackJsonpmusescore` is available
const _w = getUnsafeWindow()
let jsonp = _w['webpackJsonpmusescore']
let hooked = false
Object.defineProperty(_w, 'webpackJsonpmusescore', {
get () { return jsonp },
set (v: WebpackJson) {
jsonp = v
if (!hooked && v.push.toString().match(CHUNK_PUSH_FN)) {
hooked = true
hookNative(v, 'push', (_fn) => {
return function (pack) {
onPackLoadFns.forEach(fn => fn(pack))
applyOverride(pack)
return _fn.call(this, pack)
}
})
}
},
})
return [
// set overrides
(moduleId: string, override: Module) => {
moduleOverrides[moduleId] = override
},
// set onPackLoad listeners
(fn: OnPackLoadFn) => {
onPackLoadFns.push(fn)
},
] as const
})()
export const webpackContext = new Promise<any>((resolve) => {
webpackGlobalOverride(ALL, (n, r, t) => {
resolve(t)
})
})
const PACK_ID_REG = /\+(\{.*?"\})\[\w\]\+/
export const loadAllPacks = () => {
return webpackContext.then((ctx) => {
try {
const fn = ctx.e.toString()
const packsData = fn.match(PACK_ID_REG)[1] as string
// eslint-disable-next-line no-new-func, @typescript-eslint/no-implied-eval
const packs = Function(`return (${packsData})`)() as { [id: string]: string }
Object.keys(packs).forEach((id) => {
ctx.e(id)
})
} catch (err) {
console.error(err)
}
})
}
const OBF_FN_REG = /\w\(".{4}"\),(\w)=(\[".+?\]);\w=\1,\w=(\d+).+?\);var (\w=.+?,\w\})/
export const OBFUSCATED_REG = /(\w)\((\d+),"(.{4})"\)/g
export const getObfuscationCtx = (mod: Module): (n: number, s: string) => string => {
const str = mod.toString()
const m = str.match(OBF_FN_REG)
if (!m) return () => ''
try {
const arrVar = m[1]
const arr = JSON.parse(m[2])
let n = +m[3] + 1
for (; --n;) arr.push(arr.shift())
const fnStr = m[4]
const ctxStr = `var ${arrVar}=${JSON.stringify(arr)};return (${fnStr})`
// eslint-disable-next-line no-new-func, @typescript-eslint/no-implied-eval
const fn = new Function(ctxStr)()
return fn
} catch (err) {
console.error(err)
return () => ''
}
}
export default webpackHook

View File

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

View File

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